acp_thread: Respect terminal settings shell for terminal tool environment (#39349)

Lukas Wirth created

When sourcing the project environment for the terminal tool, we will now
do so by spawning the shell specified by the users `terminal.shell`
setting (or as usual fall back to the login shell).

Closes #37687 

Release Notes:

- N/A

Change summary

crates/acp_thread/src/acp_thread.rs         |   5 
crates/assistant_tools/src/terminal_tool.rs |   6 
crates/languages/src/python.rs              |   8 
crates/project/src/direnv.rs                |   2 
crates/project/src/environment.rs           | 168 +++++-----
crates/project/src/project.rs               |   4 
crates/project/src/terminals.rs             |  22 -
crates/task/src/shell_builder.rs            | 217 -------------
crates/task/src/task.rs                     |  20 +
crates/terminal/src/terminal.rs             |   2 
crates/util/src/shell.rs                    | 340 +++++++++++++++++++++++
crates/util/src/shell_env.rs                | 195 ++++++++----
crates/util/src/util.rs                     | 129 --------
13 files changed, 617 insertions(+), 501 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

@@ -3,6 +3,7 @@ mod diff;
 mod mention;
 mod terminal;
 
+use ::terminal::terminal_settings::TerminalSettings;
 use agent_settings::AgentSettings;
 use collections::HashSet;
 pub use connection::*;
@@ -1961,11 +1962,11 @@ impl AcpThread {
     ) -> Task<Result<Entity<Terminal>>> {
         let env = match &cwd {
             Some(dir) => self.project.update(cx, |project, cx| {
-                project.directory_environment(dir.as_path().into(), cx)
+                let shell = TerminalSettings::get_global(cx).shell.clone();
+                project.directory_environment(&shell, dir.as_path().into(), cx)
             }),
             None => Task::ready(None).shared(),
         };
-
         let env = cx.spawn(async move |_, _| {
             let mut env = env.await.unwrap_or_default();
             // Disables paging for `git` and hopefully other commands

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -27,6 +27,7 @@ use std::{
     time::{Duration, Instant},
 };
 use task::{Shell, ShellBuilder};
+use terminal::terminal_settings::TerminalSettings;
 use terminal_view::TerminalView;
 use theme::ThemeSettings;
 use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
@@ -119,9 +120,10 @@ impl Tool for TerminalTool {
         };
 
         let cwd = working_dir.clone();
-        let env = match &working_dir {
+        let env = match &cwd {
             Some(dir) => project.update(cx, |project, cx| {
-                project.directory_environment(dir.as_path().into(), cx)
+                let shell = TerminalSettings::get_global(cx).shell.clone();
+                project.directory_environment(&shell, dir.as_path().into(), cx)
             }),
             None => Task::ready(None).shared(),
         };

crates/languages/src/python.rs 🔗

@@ -1187,11 +1187,13 @@ impl ToolchainLister for PythonToolchainProvider {
                         ShellKind::PowerShell => ".",
                         ShellKind::Fish => "source",
                         ShellKind::Csh => "source",
-                        ShellKind::Posix => "source",
+                        ShellKind::Tcsh => "source",
+                        ShellKind::Posix | ShellKind::Rc => "source",
                     };
                     let activate_script_name = match shell {
-                        ShellKind::Posix => "activate",
+                        ShellKind::Posix | ShellKind::Rc => "activate",
                         ShellKind::Csh => "activate.csh",
+                        ShellKind::Tcsh => "activate.csh",
                         ShellKind::Fish => "activate.fish",
                         ShellKind::Nushell => "activate.nu",
                         ShellKind::PowerShell => "activate.ps1",
@@ -1220,7 +1222,9 @@ impl ToolchainLister for PythonToolchainProvider {
                     ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
                     ShellKind::PowerShell => None,
                     ShellKind::Csh => None,
+                    ShellKind::Tcsh => None,
                     ShellKind::Cmd => None,
+                    ShellKind::Rc => None,
                 })
             }
             _ => {}

crates/project/src/direnv.rs 🔗

@@ -1,7 +1,6 @@
 use crate::environment::EnvironmentErrorMessage;
 use std::process::ExitStatus;
 
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
 use {collections::HashMap, std::path::Path, util::ResultExt};
 
 #[derive(Clone)]
@@ -28,7 +27,6 @@ impl From<DirenvError> for Option<EnvironmentErrorMessage> {
     }
 }
 
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
 pub async fn load_direnv_environment(
     env: &HashMap<String, String>,
     dir: &Path,

crates/project/src/environment.rs 🔗

@@ -1,6 +1,7 @@
 use futures::{FutureExt, future::Shared};
 use language::Buffer;
 use std::{path::Path, sync::Arc};
+use task::Shell;
 use util::ResultExt;
 use worktree::Worktree;
 
@@ -16,6 +17,8 @@ use crate::{
 pub struct ProjectEnvironment {
     cli_environment: Option<HashMap<String, String>>,
     environments: HashMap<Arc<Path>, Shared<Task<Option<HashMap<String, String>>>>>,
+    shell_based_environments:
+        HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
     environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
 }
 
@@ -30,6 +33,7 @@ impl ProjectEnvironment {
         Self {
             cli_environment,
             environments: Default::default(),
+            shell_based_environments: Default::default(),
             environment_error_messages: Default::default(),
         }
     }
@@ -134,7 +138,22 @@ impl ProjectEnvironment {
 
         self.environments
             .entry(abs_path.clone())
-            .or_insert_with(|| get_directory_env_impl(abs_path.clone(), cx).shared())
+            .or_insert_with(|| {
+                get_directory_env_impl(&Shell::System, abs_path.clone(), cx).shared()
+            })
+            .clone()
+    }
+
+    /// Returns the project environment, if possible, with the given shell.
+    pub fn get_directory_environment_for_shell(
+        &mut self,
+        shell: &Shell,
+        abs_path: Arc<Path>,
+        cx: &mut Context<Self>,
+    ) -> Shared<Task<Option<HashMap<String, String>>>> {
+        self.shell_based_environments
+            .entry((shell.clone(), abs_path.clone()))
+            .or_insert_with(|| get_directory_env_impl(shell, abs_path.clone(), cx).shared())
             .clone()
     }
 }
@@ -176,6 +195,7 @@ impl EnvironmentErrorMessage {
 }
 
 async fn load_directory_shell_environment(
+    shell: &Shell,
     abs_path: &Path,
     load_direnv: &DirenvSettings,
 ) -> (
@@ -198,7 +218,7 @@ async fn load_directory_shell_environment(
                 );
             };
 
-            load_shell_environment(dir, load_direnv).await
+            load_shell_environment(shell, dir, load_direnv).await
         }
         Err(err) => (
             None,
@@ -211,51 +231,8 @@ async fn load_directory_shell_environment(
     }
 }
 
-#[cfg(any(test, feature = "test-support"))]
-async fn load_shell_environment(
-    _dir: &Path,
-    _load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
-        .into_iter()
-        .collect();
-    (Some(fake_env), None)
-}
-
-#[cfg(all(target_os = "windows", not(any(test, feature = "test-support"))))]
-async fn load_shell_environment(
-    dir: &Path,
-    _load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    use util::shell_env;
-
-    let envs = match shell_env::capture(dir).await {
-        Ok(envs) => envs,
-        Err(err) => {
-            util::log_err(&err);
-            return (
-                None,
-                Some(EnvironmentErrorMessage(format!(
-                    "Failed to load environment variables: {}",
-                    err
-                ))),
-            );
-        }
-    };
-
-    // Note: direnv is not available on Windows, so we skip direnv processing
-    // and just return the shell environment
-    (Some(envs), None)
-}
-
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
 async fn load_shell_environment(
+    shell: &Shell,
     dir: &Path,
     load_direnv: &DirenvSettings,
 ) -> (
@@ -265,55 +242,86 @@ async fn load_shell_environment(
     use crate::direnv::load_direnv_environment;
     use util::shell_env;
 
-    let dir_ = dir.to_owned();
-    let mut envs = match shell_env::capture(&dir_).await {
-        Ok(envs) => envs,
-        Err(err) => {
-            util::log_err(&err);
-            return (
-                None,
-                Some(EnvironmentErrorMessage::from_str(
-                    "Failed to load environment variables. See log for details",
-                )),
-            );
-        }
-    };
-
-    // If the user selects `Direct` for direnv, it would set an environment
-    // variable that later uses to know that it should not run the hook.
-    // We would include in `.envs` call so it is okay to run the hook
-    // even if direnv direct mode is enabled.
-    let (direnv_environment, direnv_error) = match load_direnv {
-        DirenvSettings::ShellHook => (None, None),
-        DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
-            Ok(env) => (Some(env), None),
-            Err(err) => (None, err.into()),
-        },
-    };
-    if let Some(direnv_environment) = direnv_environment {
-        for (key, value) in direnv_environment {
-            if let Some(value) = value {
-                envs.insert(key, value);
-            } else {
-                envs.remove(&key);
+    if cfg!(any(test, feature = "test-support")) {
+        let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
+            .into_iter()
+            .collect();
+        (Some(fake_env), None)
+    } else if cfg!(target_os = "windows",) {
+        let (shell, args) = shell.program_and_args();
+        let envs = match shell_env::capture(shell, args, dir).await {
+            Ok(envs) => envs,
+            Err(err) => {
+                util::log_err(&err);
+                return (
+                    None,
+                    Some(EnvironmentErrorMessage(format!(
+                        "Failed to load environment variables: {}",
+                        err
+                    ))),
+                );
+            }
+        };
+
+        // Note: direnv is not available on Windows, so we skip direnv processing
+        // and just return the shell environment
+        (Some(envs), None)
+    } else {
+        let dir_ = dir.to_owned();
+        let (shell, args) = shell.program_and_args();
+        let mut envs = match shell_env::capture(shell, args, &dir_).await {
+            Ok(envs) => envs,
+            Err(err) => {
+                util::log_err(&err);
+                return (
+                    None,
+                    Some(EnvironmentErrorMessage::from_str(
+                        "Failed to load environment variables. See log for details",
+                    )),
+                );
+            }
+        };
+
+        // If the user selects `Direct` for direnv, it would set an environment
+        // variable that later uses to know that it should not run the hook.
+        // We would include in `.envs` call so it is okay to run the hook
+        // even if direnv direct mode is enabled.
+        let (direnv_environment, direnv_error) = match load_direnv {
+            DirenvSettings::ShellHook => (None, None),
+            DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
+                Ok(env) => (Some(env), None),
+                Err(err) => (None, err.into()),
+            },
+        };
+        if let Some(direnv_environment) = direnv_environment {
+            for (key, value) in direnv_environment {
+                if let Some(value) = value {
+                    envs.insert(key, value);
+                } else {
+                    envs.remove(&key);
+                }
             }
         }
-    }
 
-    (Some(envs), direnv_error)
+        (Some(envs), direnv_error)
+    }
 }
 
 fn get_directory_env_impl(
+    shell: &Shell,
     abs_path: Arc<Path>,
     cx: &Context<ProjectEnvironment>,
 ) -> Task<Option<HashMap<String, String>>> {
     let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
 
+    let shell = shell.clone();
     cx.spawn(async move |this, cx| {
         let (mut shell_env, error_message) = cx
             .background_spawn({
                 let abs_path = abs_path.clone();
-                async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
+                async move {
+                    load_directory_shell_environment(&shell, &abs_path, &load_direnv).await
+                }
             })
             .await;
 

crates/project/src/project.rs 🔗

@@ -33,6 +33,7 @@ pub mod search_history;
 mod yarn;
 
 use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
+use task::Shell;
 
 use crate::{
     agent_server_store::{AgentServerStore, AllAgentServersSettings},
@@ -1894,11 +1895,12 @@ impl Project {
 
     pub fn directory_environment(
         &self,
+        shell: &Shell,
         abs_path: Arc<Path>,
         cx: &mut App,
     ) -> Shared<Task<Option<HashMap<String, String>>>> {
         self.environment.update(cx, |environment, cx| {
-            environment.get_directory_environment(abs_path, cx)
+            environment.get_directory_environment_for_shell(shell, abs_path, cx)
         })
     }
 

crates/project/src/terminals.rs 🔗

@@ -16,7 +16,7 @@ use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
 use terminal::{
     TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
 };
-use util::{get_default_system_shell, get_system_shell, maybe, rel_path::RelPath};
+use util::{get_default_system_shell, maybe, rel_path::RelPath};
 
 use crate::{Project, ProjectPath};
 
@@ -98,15 +98,7 @@ impl Project {
                 .read(cx)
                 .shell()
                 .unwrap_or_else(get_default_system_shell),
-            None => match &settings.shell {
-                Shell::Program(program) => program.clone(),
-                Shell::WithArguments {
-                    program,
-                    args: _,
-                    title_override: _,
-                } => program.clone(),
-                Shell::System => get_system_shell(),
-            },
+            None => settings.shell.program(),
         };
 
         let project_path_contexts = self
@@ -332,15 +324,7 @@ impl Project {
                 .read(cx)
                 .shell()
                 .unwrap_or_else(get_default_system_shell),
-            None => match &settings.shell {
-                Shell::Program(program) => program.clone(),
-                Shell::WithArguments {
-                    program,
-                    args: _,
-                    title_override: _,
-                } => program.clone(),
-                Shell::System => get_system_shell(),
-            },
+            None => settings.shell.program(),
         });
 
         let lang_registry = self.languages.clone();

crates/task/src/shell_builder.rs 🔗

@@ -1,207 +1,8 @@
-use std::fmt;
-
-use util::get_system_shell;
+use util::shell::get_system_shell;
 
 use crate::Shell;
 
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum ShellKind {
-    #[default]
-    Posix,
-    Csh,
-    Fish,
-    PowerShell,
-    Nushell,
-    Cmd,
-}
-
-impl fmt::Display for ShellKind {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            ShellKind::Posix => write!(f, "sh"),
-            ShellKind::Csh => write!(f, "csh"),
-            ShellKind::Fish => write!(f, "fish"),
-            ShellKind::PowerShell => write!(f, "powershell"),
-            ShellKind::Nushell => write!(f, "nu"),
-            ShellKind::Cmd => write!(f, "cmd"),
-        }
-    }
-}
-
-impl ShellKind {
-    pub fn system() -> Self {
-        Self::new(&get_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"
-            || program.ends_with("powershell.exe")
-            || program == "pwsh"
-            || program.ends_with("pwsh.exe")
-        {
-            ShellKind::PowerShell
-        } else if program == "cmd" || program.ends_with("cmd.exe") {
-            ShellKind::Cmd
-        } else if program == "nu" {
-            ShellKind::Nushell
-        } else if program == "fish" {
-            ShellKind::Fish
-        } else if program == "csh" {
-            ShellKind::Csh
-        } else {
-            // Some other shell detected, the user might install and use a
-            // unix-like shell.
-            ShellKind::Posix
-        }
-    }
-
-    fn to_shell_variable(self, input: &str) -> String {
-        match self {
-            Self::PowerShell => Self::to_powershell_variable(input),
-            Self::Cmd => Self::to_cmd_variable(input),
-            Self::Posix => input.to_owned(),
-            Self::Fish => input.to_owned(),
-            Self::Csh => input.to_owned(),
-            Self::Nushell => Self::to_nushell_variable(input),
-        }
-    }
-
-    fn to_cmd_variable(input: &str) -> String {
-        if let Some(var_str) = input.strip_prefix("${") {
-            if var_str.find(':').is_none() {
-                // If the input starts with "${", remove the trailing "}"
-                format!("%{}%", &var_str[..var_str.len() - 1])
-            } else {
-                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
-                // which will result in the task failing to run in such cases.
-                input.into()
-            }
-        } else if let Some(var_str) = input.strip_prefix('$') {
-            // If the input starts with "$", directly append to "$env:"
-            format!("%{}%", var_str)
-        } else {
-            // If no prefix is found, return the input as is
-            input.into()
-        }
-    }
-    fn to_powershell_variable(input: &str) -> String {
-        if let Some(var_str) = input.strip_prefix("${") {
-            if var_str.find(':').is_none() {
-                // If the input starts with "${", remove the trailing "}"
-                format!("$env:{}", &var_str[..var_str.len() - 1])
-            } else {
-                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
-                // which will result in the task failing to run in such cases.
-                input.into()
-            }
-        } else if let Some(var_str) = input.strip_prefix('$') {
-            // If the input starts with "$", directly append to "$env:"
-            format!("$env:{}", var_str)
-        } else {
-            // If no prefix is found, return the input as is
-            input.into()
-        }
-    }
-
-    fn to_nushell_variable(input: &str) -> String {
-        let mut result = String::new();
-        let mut source = input;
-        let mut is_start = true;
-
-        loop {
-            match source.chars().next() {
-                None => return result,
-                Some('$') => {
-                    source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
-                    is_start = false;
-                }
-                Some(_) => {
-                    is_start = false;
-                    let chunk_end = source.find('$').unwrap_or(source.len());
-                    let (chunk, rest) = source.split_at(chunk_end);
-                    result.push_str(chunk);
-                    source = rest;
-                }
-            }
-        }
-    }
-
-    fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
-        if source.starts_with("env.") {
-            text.push('$');
-            return source;
-        }
-
-        match source.chars().next() {
-            Some('{') => {
-                let source = &source[1..];
-                if let Some(end) = source.find('}') {
-                    let var_name = &source[..end];
-                    if !var_name.is_empty() {
-                        if !is_start {
-                            text.push_str("(");
-                        }
-                        text.push_str("$env.");
-                        text.push_str(var_name);
-                        if !is_start {
-                            text.push_str(")");
-                        }
-                        &source[end + 1..]
-                    } else {
-                        text.push_str("${}");
-                        &source[end + 1..]
-                    }
-                } else {
-                    text.push_str("${");
-                    source
-                }
-            }
-            Some(c) if c.is_alphabetic() || c == '_' => {
-                let end = source
-                    .find(|c: char| !c.is_alphanumeric() && c != '_')
-                    .unwrap_or(source.len());
-                let var_name = &source[..end];
-                if !is_start {
-                    text.push_str("(");
-                }
-                text.push_str("$env.");
-                text.push_str(var_name);
-                if !is_start {
-                    text.push_str(")");
-                }
-                &source[end..]
-            }
-            _ => {
-                text.push('$');
-                source
-            }
-        }
-    }
-
-    fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
-        match self {
-            ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
-            ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
-            ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive
-                .then(|| "-i".to_owned())
-                .into_iter()
-                .chain(["-c".to_owned(), combined_command])
-                .collect(),
-        }
-    }
-
-    pub fn command_prefix(&self) -> Option<char> {
-        match self {
-            ShellKind::PowerShell => Some('&'),
-            ShellKind::Nushell => Some('^'),
-            _ => None,
-        }
-    }
-}
+pub use util::shell::ShellKind;
 
 /// ShellBuilder is used to turn a user-requested task into a
 /// program that can be executed by the shell.
@@ -253,7 +54,12 @@ impl ShellBuilder {
                 ShellKind::Cmd => {
                     format!("{} /C '{}'", self.program, command_to_use_in_label)
                 }
-                ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
+                ShellKind::Posix
+                | ShellKind::Nushell
+                | ShellKind::Fish
+                | ShellKind::Csh
+                | ShellKind::Tcsh
+                | ShellKind::Rc => {
                     let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
                     format!(
                         "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -283,7 +89,12 @@ impl ShellBuilder {
             });
             if self.redirect_stdin {
                 match self.kind {
-                    ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
+                    ShellKind::Posix
+                    | ShellKind::Nushell
+                    | ShellKind::Fish
+                    | ShellKind::Csh
+                    | ShellKind::Tcsh
+                    | ShellKind::Rc => {
                         combined_command.insert(0, '(');
                         combined_command.push_str(") </dev/null");
                     }

crates/task/src/task.rs 🔗

@@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
 use std::path::PathBuf;
 use std::str::FromStr;
+use util::get_system_shell;
 
 pub use adapter_schema::{AdapterSchema, AdapterSchemas};
 pub use debug_format::{
@@ -317,7 +318,7 @@ pub struct TaskContext {
 pub struct RunnableTag(pub SharedString);
 
 /// Shell configuration to open the terminal with.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 #[serde(rename_all = "snake_case")]
 pub enum Shell {
     /// Use the system's default terminal configuration in /etc/passwd
@@ -336,6 +337,23 @@ pub enum Shell {
     },
 }
 
+impl Shell {
+    pub fn program(&self) -> String {
+        match self {
+            Shell::Program(program) => program.clone(),
+            Shell::WithArguments { program, .. } => program.clone(),
+            Shell::System => get_system_shell(),
+        }
+    }
+    pub fn program_and_args(&self) -> (String, &[String]) {
+        match self {
+            Shell::Program(program) => (program.clone(), &[]),
+            Shell::WithArguments { program, args, .. } => (program.clone(), args),
+            Shell::System => (get_system_shell(), &[]),
+        }
+    }
+}
+
 type VsCodeEnvVariable = String;
 type ZedEnvVariable = String;
 

crates/terminal/src/terminal.rs 🔗

@@ -398,7 +398,7 @@ impl TerminalBuilder {
                 #[cfg(target_os = "windows")]
                 {
                     Some(ShellParams::new(
-                        util::get_windows_system_shell(),
+                        util::shell::get_windows_system_shell(),
                         None,
                         None,
                     ))

crates/util/src/shell.rs 🔗

@@ -0,0 +1,340 @@
+use std::{fmt, path::Path, sync::LazyLock};
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ShellKind {
+    #[default]
+    Posix,
+    Csh,
+    Tcsh,
+    Rc,
+    Fish,
+    PowerShell,
+    Nushell,
+    Cmd,
+}
+
+pub fn get_system_shell() -> String {
+    if cfg!(windows) {
+        get_windows_system_shell()
+    } else {
+        std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
+    }
+}
+
+pub fn get_default_system_shell() -> String {
+    if cfg!(windows) {
+        get_windows_system_shell()
+    } else {
+        "/bin/sh".to_string()
+    }
+}
+
+pub fn get_windows_system_shell() -> String {
+    use std::path::PathBuf;
+
+    fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
+        #[cfg(target_pointer_width = "64")]
+        let env_var = if find_alternate {
+            "ProgramFiles(x86)"
+        } else {
+            "ProgramFiles"
+        };
+
+        #[cfg(target_pointer_width = "32")]
+        let env_var = if find_alternate {
+            "ProgramW6432"
+        } else {
+            "ProgramFiles"
+        };
+
+        let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
+        install_base_dir
+            .read_dir()
+            .ok()?
+            .filter_map(Result::ok)
+            .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
+            .filter_map(|entry| {
+                let dir_name = entry.file_name();
+                let dir_name = dir_name.to_string_lossy();
+
+                let version = if find_preview {
+                    let dash_index = dir_name.find('-')?;
+                    if &dir_name[dash_index + 1..] != "preview" {
+                        return None;
+                    };
+                    dir_name[..dash_index].parse::<u32>().ok()?
+                } else {
+                    dir_name.parse::<u32>().ok()?
+                };
+
+                let exe_path = entry.path().join("pwsh.exe");
+                if exe_path.exists() {
+                    Some((version, exe_path))
+                } else {
+                    None
+                }
+            })
+            .max_by_key(|(version, _)| *version)
+            .map(|(_, path)| path)
+    }
+
+    fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
+        let msix_app_dir =
+            PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
+        if !msix_app_dir.exists() {
+            return None;
+        }
+
+        let prefix = if find_preview {
+            "Microsoft.PowerShellPreview_"
+        } else {
+            "Microsoft.PowerShell_"
+        };
+        msix_app_dir
+            .read_dir()
+            .ok()?
+            .filter_map(|entry| {
+                let entry = entry.ok()?;
+                if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
+                    return None;
+                }
+
+                if !entry.file_name().to_string_lossy().starts_with(prefix) {
+                    return None;
+                }
+
+                let exe_path = entry.path().join("pwsh.exe");
+                exe_path.exists().then_some(exe_path)
+            })
+            .next()
+    }
+
+    fn find_pwsh_in_scoop() -> Option<PathBuf> {
+        let pwsh_exe =
+            PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
+        pwsh_exe.exists().then_some(pwsh_exe)
+    }
+
+    static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
+        find_pwsh_in_programfiles(false, false)
+            .or_else(|| find_pwsh_in_programfiles(true, false))
+            .or_else(|| find_pwsh_in_msix(false))
+            .or_else(|| find_pwsh_in_programfiles(false, true))
+            .or_else(|| find_pwsh_in_msix(true))
+            .or_else(|| find_pwsh_in_programfiles(true, true))
+            .or_else(find_pwsh_in_scoop)
+            .map(|p| p.to_string_lossy().into_owned())
+            .unwrap_or("powershell.exe".to_string())
+    });
+
+    (*SYSTEM_SHELL).clone()
+}
+
+impl fmt::Display for ShellKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ShellKind::Posix => write!(f, "sh"),
+            ShellKind::Csh => write!(f, "csh"),
+            ShellKind::Tcsh => write!(f, "tcsh"),
+            ShellKind::Fish => write!(f, "fish"),
+            ShellKind::PowerShell => write!(f, "powershell"),
+            ShellKind::Nushell => write!(f, "nu"),
+            ShellKind::Cmd => write!(f, "cmd"),
+            ShellKind::Rc => write!(f, "rc"),
+        }
+    }
+}
+
+impl ShellKind {
+    pub fn system() -> Self {
+        Self::new(&get_system_shell())
+    }
+
+    pub fn new(program: impl AsRef<Path>) -> Self {
+        let program = program.as_ref();
+        let Some(program) = program.file_name().and_then(|s| s.to_str()) else {
+            return if cfg!(windows) {
+                ShellKind::PowerShell
+            } else {
+                ShellKind::Posix
+            };
+        };
+        if program == "powershell"
+            || program.ends_with("powershell.exe")
+            || program == "pwsh"
+            || program.ends_with("pwsh.exe")
+        {
+            ShellKind::PowerShell
+        } else if program == "cmd" || program.ends_with("cmd.exe") {
+            ShellKind::Cmd
+        } else if program == "nu" {
+            ShellKind::Nushell
+        } else if program == "fish" {
+            ShellKind::Fish
+        } else if program == "csh" {
+            ShellKind::Csh
+        } else if program == "tcsh" {
+            ShellKind::Tcsh
+        } else if program == "rc" {
+            ShellKind::Rc
+        } else {
+            if cfg!(windows) {
+                ShellKind::PowerShell
+            } else {
+                // Some other shell detected, the user might install and use a
+                // unix-like shell.
+                ShellKind::Posix
+            }
+        }
+    }
+
+    pub fn to_shell_variable(self, input: &str) -> String {
+        match self {
+            Self::PowerShell => Self::to_powershell_variable(input),
+            Self::Cmd => Self::to_cmd_variable(input),
+            Self::Posix => input.to_owned(),
+            Self::Fish => input.to_owned(),
+            Self::Csh => input.to_owned(),
+            Self::Tcsh => input.to_owned(),
+            Self::Rc => input.to_owned(),
+            Self::Nushell => Self::to_nushell_variable(input),
+        }
+    }
+
+    fn to_cmd_variable(input: &str) -> String {
+        if let Some(var_str) = input.strip_prefix("${") {
+            if var_str.find(':').is_none() {
+                // If the input starts with "${", remove the trailing "}"
+                format!("%{}%", &var_str[..var_str.len() - 1])
+            } else {
+                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+                // which will result in the task failing to run in such cases.
+                input.into()
+            }
+        } else if let Some(var_str) = input.strip_prefix('$') {
+            // If the input starts with "$", directly append to "$env:"
+            format!("%{}%", var_str)
+        } else {
+            // If no prefix is found, return the input as is
+            input.into()
+        }
+    }
+    fn to_powershell_variable(input: &str) -> String {
+        if let Some(var_str) = input.strip_prefix("${") {
+            if var_str.find(':').is_none() {
+                // If the input starts with "${", remove the trailing "}"
+                format!("$env:{}", &var_str[..var_str.len() - 1])
+            } else {
+                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+                // which will result in the task failing to run in such cases.
+                input.into()
+            }
+        } else if let Some(var_str) = input.strip_prefix('$') {
+            // If the input starts with "$", directly append to "$env:"
+            format!("$env:{}", var_str)
+        } else {
+            // If no prefix is found, return the input as is
+            input.into()
+        }
+    }
+
+    fn to_nushell_variable(input: &str) -> String {
+        let mut result = String::new();
+        let mut source = input;
+        let mut is_start = true;
+
+        loop {
+            match source.chars().next() {
+                None => return result,
+                Some('$') => {
+                    source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
+                    is_start = false;
+                }
+                Some(_) => {
+                    is_start = false;
+                    let chunk_end = source.find('$').unwrap_or(source.len());
+                    let (chunk, rest) = source.split_at(chunk_end);
+                    result.push_str(chunk);
+                    source = rest;
+                }
+            }
+        }
+    }
+
+    fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
+        if source.starts_with("env.") {
+            text.push('$');
+            return source;
+        }
+
+        match source.chars().next() {
+            Some('{') => {
+                let source = &source[1..];
+                if let Some(end) = source.find('}') {
+                    let var_name = &source[..end];
+                    if !var_name.is_empty() {
+                        if !is_start {
+                            text.push_str("(");
+                        }
+                        text.push_str("$env.");
+                        text.push_str(var_name);
+                        if !is_start {
+                            text.push_str(")");
+                        }
+                        &source[end + 1..]
+                    } else {
+                        text.push_str("${}");
+                        &source[end + 1..]
+                    }
+                } else {
+                    text.push_str("${");
+                    source
+                }
+            }
+            Some(c) if c.is_alphabetic() || c == '_' => {
+                let end = source
+                    .find(|c: char| !c.is_alphanumeric() && c != '_')
+                    .unwrap_or(source.len());
+                let var_name = &source[..end];
+                if !is_start {
+                    text.push_str("(");
+                }
+                text.push_str("$env.");
+                text.push_str(var_name);
+                if !is_start {
+                    text.push_str(")");
+                }
+                &source[end..]
+            }
+            _ => {
+                text.push('$');
+                source
+            }
+        }
+    }
+
+    pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
+        match self {
+            ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
+            ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
+            ShellKind::Posix
+            | ShellKind::Nushell
+            | ShellKind::Fish
+            | ShellKind::Csh
+            | ShellKind::Tcsh
+            | ShellKind::Rc => interactive
+                .then(|| "-i".to_owned())
+                .into_iter()
+                .chain(["-c".to_owned(), combined_command])
+                .collect(),
+        }
+    }
+
+    pub fn command_prefix(&self) -> Option<char> {
+        match self {
+            ShellKind::PowerShell => Some('&'),
+            ShellKind::Nushell => Some('^'),
+            _ => None,
+        }
+    }
+}

crates/util/src/shell_env.rs 🔗

@@ -1,60 +1,81 @@
-#![cfg_attr(not(unix), allow(unused))]
+use std::path::Path;
 
 use anyhow::{Context as _, Result};
 use collections::HashMap;
 
-/// Capture all environment variables from the login shell.
+use crate::shell::ShellKind;
+
+pub fn print_env() {
+    let env_vars: HashMap<String, String> = std::env::vars().collect();
+    let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
+        eprintln!("Error serializing environment variables: {}", err);
+        std::process::exit(1);
+    });
+    println!("{}", json);
+}
+
+/// Capture all environment variables from the login shell in the given directory.
+pub async fn capture(
+    shell_path: impl AsRef<Path>,
+    args: &[String],
+    directory: impl AsRef<Path>,
+) -> Result<collections::HashMap<String, String>> {
+    #[cfg(windows)]
+    return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
+    #[cfg(unix)]
+    return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
+}
+
 #[cfg(unix)]
-pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
+async fn capture_unix(
+    shell_path: &Path,
+    args: &[String],
+    directory: &Path,
+) -> Result<collections::HashMap<String, String>> {
     use std::os::unix::process::CommandExt;
     use std::process::Stdio;
 
     let zed_path = super::get_shell_safe_zed_path()?;
-    let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
-    let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
+    let shell_kind = ShellKind::new(shell_path);
 
     let mut command_string = String::new();
-    let mut command = std::process::Command::new(&shell_path);
+    let mut command = std::process::Command::new(shell_path);
+    command.args(args);
     // In some shells, file descriptors greater than 2 cannot be used in interactive mode,
     // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
     // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
     const FD_STDIN: std::os::fd::RawFd = 0;
     const FD_STDOUT: std::os::fd::RawFd = 1;
 
-    let (fd_num, redir) = match shell_name {
-        Some("rc") => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
-        Some("nu") | Some("tcsh") => (FD_STDOUT, "".to_string()),
+    let (fd_num, redir) = match shell_kind {
+        ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
+        ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
         _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
     };
     command.stdin(Stdio::null());
     command.stdout(Stdio::piped());
     command.stderr(Stdio::piped());
 
-    let mut command_prefix = String::new();
-    match shell_name {
-        Some("tcsh" | "csh") => {
+    match shell_kind {
+        ShellKind::Csh | ShellKind::Tcsh => {
             // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
             command.arg0("-");
         }
-        Some("fish") => {
+        ShellKind::Fish => {
             // in fish, asdf, direnv attach to the `fish_prompt` event
             command_string.push_str("emit fish_prompt;");
             command.arg("-l");
         }
-        Some("nu") => {
-            // nu needs special handling for -- options.
-            command_prefix = String::from("^");
-        }
         _ => {
             command.arg("-l");
         }
     }
     // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
     command_string.push_str(&format!("cd '{}';", directory.display()));
-    command_string.push_str(&format!(
-        "{}{} --printenv {}",
-        command_prefix, zed_path, redir
-    ));
+    if let Some(prefix) = shell_kind.command_prefix() {
+        command_string.push(prefix);
+    }
+    command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
     command.args(["-i", "-c", &command_string]);
 
     super::set_pre_exec_to_start_new_session(&mut command);
@@ -99,54 +120,104 @@ async fn spawn_and_read_fd(
     Ok((buffer, process.output().await?))
 }
 
-/// Capture all environment variables from the shell on Windows.
 #[cfg(windows)]
-pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
+async fn capture_windows(
+    shell_path: &Path,
+    _args: &[String],
+    directory: &Path,
+) -> Result<collections::HashMap<String, String>> {
     use std::process::Stdio;
 
     let zed_path =
         std::env::current_exe().context("Failed to determine current zed executable path.")?;
 
-    // Use PowerShell to get environment variables in the directory context
-    let output = crate::command::new_smol_command(crate::get_windows_system_shell())
-        .args([
-            "-NonInteractive",
-            "-NoProfile",
-            "-Command",
-            &format!(
-                "Set-Location '{}'; & '{}' --printenv",
-                directory.display(),
-                zed_path.display()
-            ),
-        ])
-        .stdin(Stdio::null())
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .output()
-        .await?;
-
-    anyhow::ensure!(
-        output.status.success(),
-        "PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
-        output.status,
-        String::from_utf8_lossy(&output.stdout),
-        String::from_utf8_lossy(&output.stderr),
-    );
+    let shell_kind = ShellKind::new(shell_path);
+    let env_output = match shell_kind {
+        ShellKind::Posix | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Rc | ShellKind::Fish => {
+            return Err(anyhow::anyhow!("unsupported shell kind"));
+        }
+        ShellKind::PowerShell => {
+            let output = crate::command::new_smol_command(shell_path)
+                .args([
+                    "-NonInteractive",
+                    "-NoProfile",
+                    "-Command",
+                    &format!(
+                        "Set-Location '{}'; & '{}' --printenv",
+                        directory.display(),
+                        zed_path.display()
+                    ),
+                ])
+                .stdin(Stdio::null())
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped())
+                .output()
+                .await?;
+
+            anyhow::ensure!(
+                output.status.success(),
+                "PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
+                output.status,
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr),
+            );
+            output
+        }
+        ShellKind::Nushell => {
+            let output = crate::command::new_smol_command(shell_path)
+                .args([
+                    "-c",
+                    &format!(
+                        "cd '{}'; {} --printenv",
+                        directory.display(),
+                        zed_path.display()
+                    ),
+                ])
+                .stdin(Stdio::null())
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped())
+                .output()
+                .await?;
+
+            anyhow::ensure!(
+                output.status.success(),
+                "Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
+                output.status,
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr),
+            );
+            output
+        }
+        ShellKind::Cmd => {
+            let output = crate::command::new_smol_command(shell_path)
+                .args([
+                    "/c",
+                    &format!(
+                        "cd '{}'; {} --printenv",
+                        directory.display(),
+                        zed_path.display()
+                    ),
+                ])
+                .stdin(Stdio::null())
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped())
+                .output()
+                .await?;
+
+            anyhow::ensure!(
+                output.status.success(),
+                "Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
+                output.status,
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr),
+            );
+            output
+        }
+    };
 
