diff --git a/Cargo.lock b/Cargo.lock index 2b01b45f637dd2e51802b91cec62f05b2f3f55fd..caeb7e714da6c7f2c689d51e82500cc69d68f35d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,6 @@ dependencies = [ "util", "uuid", "watch", - "which 6.0.3", "workspace-hack", ] @@ -1023,7 +1022,6 @@ dependencies = [ "util", "watch", "web_search", - "which 6.0.3", "workspace", "workspace-hack", "zlog", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index a0bbda848f9ec761aebdf66b644a8b2926685122..ac24a6ed0f41c75d5c4dcd9b9b4122336022ddf3 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -45,7 +45,6 @@ url.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true -which.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index afbb4781f61d5ccf1ea753df1fd0379e533e8e46..c89b742742536308a6064fe9ddabff3f8e73c341 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -7,12 +7,12 @@ use agent_settings::AgentSettings; use collections::HashSet; pub use connection::*; pub use diff::*; -use futures::future::Shared; use language::language_settings::FormatOnSave; pub use mention::*; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use serde::{Deserialize, Serialize}; use settings::Settings as _; +use task::{Shell, ShellBuilder}; pub use terminal::*; use action_log::ActionLog; @@ -34,7 +34,7 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; use ui::App; -use util::{ResultExt, get_system_shell}; +use util::{ResultExt, get_default_system_shell}; use uuid::Uuid; #[derive(Debug)] @@ -786,7 +786,6 @@ pub struct AcpThread { token_usage: Option, prompt_capabilities: acp::PromptCapabilities, _observe_prompt_capabilities: Task>, - determine_shell: Shared>, terminals: HashMap>, } @@ -873,20 +872,6 @@ impl AcpThread { } }); - let determine_shell = cx - .background_spawn(async move { - if cfg!(windows) { - return get_system_shell(); - } - - if which::which("bash").is_ok() { - "bash".into() - } else { - get_system_shell() - } - }) - .shared(); - Self { action_log, shared_buffers: Default::default(), @@ -901,7 +886,6 @@ impl AcpThread { prompt_capabilities, _observe_prompt_capabilities: task, terminals: HashMap::default(), - determine_shell, } } @@ -1940,28 +1924,13 @@ impl AcpThread { pub fn create_terminal( &self, - mut command: String, + command: String, args: Vec, extra_env: Vec, cwd: Option, output_byte_limit: Option, cx: &mut Context, ) -> Task>> { - for arg in args { - command.push(' '); - command.push_str(&arg); - } - - let shell_command = if cfg!(windows) { - format!("$null | & {{{}}}", command.replace("\"", "'")) - } else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) { - // Make sure once we're *inside* the shell, we cd into `cwd` - format!("(cd {cwd}; {}) self.project.update(cx, |project, cx| { project.directory_environment(dir.as_path().into(), cx) @@ -1982,20 +1951,30 @@ impl AcpThread { let project = self.project.clone(); let language_registry = project.read(cx).languages().clone(); - let determine_shell = self.determine_shell.clone(); let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into()); let terminal_task = cx.spawn({ let terminal_id = terminal_id.clone(); async move |_this, cx| { - let program = determine_shell.await; let env = env.await; + let (command, args) = ShellBuilder::new( + project + .update(cx, |project, cx| { + project + .remote_client() + .and_then(|r| r.read(cx).default_system_shell()) + })? + .as_deref(), + &Shell::Program(get_default_system_shell()), + ) + .redirect_stdin_to_dev_null() + .build(Some(command), &args); let terminal = project .update(cx, |project, cx| { project.create_terminal_task( task::SpawnInTerminal { - command: Some(program), - args, + command: Some(command.clone()), + args: args.clone(), cwd: cwd.clone(), env, ..Default::default() @@ -2008,7 +1987,7 @@ impl AcpThread { cx.new(|cx| { Terminal::new( terminal_id, - command, + &format!("{} {}", command, args.join(" ")), cwd, output_byte_limit.map(|l| l as usize), terminal, diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index a927083b0bd576f1580ba261d4028407fcea7a5c..888c7698c3d2270769f3afbe712ecba7d08b055f 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -28,7 +28,7 @@ pub struct TerminalOutput { impl Terminal { pub fn new( id: acp::TerminalId, - command: String, + command_label: &str, working_dir: Option, output_byte_limit: Option, terminal: Entity, @@ -40,7 +40,7 @@ impl Terminal { id, command: cx.new(|cx| { Markdown::new( - format!("```\n{}\n```", command).into(), + format!("```\n{}\n```", command_label).into(), Some(language_registry.clone()), None, cx, diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 5a8ca8a5e995fd2c738eb3b309f2bb4ebe9595a1..9b9b8196d1c342c536d605306a1a062e73768c56 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -63,7 +63,6 @@ ui.workspace = true util.workspace = true watch.workspace = true web_search.workspace = true -which.workspace = true workspace-hack.workspace = true workspace.workspace = true diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index ce3b639cb2c46d3f736490c0b2153260f970963c..17e2ba12f706387859ca3393aa44f5c05570e50a 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -52,7 +52,7 @@ pub fn init(http_client: Arc, cx: &mut App) { assistant_tool::init(cx); let registry = ToolRegistry::global(cx); - registry.register_tool(TerminalTool::new(cx)); + registry.register_tool(TerminalTool); registry.register_tool(CreateDirectoryTool); registry.register_tool(CopyPathTool); registry.register_tool(DeletePathTool); diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 1605003671621b90e58a5f62e521c0aba2c990c6..8014a39e23137ad71b91e5c24d5d79699b530e5d 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -6,7 +6,7 @@ use action_log::ActionLog; use agent_settings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; -use futures::{FutureExt as _, future::Shared}; +use futures::FutureExt as _; use gpui::{ AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, WeakEntity, Window, @@ -26,11 +26,12 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use task::{Shell, ShellBuilder}; use terminal_view::TerminalView; use theme::ThemeSettings; use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; use util::{ - ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size, + ResultExt, get_default_system_shell, markdown::MarkdownInlineCode, size::format_file_size, time::duration_alt_display, }; use workspace::Workspace; @@ -45,29 +46,10 @@ pub struct TerminalToolInput { cd: String, } -pub struct TerminalTool { - determine_shell: Shared>, -} +pub struct TerminalTool; impl TerminalTool { pub const NAME: &str = "terminal"; - - pub(crate) fn new(cx: &mut App) -> Self { - let determine_shell = cx.background_spawn(async move { - if cfg!(windows) { - return get_system_shell(); - } - - if which::which("bash").is_ok() { - "bash".into() - } else { - get_system_shell() - } - }); - Self { - determine_shell: determine_shell.shared(), - } - } } impl Tool for TerminalTool { @@ -135,19 +117,6 @@ impl Tool for TerminalTool { Ok(dir) => dir, Err(err) => return Task::ready(Err(err)).into(), }; - let program = self.determine_shell.clone(); - let command = if cfg!(windows) { - format!("$null | & {{{}}}", input.command.replace("\"", "'")) - } else if let Some(cwd) = working_dir - .as_ref() - .and_then(|cwd| cwd.as_os_str().to_str()) - { - // Make sure once we're *inside* the shell, we cd into `cwd` - format!("(cd {cwd}; {}) Task::ready(None).shared(), }; + let remote_shell = project.update(cx, |project, cx| { + project + .remote_client() + .and_then(|r| r.read(cx).default_system_shell()) + }); let env = cx.spawn(async move |_| { let mut env = env.await.unwrap_or_default(); @@ -171,8 +145,13 @@ impl Tool for TerminalTool { let task = cx.background_spawn(async move { let env = env.await; let pty_system = native_pty_system(); - let program = program.await; - let mut cmd = CommandBuilder::new(program); + let (command, args) = ShellBuilder::new( + remote_shell.as_deref(), + &Shell::Program(get_default_system_shell()), + ) + .redirect_stdin_to_dev_null() + .build(Some(input.command.clone()), &[]); + let mut cmd = CommandBuilder::new(command); cmd.args(args); for (k, v) in env { cmd.env(k, v); @@ -208,16 +187,22 @@ impl Tool for TerminalTool { }; }; + let command = input.command.clone(); let terminal = cx.spawn({ let project = project.downgrade(); async move |cx| { - let program = program.await; + let (command, args) = ShellBuilder::new( + remote_shell.as_deref(), + &Shell::Program(get_default_system_shell()), + ) + .redirect_stdin_to_dev_null() + .build(Some(input.command), &[]); let env = env.await; project .update(cx, |project, cx| { project.create_terminal_task( task::SpawnInTerminal { - command: Some(program), + command: Some(command), args, cwd, env, @@ -230,14 +215,8 @@ impl Tool for TerminalTool { } }); - let command_markdown = cx.new(|cx| { - Markdown::new( - format!("```bash\n{}\n```", input.command).into(), - None, - None, - cx, - ) - }); + let command_markdown = + cx.new(|cx| Markdown::new(format!("```bash\n{}\n```", command).into(), None, None, cx)); let card = cx.new(|cx| { TerminalToolCard::new( @@ -288,7 +267,7 @@ impl Tool for TerminalTool { let previous_len = content.len(); let (processed_content, finished_with_empty_output) = process_content( &content, - &input.command, + &command, exit_status.map(portable_pty::ExitStatus::from), ); @@ -740,7 +719,6 @@ mod tests { if cfg!(windows) { return; } - init_test(&executor, cx); let fs = Arc::new(RealFs::new(None, executor)); @@ -763,7 +741,7 @@ mod tests { }; let result = cx.update(|cx| { TerminalTool::run( - Arc::new(TerminalTool::new(cx)), + Arc::new(TerminalTool), serde_json::to_value(input).unwrap(), Arc::default(), project.clone(), @@ -783,7 +761,6 @@ mod tests { if cfg!(windows) { return; } - init_test(&executor, cx); let fs = Arc::new(RealFs::new(None, executor)); @@ -798,7 +775,7 @@ mod tests { let check = |input, expected, cx: &mut App| { let headless_result = TerminalTool::run( - Arc::new(TerminalTool::new(cx)), + Arc::new(TerminalTool), serde_json::to_value(input).unwrap(), Arc::default(), project.clone(), diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 34dfe4a4287c80a91efd285dc020657f7e0a0fe8..27af3eea17c9858d11e01cd7187975e804736ba2 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1131,7 +1131,7 @@ impl ToolchainLister for PythonToolchainProvider { let activate_keyword = match shell { ShellKind::Cmd => ".", ShellKind::Nushell => "overlay use", - ShellKind::Powershell => ".", + ShellKind::PowerShell => ".", ShellKind::Fish => "source", ShellKind::Csh => "source", ShellKind::Posix => "source", @@ -1141,7 +1141,7 @@ impl ToolchainLister for PythonToolchainProvider { ShellKind::Csh => "activate.csh", ShellKind::Fish => "activate.fish", ShellKind::Nushell => "activate.nu", - ShellKind::Powershell => "activate.ps1", + ShellKind::PowerShell => "activate.ps1", ShellKind::Cmd => "activate.bat", }; let path = prefix.join(BINARY_DIR).join(activate_script_name); @@ -1165,7 +1165,7 @@ impl ToolchainLister for PythonToolchainProvider { 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 => None, + ShellKind::PowerShell => None, ShellKind::Csh => None, ShellKind::Cmd => None, }) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 04f98d6dba6794116be9a6dcf4d2cbb32cfb85b2..94e9999e1344efbc391476e22d107f10052d7694 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -179,7 +179,7 @@ impl Project { } }; - let shell = { + let (shell, env) = { env.extend(spawn_task.env); match remote_client { Some(remote_client) => match activation_script.clone() { @@ -189,8 +189,14 @@ impl Project { let args = vec!["-c".to_owned(), format!("{activation_script}; {to_run}")]; create_remote_shell( - Some((&shell, &args)), - &mut env, + Some(( + &remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + &args, + )), + env, path, remote_client, cx, @@ -201,7 +207,7 @@ impl Project { .command .as_ref() .map(|command| (command, &spawn_task.args)), - &mut env, + env, path, remote_client, cx, @@ -220,13 +226,16 @@ impl Project { #[cfg(not(windows))] let arg = format!("{activation_script}; {to_run}"); - Shell::WithArguments { - program: shell, - args: vec!["-c".to_owned(), arg], - title_override: None, - } + ( + Shell::WithArguments { + program: shell, + args: vec!["-c".to_owned(), arg], + title_override: None, + }, + env, + ) } - _ => { + _ => ( if let Some(program) = spawn_task.command { Shell::WithArguments { program, @@ -235,8 +244,9 @@ impl Project { } } else { Shell::System - } - } + }, + env, + ), }, } }; @@ -330,7 +340,7 @@ impl Project { .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)) .collect::>(); let remote_client = self.remote_client.clone(); - let shell = match &remote_client { + let shell_kind = ShellKind::new(&match &remote_client { Some(remote_client) => remote_client .read(cx) .shell() @@ -344,7 +354,7 @@ impl Project { } => program.clone(), Shell::System => get_system_shell(), }, - }; + }); let lang_registry = self.languages.clone(); let fs = self.fs.clone(); @@ -361,7 +371,7 @@ impl Project { let lister = language?.toolchain_lister(); return Some( lister? - .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref()) + .activation_script(&toolchain, shell_kind, fs.as_ref()) .await, ); } @@ -370,12 +380,12 @@ impl Project { .await .unwrap_or_default(); project.update(cx, move |this, cx| { - let shell = { + let (shell, env) = { match remote_client { Some(remote_client) => { - create_remote_shell(None, &mut env, path, remote_client, cx)? + create_remote_shell(None, env, path, remote_client, cx)? } - None => settings.shell, + None => (settings.shell, env), } }; TerminalBuilder::new( @@ -545,11 +555,11 @@ fn quote_arg(argument: &str, quote: bool) -> String { fn create_remote_shell( spawn_command: Option<(&String, &Vec)>, - env: &mut HashMap, + mut env: HashMap, working_directory: Option>, remote_client: Entity, cx: &mut App, -) -> Result { +) -> Result<(Shell, HashMap)> { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed // to properly display colors. // We do not have the luxury of assuming the host has it installed, @@ -565,18 +575,20 @@ fn create_remote_shell( let command = remote_client.read(cx).build_command( program, args.as_slice(), - env, + &env, working_directory.map(|path| path.display().to_string()), None, )?; - *env = command.env; log::debug!("Connecting to a remote server: {:?}", command.program); let host = remote_client.read(cx).connection_options().display_name(); - Ok(Shell::WithArguments { - program: command.program, - args: command.args, - title_override: Some(format!("{} — Terminal", host).into()), - }) + Ok(( + Shell::WithArguments { + program: command.program, + args: command.args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + command.env, + )) } diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 0ccea81ec69425554ad8faf7d63e528728403594..0363fc721a2d51971f49112af520b5dd34b52cb1 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -772,6 +772,10 @@ impl RemoteClient { Some(self.remote_connection()?.shell()) } + pub fn default_system_shell(&self) -> Option { + Some(self.remote_connection()?.default_system_shell()) + } + pub fn shares_network_interface(&self) -> bool { self.remote_connection() .map_or(false, |connection| connection.shares_network_interface()) @@ -1062,6 +1066,7 @@ pub(crate) trait RemoteConnection: Send + Sync { fn connection_options(&self) -> RemoteConnectionOptions; fn path_style(&self) -> PathStyle; fn shell(&self) -> String; + fn default_system_shell(&self) -> String; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1503,6 +1508,10 @@ mod fake { fn shell(&self) -> String { "sh".to_owned() } + + fn default_system_shell(&self) -> String { + "sh".to_owned() + } } pub(super) struct Delegate; diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 932cb7145e4404c4529c483562e626205831d145..2a589ea836fc2576c4afd0fcff1e7b35aa6fde73 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -37,6 +37,7 @@ pub(crate) struct SshRemoteConnection { ssh_platform: RemotePlatform, ssh_path_style: PathStyle, ssh_shell: String, + ssh_default_system_shell: String, _temp_dir: TempDir, } @@ -105,6 +106,10 @@ impl RemoteConnection for SshRemoteConnection { self.ssh_shell.clone() } + fn default_system_shell(&self) -> String { + self.ssh_default_system_shell.clone() + } + fn build_command( &self, input_program: Option, @@ -347,6 +352,7 @@ impl SshRemoteConnection { _ => PathStyle::Posix, }; let ssh_shell = socket.shell().await; + let ssh_default_system_shell = String::from("/bin/sh"); let mut this = Self { socket, @@ -356,6 +362,7 @@ impl SshRemoteConnection { ssh_path_style, ssh_platform, ssh_shell, + ssh_default_system_shell, }; let (release_channel, version, commit) = cx.update(|cx| { diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index 2b4d29eafeede14f305c4d21f61188b858253285..6b386ee361c763e30c9e31c15b47c836ef922dae 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -29,6 +29,7 @@ pub(crate) struct WslRemoteConnection { remote_binary_path: Option, platform: RemotePlatform, shell: String, + default_system_shell: String, connection_options: WslConnectionOptions, } @@ -56,6 +57,7 @@ impl WslRemoteConnection { remote_binary_path: None, platform: RemotePlatform { os: "", arch: "" }, shell: String::new(), + default_system_shell: String::from("/bin/sh"), }; delegate.set_status(Some("Detecting WSL environment"), cx); this.platform = this.detect_platform().await?; @@ -84,7 +86,11 @@ impl WslRemoteConnection { .run_wsl_command("sh", &["-c", "echo $SHELL"]) .await .ok() - .and_then(|shell_path| shell_path.trim().split('/').next_back().map(str::to_string)) + .and_then(|shell_path| { + Path::new(shell_path.trim()) + .file_name() + .map(|it| it.to_str().unwrap().to_owned()) + }) .unwrap_or_else(|| "bash".to_string())) } @@ -427,6 +433,10 @@ impl RemoteConnection for WslRemoteConnection { fn shell(&self) -> String { self.shell.clone() } + + fn default_system_shell(&self) -> String { + self.default_system_shell.clone() + } } /// `wslpath` is a executable available in WSL, it's a linux binary. diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 4688ac0eb9dd306d0dedbe07c98adbfb5df4f45b..c3f0646c02cc427a07505c2ff30157e84d2ca0fe 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -10,7 +10,7 @@ pub enum ShellKind { Posix, Csh, Fish, - Powershell, + PowerShell, Nushell, Cmd, } @@ -21,7 +21,7 @@ impl fmt::Display for ShellKind { ShellKind::Posix => write!(f, "sh"), ShellKind::Csh => write!(f, "csh"), ShellKind::Fish => write!(f, "fish"), - ShellKind::Powershell => write!(f, "powershell"), + ShellKind::PowerShell => write!(f, "powershell"), ShellKind::Nushell => write!(f, "nu"), ShellKind::Cmd => write!(f, "cmd"), } @@ -43,7 +43,7 @@ impl ShellKind { || program == "pwsh" || program.ends_with("pwsh.exe") { - ShellKind::Powershell + ShellKind::PowerShell } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd } else if program == "nu" { @@ -61,7 +61,7 @@ impl ShellKind { fn to_shell_variable(self, input: &str) -> String { match self { - Self::Powershell => Self::to_powershell_variable(input), + Self::PowerShell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), Self::Fish => input.to_owned(), @@ -184,7 +184,7 @@ impl ShellKind { fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { match self { - ShellKind::Powershell => vec!["-C".to_owned(), combined_command], + ShellKind::PowerShell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive .then(|| "-i".to_owned()) @@ -196,7 +196,7 @@ impl ShellKind { pub fn command_prefix(&self) -> Option { match self { - ShellKind::Powershell => Some('&'), + ShellKind::PowerShell => Some('&'), ShellKind::Nushell => Some('^'), _ => None, } @@ -210,6 +210,7 @@ pub struct ShellBuilder { program: String, args: Vec, interactive: bool, + redirect_stdin: bool, kind: ShellKind, } @@ -231,6 +232,7 @@ impl ShellBuilder { args, interactive: true, kind, + redirect_stdin: false, } } pub fn non_interactive(mut self) -> Self { @@ -241,7 +243,7 @@ impl ShellBuilder { /// Returns the label to show in the terminal tab pub fn command_label(&self, command_label: &str) -> String { match self.kind { - ShellKind::Powershell => { + ShellKind::PowerShell => { format!("{} -C '{}'", self.program, command_label) } ShellKind::Cmd => { @@ -256,6 +258,12 @@ impl ShellBuilder { } } } + + pub fn redirect_stdin_to_dev_null(mut self) -> Self { + self.redirect_stdin = true; + self + } + /// Returns the program and arguments to run this task in a shell. pub fn build( mut self, @@ -263,11 +271,24 @@ impl ShellBuilder { task_args: &[String], ) -> (String, Vec) { if let Some(task_command) = task_command { - let combined_command = task_args.iter().fold(task_command, |mut command, arg| { + 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 }); + if self.redirect_stdin { + match self.kind { + ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => { + combined_command.push_str(" { + combined_command.insert_str(0, "$null | "); + } + ShellKind::Cmd => { + combined_command.push_str("< NUL"); + } + } + } self.args .extend(self.kind.args_for_shell(self.interactive, combined_command));