Detailed changes
@@ -2421,7 +2421,6 @@ 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({
@@ -2435,10 +2434,9 @@ 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), is_windows)
- .redirect_stdin_to_dev_null()
- .build(Some(command.clone()), &args);
+ let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
+ .redirect_stdin_to_dev_null()
+ .build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
@@ -2792,7 +2790,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, false).build(
+ let (program, args) = ShellBuilder::new(&Shell::System).build(
Some("printf 'output_before_kill\\n' && sleep 60".to_owned()),
&[],
);
@@ -227,8 +227,7 @@ pub async fn create_terminal_entity(
.map(Shell::Program)
})
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
- 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)
+ let (task_command, task_args) = task::ShellBuilder::new(&shell)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
@@ -654,7 +654,9 @@ 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().supports_posix_chaining()
+ ShellKind::system()
+ .map(|k| k.supports_posix_chaining())
+ .unwrap_or(false)
} else {
true
};
@@ -30,15 +30,22 @@ impl ToolPermissionDecision {
///
/// # Shell Compatibility (Terminal Tool Only)
///
- /// For the terminal tool, commands are parsed to extract sub-commands for security.
- /// This parsing only works for shells with POSIX-like `&&` / `||` / `;` / `|` syntax:
+ /// 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 /`.
///
- /// **Compatible shells:** Posix (sh, bash, dash, zsh), Fish 3.0+, PowerShell 7+/Pwsh,
- /// Cmd, Xonsh, Csh, Tcsh
+ /// The parser handles `;` (sequential execution), `|` (piping), `&&` and `||`
+ /// (conditional execution), `$()` and backticks (command substitution), and process
+ /// substitution.
///
- /// **Incompatible shells:** Nushell, Elvish, Rc (Plan 9)
+ /// # Shell Notes
///
- /// For incompatible shells, `always_allow` patterns are disabled for safety.
+ /// - **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 `&&`/`||`.
///
/// # Pattern Matching Tips
///
@@ -57,7 +64,7 @@ impl ToolPermissionDecision {
input: &str,
permissions: &ToolPermissions,
always_allow_tool_actions: bool,
- shell_kind: ShellKind,
+ shell_kind: Option<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
@@ -86,22 +93,28 @@ 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() {
- // 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() {
+ // 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 {
// 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.
- 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
- ));
+ 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);
}
// 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);
@@ -229,6 +242,7 @@ 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(
@@ -246,7 +260,7 @@ mod tests {
deny: Vec<(&'static str, bool)>,
confirm: Vec<(&'static str, bool)>,
global: bool,
- shell: ShellKind,
+ shell: Option<ShellKind>,
}
impl PermTest {
@@ -259,7 +273,7 @@ mod tests {
deny: vec![],
confirm: vec![],
global: false,
- shell: ShellKind::Posix,
+ shell: Some(ShellKind::Posix(PosixShell::Sh)),
}
}
@@ -295,7 +309,7 @@ mod tests {
self.global = g;
self
}
- fn shell(mut self, s: ShellKind) -> Self {
+ fn shell(mut self, s: Option<ShellKind>) -> Self {
self.shell = s;
self
}
@@ -370,7 +384,7 @@ mod tests {
tools: collections::HashMap::default(),
},
global,
- ShellKind::Posix,
+ Some(ShellKind::Posix(PosixShell::Sh)),
)
}
@@ -605,16 +619,34 @@ 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, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "x",
+ &p,
+ true,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Allow
);
// With always_allow_tool_actions=false, default_mode: Deny is respected
assert!(matches!(
- ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "x",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Deny(_)
));
assert_eq!(
- ToolPermissionDecision::from_input("edit_file", "x", &p, false, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "edit_file",
+ "x",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Allow
);
}
@@ -635,7 +667,13 @@ 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, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "x",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Confirm
);
}
@@ -663,12 +701,24 @@ mod tests {
};
// With global=true, all checks are bypassed including invalid pattern check
assert!(matches!(
- ToolPermissionDecision::from_input("terminal", "echo hi", &p, true, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "echo hi",
+ &p,
+ true,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Allow
));
// With global=false, invalid patterns block the tool
assert!(matches!(
- ToolPermissionDecision::from_input("terminal", "echo hi", &p, false, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "echo hi",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Deny(_)
));
}
@@ -857,7 +907,13 @@ mod tests {
);
let p = ToolPermissions { tools };
assert!(matches!(
- ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix),
+ ToolPermissionDecision::from_input(
+ "terminal",
+ "x",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh))
+ ),
ToolPermissionDecision::Deny(_)
));
assert_eq!(
@@ -866,7 +922,7 @@ mod tests {
"x",
&p,
false,
- ShellKind::Posix
+ Some(ShellKind::Posix(PosixShell::Sh))
),
ToolPermissionDecision::Allow
);
@@ -900,38 +956,88 @@ mod tests {
}
#[test]
- fn nushell_denies_when_always_allow_configured() {
- t("ls").allow(&["^ls"]).shell(ShellKind::Nushell).is_deny();
+ 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();
}
#[test]
- fn nushell_allows_deny_patterns() {
+ fn nushell_denies_with_deny_pattern() {
t("rm -rf /")
.deny(&["rm\\s+-rf"])
- .shell(ShellKind::Nushell)
+ .shell(Some(ShellKind::Nushell))
.is_deny();
}
#[test]
- fn nushell_allows_confirm_patterns() {
+ fn nushell_confirms_with_confirm_pattern() {
t("sudo reboot")
.confirm(&["sudo"])
- .shell(ShellKind::Nushell)
+ .shell(Some(ShellKind::Nushell))
.is_confirm();
}
#[test]
- fn nushell_no_allow_patterns_uses_default() {
+ fn nushell_falls_through_to_default() {
t("ls")
.deny(&["rm"])
.mode(ToolPermissionMode::Allow)
- .shell(ShellKind::Nushell)
+ .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))
.is_allow();
}
#[test]
- fn elvish_denies_when_always_allow_configured() {
- t("ls").allow(&["^ls"]).shell(ShellKind::Elvish).is_deny();
+ 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();
}
#[test]
@@ -960,8 +1066,13 @@ mod tests {
);
let p = ToolPermissions { tools };
- let result =
- ToolPermissionDecision::from_input("terminal", "x", &p, false, ShellKind::Posix);
+ let result = ToolPermissionDecision::from_input(
+ "terminal",
+ "x",
+ &p,
+ false,
+ Some(ShellKind::Posix(PosixShell::Sh)),
+ );
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, cfg!(windows)).non_interactive();
+ let builder = ShellBuilder::new(&shell).non_interactive();
let mut child =
builder.build_std_command(Some(command.path.display().to_string()), &command.args);
child.envs(command.env.iter().flatten());
@@ -20,7 +20,11 @@ use futures::{
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
-use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind};
+use util::{
+ ResultExt as _, debug_panic, maybe,
+ paths::PathExt,
+ shell::{PosixShell, ShellKind},
+};
/// Path to the program used for askpass
///
@@ -208,7 +212,8 @@ impl PasswordProxy {
let shell_kind = if cfg!(windows) {
ShellKind::PowerShell
} else {
- ShellKind::Posix
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ ShellKind::Posix(PosixShell::Sh)
};
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
// Create an askpass script that communicates back to this process.
@@ -354,7 +359,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(shell_kind)
+ .try_shell_safe(Some(&shell_kind))
.context("Failed to shell-escape Askpass socket path")?;
let print_args = "printf '%s\\0' \"$@\"";
let shebang = "#!/bin/sh";
@@ -379,7 +384,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(shell_kind)
+ .try_shell_safe(Some(&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, cfg!(windows)).non_interactive();
+ let builder = ShellBuilder::new(&shell).non_interactive();
let mut command =
builder.build_smol_command(Some(binary.executable.display().to_string()), &binary.args);
@@ -6,7 +6,10 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{path::PathBuf, sync::OnceLock};
use task::DebugRequest;
-use util::{ResultExt, maybe, shell::ShellKind};
+use util::{
+ ResultExt, maybe,
+ shell::{PosixShell, ShellKind},
+};
use crate::*;
@@ -67,7 +70,10 @@ impl JsDebugAdapter {
.get("type")
.filter(|value| value == &"node-terminal")?;
let command = configuration.get("command")?.as_str()?.to_owned();
- let mut args = ShellKind::Posix.split(&command)?.into_iter();
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ let mut args = ShellKind::Posix(PosixShell::Sh)
+ .split(&command)?
+ .into_iter();
let program = args.next()?;
configuration.insert("runtimeExecutable".to_owned(), program.into());
configuration.insert(
@@ -28,7 +28,11 @@ use ui::{
prelude::*,
};
use ui_input::InputField;
-use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
+use util::{
+ ResultExt, debug_panic,
+ rel_path::RelPath,
+ shell::{PosixShell, ShellKind},
+};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{
@@ -870,7 +874,8 @@ impl ConfigureMode {
};
}
let command = self.program.read(cx).text(cx);
- let mut args = ShellKind::Posix
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ let mut args = ShellKind::Posix(PosixShell::Sh)
.split(&command)
.into_iter()
.flatten()
@@ -1298,7 +1303,8 @@ impl PickerDelegate for DebugDelegate {
})
.unwrap_or_default();
- let mut args = ShellKind::Posix
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ let mut args = ShellKind::Posix(PosixShell::Sh)
.split(&text)
.into_iter()
.flatten()
@@ -971,7 +971,6 @@ 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()
@@ -1089,7 +1088,7 @@ impl RunningState {
task.resolved.shell = Shell::Program(remote_shell);
}
- let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
+ let builder = ShellBuilder::new(&task.resolved.shell);
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: ShellKind,
+ shell: Option<ShellKind>,
cx: &App,
) -> BoxFuture<'static, Vec<String>>;
@@ -46,7 +46,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
+use task::{PosixShell, ShellKind, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, maybe};
#[derive(Debug, Serialize, Deserialize)]
@@ -1168,13 +1168,15 @@ fn wr_distance(
}
}
-fn micromamba_shell_name(kind: ShellKind) -> &'static str {
+fn micromamba_shell_name(kind: Option<ShellKind>) -> &'static str {
match kind {
- ShellKind::Csh => "csh",
- ShellKind::Fish => "fish",
- ShellKind::Nushell => "nu",
- ShellKind::PowerShell => "powershell",
- ShellKind::Cmd => "cmd.exe",
+ 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",
// default / catch-all:
_ => "posix",
}
@@ -1339,7 +1341,7 @@ impl ToolchainLister for PythonToolchainProvider {
fn activation_script(
&self,
toolchain: &Toolchain,
- shell: ShellKind,
+ shell: Option<ShellKind>,
cx: &App,
) -> BoxFuture<'static, Vec<String>> {
let settings = TerminalSettings::get_global(cx);
@@ -1406,10 +1408,21 @@ impl ToolchainLister for PythonToolchainProvider {
| PythonEnvironmentKind::Poetry,
) => {
if let Some(activation_scripts) = &toolchain.activation_scripts {
- if let Some(activate_script_path) = activation_scripts.get(&shell) {
- let activate_keyword = shell.activate_keyword();
+ // 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(quoted) =
- shell.try_quote(&activate_script_path.to_string_lossy())
+ shell_kind.try_quote(&activate_script_path.to_string_lossy())
{
activation_script.push(format!("{activate_keyword} {quoted}"));
}
@@ -1423,17 +1436,27 @@ 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 {
- 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,
+ 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,
})
}
_ => {}
@@ -1497,8 +1520,9 @@ 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, "activate"),
+ (ShellKind::Posix(PosixShell::Sh), "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, cfg!(windows)).non_interactive();
+ let builder = ShellBuilder::new(&build_config.shell).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(cfg!(windows));
+ let shell_kind = task_template.shell.shell_kind();
task_template.args.push("--".to_string());
task_template.args.extend(
cargo
@@ -683,7 +683,8 @@ 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| {
- shell_kind.try_quote(&extra_arg).map(|s| s.to_string())
+ util::shell::try_quote_option(shell_kind, &extra_arg)
+ .map(|s| s.to_string())
}),
);
}
@@ -12,11 +12,12 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
+use task::{Shell, ShellBuilder, 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};
@@ -92,7 +93,7 @@ impl Project {
None => settings.shell.program(),
};
let path_style = self.path_style(cx);
- let shell_kind = ShellKind::new(&shell, path_style.is_windows());
+ let shell_kind = ShellKind::new(&shell);
// Prepare a task for resolving the environment
let env_task =
@@ -142,12 +143,14 @@ impl Project {
.update(cx, move |_, cx| {
let format_to_run = || {
if let Some(command) = &spawn_task.command {
- let command = shell_kind.prepend_command_prefix(command);
- let command = shell_kind.try_quote_prefix_aware(&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 args = spawn_task
.args
.iter()
- .filter_map(|arg| shell_kind.try_quote(&arg));
+ .filter_map(|arg| util::shell::try_quote_option(shell_kind, arg));
command.into_iter().chain(args).join(" ")
} else {
@@ -161,13 +164,17 @@ impl Project {
match remote_client {
Some(remote_client) => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
- let separator = shell_kind.sequential_commands_separator();
+ let separator =
+ util::shell::sequential_commands_separator_option(
+ shell_kind,
+ );
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let arg = format!("{activation_script}{separator} {to_run}");
- let args = shell_kind.args_for_shell(false, arg);
+ let args =
+ util::shell::args_for_shell_option(shell_kind, false, arg);
let shell = remote_client
.read(cx)
.shell()
@@ -194,13 +201,17 @@ impl Project {
},
None => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
- let separator = shell_kind.sequential_commands_separator();
+ let separator =
+ util::shell::sequential_commands_separator_option(
+ shell_kind,
+ );
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let arg = format!("{activation_script}{separator} {to_run}");
- let args = shell_kind.args_for_shell(false, arg);
+ let args =
+ util::shell::args_for_shell_option(shell_kind, false, arg);
(
Shell::WithArguments {
@@ -358,7 +369,7 @@ impl Project {
let lang_registry = self.languages.clone();
cx.spawn(async move |project, cx| {
- let shell_kind = ShellKind::new(&shell, path_style.is_windows());
+ let shell_kind = ShellKind::new(&shell);
let mut env = env_task.await.unwrap_or_default();
env.extend(settings.env);
@@ -513,8 +524,7 @@ impl Project {
.and_then(|remote_client| remote_client.read(cx).shell())
.map(Shell::Program)
.unwrap_or_else(|| settings.shell.clone());
- let is_windows = self.path_style(cx).is_windows();
- let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
+ let builder = ShellBuilder::new(&shell).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,
- _: ShellKind,
+ _: Option<ShellKind>,
_: &gpui::App,
) -> futures::future::BoxFuture<'static, Vec<String>> {
Box::pin(async { vec![] })
@@ -57,8 +57,12 @@ impl ProjectContext {
user_rules: default_user_rules,
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
- shell: ShellKind::new(&get_default_system_shell_preferring_bash(), cfg!(windows))
- .to_string(),
+ shell: ShellKind::new_with_fallback(
+ &get_default_system_shell_preferring_bash(),
+ cfg!(windows),
+ )
+ .name()
+ .to_string(),
}
}
}
@@ -13,7 +13,7 @@ use std::{
sync::Arc,
};
use util::ResultExt;
-use util::shell::ShellKind;
+use util::shell::{PosixShell, ShellKind};
use util::{
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
@@ -301,7 +301,8 @@ impl DockerExecConnection {
delegate.set_status(Some("Extracting remote development server"), cx);
let server_mode = 0o755;
- let shell_kind = ShellKind::Posix;
+ // TODO: Consider using the remote's actual shell instead of hardcoding "sh"
+ let shell_kind = ShellKind::Posix(PosixShell::Sh);
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::ShellKind,
+ shell::{PosixShell, 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(&ssh_shell, is_windows);
+ let ssh_shell_kind = ShellKind::new_with_fallback(&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 = ShellKind::Posix;
+ let shell_kind = self.ssh_shell_kind;
let server_mode = 0o755;
let orig_tmp_path = tmp_path.display(self.path_style());
let server_mode = format!("{:o}", server_mode);
@@ -1138,7 +1138,6 @@ 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"
@@ -1291,8 +1290,14 @@ 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, "sh", &["-c", "echo $SHELL"], false)
+ .run_command(
+ ShellKind::Posix(PosixShell::Sh),
+ "sh",
+ &["-c", "echo $SHELL"],
+ false,
+ )
.await
{
Ok(output) => parse_shell(&output, DEFAULT_SHELL),
@@ -1386,7 +1391,8 @@ impl SshConnectionOptions {
"-w",
];
- let mut tokens = ShellKind::Posix
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ let mut tokens = ShellKind::Posix(PosixShell::Sh)
.split(input)
.context("invalid input")?
.into_iter();
@@ -1624,7 +1630,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 {
@@ -1666,7 +1672,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> {
@@ -1685,7 +1691,7 @@ fn build_command_windows(
shell_kind
.try_quote(&working_dir)
.context("shell quoting")?,
- shell_kind.sequential_and_commands_separator()
+ ssh_shell_kind.sequential_and_commands_separator()
)?;
}
@@ -1772,7 +1778,7 @@ mod tests {
env.clone(),
PathStyle::Posix,
"/bin/bash",
- ShellKind::Posix,
+ ShellKind::Posix(PosixShell::Bash),
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::{Shell, ShellKind},
+ shell::{PosixShell, Shell, ShellKind},
shell_builder::ShellBuilder,
};
@@ -75,7 +75,8 @@ impl WslRemoteConnection {
arch: RemoteArch::X86_64,
},
shell: String::new(),
- shell_kind: ShellKind::Posix,
+ // TODO: Consider using the user's actual shell instead of hardcoding "sh"
+ shell_kind: ShellKind::Posix(PosixShell::Sh),
default_system_shell: String::from("/bin/sh"),
has_wsl_interop: false,
};
@@ -85,7 +86,8 @@ impl WslRemoteConnection {
.await
.context("failed detecting shell")?;
log::info!("Remote shell discovered: {}", this.shell);
- this.shell_kind = ShellKind::new(&this.shell, false);
+ // WSL is always Linux, so is_windows = false
+ this.shell_kind = ShellKind::new_with_fallback(&this.shell, false);
this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default();
log::info!(
"Remote has wsl interop {}",
@@ -468,7 +470,7 @@ impl RemoteConnection for WslRemoteConnection {
write!(&mut exec, "{} -l", self.shell)?;
}
let (command, args) =
- ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]);
+ ShellBuilder::new(&Shell::Program(self.shell.clone())).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::{Shell, ShellKind};
+pub use util::shell::{PosixShell, 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(cfg!(windows));
+ let shell_kind = shell.shell_kind();
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.tty_escape_args(),
+ escape_args: shell_kind.map(|k| k.tty_escape_args()).unwrap_or(true),
}
};
@@ -664,7 +664,10 @@ 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.
- terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
+ let clear_cmd = shell_kind
+ .map(|k| k.clear_screen_command())
+ .unwrap_or("clear");
+ terminal.write_to_pty(clear_cmd.as_bytes());
// Simulate enter key press
terminal.write_to_pty(b"\x0d");
}
@@ -2558,7 +2561,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, false).build(Some(command.to_owned()), &args);
+ ShellBuilder::new(&Shell::System).build(Some(command.to_owned()), &args);
let builder = cx
.update(|cx| {
TerminalBuilder::new(
@@ -2779,7 +2782,7 @@ mod tests {
cx.executor().allow_parking();
let (completion_tx, completion_rx) = smol::channel::unbounded();
- let (program, args) = ShellBuilder::new(&Shell::System, false)
+ let (program, args) = ShellBuilder::new(&Shell::System)
.build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]);
let builder = cx
.update(|cx| {
@@ -562,7 +562,6 @@ 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());
@@ -575,7 +574,7 @@ impl TerminalPanel {
task.shell.clone()
};
- let task = prepare_task_for_spawn(task, &shell, is_windows);
+ let task = prepare_task_for_spawn(task, &shell);
if task.allow_concurrent_runs && task.use_new_terminal {
return self.spawn_in_new_terminal(task, window, cx);
@@ -1136,12 +1135,8 @@ 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,
- is_windows: bool,
-) -> SpawnInTerminal {
- let builder = ShellBuilder::new(shell, is_windows);
+pub fn prepare_task_for_spawn(task: &SpawnInTerminal, shell: &Shell) -> SpawnInTerminal {
+ let builder = ShellBuilder::new(shell);
let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
@@ -1833,7 +1828,7 @@ mod tests {
let input = SpawnInTerminal::default();
let shell = Shell::System;
- let result = prepare_task_for_spawn(&input, &shell, false);
+ let result = prepare_task_for_spawn(&input, &shell);
let expected_shell = util::get_system_shell();
assert_eq!(result.env, HashMap::default());
@@ -1905,7 +1900,7 @@ mod tests {
};
let shell = Shell::System;
- let result = prepare_task_for_spawn(&input, &shell, false);
+ let result = prepare_task_for_spawn(&input, &shell);
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: ShellKind) -> anyhow::Result<String>;
+ fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String>;
}
impl<T: AsRef<Path>> PathExt for T {
@@ -164,13 +164,19 @@ impl<T: AsRef<Path>> PathExt for T {
Some(parts.into_iter().join("."))
}
- fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result<String> {
+ fn try_shell_safe(&self, shell_kind: Option<&ShellKind>) -> anyhow::Result<String> {
let path_str = self
.as_ref()
.to_str()
.with_context(|| "Path contains invalid UTF-8")?;
- shell_kind
- .try_quote(path_str)
+ 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
.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, fmt, path::Path, sync::LazyLock};
+use std::{borrow::Cow, ffi::OsStr, path::Path, sync::LazyLock};
/// Shell configuration to open the terminal with.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
@@ -39,19 +39,45 @@ impl Shell {
}
}
- pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
+ pub fn shell_kind(&self) -> Option<ShellKind> {
match self {
- Shell::Program(program) => ShellKind::new(program, is_windows),
- Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
+ Shell::Program(program) => ShellKind::new(program),
+ Shell::WithArguments { program, .. } => ShellKind::new(program),
Shell::System => ShellKind::system(),
}
}
}
+/// Specific POSIX-compatible shell variants.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub enum ShellKind {
+pub enum PosixShell {
#[default]
- Posix,
+ 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),
Csh,
Tcsh,
Rc,
@@ -233,84 +259,127 @@ pub fn get_windows_system_shell() -> String {
(*SYSTEM_SHELL).clone()
}
-impl fmt::Display for ShellKind {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+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 => 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"),
+ ShellKind::Posix(_) => ShellKind::Posix(PosixShell::Sh),
+ other => *other,
}
}
-}
-impl ShellKind {
- pub fn system() -> Self {
- Self::new(&get_system_shell(), cfg!(windows))
+ /// 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 whether this shell uses POSIX-like command chaining syntax (`&&`, `||`, `;`, `|`).
+ /// Returns the name of the shell.
+ pub fn name(&self) -> &'static str {
+ 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",
+ }
+ }
+
+ pub fn system() -> Option<Self> {
+ Self::new(&get_system_shell())
+ }
+
+ /// Returns whether this shell's command chaining syntax can be parsed by brush-parser.
///
/// 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).
///
- /// **Compatible shells:** Posix (sh, bash, dash, zsh), Fish 3.0+, PowerShell 7+/Pwsh,
- /// Cmd, Xonsh, Csh, Tcsh
+ /// 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
///
- /// **Incompatible shells:** Nushell (uses `and`/`or` keywords), Elvish (uses `and`/`or`
- /// keywords), Rc (Plan 9 shell - no `&&`/`||` operators)
+ /// - **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.
pub fn supports_posix_chaining(&self) -> bool {
- 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 {
+ 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 {
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(),
@@ -340,7 +409,7 @@ impl ShellKind {
}
}
- fn to_powershell_variable(input: &str) -> String {
+ pub 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 "}"
@@ -436,13 +505,15 @@ 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
@@ -461,7 +532,7 @@ impl ShellKind {
match self {
ShellKind::PowerShell | ShellKind::Pwsh => Some('&'),
ShellKind::Nushell => Some('^'),
- ShellKind::Posix
+ ShellKind::Posix(_)
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
@@ -484,7 +555,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
@@ -500,7 +571,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
@@ -516,7 +587,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
@@ -527,7 +598,7 @@ impl ShellKind {
}
}
- fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
+ pub fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
if arg.is_empty() {
return Cow::Borrowed("\"\"");
}
@@ -739,7 +810,7 @@ impl ShellKind {
ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
- | ShellKind::Posix
+ | ShellKind::Posix(_)
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => "source",
@@ -749,7 +820,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
@@ -768,7 +839,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
@@ -782,6 +853,109 @@ 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,7 +1,15 @@
use std::borrow::Cow;
use crate::shell::get_system_shell;
-use crate::shell::{Shell, ShellKind};
+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 = "}";
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
@@ -12,19 +20,19 @@ pub struct ShellBuilder {
interactive: bool,
/// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
redirect_stdin: bool,
- kind: ShellKind,
+ kind: Option<ShellKind>,
}
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
- pub fn new(shell: &Shell, is_windows: bool) -> Self {
+ pub fn new(shell: &Shell) -> 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, is_windows);
+ let kind = ShellKind::new(&program);
Self {
program,
args,
@@ -44,20 +52,34 @@ impl ShellBuilder {
self.program.clone()
} else {
match self.kind {
- ShellKind::PowerShell | ShellKind::Pwsh => {
+ Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => {
+ format!("{} -C '{}'", self.program, command_to_use_in_label)
+ }
+ #[cfg(windows)]
+ None => {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
- ShellKind::Cmd => {
+ Some(ShellKind::Cmd) => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
}
- ShellKind::Posix
- | ShellKind::Nushell
- | ShellKind::Fish
- | ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Rc
- | ShellKind::Xonsh
- | ShellKind::Elvish => {
+ 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 => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -73,6 +95,44 @@ 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,
@@ -81,7 +141,7 @@ impl ShellBuilder {
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let task_command = if !task_args.is_empty() {
- match self.kind.try_quote_prefix_aware(&task_command) {
+ match try_quote_prefix_aware_option(self.kind, &task_command) {
Some(task_command) => task_command.into_owned(),
None => task_command,
}
@@ -90,41 +150,22 @@ impl ShellBuilder {
};
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
- let shell_variable = self.kind.to_shell_variable(arg);
- command.push_str(&match self.kind.try_quote(&shell_variable) {
+ let shell_variable = to_shell_variable_option(self.kind, arg);
+ command.push_str(&match try_quote_option(self.kind, &shell_variable) {
Some(shell_variable) => shell_variable,
None => Cow::Owned(shell_variable),
});
command
});
if self.redirect_stdin {
- 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.apply_stdin_redirect(&mut combined_command);
}
- self.args
- .extend(self.kind.args_for_shell(self.interactive, combined_command));
+ self.args.extend(args_for_shell_option(
+ self.kind,
+ self.interactive,
+ combined_command,
+ ));
}
(self.program, self.args)
@@ -140,37 +181,18 @@ 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(&self.kind.to_shell_variable(arg));
+ command.push_str(&to_shell_variable_option(self.kind, arg));
command
});
if self.redirect_stdin {
- 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.apply_stdin_redirect(&mut combined_command);
}
- self.args
- .extend(self.kind.args_for_shell(self.interactive, combined_command));
+ self.args.extend(args_for_shell_option(
+ self.kind,
+ self.interactive,
+ combined_command,
+ ));
}
(self.program, self.args)
@@ -202,7 +224,7 @@ impl ShellBuilder {
if task_args.is_empty() {
task_command = task_command
.as_ref()
- .map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned))
+ .map(|cmd| try_quote_prefix_aware_option(self.kind, cmd).map(Cow::into_owned))
.unwrap_or(task_command);
}
let (program, args) = self.build(task_command, task_args);
@@ -210,7 +232,7 @@ impl ShellBuilder {
let mut child = crate::command::new_std_command(program);
#[cfg(windows)]
- if kind == ShellKind::Cmd {
+ if kind == Some(ShellKind::Cmd) {
use std::os::windows::process::CommandExt;
for arg in args {
@@ -226,7 +248,7 @@ impl ShellBuilder {
child
}
- pub fn kind(&self) -> ShellKind {
+ pub fn kind(&self) -> Option<ShellKind> {
self.kind
}
}
@@ -238,7 +260,7 @@ mod test {
#[test]
fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned());
- let shell_builder = ShellBuilder::new(&shell, false);
+ let shell_builder = ShellBuilder::new(&shell);
let (program, args) = shell_builder.build(
Some("echo".into()),
@@ -266,7 +288,7 @@ mod test {
#[test]
fn redirect_stdin_to_dev_null_precedence() {
let shell = Shell::Program("nu".to_owned());
- let shell_builder = ShellBuilder::new(&shell, false);
+ let shell_builder = ShellBuilder::new(&shell);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
@@ -279,7 +301,7 @@ mod test {
#[test]
fn redirect_stdin_to_dev_null_fish() {
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell, false);
+ let shell_builder = ShellBuilder::new(&shell);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
@@ -292,7 +314,7 @@ mod test {
#[test]
fn does_not_quote_sole_command_only() {
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell, false);
+ let shell_builder = ShellBuilder::new(&shell);
let (program, args) = shell_builder.build(Some("echo".into()), &[]);
@@ -300,7 +322,7 @@ mod test {
assert_eq!(args, vec!["-i", "-c", "echo"]);
let shell = Shell::Program("fish".to_owned());
- let shell_builder = ShellBuilder::new(&shell, false);
+ let shell_builder = ShellBuilder::new(&shell);
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, false);
- let zed_path = super::get_shell_safe_zed_path(shell_kind)?;
+ let shell_kind = ShellKind::new(shell_path);
+ let zed_path = super::get_shell_safe_zed_path(shell_kind.as_ref())?;
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 {
- ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
- ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
+ Some(ShellKind::Rc) => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
+ Some(ShellKind::Nushell) | Some(ShellKind::Tcsh) => (FD_STDOUT, "".to_string()),
// xonsh doesn't support redirecting to stdin, and control sequences are printed to
// stdout on startup
- ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
- ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)),
+ Some(ShellKind::Xonsh) => (FD_STDERR, "o>e".to_string()),
+ Some(ShellKind::PowerShell) => (FD_STDIN, format!(">{}", FD_STDIN)),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
};
match shell_kind {
- ShellKind::Csh | ShellKind::Tcsh => {
+ Some(ShellKind::Csh) | Some(ShellKind::Tcsh) => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
command.arg0("-");
}
- ShellKind::Fish => {
+ Some(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.command_prefix() {
+ if let Some(prefix) = shell_kind.and_then(|k| k.command_prefix()) {
command_string.push(prefix);
}
command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
@@ -140,16 +140,18 @@ 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, true);
+ let shell_kind = ShellKind::new(shell_path);
let mut cmd = crate::command::new_smol_command(shell_path);
cmd.args(args);
let cmd = match shell_kind {
- ShellKind::Csh
- | ShellKind::Tcsh
- | ShellKind::Rc
- | ShellKind::Fish
- | ShellKind::Xonsh
- | ShellKind::Posix => cmd.args([
+ Some(
+ ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc
+ | ShellKind::Fish
+ | ShellKind::Xonsh
+ | ShellKind::Posix(_),
+ ) => cmd.args([
"-l",
"-i",
"-c",
@@ -159,7 +161,7 @@ async fn capture_windows(
zed_path.display()
),
]),
- ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
+ Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) | None => cmd.args([
"-NonInteractive",
"-NoProfile",
"-Command",
@@ -169,7 +171,7 @@ async fn capture_windows(
zed_path.display()
),
]),
- ShellKind::Elvish => cmd.args([
+ Some(ShellKind::Elvish) => cmd.args([
"-c",
&format!(
"cd '{}'; '{}' --printenv",
@@ -177,19 +179,19 @@ async fn capture_windows(
zed_path.display()
),
]),
- ShellKind::Nushell => cmd.args([
+ Some(ShellKind::Nushell) => cmd.args([
"-c",
&format!(
"cd '{}'; {}'{}' --printenv",
directory.display(),
- shell_kind
+ ShellKind::Nushell
.command_prefix()
.map(|prefix| prefix.to_string())
.unwrap_or_default(),
zed_path.display()
),
]),
- ShellKind::Cmd => cmd.args([
+ Some(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: shell::ShellKind) -> anyhow::Result<String> {
+pub fn get_shell_safe_zed_path(shell_kind: Option<&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")