-    let env_output = String::from_utf8_lossy(&output.stdout);
+    let env_output = String::from_utf8_lossy(&env_output.stdout);
 
     // Parse the JSON output from zed --printenv
-    let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
-        .with_context(|| "Failed to deserialize environment variables from json")?;
-    Ok(env_map)
-}
-
-pub fn print_env() {
-    let env_vars: HashMap<String, String> = std::env::vars().collect();
-    let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
-        eprintln!("Error serializing environment variables: {}", err);
-        std::process::exit(1);
-    });
-    println!("{}", json);
-    std::process::exit(0);
+    serde_json::from_str(&env_output)
+        .with_context(|| "Failed to deserialize environment variables from json")
 }

crates/util/src/util.rs 🔗

@@ -8,6 +8,7 @@ pub mod redact;
 pub mod rel_path;
 pub mod schemars;
 pub mod serde;
+pub mod shell;
 pub mod shell_env;
 pub mod size;
 #[cfg(any(test, feature = "test-support"))]
@@ -367,7 +368,7 @@ pub async fn load_login_shell_environment() -> Result<()> {
     // into shell's `cd` command (and hooks) to manipulate env.
     // We do this so that we get the env a user would have when spawning a shell
     // in home directory.
-    for (name, value) in shell_env::capture(paths::home_dir()).await? {
+    for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()).await? {
         unsafe { env::set_var(&name, &value) };
     }
 
@@ -555,108 +556,6 @@ pub fn wrapped_usize_outward_from(
     })
 }
 
