Detailed changes
@@ -2421,6 +2421,7 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
+ let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
let terminal_task = cx.spawn({
@@ -2434,9 +2435,10 @@ impl AcpThread {
.and_then(|r| r.read(cx).default_system_shell())
})
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
- let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
- .redirect_stdin_to_dev_null()
- .build(Some(command.clone()), &args);
+ let (task_command, task_args) =
+ ShellBuilder::new(&Shell::Program(shell), is_windows)
+ .redirect_stdin_to_dev_null()
+ .build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
@@ -2790,7 +2792,7 @@ mod tests {
// Create a real PTY terminal that runs a command which prints output then sleeps
// We use printf instead of echo and chain with && sleep to ensure proper execution
let (completion_tx, _completion_rx) = smol::channel::unbounded();
- let (program, args) = ShellBuilder::new(&Shell::System).build(
+ let (program, args) = ShellBuilder::new(&Shell::System, false).build(
Some("printf 'output_before_kill\\n' && sleep 60".to_owned()),
&[],
);
@@ -227,7 +227,8 @@ pub async fn create_terminal_entity(
.map(Shell::Program)
})
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
- let (task_command, task_args) = task::ShellBuilder::new(&shell)
+ let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());
+ let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
@@ -665,9 +665,7 @@ impl ToolPermissionContext {
// Check if the user's shell supports POSIX-like command chaining.
// See the doc comment above for the full explanation of why this is needed.
let shell_supports_always_allow = if tool_name == TerminalTool::name() {
- ShellKind::system()
- .map(|k| k.supports_posix_chaining())
- .unwrap_or(false)
+ ShellKind::system().supports_posix_chaining()
} else {
true
};
@@ -30,22 +30,15 @@ impl ToolPermissionDecision {
///
/// # Shell Compatibility (Terminal Tool Only)
///
- /// For the terminal tool, commands are parsed using brush-parser to extract sub-commands
- /// for security. This ensures that patterns like `^cargo\b` cannot be bypassed with
- /// shell injection like `cargo && rm -rf /`.
+ /// For the terminal tool, commands are parsed to extract sub-commands for security.
+ /// This parsing only works for shells with POSIX-like `&&` / `||` / `;` / `|` syntax:
///
- /// The parser handles `;` (sequential execution), `|` (piping), `&&` and `||`
- /// (conditional execution), `$()` and backticks (command substitution), and process
- /// substitution.
+ /// **Compatible shells:** Posix (sh, bash, dash, zsh), Fish 3.0+, PowerShell 7+/Pwsh,
+ /// Cmd, Xonsh, Csh, Tcsh
///
- /// # Shell Notes
+ /// **Incompatible shells:** Nushell, Elvish, Rc (Plan 9)
///
- /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are
- /// boolean operators on values (e.g., `$true and $false`), not command chaining.
- /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish
- /// does not have `&&` or `||` operators. Its `and`/`or` are special commands that
- /// operate on values, not command chaining (e.g., `and $true $false`).
- /// - **Rc (Plan 9)**: Uses `;` for sequential execution. Does not have `&&`/`||`.
+ /// For incompatible shells, `always_allow` patterns are disabled for safety.
///
/// # Pattern Matching Tips
///
@@ -64,7 +57,7 @@ impl ToolPermissionDecision {
input: &str,
permissions: &ToolPermissions,
always_allow_tool_actions: bool,
- shell_kind: Option<ShellKind>,
+ shell_kind: ShellKind,
) -> ToolPermissionDecision {
// If always_allow_tool_actions is enabled, bypass all permission checks.
// This is intentionally placed first - it's a global override that the user
@@ -93,28 +86,22 @@ impl ToolPermissionDecision {
// If parsing fails or the shell syntax is unsupported, always_allow is
// disabled for this command (we set allow_enabled to false to signal this).
if tool_name == TerminalTool::name() {
- // Check if this shell's syntax can be parsed by brush-parser.
- // See the doc comment above for shell compatibility notes.
- let supports_chaining = shell_kind
- .map(|k| k.supports_posix_chaining())
- .unwrap_or(false);
- if !supports_chaining {
+ // Our shell parser (brush-parser) only supports POSIX-like shell syntax.
+ // See the doc comment above for the list of compatible/incompatible shells.
+ if !shell_kind.supports_posix_chaining() {
// For shells with incompatible syntax, we can't reliably parse
// the command to extract sub-commands.
if !rules.always_allow.is_empty() {
// If the user has configured always_allow patterns, we must deny
// because we can't safely verify the command doesn't contain
// hidden sub-commands that bypass the allow patterns.
- const SUFFIX: &str = " does not support \"always allow\" patterns for the \
- terminal tool because Zed cannot parse its command chaining syntax. \
- Please remove the always_allow patterns from your tool_permissions \
- settings, or switch to a supported shell.";
- let message = if let Some(name) = shell_kind.map(|k| k.name()) {
- format!("The {} shell{}", name, SUFFIX)
- } else {
- format!("This shell is unrecognized, and{}", SUFFIX)
- };
- return ToolPermissionDecision::Deny(message);
+ return ToolPermissionDecision::Deny(format!(
+ "The {} shell does not support \"always allow\" patterns for the terminal \
+ tool because Zed cannot parse its command chaining syntax. Please remove \
+ the always_allow patterns from your tool_permissions settings, or switch \
+ to a POSIX-conforming shell.",
+ shell_kind
+ ));
}
// No always_allow rules, so we can still check deny/confirm patterns.
return check_commands(std::iter::once(input.to_string()), rules, tool_name, false);
@@ -242,7 +229,6 @@ mod tests {
use crate::pattern_extraction::extract_terminal_pattern;
use agent_settings::{CompiledRegex, InvalidRegexPattern, ToolRules};
use std::sync::Arc;
- use util::shell::PosixShell;
fn pattern(command: &str) -> &'static str {
Box::leak(
@@ -260,7 +246,7 @@ mod tests {
deny: Vec<(&'static str, bool)>,
confirm: Vec<(&'static str, bool)>,
global: bool,
- shell: Option<ShellKind>,
+ shell: ShellKind,
}
impl PermTest {
@@ -273,7 +259,7 @@ mod tests {
deny: vec![],
confirm: vec![],
global: false,
- shell: Some(ShellKind::Posix(PosixShell::Sh)),
+ shell: ShellKind::Posix,
}
}
@@ -309,7 +295,7 @@ mod tests {
self.global = g;
self
}
- fn shell(mut self, s: Option<ShellKind>) -> Self {
+ fn shell(mut self, s: ShellKind) -> Self {
self.shell = s;
self
}
@@ -384,7 +370,7 @@ mod tests {
tools: collections::HashMap::default(),
},
global,
- Some(ShellKind::Posix(PosixShell::Sh)),
+ ShellKind::Posix,
)
}
@@ -619,34 +605,16 @@ mod tests {
let p = ToolPermissions { tools };
// With always_allow_tool_actions=true, even default_mode: Deny is overridden
assert_eq!(
- ToolPermissionDecision::from_input(
- "terminal",
- "x",
- &p,
- true,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "x", &p, true, ShellKind::Posix),
ToolPermissionDecision::Allow
);
// With always_allow_tool_actions=false, default_mode: Deny is respected
assert!(matches!(
- ToolPermissionDecision::from_input(
- "terminal",
- "x",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix),
ToolPermissionDecision::Deny(_)
));
assert_eq!(
- ToolPermissionDecision::from_input(
- "edit_file",
- "x",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("edit_file", "x", &p, false, ShellKind::Posix),
ToolPermissionDecision::Allow
);
}
@@ -667,13 +635,7 @@ mod tests {
let p = ToolPermissions { tools };
// "terminal" should not match "term" rules, so falls back to Confirm (no rules)
assert_eq!(
- ToolPermissionDecision::from_input(
- "terminal",
- "x",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix),
ToolPermissionDecision::Confirm
);
}
@@ -701,24 +663,12 @@ mod tests {
};
// With global=true, all checks are bypassed including invalid pattern check
assert!(matches!(
- ToolPermissionDecision::from_input(
- "terminal",
- "echo hi",
- &p,
- true,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "echo hi", &p, true, ShellKind::Posix),
ToolPermissionDecision::Allow
));
// With global=false, invalid patterns block the tool
assert!(matches!(
- ToolPermissionDecision::from_input(
- "terminal",
- "echo hi",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "echo hi", &p, false, ShellKind::Posix),
ToolPermissionDecision::Deny(_)
));
}
@@ -907,13 +857,7 @@ mod tests {
);
let p = ToolPermissions { tools };
assert!(matches!(
- ToolPermissionDecision::from_input(
- "terminal",
- "x",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh))
- ),
+ ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix),
ToolPermissionDecision::Deny(_)
));
assert_eq!(
@@ -922,7 +866,7 @@ mod tests {
"x",
&p,
false,
- Some(ShellKind::Posix(PosixShell::Sh))
+ ShellKind::Posix
),
ToolPermissionDecision::Allow
);
@@ -956,88 +900,38 @@ mod tests {
}
#[test]
- fn nushell_allows_with_allow_pattern() {
- // Nushell uses `;` for sequential execution, which brush-parser handles.
- // The `and`/`or` keywords are boolean operators, not command chaining.
- t("ls")
- .allow(&["^ls"])
- .shell(Some(ShellKind::Nushell))
- .is_allow();
+ fn nushell_denies_when_always_allow_configured() {
+ t("ls").allow(&["^ls"]).shell(ShellKind::Nushell).is_deny();
}
#[test]
- fn nushell_denies_with_deny_pattern() {
+ fn nushell_allows_deny_patterns() {
t("rm -rf /")
.deny(&["rm\\s+-rf"])
- .shell(Some(ShellKind::Nushell))
+ .shell(ShellKind::Nushell)
.is_deny();
}
#[test]
- fn nushell_confirms_with_confirm_pattern() {
+ fn nushell_allows_confirm_patterns() {
t("sudo reboot")
.confirm(&["sudo"])
- .shell(Some(ShellKind::Nushell))
+ .shell(ShellKind::Nushell)
.is_confirm();
}
#[test]
- fn nushell_falls_through_to_default() {
+ fn nushell_no_allow_patterns_uses_default() {
t("ls")
.deny(&["rm"])
.mode(ToolPermissionMode::Allow)
- .shell(Some(ShellKind::Nushell))
- .is_allow();
- }
-
- #[test]
- fn elvish_allows_with_allow_pattern() {
- // Elvish uses `;` to separate pipelines, which brush-parser handles.
- // The `and`/`or` special commands require parentheses around expressions.
- t("ls")
- .allow(&["^ls"])
- .shell(Some(ShellKind::Elvish))
- .is_allow();
- }
-
- #[test]
- fn rc_allows_with_allow_pattern() {
- // Rc (Plan 9) uses `;` for sequential execution, which brush-parser handles.
- // Rc does not have `&&`/`||` operators.
- t("ls")
- .allow(&["^ls"])
- .shell(Some(ShellKind::Rc))
+ .shell(ShellKind::Nushell)
.is_allow();
}
#[test]
- fn unknown_shell_denies_when_always_allow_configured() {
- // Unknown shells have unrecognized syntax, so we cannot safely parse them.
- // For security, always_allow patterns are disabled.
- t("ls").allow(&["^ls"]).shell(None).is_deny();
- }
-
- #[test]
- fn unknown_shell_allows_deny_patterns() {
- // Deny patterns still work for unknown shells since they're checked
- // against the raw input string.
- t("rm -rf /").deny(&["rm\\s+-rf"]).shell(None).is_deny();
- }
-
- #[test]
- fn unknown_shell_allows_confirm_patterns() {
- // Confirm patterns still work for unknown shells.
- t("sudo reboot").confirm(&["sudo"]).shell(None).is_confirm();
- }
-
- #[test]
- fn unknown_shell_falls_through_to_default() {
- // With no always_allow patterns, unknown shells use the default mode.
- t("ls")
- .deny(&["rm"])
- .mode(ToolPermissionMode::Allow)
- .shell(None)
- .is_allow();
+ fn elvish_denies_when_always_allow_configured() {
+ t("ls").allow(&["^ls"]).shell(ShellKind::Elvish).is_deny();
}
#[test]
@@ -1066,13 +960,8 @@ mod tests {
);
let p = ToolPermissions { tools };
- let result = ToolPermissionDecision::from_input(
- "terminal",
- "x",
- &p,
- false,
- Some(ShellKind::Posix(PosixShell::Sh)),
- );
+ let result =
+ ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix);
match result {
ToolPermissionDecision::Deny(msg) => {
assert!(msg.contains("2 regex patterns"), "Expected plural: {}", msg);
@@ -195,7 +195,7 @@ impl AcpConnection {
cx: &mut AsyncApp,
) -> Result<Self> {
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone());
- let builder = ShellBuilder::new(&shell).non_interactive();
+ let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
let mut child =
builder.build_std_command(Some(command.path.display().to_string()), &command.args);
child.envs(command.env.iter().flatten());
@@ -20,11 +20,7 @@ use futures::{
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
-use util::{
- ResultExt as _, debug_panic, maybe,
- paths::PathExt,
- shell::{PosixShell, ShellKind},
-};
+use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind};
/// Path to the program used for askpass
///
@@ -212,8 +208,7 @@ impl PasswordProxy {
let shell_kind = if cfg!(windows) {
ShellKind::PowerShell
} else {
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- ShellKind::Posix(PosixShell::Sh)
+ ShellKind::Posix
};
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
// Create an askpass script that communicates back to this process.
@@ -359,7 +354,7 @@ fn generate_askpass_script(
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
- .try_shell_safe(Some(&shell_kind))
+ .try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
let print_args = "printf '%s\\0' \"$@\"";
let shebang = "#!/bin/sh";
@@ -384,7 +379,7 @@ fn generate_askpass_script(
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
- .try_shell_safe(Some(&shell_kind))
+ .try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
Ok(format!(
r#"
@@ -32,7 +32,7 @@ impl StdioTransport {
cx: &AsyncApp,
) -> Result<Self> {
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone());
- let builder = ShellBuilder::new(&shell).non_interactive();
+ let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
let mut command =
builder.build_smol_command(Some(binary.executable.display().to_string()), &binary.args);
@@ -6,10 +6,7 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{path::PathBuf, sync::OnceLock};
use task::DebugRequest;
-use util::{
- ResultExt, maybe,
- shell::{PosixShell, ShellKind},
-};
+use util::{ResultExt, maybe, shell::ShellKind};
use crate::*;
@@ -70,10 +67,7 @@ impl JsDebugAdapter {
.get("type")
.filter(|value| value == &"node-terminal")?;
let command = configuration.get("command")?.as_str()?.to_owned();
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- let mut args = ShellKind::Posix(PosixShell::Sh)
- .split(&command)?
- .into_iter();
+ let mut args = ShellKind::Posix.split(&command)?.into_iter();
let program = args.next()?;
configuration.insert("runtimeExecutable".to_owned(), program.into());
configuration.insert(
@@ -28,11 +28,7 @@ use ui::{
prelude::*,
};
use ui_input::InputField;
-use util::{
- ResultExt, debug_panic,
- rel_path::RelPath,
- shell::{PosixShell, ShellKind},
-};
+use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{
@@ -874,8 +870,7 @@ impl ConfigureMode {
};
}
let command = self.program.read(cx).text(cx);
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- let mut args = ShellKind::Posix(PosixShell::Sh)
+ let mut args = ShellKind::Posix
.split(&command)
.into_iter()
.flatten()
@@ -1303,8 +1298,7 @@ impl PickerDelegate for DebugDelegate {
})
.unwrap_or_default();
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- let mut args = ShellKind::Posix(PosixShell::Sh)
+ let mut args = ShellKind::Posix
.split(&text)
.into_iter()
.flatten()
@@ -971,6 +971,7 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
+ let is_windows = project.read(cx).path_style(cx).is_windows();
let remote_shell = project
.read(cx)
.remote_client()
@@ -1088,7 +1089,7 @@ impl RunningState {
task.resolved.shell = Shell::Program(remote_shell);
}
- let builder = ShellBuilder::new(&task.resolved.shell);
+ let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);
@@ -117,7 +117,7 @@ pub trait ToolchainLister: Send + Sync + 'static {
fn activation_script(
&self,
toolchain: &Toolchain,
- shell: Option<ShellKind>,
+ shell: ShellKind,
cx: &App,
) -> BoxFuture<'static, Vec<String>>;
@@ -46,7 +46,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use task::{PosixShell, ShellKind, TaskTemplate, TaskTemplates, VariableName};
+use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, maybe};
#[derive(Debug, Serialize, Deserialize)]
@@ -1168,15 +1168,13 @@ fn wr_distance(
}
}
-fn micromamba_shell_name(kind: Option<ShellKind>) -> &'static str {
+fn micromamba_shell_name(kind: ShellKind) -> &'static str {
match kind {
- Some(ShellKind::Csh) => "csh",
- Some(ShellKind::Fish) => "fish",
- Some(ShellKind::Nushell) => "nu",
- Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => "powershell",
- Some(ShellKind::Cmd) => "cmd.exe",
- #[cfg(windows)]
- None => "powershell",
+ ShellKind::Csh => "csh",
+ ShellKind::Fish => "fish",
+ ShellKind::Nushell => "nu",
+ ShellKind::PowerShell => "powershell",
+ ShellKind::Cmd => "cmd.exe",
// default / catch-all:
_ => "posix",
}
@@ -1341,7 +1339,7 @@ impl ToolchainLister for PythonToolchainProvider {
fn activation_script(
&self,
toolchain: &Toolchain,
- shell: Option<ShellKind>,
+ shell: ShellKind,
cx: &App,
) -> BoxFuture<'static, Vec<String>> {
let settings = TerminalSettings::get_global(cx);
@@ -1408,21 +1406,10 @@ impl ToolchainLister for PythonToolchainProvider {
| PythonEnvironmentKind::Poetry,
) => {
if let Some(activation_scripts) = &toolchain.activation_scripts {
- // For unknown shells, fall back to platform-appropriate defaults:
- // POSIX (sh) on Unix, PowerShell on Windows
- #[cfg(unix)]
- let fallback_shell = ShellKind::Posix(PosixShell::Sh);
- #[cfg(windows)]
- let fallback_shell = ShellKind::PowerShell;
-
- let shell_kind = shell.unwrap_or(fallback_shell);
- // Use activation_script_key() to normalize POSIX shells (e.g., Bash, Zsh)
- // to Posix(Sh) for lookup, since activation scripts are stored with Posix(Sh) key
- let lookup_key = shell_kind.activation_script_key();
- if let Some(activate_script_path) = activation_scripts.get(&lookup_key) {
- let activate_keyword = shell_kind.activate_keyword();
+ if let Some(activate_script_path) = activation_scripts.get(&shell) {
+ let activate_keyword = shell.activate_keyword();
if let Some(quoted) =
- shell_kind.try_quote(&activate_script_path.to_string_lossy())
+ shell.try_quote(&activate_script_path.to_string_lossy())
{
activation_script.push(format!("{activate_keyword} {quoted}"));
}
@@ -1436,27 +1423,17 @@ impl ToolchainLister for PythonToolchainProvider {
let version = toolchain.environment.version.as_deref().unwrap_or("system");
let pyenv = &manager.executable;
let pyenv = pyenv.display();
- let pyenv_sh_activation = format!("\"{pyenv}\" shell - sh {version}");
activation_script.extend(match shell {
- Some(ShellKind::Fish) => {
- Some(format!("\"{pyenv}\" shell - fish {version}"))
- }
- Some(ShellKind::Posix(_)) => Some(pyenv_sh_activation),
- #[cfg(unix)]
- None => Some(pyenv_sh_activation),
- Some(ShellKind::Nushell) => {
- Some(format!("^\"{pyenv}\" shell - nu {version}"))
- }
- Some(ShellKind::PowerShell)
- | Some(ShellKind::Pwsh)
- | Some(ShellKind::Csh)
- | Some(ShellKind::Tcsh)
- | Some(ShellKind::Cmd)
- | Some(ShellKind::Rc)
- | Some(ShellKind::Xonsh)
- | Some(ShellKind::Elvish) => None,
- #[cfg(windows)]
- None => None,
+ ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
+ ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
+ ShellKind::Nushell => Some(format!("^\"{pyenv}\" shell - nu {version}")),
+ ShellKind::PowerShell | ShellKind::Pwsh => None,
+ ShellKind::Csh => None,
+ ShellKind::Tcsh => None,
+ ShellKind::Cmd => None,
+ ShellKind::Rc => None,
+ ShellKind::Xonsh => None,
+ ShellKind::Elvish => None,
})
}
_ => {}
@@ -1520,9 +1497,8 @@ async fn resolve_venv_activation_scripts(
) {
log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
if let Some(prefix) = &venv.prefix {
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
for (shell_kind, script_name) in &[
- (ShellKind::Posix(PosixShell::Sh), "activate"),
+ (ShellKind::Posix, "activate"),
(ShellKind::Rc, "activate"),
(ShellKind::Csh, "activate.csh"),
(ShellKind::Tcsh, "activate.csh"),
@@ -122,7 +122,7 @@ impl DapLocator for CargoLocator {
.cwd
.clone()
.context("Couldn't get cwd from debug config which is needed for locators")?;
- let builder = ShellBuilder::new(&build_config.shell).non_interactive();
+ let builder = ShellBuilder::new(&build_config.shell, cfg!(windows)).non_interactive();
let mut child = builder
.build_smol_command(
Some("cargo".into()),
@@ -657,7 +657,7 @@ impl LspCommand for GetLspRunnables {
);
task_template.args.extend(cargo.cargo_args);
if !cargo.executable_args.is_empty() {
- let shell_kind = task_template.shell.shell_kind();
+ let shell_kind = task_template.shell.shell_kind(cfg!(windows));
task_template.args.push("--".to_string());
task_template.args.extend(
cargo
@@ -683,8 +683,7 @@ impl LspCommand for GetLspRunnables {
// That bit is not auto-expanded when using single quotes.
// Escape extra cargo args unconditionally as those are unlikely to contain `~`.
.flat_map(|extra_arg| {
- util::shell::try_quote_option(shell_kind, &extra_arg)
- .map(|s| s.to_string())
+ shell_kind.try_quote(&extra_arg).map(|s| s.to_string())
}),
);
}
@@ -12,12 +12,11 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use task::{Shell, ShellBuilder, SpawnInTerminal};
+use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, insert_zed_terminal_env,
terminal_settings::TerminalSettings,
};
-use util::shell::ShellKind;
use util::{command::new_std_command, get_default_system_shell, maybe, rel_path::RelPath};
use crate::{Project, ProjectPath};
@@ -93,7 +92,7 @@ impl Project {
None => settings.shell.program(),
};
let path_style = self.path_style(cx);
- let shell_kind = ShellKind::new(&shell);
+ let shell_kind = ShellKind::new(&shell, path_style.is_windows());
// Prepare a task for resolving the environment
let env_task =
@@ -143,14 +142,12 @@ impl Project {
.update(cx, move |_, cx| {
let format_to_run = || {
if let Some(command) = &spawn_task.command {
- let command =
- util::shell::prepend_command_prefix_option(shell_kind, command);
- let command =
- util::shell::try_quote_prefix_aware_option(shell_kind, &command);
+ let command = shell_kind.prepend_command_prefix(command);
+ let command = shell_kind.try_quote_prefix_aware(&command);
let args = spawn_task
.args
.iter()
- .filter_map(|arg| util::shell::try_quote_option(shell_kind, arg));
+ .filter_map(|arg| shell_kind.try_quote(&arg));
command.into_iter().chain(args).join(" ")
} else {
@@ -164,17 +161,13 @@ impl Project {
match remote_client {
Some(remote_client) => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
- let separator =
- util::shell::sequential_commands_separator_option(
- shell_kind,
- );
+ let separator = shell_kind.sequential_commands_separator();
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let arg = format!("{activation_script}{separator} {to_run}");
- let args =
- util::shell::args_for_shell_option(shell_kind, false, arg);
+ let args = shell_kind.args_for_shell(false, arg);
let shell = remote_client
.read(cx)
.shell()
@@ -201,17 +194,13 @@ impl Project {
},
None => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
- let separator =
- util::shell::sequential_commands_separator_option(
- shell_kind,
- );
+ let separator = shell_kind.sequential_commands_separator();
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let arg = format!("{activation_script}{separator} {to_run}");
- let args =
- util::shell::args_for_shell_option(shell_kind, false, arg);
+ let args = shell_kind.args_for_shell(false, arg);
(
Shell::WithArguments {
@@ -369,7 +358,7 @@ impl Project {
let lang_registry = self.languages.clone();
cx.spawn(async move |project, cx| {
- let shell_kind = ShellKind::new(&shell);
+ let shell_kind = ShellKind::new(&shell, path_style.is_windows());
let mut env = env_task.await.unwrap_or_default();
env.extend(settings.env);
@@ -524,7 +513,8 @@ impl Project {
.and_then(|remote_client| remote_client.read(cx).shell())
.map(Shell::Program)
.unwrap_or_else(|| settings.shell.clone());
- let builder = ShellBuilder::new(&shell).non_interactive();
+ let is_windows = self.path_style(cx).is_windows();
+ let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
let (command, args) = builder.build(Some(command), &Vec::new());
let env_task = self.resolve_directory_environment(
@@ -11266,7 +11266,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
fn activation_script(
&self,
_: &Toolchain,
- _: Option<ShellKind>,
+ _: ShellKind,
_: &gpui::App,
) -> futures::future::BoxFuture<'static, Vec<String>> {
Box::pin(async { vec![] })
@@ -57,12 +57,8 @@ impl ProjectContext {
user_rules: default_user_rules,
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
- shell: ShellKind::new_with_fallback(
- &get_default_system_shell_preferring_bash(),
- cfg!(windows),
- )
- .name()
- .to_string(),
+ shell: ShellKind::new(&get_default_system_shell_preferring_bash(), cfg!(windows))
+ .to_string(),
}
}
}
@@ -13,7 +13,7 @@ use std::{
sync::Arc,
};
use util::ResultExt;
-use util::shell::{PosixShell, ShellKind};
+use util::shell::ShellKind;
use util::{
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
@@ -301,8 +301,7 @@ impl DockerExecConnection {
delegate.set_status(Some("Extracting remote development server"), cx);
let server_mode = 0o755;
- // TODO: Consider using the remote's actual shell instead of hardcoding "sh"
- let shell_kind = ShellKind::Posix(PosixShell::Sh);
+ let shell_kind = ShellKind::Posix;
let orig_tmp_path = tmp_path.display(self.path_style());
let server_mode = format!("{:o}", server_mode);
let server_mode = shell_kind
@@ -32,7 +32,7 @@ use tempfile::TempDir;
use util::{
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
- shell::{PosixShell, ShellKind},
+ shell::ShellKind,
};
pub(crate) struct SshRemoteConnection {
@@ -591,7 +591,7 @@ impl SshRemoteConnection {
let ssh_shell = socket.shell(is_windows).await;
log::info!("Remote shell discovered: {}", ssh_shell);
- let ssh_shell_kind = ShellKind::new_with_fallback(&ssh_shell, is_windows);
+ let ssh_shell_kind = ShellKind::new(&ssh_shell, is_windows);
let ssh_platform = socket.platform(ssh_shell_kind, is_windows).await?;
log::info!("Remote platform discovered: {:?}", ssh_platform);
@@ -918,7 +918,7 @@ impl SshRemoteConnection {
dst_path: &RelPath,
tmp_path: &RelPath,
) -> Result<()> {
- let shell_kind = self.ssh_shell_kind;
+ let shell_kind = ShellKind::Posix;
let server_mode = 0o755;
let orig_tmp_path = tmp_path.display(self.path_style());
let server_mode = format!("{:o}", server_mode);
@@ -1138,6 +1138,7 @@ impl SshSocket {
.expect("shell quoting")
.into_owned();
for arg in args {
+ // We're trying to work with: sh, bash, zsh, fish, tcsh, ...?
debug_assert!(
!arg.as_ref().contains('\n'),
"multiline arguments do not work in all shells"
@@ -1290,14 +1291,8 @@ impl SshSocket {
async fn shell_posix(&self) -> String {
const DEFAULT_SHELL: &str = "sh";
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
match self
- .run_command(
- ShellKind::Posix(PosixShell::Sh),
- "sh",
- &["-c", "echo $SHELL"],
- false,
- )
+ .run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false)
.await
{
Ok(output) => parse_shell(&output, DEFAULT_SHELL),
@@ -1391,8 +1386,7 @@ impl SshConnectionOptions {
"-w",
];
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- let mut tokens = ShellKind::Posix(PosixShell::Sh)
+ let mut tokens = ShellKind::Posix
.split(input)
.context("invalid input")?
.into_iter();
@@ -1630,7 +1624,7 @@ fn build_command_posix(
.context("shell quoting")?
)?;
for arg in input_args {
- let arg = ssh_shell_kind.try_quote(arg).context("shell quoting")?;
+ let arg = ssh_shell_kind.try_quote(&arg).context("shell quoting")?;
write!(exec, " {}", &arg)?;
}
} else {
@@ -1672,7 +1666,7 @@ fn build_command_windows(
ssh_env: HashMap<String, String>,
ssh_path_style: PathStyle,
ssh_shell: &str,
- ssh_shell_kind: ShellKind,
+ _ssh_shell_kind: ShellKind,
ssh_args: Vec<String>,
interactive: Interactive,
) -> Result<CommandTemplate> {
@@ -1691,7 +1685,7 @@ fn build_command_windows(
shell_kind
.try_quote(&working_dir)
.context("shell quoting")?,
- ssh_shell_kind.sequential_and_commands_separator()
+ shell_kind.sequential_and_commands_separator()
)?;
}
@@ -1778,7 +1772,7 @@ mod tests {
env.clone(),
PathStyle::Posix,
"/bin/bash",
- ShellKind::Posix(PosixShell::Bash),
+ ShellKind::Posix,
vec!["-o".to_string(), "ControlMaster=auto".to_string()],
Interactive::No,
)?;
@@ -23,7 +23,7 @@ use std::{
use util::{
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
- shell::{PosixShell, Shell, ShellKind},
+ shell::{Shell, ShellKind},
shell_builder::ShellBuilder,
};
@@ -75,8 +75,7 @@ impl WslRemoteConnection {
arch: RemoteArch::X86_64,
},
shell: String::new(),
- // TODO: Consider using the user's actual shell instead of hardcoding "sh"
- shell_kind: ShellKind::Posix(PosixShell::Sh),
+ shell_kind: ShellKind::Posix,
default_system_shell: String::from("/bin/sh"),
has_wsl_interop: false,
};
@@ -86,8 +85,7 @@ impl WslRemoteConnection {
.await
.context("failed detecting shell")?;
log::info!("Remote shell discovered: {}", this.shell);
- // WSL is always Linux, so is_windows = false
- this.shell_kind = ShellKind::new_with_fallback(&this.shell, false);
+ this.shell_kind = ShellKind::new(&this.shell, false);
this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default();
log::info!(
"Remote has wsl interop {}",
@@ -470,7 +468,7 @@ impl RemoteConnection for WslRemoteConnection {
write!(&mut exec, "{} -l", self.shell)?;
}
let (command, args) =
- ShellBuilder::new(&Shell::Program(self.shell.clone())).build(Some(exec), &[]);
+ ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]);
let mut wsl_args = if let Some(user) = &self.connection_options.user {
vec![
@@ -25,7 +25,7 @@ pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_variables_in_map, substitute_variables_in_str,
};
-pub use util::shell::{PosixShell, Shell, ShellKind};
+pub use util::shell::{Shell, ShellKind};
pub use util::shell_builder::ShellBuilder;
pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile;
@@ -516,7 +516,7 @@ impl TerminalBuilder {
// the compilation target. This is fine right now due to the restricted
// way we use the return value, but would become incorrect if we
// supported remoting into windows.
- let shell_kind = shell.shell_kind();
+ let shell_kind = shell.shell_kind(cfg!(windows));
let pty_options = {
let alac_shell = shell_params.as_ref().map(|params| {
@@ -532,7 +532,7 @@ impl TerminalBuilder {
drain_on_exit: true,
env: env.clone().into_iter().collect(),
#[cfg(windows)]
- escape_args: shell_kind.map(|k| k.tty_escape_args()).unwrap_or(true),
+ escape_args: shell_kind.tty_escape_args(),
}
};
@@ -664,10 +664,7 @@ impl TerminalBuilder {
// and while we have sent the activation script to the pty, it will be executed asynchronously.
// Therefore, we somehow need to wait for the activation script to finish executing before we
// can proceed with clearing the screen.
- let clear_cmd = shell_kind
- .map(|k| k.clear_screen_command())
- .unwrap_or("clear");
- terminal.write_to_pty(clear_cmd.as_bytes());
+ terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
// Simulate enter key press
terminal.write_to_pty(b"\x0d");
}
@@ -2562,7 +2559,7 @@ mod tests {
let (completion_tx, completion_rx) = smol::channel::unbounded();
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let (program, args) =
- ShellBuilder::new(&Shell::System).build(Some(command.to_owned()), &args);
+ ShellBuilder::new(&Shell::System, false).build(Some(command.to_owned()), &args);
let builder = cx
.update(|cx| {
TerminalBuilder::new(
@@ -2783,7 +2780,7 @@ mod tests {
cx.executor().allow_parking();
let (completion_tx, completion_rx) = smol::channel::unbounded();
- let (program, args) = ShellBuilder::new(&Shell::System)
+ let (program, args) = ShellBuilder::new(&Shell::System, false)
.build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]);
let builder = cx
.update(|cx| {
@@ -562,6 +562,7 @@ impl TerminalPanel {
}
let remote_client = project.remote_client();
+ let is_windows = project.path_style(cx).is_windows();
let remote_shell = remote_client
.as_ref()
.and_then(|remote_client| remote_client.read(cx).shell());
@@ -574,7 +575,7 @@ impl TerminalPanel {
task.shell.clone()
};
- let task = prepare_task_for_spawn(task, &shell);
+ let task = prepare_task_for_spawn(task, &shell, is_windows);
if task.allow_concurrent_runs && task.use_new_terminal {
return self.spawn_in_new_terminal(task, window, cx);
@@ -1161,8 +1162,12 @@ impl TerminalPanel {
/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
/// based on the shell configuration. This is a pure function that can be tested
/// without spawning actual terminals.
-pub fn prepare_task_for_spawn(task: &SpawnInTerminal, shell: &Shell) -> SpawnInTerminal {
- let builder = ShellBuilder::new(shell);
+pub fn prepare_task_for_spawn(
+ task: &SpawnInTerminal,
+ shell: &Shell,
+ is_windows: bool,
+) -> SpawnInTerminal {
+ let builder = ShellBuilder::new(shell, is_windows);
let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
@@ -1854,7 +1859,7 @@ mod tests {
let input = SpawnInTerminal::default();
let shell = Shell::System;
- let result = prepare_task_for_spawn(&input, &shell);
+ let result = prepare_task_for_spawn(&input, &shell, false);
let expected_shell = util::get_system_shell();
assert_eq!(result.env, HashMap::default());
@@ -1926,7 +1931,7 @@ mod tests {
};
let shell = Shell::System;
- let result = prepare_task_for_spawn(&input, &shell);
+ let result = prepare_task_for_spawn(&input, &shell, false);
let system_shell = util::get_system_shell();
assert_eq!(result.env, HashMap::default());
@@ -86,7 +86,7 @@ pub trait PathExt {
fn multiple_extensions(&self) -> Option<String>;
/// Try to make a shell-safe representation of the path.
- fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String>;
+ fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String>;
}
impl<T: AsRef<Path>> PathExt for T {
@@ -164,19 +164,13 @@ impl<T: AsRef<Path>> PathExt for T {
Some(parts.into_iter().join("."))
}
- fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String> {
+ fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String> {
let path_str = self
.as_ref()
.to_str()
.with_context(|| "Path contains invalid UTF-8")?;
- let quoted = match shell_kind {
- Some(kind) => kind.try_quote(path_str),
- #[cfg(windows)]
- None => Some(ShellKind::quote_powershell(path_str)),
- #[cfg(unix)]
- None => shlex::try_quote(path_str).ok(),
- };
- quoted
+ shell_kind
+ .try_quote(path_str)
.as_deref()
.map(ToOwned::to_owned)
.context("Failed to quote path")
@@ -1,6 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::{borrow::Cow, ffi::OsStr, path::Path, sync::LazyLock};
+use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
/// Shell configuration to open the terminal with.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
@@ -39,45 +39,19 @@ impl Shell {
}
}
- pub fn shell_kind(&self) -> Option<ShellKind> {
+ pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
match self {
- Shell::Program(program) => ShellKind::new(program),
- Shell::WithArguments { program, .. } => ShellKind::new(program),
+ Shell::Program(program) => ShellKind::new(program, is_windows),
+ Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
Shell::System => ShellKind::system(),
}
}
}
-/// Specific POSIX-compatible shell variants.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub enum PosixShell {
- #[default]
- Sh,
- Bash,
- Zsh,
- Dash,
- Ksh,
- Mksh,
- Ash,
-}
-
-impl PosixShell {
- pub fn name(&self) -> &'static str {
- match self {
- PosixShell::Sh => "sh",
- PosixShell::Bash => "bash",
- PosixShell::Zsh => "zsh",
- PosixShell::Dash => "dash",
- PosixShell::Ksh => "ksh",
- PosixShell::Mksh => "mksh",
- PosixShell::Ash => "ash",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ShellKind {
- Posix(PosixShell),
+ #[default]
+ Posix,
Csh,
Tcsh,
Rc,
@@ -259,127 +233,84 @@ pub fn get_windows_system_shell() -> String {
(*SYSTEM_SHELL).clone()
}
-impl ShellKind {
- /// Returns the canonical shell kind for activation script lookups.
- ///
- /// This normalizes all POSIX shell variants to `Posix(Sh)` so that
- /// activation scripts stored with a single POSIX key can be found
- /// regardless of which specific POSIX shell the user has.
- pub fn activation_script_key(&self) -> ShellKind {
- match self {
- ShellKind::Posix(_) => ShellKind::Posix(PosixShell::Sh),
- other => *other,
- }
- }
-
- /// Creates a ShellKind from a program path, with a platform-aware fallback for unknown shells.
- ///
- /// Unlike `new()` which returns `None` for unrecognized shells, this method
- /// falls back to `PowerShell` when `is_windows` is true, or `Posix(Sh)` otherwise.
- /// This is useful for remote connections where we know the target platform but
- /// may not recognize the specific shell.
- pub fn new_with_fallback(program: impl AsRef<Path>, is_windows: bool) -> Self {
- Self::new(program).unwrap_or_else(|| {
- if is_windows {
- ShellKind::PowerShell
- } else {
- ShellKind::Posix(PosixShell::Sh)
- }
- })
- }
-
- /// Returns the name of the shell.
- pub fn name(&self) -> &'static str {
+impl fmt::Display for ShellKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- ShellKind::Posix(posix) => posix.name(),
- ShellKind::Csh => "csh",
- ShellKind::Tcsh => "tcsh",
- ShellKind::Fish => "fish",
- ShellKind::PowerShell => "powershell",
- ShellKind::Pwsh => "pwsh",
- ShellKind::Nushell => "nu",
- ShellKind::Cmd => "cmd",
- ShellKind::Rc => "rc",
- ShellKind::Xonsh => "xonsh",
- ShellKind::Elvish => "elvish",
+ 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::Pwsh => write!(f, "pwsh"),
+ ShellKind::Nushell => write!(f, "nu"),
+ ShellKind::Cmd => write!(f, "cmd"),
+ ShellKind::Rc => write!(f, "rc"),
+ ShellKind::Xonsh => write!(f, "xonsh"),
+ ShellKind::Elvish => write!(f, "elvish"),
}
}
+}
- pub fn system() -> Option<Self> {
- Self::new(&get_system_shell())
+impl ShellKind {
+ pub fn system() -> Self {
+ Self::new(&get_system_shell(), cfg!(windows))
}
- /// Returns whether this shell's command chaining syntax can be parsed by brush-parser.
+ /// Returns whether this shell uses POSIX-like command chaining syntax (`&&`, `||`, `;`, `|`).
///
/// This is used to determine if we can safely parse shell commands to extract sub-commands
/// for security purposes (e.g., preventing shell injection in "always allow" patterns).
///
- /// The brush-parser handles `;` (sequential execution) and `|` (piping), which are
- /// supported by all common shells. It also handles `&&` and `||` for conditional
- /// execution, `$()` and backticks for command substitution, and process substitution.
- ///
- /// # Security Note
- ///
- /// Only explicitly recognized shells return `true`. Unknown shells (None) return `false` to
- /// prevent security bypasses - if we don't know a shell's syntax, we can't safely
- /// parse it for `always_allow` patterns.
- ///
- /// # Shell Notes
+ /// **Compatible shells:** Posix (sh, bash, dash, zsh), Fish 3.0+, PowerShell 7+/Pwsh,
+ /// Cmd, Xonsh, Csh, Tcsh
///
- /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are boolean
- /// operators on values (e.g., `$true and $false`), not command chaining operators.
- /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish does
- /// not have `&&` or `||` operators. Its `and`/`or` are special commands that operate
- /// on values, not command chaining (e.g., `and $true $false`).
- /// - **Rc (Plan 9)**: Uses `;` for sequential execution and `|` for piping. Does not
- /// have `&&`/`||` operators for conditional chaining.
+ /// **Incompatible shells:** Nushell (uses `and`/`or` keywords), Elvish (uses `and`/`or`
+ /// keywords), Rc (Plan 9 shell - no `&&`/`||` operators)
pub fn supports_posix_chaining(&self) -> bool {
- match self {
- ShellKind::Posix(_)
- | ShellKind::Fish
- | ShellKind::PowerShell
- | ShellKind::Pwsh
- | ShellKind::Cmd
- | ShellKind::Xonsh
- | ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Nushell
- | ShellKind::Elvish
- | ShellKind::Rc => true,
- }
- }
-
- pub fn new(program: impl AsRef<Path>) -> Option<Self> {
- match program.as_ref().file_stem().and_then(OsStr::to_str)? {
- "powershell" => Some(ShellKind::PowerShell),
- "pwsh" => Some(ShellKind::Pwsh),
- "cmd" => Some(ShellKind::Cmd),
- "nu" => Some(ShellKind::Nushell),
- "fish" => Some(ShellKind::Fish),
- "csh" => Some(ShellKind::Csh),
- "tcsh" => Some(ShellKind::Tcsh),
- "rc" => Some(ShellKind::Rc),
- "xonsh" => Some(ShellKind::Xonsh),
- "elvish" => Some(ShellKind::Elvish),
- "sh" => Some(ShellKind::Posix(PosixShell::Sh)),
- "bash" => Some(ShellKind::Posix(PosixShell::Bash)),
- "zsh" => Some(ShellKind::Posix(PosixShell::Zsh)),
- "dash" => Some(ShellKind::Posix(PosixShell::Dash)),
- "ksh" => Some(ShellKind::Posix(PosixShell::Ksh)),
- "mksh" => Some(ShellKind::Posix(PosixShell::Mksh)),
- "ash" => Some(ShellKind::Posix(PosixShell::Ash)),
- // Unrecognized shell - we cannot safely parse its syntax for
- // `always_allow` patterns, so they will be disabled.
- // Fall back to platform-specific behavior for non-security purposes.
- _ => None,
- }
- }
-
- pub fn to_shell_variable(&self, input: &str) -> String {
+ matches!(
+ self,
+ ShellKind::Posix
+ | ShellKind::Fish
+ | ShellKind::PowerShell
+ | ShellKind::Pwsh
+ | ShellKind::Cmd
+ | ShellKind::Xonsh
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ )
+ }
+
+ pub fn new(program: impl AsRef<Path>, is_windows: bool) -> Self {
+ let program = program.as_ref();
+ let program = program
+ .file_stem()
+ .unwrap_or_else(|| program.as_os_str())
+ .to_string_lossy();
+
+ match &*program {
+ "powershell" => ShellKind::PowerShell,
+ "pwsh" => ShellKind::Pwsh,
+ "cmd" => ShellKind::Cmd,
+ "nu" => ShellKind::Nushell,
+ "fish" => ShellKind::Fish,
+ "csh" => ShellKind::Csh,
+ "tcsh" => ShellKind::Tcsh,
+ "rc" => ShellKind::Rc,
+ "xonsh" => ShellKind::Xonsh,
+ "elvish" => ShellKind::Elvish,
+ "sh" | "bash" | "zsh" => ShellKind::Posix,
+ _ if is_windows => ShellKind::PowerShell,
+ // 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::Pwsh => Self::to_powershell_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::Tcsh => input.to_owned(),
@@ -409,7 +340,7 @@ impl ShellKind {
}
}
- pub fn to_powershell_variable(input: &str) -> String {
+ 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 "}"
@@ -505,15 +436,13 @@ impl ShellKind {
pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
match self {
- ShellKind::PowerShell | ShellKind::Pwsh => {
- vec!["-C".to_owned(), combined_command]
- }
+ ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command],
ShellKind::Cmd => vec![
"/S".to_owned(),
"/C".to_owned(),
format!("\"{combined_command}\""),
],
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Fish
| ShellKind::Csh
@@ -532,7 +461,7 @@ impl ShellKind {
match self {
ShellKind::PowerShell | ShellKind::Pwsh => Some('&'),
ShellKind::Nushell => Some('^'),
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -555,7 +484,7 @@ impl ShellKind {
pub const fn sequential_commands_separator(&self) -> char {
match self {
ShellKind::Cmd => '&',
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -571,7 +500,7 @@ impl ShellKind {
pub const fn sequential_and_commands_separator(&self) -> &'static str {
match self {
ShellKind::Cmd
- | ShellKind::Posix(_)
+ | ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -587,7 +516,7 @@ impl ShellKind {
ShellKind::PowerShell => Some(Self::quote_powershell(arg)),
ShellKind::Pwsh => Some(Self::quote_pwsh(arg)),
ShellKind::Cmd => Some(Self::quote_cmd(arg)),
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -598,7 +527,7 @@ impl ShellKind {
}
}
- pub fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
+ fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
if arg.is_empty() {
return Cow::Borrowed("\"\"");
}
@@ -810,7 +739,7 @@ impl ShellKind {
ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
- | ShellKind::Posix(_)
+ | ShellKind::Posix
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => "source",
@@ -820,7 +749,7 @@ impl ShellKind {
pub const fn clear_screen_command(&self) -> &'static str {
match self {
ShellKind::Cmd => "cls",
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -839,7 +768,7 @@ impl ShellKind {
pub const fn tty_escape_args(&self) -> bool {
match self {
ShellKind::Cmd => false,
- ShellKind::Posix(_)
+ ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -853,109 +782,6 @@ impl ShellKind {
}
}
-// Helper functions for `Option<ShellKind>` that provide platform-specific defaults for unknown shells.
-// These should be used when you have an `Option<ShellKind>` and need to handle the `None` case
-// with appropriate fallback behavior (POSIX-like on Unix, PowerShell-like on Windows).
-
-/// Quote an argument for the shell, with platform-specific fallback for unknown shells.
-pub fn try_quote_option(shell_kind: Option<ShellKind>, arg: &str) -> Option<Cow<'_, str>> {
- match shell_kind {
- Some(kind) => kind.try_quote(arg),
- #[cfg(windows)]
- None => Some(ShellKind::quote_powershell(arg)),
- #[cfg(unix)]
- None => shlex::try_quote(arg).ok(),
- }
-}
-
-/// Quote an argument for the shell (prefix-aware), with platform-specific fallback for unknown shells.
-pub fn try_quote_prefix_aware_option(
- shell_kind: Option<ShellKind>,
- arg: &str,
-) -> Option<Cow<'_, str>> {
- match shell_kind {
- Some(kind) => kind.try_quote_prefix_aware(arg),
- #[cfg(windows)]
- None => Some(ShellKind::quote_powershell(arg)),
- #[cfg(unix)]
- None => shlex::try_quote(arg).ok(),
- }
-}
-
-/// Get the command prefix for the shell, with platform-specific fallback for unknown shells.
-pub fn command_prefix_option(shell_kind: Option<ShellKind>) -> Option<char> {
- match shell_kind {
- Some(kind) => kind.command_prefix(),
- #[cfg(windows)]
- None => Some('&'),
- #[cfg(unix)]
- None => None,
- }
-}
-
-/// Prepend the command prefix if needed, with platform-specific fallback for unknown shells.
-pub fn prepend_command_prefix_option<'a>(
- shell_kind: Option<ShellKind>,
- command: &'a str,
-) -> Cow<'a, str> {
- match command_prefix_option(shell_kind) {
- Some(prefix) if !command.starts_with(prefix) => Cow::Owned(format!("{prefix}{command}")),
- _ => Cow::Borrowed(command),
- }
-}
-
-/// Get the sequential commands separator, with platform-specific fallback for unknown shells.
-pub fn sequential_commands_separator_option(shell_kind: Option<ShellKind>) -> char {
- match shell_kind {
- Some(kind) => kind.sequential_commands_separator(),
- #[cfg(windows)]
- None => ';',
- #[cfg(unix)]
- None => ';',
- }
-}
-
-/// Get the sequential-and commands separator, with platform-specific fallback for unknown shells.
-pub fn sequential_and_commands_separator_option(shell_kind: Option<ShellKind>) -> &'static str {
- match shell_kind {
- Some(kind) => kind.sequential_and_commands_separator(),
- #[cfg(windows)]
- None => ";",
- #[cfg(unix)]
- None => ";",
- }
-}
-
-/// Get shell arguments for running a command, with platform-specific fallback for unknown shells.
-pub fn args_for_shell_option(
- shell_kind: Option<ShellKind>,
- interactive: bool,
- combined_command: String,
-) -> Vec<String> {
- match shell_kind {
- Some(kind) => kind.args_for_shell(interactive, combined_command),
- #[cfg(windows)]
- None => vec!["-C".to_owned(), combined_command],
- #[cfg(unix)]
- None => interactive
- .then(|| "-i".to_owned())
- .into_iter()
- .chain(["-c".to_owned(), combined_command])
- .collect(),
- }
-}
-
-/// Convert a variable to shell syntax, with platform-specific fallback for unknown shells.
-pub fn to_shell_variable_option(shell_kind: Option<ShellKind>, input: &str) -> String {
- match shell_kind {
- Some(kind) => kind.to_shell_variable(input),
- #[cfg(windows)]
- None => ShellKind::to_powershell_variable(input),
- #[cfg(unix)]
- None => input.to_owned(),
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1,15 +1,7 @@
use std::borrow::Cow;
use crate::shell::get_system_shell;
-use crate::shell::{
- Shell, ShellKind, args_for_shell_option, to_shell_variable_option, try_quote_option,
- try_quote_prefix_aware_option,
-};
-
-const POSIX_STDIN_REDIRECT_PREFIX: char = '(';
-const POSIX_STDIN_REDIRECT_SUFFIX: &str = ") </dev/null";
-const POWERSHELL_STDIN_REDIRECT_PREFIX: &str = "$null | & {";
-const POWERSHELL_STDIN_REDIRECT_SUFFIX: &str = "}";
+use crate::shell::{Shell, ShellKind};
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
@@ -20,19 +12,19 @@ pub struct ShellBuilder {
interactive: bool,
/// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
redirect_stdin: bool,
- kind: Option<ShellKind>,
+ kind: ShellKind,
}
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
- pub fn new(shell: &Shell) -> Self {
+ pub fn new(shell: &Shell, is_windows: bool) -> Self {
let (program, args) = match shell {
Shell::System => (get_system_shell(), Vec::new()),
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
- let kind = ShellKind::new(&program);
+ let kind = ShellKind::new(&program, is_windows);
Self {
program,
args,
@@ -52,34 +44,20 @@ impl ShellBuilder {
self.program.clone()
} else {
match self.kind {
- Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => {
- format!("{} -C '{}'", self.program, command_to_use_in_label)
- }
- #[cfg(windows)]
- None => {
+ ShellKind::PowerShell | ShellKind::Pwsh => {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
- Some(ShellKind::Cmd) => {
+ ShellKind::Cmd => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
}
- Some(
- ShellKind::Posix(_)
- | ShellKind::Nushell
- | ShellKind::Fish
- | ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Rc
- | ShellKind::Xonsh
- | ShellKind::Elvish,
- ) => {
- let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
- format!(
- "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
- PROGRAM = self.program
- )
- }
- #[cfg(unix)]
- None => {
+ ShellKind::Posix
+ | ShellKind::Nushell
+ | ShellKind::Fish
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc
+ | ShellKind::Xonsh
+ | ShellKind::Elvish => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -95,44 +73,6 @@ impl ShellBuilder {
self
}
- fn apply_stdin_redirect(&self, combined_command: &mut String) {
- match self.kind {
- Some(ShellKind::Fish) => {
- combined_command.insert_str(0, "begin; ");
- combined_command.push_str("; end </dev/null");
- }
- Some(
- ShellKind::Posix(_)
- | ShellKind::Nushell
- | ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Rc
- | ShellKind::Xonsh
- | ShellKind::Elvish,
- ) => {
- combined_command.insert(0, POSIX_STDIN_REDIRECT_PREFIX);
- combined_command.push_str(POSIX_STDIN_REDIRECT_SUFFIX);
- }
- #[cfg(unix)]
- None => {
- combined_command.insert(0, POSIX_STDIN_REDIRECT_PREFIX);
- combined_command.push_str(POSIX_STDIN_REDIRECT_SUFFIX);
- }
- Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => {
- combined_command.insert_str(0, POWERSHELL_STDIN_REDIRECT_PREFIX);
- combined_command.push_str(POWERSHELL_STDIN_REDIRECT_SUFFIX);
- }
- #[cfg(windows)]
- None => {
- combined_command.insert_str(0, POWERSHELL_STDIN_REDIRECT_PREFIX);
- combined_command.push_str(POWERSHELL_STDIN_REDIRECT_SUFFIX);
- }
- Some(ShellKind::Cmd) => {
- combined_command.push_str("< NUL");
- }
- }
- }
-
/// Returns the program and arguments to run this task in a shell.
pub fn build(
mut self,
@@ -141,7 +81,7 @@ impl ShellBuilder {
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let task_command = if !task_args.is_empty() {
- match try_quote_prefix_aware_option(self.kind, &task_command) {
+ match self.kind.try_quote_prefix_aware(&task_command) {
Some(task_command) => task_command.into_owned(),
None => task_command,
}
@@ -150,22 +90,41 @@ impl ShellBuilder {
};
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
- let shell_variable = to_shell_variable_option(self.kind, arg);
- command.push_str(&match try_quote_option(self.kind, &shell_variable) {
+ let shell_variable = self.kind.to_shell_variable(arg);
+ command.push_str(&match self.kind.try_quote(&shell_variable) {
Some(shell_variable) => shell_variable,
None => Cow::Owned(shell_variable),
});
command
});
if self.redirect_stdin {
- self.apply_stdin_redirect(&mut combined_command);
+ match self.kind {
+ ShellKind::Fish => {
+ combined_command.insert_str(0, "begin; ");
+ combined_command.push_str("; end </dev/null");
+ }
+ ShellKind::Posix
+ | ShellKind::Nushell
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc
+ | ShellKind::Xonsh
+ | ShellKind::Elvish => {
+ combined_command.insert(0, '(');
+ combined_command.push_str(") </dev/null");
+ }
+ ShellKind::PowerShell | ShellKind::Pwsh => {
+ combined_command.insert_str(0, "$null | & {");
+ combined_command.push_str("}");
+ }
+ ShellKind::Cmd => {
+ combined_command.push_str("< NUL");
+ }
+ }
}
- self.args.extend(args_for_shell_option(
- self.kind,
- self.interactive,
- combined_command,
- ));
+ self.args
+ .extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
@@ -181,18 +140,37 @@ impl ShellBuilder {
if let Some(task_command) = task_command {
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
- command.push_str(&to_shell_variable_option(self.kind, arg));
+ command.push_str(&self.kind.to_shell_variable(arg));
command
});
if self.redirect_stdin {
- self.apply_stdin_redirect(&mut combined_command);
+ match self.kind {
+ ShellKind::Fish => {
+ combined_command.insert_str(0, "begin; ");
+ combined_command.push_str("; end </dev/null");
+ }
+ ShellKind::Posix
+ | ShellKind::Nushell
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc
+ | ShellKind::Xonsh
+ | ShellKind::Elvish => {
+ combined_command.insert(0, '(');
+ combined_command.push_str(") </dev/null");
+ }
+ ShellKind::PowerShell | ShellKind::Pwsh => {
+ combined_command.insert_str(0, "$null | & {");
+ combined_command.push_str("}");
+ }
+ ShellKind::Cmd => {
+ combined_command.push_str("< NUL");
+ }
+ }
}
- self.args.extend(args_for_shell_option(
- self.kind,
- self.interactive,
- combined_command,
- ));
+ self.args
+ .extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
@@ -224,7 +202,7 @@ impl ShellBuilder {
if task_args.is_empty() {
task_command = task_command
.as_ref()
- .map(|cmd| try_quote_prefix_aware_option(self.kind, cmd).map(Cow::into_owned))
+ .map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned))
.unwrap_or(task_command);
}
let (program, args) = self.build(task_command, task_args);
@@ -232,7 +210,7 @@ impl ShellBuilder {
let mut child = crate::command::new_std_command(program);
#[cfg(windows)]
- if kind == Some(ShellKind::Cmd) {
+ if kind == ShellKind::Cmd {
use std::os::windows::process::CommandExt;
for arg in args {
@@ -248,7 +226,7 @@ impl ShellBuilder {
child
}
- pub fn kind(&self) -> Option<ShellKind> {
+ pub fn kind(&self) -> ShellKind {
self.kind
}
}
@@ -260,7 +238,7 @@ mod test {
#[test]
fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned());
- let shell_builder = ShellBuilder::new(&shell);
+ let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(
Some("echo".into()),
@@ -288,7 +266,7 @@ mod test {
#[test]
fn redirect_stdin_to_dev_null_precedence() {
let shell = Shell::Program("nu".to_owned());
- let shell_builder = ShellBuilder::new(&shell);
+ let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
@@ -301,7 +279,7 @@ mod test {
#[test]
fn redirect_stdin_to_dev_null_fish() {
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell);
+ let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
@@ -314,7 +292,7 @@ mod test {
#[test]
fn does_not_quote_sole_command_only() {
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell);
+ let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo".into()), &[]);
@@ -322,7 +300,7 @@ mod test {
assert_eq!(args, vec!["-i", "-c", "echo"]);
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell);
+ let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
@@ -36,8 +36,8 @@ async fn capture_unix(
use crate::command::new_std_command;
- let shell_kind = ShellKind::new(shell_path);
- let zed_path = super::get_shell_safe_zed_path(shell_kind.as_ref())?;
+ let shell_kind = ShellKind::new(shell_path, false);
+ let zed_path = super::get_shell_safe_zed_path(shell_kind)?;
let mut command_string = String::new();
let mut command = new_std_command(shell_path);
@@ -50,21 +50,21 @@ async fn capture_unix(
const FD_STDERR: std::os::fd::RawFd = 2;
let (fd_num, redir) = match shell_kind {
- Some(ShellKind::Rc) => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
- Some(ShellKind::Nushell) | Some(ShellKind::Tcsh) => (FD_STDOUT, "".to_string()),
+ ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
+ ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
// xonsh doesn't support redirecting to stdin, and control sequences are printed to
// stdout on startup
- Some(ShellKind::Xonsh) => (FD_STDERR, "o>e".to_string()),
- Some(ShellKind::PowerShell) => (FD_STDIN, format!(">{}", FD_STDIN)),
+ ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
+ ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
};
match shell_kind {
- Some(ShellKind::Csh) | Some(ShellKind::Tcsh) => {
+ ShellKind::Csh | ShellKind::Tcsh => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
command.arg0("-");
}
- Some(ShellKind::Fish) => {
+ ShellKind::Fish => {
// in fish, asdf, direnv attach to the `fish_prompt` event
command_string.push_str("emit fish_prompt;");
command.arg("-l");
@@ -75,7 +75,7 @@ async fn capture_unix(
}
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd '{}';", directory.display()));
- if let Some(prefix) = shell_kind.and_then(|k| k.command_prefix()) {
+ if let Some(prefix) = shell_kind.command_prefix() {
command_string.push(prefix);
}
command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
@@ -140,18 +140,16 @@ async fn capture_windows(
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
- let shell_kind = ShellKind::new(shell_path);
+ let shell_kind = ShellKind::new(shell_path, true);
let mut cmd = crate::command::new_smol_command(shell_path);
cmd.args(args);
let cmd = match shell_kind {
- Some(
- ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Rc
- | ShellKind::Fish
- | ShellKind::Xonsh
- | ShellKind::Posix(_),
- ) => cmd.args([
+ ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc
+ | ShellKind::Fish
+ | ShellKind::Xonsh
+ | ShellKind::Posix => cmd.args([
"-l",
"-i",
"-c",
@@ -161,7 +159,7 @@ async fn capture_windows(
zed_path.display()
),
]),
- Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) | None => cmd.args([
+ ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
"-NonInteractive",
"-NoProfile",
"-Command",
@@ -171,7 +169,7 @@ async fn capture_windows(
zed_path.display()
),
]),
- Some(ShellKind::Elvish) => cmd.args([
+ ShellKind::Elvish => cmd.args([
"-c",
&format!(
"cd '{}'; '{}' --printenv",
@@ -179,19 +177,19 @@ async fn capture_windows(
zed_path.display()
),
]),
- Some(ShellKind::Nushell) => cmd.args([
+ ShellKind::Nushell => cmd.args([
"-c",
&format!(
"cd '{}'; {}'{}' --printenv",
directory.display(),
- ShellKind::Nushell
+ shell_kind
.command_prefix()
.map(|prefix| prefix.to_string())
.unwrap_or_default(),
zed_path.display()
),
]),
- Some(ShellKind::Cmd) => cmd.args([
+ ShellKind::Cmd => cmd.args([
"/c",
"cd",
&directory.display().to_string(),
@@ -303,7 +303,7 @@ fn load_shell_from_passwd() -> Result<()> {
}
/// Returns a shell escaped path for the current zed executable
-pub fn get_shell_safe_zed_path(shell_kind: Option<&shell::ShellKind>) -> anyhow::Result<String> {
+pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<String> {
let mut zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
if cfg!(target_os = "linux")