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
This commit is contained in:
Anthony Eid 2025-07-08 10:57:37 -04:00 committed by GitHub
parent 0ca0914cca
commit bcac748c2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 8 deletions

View file

@ -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<String> {
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"
]
);
}
}

View file

@ -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,