From bcac748c2bc4aaa25bda987ce1c42e17f2c9a18d Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:57:37 -0400 Subject: [PATCH] Add support for Nushell in shell builder (#33806) We also swap out env variables before sending them to shells now in the task system. This fixed issues Fish and Nushell had where an empty argument could be sent into a command when no argument should be sent. This only happened from task's generated by Zed. Closes #31297 #31240 Release Notes: - Fix bug where spawning a Zed generated task or debug session with Fish or Nushell failed --- crates/task/src/shell_builder.rs | 121 ++++++++++++++++++++++++++++++- crates/task/src/task_template.rs | 10 +-- 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 5446637139..b8c49d4230 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -5,6 +5,7 @@ enum ShellKind { #[default] Posix, Powershell, + Nushell, Cmd, } @@ -18,6 +19,8 @@ impl ShellKind { ShellKind::Powershell } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd + } else if program == "nu" { + ShellKind::Nushell } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -30,6 +33,7 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), + Self::Nushell => Self::to_nushell_variable(input), } } @@ -70,11 +74,86 @@ impl ShellKind { } } + fn to_nushell_variable(input: &str) -> String { + let mut result = String::new(); + let mut source = input; + let mut is_start = true; + + loop { + match source.chars().next() { + None => return result, + Some('$') => { + source = Self::parse_nushell_var(&source[1..], &mut result, is_start); + is_start = false; + } + Some(_) => { + is_start = false; + let chunk_end = source.find('$').unwrap_or(source.len()); + let (chunk, rest) = source.split_at(chunk_end); + result.push_str(chunk); + source = rest; + } + } + } + } + + fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { + if source.starts_with("env.") { + text.push('$'); + return source; + } + + match source.chars().next() { + Some('{') => { + let source = &source[1..]; + if let Some(end) = source.find('}') { + let var_name = &source[..end]; + if !var_name.is_empty() { + if !is_start { + text.push_str("("); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push_str(")"); + } + &source[end + 1..] + } else { + text.push_str("${}"); + &source[end + 1..] + } + } else { + text.push_str("${"); + source + } + } + Some(c) if c.is_alphabetic() || c == '_' => { + let end = source + .find(|c: char| !c.is_alphanumeric() && c != '_') + .unwrap_or(source.len()); + let var_name = &source[..end]; + if !is_start { + text.push_str("("); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push_str(")"); + } + &source[end..] + } + _ => { + text.push('$'); + source + } + } + } + fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix => interactive + ShellKind::Posix | ShellKind::Nushell => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -142,9 +221,12 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix => { + ShellKind::Posix | ShellKind::Nushell => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); - format!("{} {interactivity}-c '{}'", self.program, command_label) + format!( + "{} {interactivity}-c '$\"{}\"'", + self.program, command_label + ) } } } @@ -170,3 +252,36 @@ impl ShellBuilder { (self.program, self.args) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_nu_shell_variable_substitution() { + let shell = Shell::Program("nu".to_owned()); + let shell_builder = ShellBuilder::new(true, &shell); + + let (program, args) = shell_builder.build( + Some("echo".into()), + &vec![ + "${hello}".to_string(), + "$world".to_string(), + "nothing".to_string(), + "--$something".to_string(), + "$".to_string(), + "${test".to_string(), + ], + ); + + assert_eq!(program, "nu"); + assert_eq!( + args, + vec![ + "-i", + "-c", + "echo $env.hello $env.world nothing --($env.something) $ ${test" + ] + ); + } +} diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index ae5054ac55..24e11d7715 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -256,7 +256,7 @@ impl TaskTemplate { }, ), command: Some(command), - args: self.args.clone(), + args: args_with_substitutions, env, use_new_terminal: self.use_new_terminal, allow_concurrent_runs: self.allow_concurrent_runs, @@ -642,11 +642,11 @@ mod tests { assert_eq!( spawn_in_terminal.args, &[ - "arg1 $ZED_SELECTED_TEXT", - "arg2 $ZED_COLUMN", - "arg3 $ZED_SYMBOL", + "arg1 test_selected_text", + "arg2 5678", + "arg3 010101010101010101010101010101010101010101010101010101010101", ], - "Args should not be substituted with variables" + "Args should be substituted with variables" ); assert_eq!( spawn_in_terminal.command_label,