-#[cfg(target_os = "windows")]
-pub fn get_windows_system_shell() -> String {
-    use std::path::PathBuf;
-
-    fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
-        #[cfg(target_pointer_width = "64")]
-        let env_var = if find_alternate {
-            "ProgramFiles(x86)"
-        } else {
-            "ProgramFiles"
-        };
-
-        #[cfg(target_pointer_width = "32")]
-        let env_var = if find_alternate {
-            "ProgramW6432"
-        } else {
-            "ProgramFiles"
-        };
-
-        let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
-        install_base_dir
-            .read_dir()
-            .ok()?
-            .filter_map(Result::ok)
-            .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
-            .filter_map(|entry| {
-                let dir_name = entry.file_name();
-                let dir_name = dir_name.to_string_lossy();
-
-                let version = if find_preview {
-                    let dash_index = dir_name.find('-')?;
-                    if &dir_name[dash_index + 1..] != "preview" {
-                        return None;
-                    };
-                    dir_name[..dash_index].parse::<u32>().ok()?
-                } else {
-                    dir_name.parse::<u32>().ok()?
-                };
-
-                let exe_path = entry.path().join("pwsh.exe");
-                if exe_path.exists() {
-                    Some((version, exe_path))
-                } else {
-                    None
-                }
-            })
-            .max_by_key(|(version, _)| *version)
-            .map(|(_, path)| path)
-    }
-
-    fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
-        let msix_app_dir =
-            PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
-        if !msix_app_dir.exists() {
-            return None;
-        }
-
-        let prefix = if find_preview {
-            "Microsoft.PowerShellPreview_"
-        } else {
-            "Microsoft.PowerShell_"
-        };
-        msix_app_dir
-            .read_dir()
-            .ok()?
-            .filter_map(|entry| {
-                let entry = entry.ok()?;
-                if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
-                    return None;
-                }
-
-                if !entry.file_name().to_string_lossy().starts_with(prefix) {
-                    return None;
-                }
-
-                let exe_path = entry.path().join("pwsh.exe");
-                exe_path.exists().then_some(exe_path)
-            })
-            .next()
-    }
-
-    fn find_pwsh_in_scoop() -> Option<PathBuf> {
-        let pwsh_exe =
-            PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
-        pwsh_exe.exists().then_some(pwsh_exe)
-    }
-
-    static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
-        find_pwsh_in_programfiles(false, false)
-            .or_else(|| find_pwsh_in_programfiles(true, false))
-            .or_else(|| find_pwsh_in_msix(false))
-            .or_else(|| find_pwsh_in_programfiles(false, true))
-            .or_else(|| find_pwsh_in_msix(true))
-            .or_else(|| find_pwsh_in_programfiles(true, true))
-            .or_else(find_pwsh_in_scoop)
-            .map(|p| p.to_string_lossy().into_owned())
-            .unwrap_or("powershell.exe".to_string())
-    });
-
-    (*SYSTEM_SHELL).clone()
-}
-
 pub trait ResultExt<E> {
     type Ok;
 
@@ -1100,29 +999,7 @@ pub fn default<D: Default>() -> D {
     Default::default()
 }
 
-pub fn get_system_shell() -> String {
-    #[cfg(target_os = "windows")]
-    {
-        get_windows_system_shell()
-    }
-
-    #[cfg(not(target_os = "windows"))]
-    {
-        std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
-    }
-}
-
-pub fn get_default_system_shell() -> String {
-    #[cfg(target_os = "windows")]
-    {
-        get_windows_system_shell()
-    }
-
-    #[cfg(not(target_os = "windows"))]
-    {
-        "/bin/sh".to_string()
-    }
-}
+pub use self::shell::{get_default_system_shell, get_system_shell};
 
 #[derive(Debug)]
 pub enum ConnectionResult<O> {