remote: Fetch shell on ssh remote to use for preparing commands (#36690)

Prerequisite for https://github.com/zed-industries/zed/pull/36576 to
allow us to differentiate the shell in a remote.

Release Notes:

- N/A
This commit is contained in:
Lukas Wirth 2025-08-21 19:08:26 +02:00 committed by GitHub
parent 6f32d36ec9
commit b284b1a0b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 121 additions and 52 deletions

View file

@ -916,7 +916,10 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade(); let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade(); let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade(); let weak_workspace = workspace.downgrade();
let is_local = project.read(cx).is_local(); let ssh_info = project
.read(cx)
.ssh_client()
.and_then(|it| it.read(cx).ssh_info());
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let DebugScenario { let DebugScenario {
@ -1000,7 +1003,7 @@ impl RunningState {
None None
}; };
let builder = ShellBuilder::new(is_local, &task.resolved.shell); let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
let command_label = builder.command_label(&task.resolved.command_label); let command_label = builder.command_label(&task.resolved.command_label);
let (command, args) = let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args); builder.build(task.resolved.command.clone(), &task.resolved.args);

View file

@ -34,7 +34,7 @@ use http_client::HttpClient;
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use remote::{SshRemoteClient, ssh_session::SshArgs}; use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
use rpc::{ use rpc::{
AnyProtoClient, TypedEnvelope, AnyProtoClient, TypedEnvelope,
proto::{self}, proto::{self},
@ -254,14 +254,18 @@ impl DapStore {
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let response = request.await?; let response = request.await?;
let binary = DebugAdapterBinary::from_proto(response)?; let binary = DebugAdapterBinary::from_proto(response)?;
let (mut ssh_command, envs, path_style) = let (mut ssh_command, envs, path_style, ssh_shell) =
ssh_client.read_with(cx, |ssh, _| { ssh_client.read_with(cx, |ssh, _| {
let (SshArgs { arguments, envs }, path_style) = let SshInfo {
ssh.ssh_info().context("SSH arguments not found")?; args: SshArgs { arguments, envs },
path_style,
shell,
} = ssh.ssh_info().context("SSH arguments not found")?;
anyhow::Ok(( anyhow::Ok((
SshCommand { arguments }, SshCommand { arguments },
envs.unwrap_or_default(), envs.unwrap_or_default(),
path_style, path_style,
shell,
)) ))
})??; })??;
@ -280,6 +284,7 @@ impl DapStore {
} }
let (program, args) = wrap_for_ssh( let (program, args) = wrap_for_ssh(
&ssh_shell,
&ssh_command, &ssh_command,
binary binary
.command .command

View file

@ -117,7 +117,7 @@ impl DapLocator for CargoLocator {
.cwd .cwd
.clone() .clone()
.context("Couldn't get cwd from debug config which is needed for locators")?; .context("Couldn't get cwd from debug config which is needed for locators")?;
let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); let builder = ShellBuilder::new(None, &build_config.shell).non_interactive();
let (program, args) = builder.build( let (program, args) = builder.build(
Some("cargo".into()), Some("cargo".into()),
&build_config &build_config
@ -126,7 +126,7 @@ impl DapLocator for CargoLocator {
.cloned() .cloned()
.take_while(|arg| arg != "--") .take_while(|arg| arg != "--")
.chain(Some("--message-format=json".to_owned())) .chain(Some("--message-format=json".to_owned()))
.collect(), .collect::<Vec<_>>(),
); );
let mut child = util::command::new_smol_command(program) let mut child = util::command::new_smol_command(program)
.args(args) .args(args)

View file

@ -4,7 +4,7 @@ use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
use itertools::Itertools; use itertools::Itertools;
use language::LanguageName; use language::LanguageName;
use remote::ssh_session::SshArgs; use remote::{SshInfo, ssh_session::SshArgs};
use settings::{Settings, SettingsLocation}; use settings::{Settings, SettingsLocation};
use smol::channel::bounded; use smol::channel::bounded;
use std::{ use std::{
@ -13,7 +13,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; use task::{Shell, ShellBuilder, SpawnInTerminal};
use terminal::{ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, TaskState, TaskStatus, Terminal, TerminalBuilder,
terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
@ -58,11 +58,13 @@ impl SshCommand {
} }
} }
#[derive(Debug)]
pub struct SshDetails { pub struct SshDetails {
pub host: String, pub host: String,
pub ssh_command: SshCommand, pub ssh_command: SshCommand,
pub envs: Option<HashMap<String, String>>, pub envs: Option<HashMap<String, String>>,
pub path_style: PathStyle, pub path_style: PathStyle,
pub shell: String,
} }
impl Project { impl Project {
@ -87,12 +89,18 @@ impl Project {
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> { pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
if let Some(ssh_client) = &self.ssh_client { if let Some(ssh_client) = &self.ssh_client {
let ssh_client = ssh_client.read(cx); let ssh_client = ssh_client.read(cx);
if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { if let Some(SshInfo {
args: SshArgs { arguments, envs },
path_style,
shell,
}) = ssh_client.ssh_info()
{
return Some(SshDetails { return Some(SshDetails {
host: ssh_client.connection_options().host, host: ssh_client.connection_options().host,
ssh_command: SshCommand { arguments }, ssh_command: SshCommand { arguments },
envs, envs,
path_style, path_style,
shell,
}); });
} }
} }
@ -165,7 +173,9 @@ impl Project {
let ssh_details = self.ssh_details(cx); let ssh_details = self.ssh_details(cx);
let settings = self.terminal_settings(&path, cx).clone(); let settings = self.terminal_settings(&path, cx).clone();
let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); let builder =
ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell)
.non_interactive();
let (command, args) = builder.build(Some(command), &Vec::new()); let (command, args) = builder.build(Some(command), &Vec::new());
let mut env = self let mut env = self
@ -180,9 +190,11 @@ impl Project {
ssh_command, ssh_command,
envs, envs,
path_style, path_style,
shell,
.. ..
}) => { }) => {
let (command, args) = wrap_for_ssh( let (command, args) = wrap_for_ssh(
&shell,
&ssh_command, &ssh_command,
Some((&command, &args)), Some((&command, &args)),
path.as_deref(), path.as_deref(),
@ -280,6 +292,7 @@ impl Project {
ssh_command, ssh_command,
envs, envs,
path_style, path_style,
shell,
}) => { }) => {
log::debug!("Connecting to a remote server: {ssh_command:?}"); log::debug!("Connecting to a remote server: {ssh_command:?}");
@ -291,6 +304,7 @@ impl Project {
.or_insert_with(|| "xterm-256color".to_string()); .or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh( let (program, args) = wrap_for_ssh(
&shell,
&ssh_command, &ssh_command,
None, None,
path.as_deref(), path.as_deref(),
@ -343,11 +357,13 @@ impl Project {
ssh_command, ssh_command,
envs, envs,
path_style, path_style,
shell,
}) => { }) => {
log::debug!("Connecting to a remote server: {ssh_command:?}"); log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string()) env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string()); .or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh( let (program, args) = wrap_for_ssh(
&shell,
&ssh_command, &ssh_command,
spawn_task spawn_task
.command .command
@ -637,6 +653,7 @@ impl Project {
} }
pub fn wrap_for_ssh( pub fn wrap_for_ssh(
shell: &str,
ssh_command: &SshCommand, ssh_command: &SshCommand,
command: Option<(&String, &Vec<String>)>, command: Option<(&String, &Vec<String>)>,
path: Option<&Path>, path: Option<&Path>,
@ -645,16 +662,11 @@ pub fn wrap_for_ssh(
path_style: PathStyle, path_style: PathStyle,
) -> (String, Vec<String>) { ) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command { let to_run = if let Some((command, args)) = command {
// DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped let command: Option<Cow<str>> = shlex::try_quote(command).ok();
let command: Option<Cow<str>> = if command == DEFAULT_REMOTE_SHELL {
Some(command.into())
} else {
shlex::try_quote(command).ok()
};
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
command.into_iter().chain(args).join(" ") command.into_iter().chain(args).join(" ")
} else { } else {
"exec ${SHELL:-sh} -l".to_string() format!("exec {shell} -l")
}; };
let mut env_changes = String::new(); let mut env_changes = String::new();
@ -688,7 +700,7 @@ pub fn wrap_for_ssh(
} else { } else {
format!("cd; {env_changes} {to_run}") format!("cd; {env_changes} {to_run}")
}; };
let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap()); let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());
let program = "ssh".to_string(); let program = "ssh".to_string();
let mut args = ssh_command.arguments.clone(); let mut args = ssh_command.arguments.clone();

View file

@ -4,6 +4,6 @@ pub mod proxy;
pub mod ssh_session; pub mod ssh_session;
pub use ssh_session::{ pub use ssh_session::{
ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform,
SshRemoteEvent, SshRemoteClient, SshRemoteEvent,
}; };

View file

@ -89,11 +89,19 @@ pub struct SshConnectionOptions {
pub upload_binary_over_ssh: bool, pub upload_binary_over_ssh: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SshArgs { pub struct SshArgs {
pub arguments: Vec<String>, pub arguments: Vec<String>,
pub envs: Option<HashMap<String, String>>, pub envs: Option<HashMap<String, String>>,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SshInfo {
pub args: SshArgs,
pub path_style: PathStyle,
pub shell: String,
}
#[macro_export] #[macro_export]
macro_rules! shell_script { macro_rules! shell_script {
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
@ -471,6 +479,16 @@ impl SshSocket {
Ok(SshPlatform { os, arch }) Ok(SshPlatform { os, arch })
} }
async fn shell(&self) -> String {
match self.run_command("sh", &["-c", "echo $SHELL"]).await {
Ok(shell) => shell.trim().to_owned(),
Err(e) => {
log::error!("Failed to get shell: {e}");
"sh".to_owned()
}
}
}
} }
const MAX_MISSED_HEARTBEATS: usize = 5; const MAX_MISSED_HEARTBEATS: usize = 5;
@ -1152,12 +1170,16 @@ impl SshRemoteClient {
cx.notify(); cx.notify();
} }
pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { pub fn ssh_info(&self) -> Option<SshInfo> {
self.state self.state
.lock() .lock()
.as_ref() .as_ref()
.and_then(|state| state.ssh_connection()) .and_then(|state| state.ssh_connection())
.map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) .map(|ssh_connection| SshInfo {
args: ssh_connection.ssh_args(),
path_style: ssh_connection.path_style(),
shell: ssh_connection.shell(),
})
} }
pub fn upload_directory( pub fn upload_directory(
@ -1392,6 +1414,7 @@ trait RemoteConnection: Send + Sync {
fn ssh_args(&self) -> SshArgs; fn ssh_args(&self) -> SshArgs;
fn connection_options(&self) -> SshConnectionOptions; fn connection_options(&self) -> SshConnectionOptions;
fn path_style(&self) -> PathStyle; fn path_style(&self) -> PathStyle;
fn shell(&self) -> String;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn simulate_disconnect(&self, _: &AsyncApp) {} fn simulate_disconnect(&self, _: &AsyncApp) {}
@ -1403,6 +1426,7 @@ struct SshRemoteConnection {
remote_binary_path: Option<RemotePathBuf>, remote_binary_path: Option<RemotePathBuf>,
ssh_platform: SshPlatform, ssh_platform: SshPlatform,
ssh_path_style: PathStyle, ssh_path_style: PathStyle,
ssh_shell: String,
_temp_dir: TempDir, _temp_dir: TempDir,
} }
@ -1429,6 +1453,10 @@ impl RemoteConnection for SshRemoteConnection {
self.socket.connection_options.clone() self.socket.connection_options.clone()
} }
fn shell(&self) -> String {
self.ssh_shell.clone()
}
fn upload_directory( fn upload_directory(
&self, &self,
src_path: PathBuf, src_path: PathBuf,
@ -1642,6 +1670,7 @@ impl SshRemoteConnection {
"windows" => PathStyle::Windows, "windows" => PathStyle::Windows,
_ => PathStyle::Posix, _ => PathStyle::Posix,
}; };
let ssh_shell = socket.shell().await;
let mut this = Self { let mut this = Self {
socket, socket,
@ -1650,6 +1679,7 @@ impl SshRemoteConnection {
remote_binary_path: None, remote_binary_path: None,
ssh_path_style, ssh_path_style,
ssh_platform, ssh_platform,
ssh_shell,
}; };
let (release_channel, version, commit) = cx.update(|cx| { let (release_channel, version, commit) = cx.update(|cx| {
@ -2686,6 +2716,10 @@ mod fake {
fn path_style(&self) -> PathStyle { fn path_style(&self) -> PathStyle {
PathStyle::current() PathStyle::current()
} }
fn shell(&self) -> String {
"sh".to_owned()
}
} }
pub(super) struct Delegate; pub(super) struct Delegate;

View file

@ -1,26 +1,40 @@
use crate::Shell; use crate::Shell;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
enum ShellKind { pub enum ShellKind {
#[default] #[default]
Posix, Posix,
Csh,
Fish,
Powershell, Powershell,
Nushell, Nushell,
Cmd, Cmd,
} }
impl ShellKind { impl ShellKind {
fn new(program: &str) -> Self { pub fn system() -> Self {
Self::new(&system_shell())
}
pub fn new(program: &str) -> Self {
#[cfg(windows)]
let (_, program) = program.rsplit_once('\\').unwrap_or(("", program));
#[cfg(not(windows))]
let (_, program) = program.rsplit_once('/').unwrap_or(("", program));
if program == "powershell" if program == "powershell"
|| program.ends_with("powershell.exe") || program == "powershell.exe"
|| program == "pwsh" || program == "pwsh"
|| program.ends_with("pwsh.exe") || program == "pwsh.exe"
{ {
ShellKind::Powershell ShellKind::Powershell
} else if program == "cmd" || program.ends_with("cmd.exe") { } else if program == "cmd" || program == "cmd.exe" {
ShellKind::Cmd ShellKind::Cmd
} else if program == "nu" { } else if program == "nu" {
ShellKind::Nushell ShellKind::Nushell
} else if program == "fish" {
ShellKind::Fish
} else if program == "csh" {
ShellKind::Csh
} else { } else {
// Someother shell detected, the user might install and use a // Someother shell detected, the user might install and use a
// unix-like shell. // unix-like shell.
@ -33,6 +47,8 @@ impl ShellKind {
Self::Powershell => Self::to_powershell_variable(input), Self::Powershell => Self::to_powershell_variable(input),
Self::Cmd => Self::to_cmd_variable(input), Self::Cmd => Self::to_cmd_variable(input),
Self::Posix => input.to_owned(), Self::Posix => input.to_owned(),
Self::Fish => input.to_owned(),
Self::Csh => input.to_owned(),
Self::Nushell => Self::to_nushell_variable(input), Self::Nushell => Self::to_nushell_variable(input),
} }
} }
@ -153,7 +169,7 @@ impl ShellKind {
match self { match self {
ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Powershell => vec!["-C".to_owned(), combined_command],
ShellKind::Cmd => vec!["/C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
ShellKind::Posix | ShellKind::Nushell => interactive ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive
.then(|| "-i".to_owned()) .then(|| "-i".to_owned())
.into_iter() .into_iter()
.chain(["-c".to_owned(), combined_command]) .chain(["-c".to_owned(), combined_command])
@ -184,19 +200,14 @@ pub struct ShellBuilder {
kind: ShellKind, kind: ShellKind,
} }
pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\"";
impl ShellBuilder { impl ShellBuilder {
/// Create a new ShellBuilder as configured. /// Create a new ShellBuilder as configured.
pub fn new(is_local: bool, shell: &Shell) -> Self { pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self {
let (program, args) = match shell { let (program, args) = match shell {
Shell::System => { Shell::System => match remote_system_shell {
if is_local { Some(remote_shell) => (remote_shell.to_string(), Vec::new()),
(system_shell(), Vec::new()) None => (system_shell(), Vec::new()),
} else { },
(DEFAULT_REMOTE_SHELL.to_string(), Vec::new())
}
}
Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
}; };
@ -212,6 +223,7 @@ impl ShellBuilder {
self.interactive = false; self.interactive = false;
self self
} }
/// Returns the label to show in the terminal tab /// Returns the label to show in the terminal tab
pub fn command_label(&self, command_label: &str) -> String { pub fn command_label(&self, command_label: &str) -> String {
match self.kind { match self.kind {
@ -221,7 +233,7 @@ impl ShellBuilder {
ShellKind::Cmd => { ShellKind::Cmd => {
format!("{} /C '{}'", self.program, command_label) format!("{} /C '{}'", self.program, command_label)
} }
ShellKind::Posix | ShellKind::Nushell => { ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
format!( format!(
"{} {interactivity}-c '$\"{}\"'", "{} {interactivity}-c '$\"{}\"'",
@ -234,7 +246,7 @@ impl ShellBuilder {
pub fn build( pub fn build(
mut self, mut self,
task_command: Option<String>, task_command: Option<String>,
task_args: &Vec<String>, task_args: &[String],
) -> (String, Vec<String>) { ) -> (String, Vec<String>) {
if let Some(task_command) = task_command { if let Some(task_command) = task_command {
let combined_command = task_args.iter().fold(task_command, |mut command, arg| { let combined_command = task_args.iter().fold(task_command, |mut command, arg| {
@ -258,11 +270,11 @@ mod test {
#[test] #[test]
fn test_nu_shell_variable_substitution() { fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned()); let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(true, &shell); let shell_builder = ShellBuilder::new(None, &shell);
let (program, args) = shell_builder.build( let (program, args) = shell_builder.build(
Some("echo".into()), Some("echo".into()),
&vec![ &[
"${hello}".to_string(), "${hello}".to_string(),
"$world".to_string(), "$world".to_string(),
"nothing".to_string(), "nothing".to_string(),

View file

@ -22,7 +22,7 @@ pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
Request, TcpArgumentsTemplate, ZedDebugConfig, Request, TcpArgumentsTemplate, ZedDebugConfig,
}; };
pub use shell_builder::{DEFAULT_REMOTE_SHELL, ShellBuilder}; pub use shell_builder::{ShellBuilder, ShellKind};
pub use task_template::{ pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_variables_in_map, substitute_variables_in_str, substitute_variables_in_map, substitute_variables_in_str,

View file

@ -481,14 +481,17 @@ impl TerminalPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<WeakEntity<Terminal>>> { ) -> Task<Result<WeakEntity<Terminal>>> {
let Ok(is_local) = self let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| {
.workspace let project = workspace.project().read(cx);
.update(cx, |workspace, cx| workspace.project().read(cx).is_local()) (
else { project.ssh_client().and_then(|it| it.read(cx).ssh_info()),
project.is_via_collab(),
)
}) else {
return Task::ready(Err(anyhow!("Project is not local"))); return Task::ready(Err(anyhow!("Project is not local")));
}; };
let builder = ShellBuilder::new(is_local, &task.shell); let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell);
let command_label = builder.command_label(&task.command_label); let command_label = builder.command_label(&task.command_label);
let (command, args) = builder.build(task.command.clone(), &task.args); let (command, args) = builder.build(task.command.clone(), &task.args);

View file

@ -166,7 +166,7 @@ impl<T: AsRef<Path>> From<T> for SanitizedPath {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathStyle { pub enum PathStyle {
Posix, Posix,
Windows, Windows,