mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Use shell to launch MCP and ACP servers (#42382)
`npx`, and any `npm install`-ed programs, exist as batch scripts/PowerShell scripts on the PATH. We have to use a shell to launch these programs. Fixes https://github.com/zed-industries/zed/issues/41435 Closes https://github.com/zed-industries/zed/pull/42651 Release Notes: - windows: Custom MCP and ACP servers installed through `npm` now launch correctly. --------- Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
parent
8ee85eab3c
commit
d83201256d
12 changed files with 389 additions and 43 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3595,6 +3595,7 @@ dependencies = [
|
|||
"settings",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"url",
|
||||
"util",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ use futures::io::BufReader;
|
|||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use task::ShellBuilder;
|
||||
#[cfg(windows)]
|
||||
use task::ShellKind;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -21,7 +25,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
|
|||
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
||||
use terminal::TerminalBuilder;
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape};
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
|
|
@ -86,9 +90,26 @@ impl AcpConnection {
|
|||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path);
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows));
|
||||
#[cfg(windows)]
|
||||
let kind = builder.kind();
|
||||
let (cmd, args) = builder.build(Some(command.path.display().to_string()), &command.args);
|
||||
|
||||
let mut child = util::command::new_smol_command(cmd);
|
||||
#[cfg(windows)]
|
||||
if kind == ShellKind::Cmd {
|
||||
use smol::process::windows::CommandExt;
|
||||
for arg in args {
|
||||
child.raw_arg(arg);
|
||||
}
|
||||
} else {
|
||||
child.args(args);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
child.args(args);
|
||||
|
||||
child
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ impl AgentServer for CustomAgentServer {
|
|||
let default_model = self.default_model(cx);
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ smol.workspace = true
|
|||
tempfile.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
terminal.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ use futures::{
|
|||
AsyncBufReadExt as _, AsyncRead, AsyncWrite, AsyncWriteExt as _, Stream, StreamExt as _,
|
||||
};
|
||||
use gpui::AsyncApp;
|
||||
use settings::Settings as _;
|
||||
use smol::channel;
|
||||
use smol::process::Child;
|
||||
use terminal::terminal_settings::TerminalSettings;
|
||||
use util::TryFutureExt as _;
|
||||
use util::shell_builder::ShellBuilder;
|
||||
|
||||
use crate::client::ModelContextServerBinary;
|
||||
use crate::transport::Transport;
|
||||
|
|
@ -28,9 +31,14 @@ impl StdioTransport {
|
|||
working_directory: &Option<PathBuf>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows));
|
||||
let (command, args) =
|
||||
builder.build(Some(binary.executable.display().to_string()), &binary.args);
|
||||
|
||||
let mut command = util::command::new_smol_command(command);
|
||||
command
|
||||
.args(&binary.args)
|
||||
.args(args)
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -1344,7 +1344,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 | ShellKind::Pwsh => None,
|
||||
ShellKind::Csh => None,
|
||||
ShellKind::Tcsh => None,
|
||||
ShellKind::Cmd => None,
|
||||
|
|
|
|||
|
|
@ -411,11 +411,11 @@ impl ContextServerStore {
|
|||
) {
|
||||
self.stop_server(&id, cx).log_err();
|
||||
}
|
||||
|
||||
let task = cx.spawn({
|
||||
let id = server.id();
|
||||
let server = server.clone();
|
||||
let configuration = configuration.clone();
|
||||
|
||||
async move |this, cx| {
|
||||
match server.clone().start(cx).await {
|
||||
Ok(_) => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ use tempfile::TempDir;
|
|||
use util::{
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
rel_path::RelPath,
|
||||
shell::ShellKind,
|
||||
shell::{Shell, ShellKind},
|
||||
shell_builder::ShellBuilder,
|
||||
};
|
||||
|
||||
pub(crate) struct SshRemoteConnection {
|
||||
|
|
@ -1362,6 +1363,8 @@ fn build_command(
|
|||
} else {
|
||||
write!(exec, "{ssh_shell} -l")?;
|
||||
};
|
||||
let (command, command_args) = ShellBuilder::new(&Shell::Program(ssh_shell.to_owned()), false)
|
||||
.build(Some(exec.clone()), &[]);
|
||||
|
||||
let mut args = Vec::new();
|
||||
args.extend(ssh_args);
|
||||
|
|
@ -1372,7 +1375,9 @@ fn build_command(
|
|||
}
|
||||
|
||||
args.push("-t".into());
|
||||
args.push(exec);
|
||||
args.push(command);
|
||||
args.extend(command_args);
|
||||
|
||||
Ok(CommandTemplate {
|
||||
program: "ssh".into(),
|
||||
args,
|
||||
|
|
@ -1411,6 +1416,9 @@ mod tests {
|
|||
"-p",
|
||||
"2222",
|
||||
"-t",
|
||||
"/bin/fish",
|
||||
"-i",
|
||||
"-c",
|
||||
"cd \"$HOME/work\" && exec env INPUT_VA=val remote_program arg1 arg2"
|
||||
]
|
||||
);
|
||||
|
|
@ -1443,6 +1451,9 @@ mod tests {
|
|||
"-L",
|
||||
"1:foo:2",
|
||||
"-t",
|
||||
"/bin/fish",
|
||||
"-i",
|
||||
"-c",
|
||||
"cd && exec env INPUT_VA=val /bin/fish -l"
|
||||
]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ use std::{
|
|||
use util::{
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
rel_path::RelPath,
|
||||
shell::ShellKind,
|
||||
shell::{Shell, ShellKind},
|
||||
shell_builder::ShellBuilder,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -453,8 +454,10 @@ impl RemoteConnection for WslRemoteConnection {
|
|||
} else {
|
||||
write!(&mut exec, "{} -l", self.shell)?;
|
||||
}
|
||||
let (command, args) =
|
||||
ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]);
|
||||
|
||||
let wsl_args = if let Some(user) = &self.connection_options.user {
|
||||
let mut wsl_args = if let Some(user) = &self.connection_options.user {
|
||||
vec![
|
||||
"--distribution".to_string(),
|
||||
self.connection_options.distro_name.clone(),
|
||||
|
|
@ -463,9 +466,7 @@ impl RemoteConnection for WslRemoteConnection {
|
|||
"--cd".to_string(),
|
||||
working_dir,
|
||||
"--".to_string(),
|
||||
self.shell.clone(),
|
||||
"-c".to_string(),
|
||||
exec,
|
||||
command,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
|
|
@ -474,11 +475,10 @@ impl RemoteConnection for WslRemoteConnection {
|
|||
"--cd".to_string(),
|
||||
working_dir,
|
||||
"--".to_string(),
|
||||
self.shell.clone(),
|
||||
"-c".to_string(),
|
||||
exec,
|
||||
command,
|
||||
]
|
||||
};
|
||||
wsl_args.extend(args);
|
||||
|
||||
Ok(CommandTemplate {
|
||||
program: "wsl.exe".to_string(),
|
||||
|
|
|
|||
|
|
@ -56,7 +56,10 @@ pub enum ShellKind {
|
|||
Tcsh,
|
||||
Rc,
|
||||
Fish,
|
||||
/// Pre-installed "legacy" powershell for windows
|
||||
PowerShell,
|
||||
/// PowerShell 7.x
|
||||
Pwsh,
|
||||
Nushell,
|
||||
Cmd,
|
||||
Xonsh,
|
||||
|
|
@ -238,6 +241,7 @@ impl fmt::Display for ShellKind {
|
|||
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"),
|
||||
|
|
@ -260,7 +264,8 @@ impl ShellKind {
|
|||
.to_string_lossy();
|
||||
|
||||
match &*program {
|
||||
"powershell" | "pwsh" => ShellKind::PowerShell,
|
||||
"powershell" => ShellKind::PowerShell,
|
||||
"pwsh" => ShellKind::Pwsh,
|
||||
"cmd" => ShellKind::Cmd,
|
||||
"nu" => ShellKind::Nushell,
|
||||
"fish" => ShellKind::Fish,
|
||||
|
|
@ -279,7 +284,7 @@ impl ShellKind {
|
|||
|
||||
pub fn to_shell_variable(self, input: &str) -> String {
|
||||
match self {
|
||||
Self::PowerShell => Self::to_powershell_variable(input),
|
||||
Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input),
|
||||
Self::Cmd => Self::to_cmd_variable(input),
|
||||
Self::Posix => input.to_owned(),
|
||||
Self::Fish => input.to_owned(),
|
||||
|
|
@ -407,8 +412,12 @@ impl ShellKind {
|
|||
|
||||
pub 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::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command],
|
||||
ShellKind::Cmd => vec![
|
||||
"/S".to_owned(),
|
||||
"/C".to_owned(),
|
||||
format!("\"{combined_command}\""),
|
||||
],
|
||||
ShellKind::Posix
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Fish
|
||||
|
|
@ -426,7 +435,7 @@ impl ShellKind {
|
|||
|
||||
pub const fn command_prefix(&self) -> Option<char> {
|
||||
match self {
|
||||
ShellKind::PowerShell => Some('&'),
|
||||
ShellKind::PowerShell | ShellKind::Pwsh => Some('&'),
|
||||
ShellKind::Nushell => Some('^'),
|
||||
ShellKind::Posix
|
||||
| ShellKind::Csh
|
||||
|
|
@ -457,6 +466,7 @@ impl ShellKind {
|
|||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::PowerShell
|
||||
| ShellKind::Pwsh
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Xonsh
|
||||
| ShellKind::Elvish => ';',
|
||||
|
|
@ -471,6 +481,7 @@ impl ShellKind {
|
|||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Pwsh
|
||||
| ShellKind::PowerShell
|
||||
| ShellKind::Xonsh => "&&",
|
||||
ShellKind::Nushell | ShellKind::Elvish => ";",
|
||||
|
|
@ -478,11 +489,10 @@ impl ShellKind {
|
|||
}
|
||||
|
||||
pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
|
||||
shlex::try_quote(arg).ok().map(|arg| match self {
|
||||
// If we are running in PowerShell, we want to take extra care when escaping strings.
|
||||
// In particular, we want to escape strings with a backtick (`) rather than a backslash (\).
|
||||
ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"").replace("\\\\", "\\")),
|
||||
ShellKind::Cmd => Cow::Owned(arg.replace("\\\\", "\\")),
|
||||
match self {
|
||||
ShellKind::PowerShell => Some(Self::quote_powershell(arg)),
|
||||
ShellKind::Pwsh => Some(Self::quote_pwsh(arg)),
|
||||
ShellKind::Cmd => Some(Self::quote_cmd(arg)),
|
||||
ShellKind::Posix
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
|
|
@ -490,8 +500,173 @@ impl ShellKind {
|
|||
| ShellKind::Fish
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Xonsh
|
||||
| ShellKind::Elvish => arg,
|
||||
})
|
||||
| ShellKind::Elvish => shlex::try_quote(arg).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
|
||||
if arg.is_empty() {
|
||||
return Cow::Borrowed("\"\"");
|
||||
}
|
||||
|
||||
let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"');
|
||||
if !needs_quoting {
|
||||
return Cow::Borrowed(arg);
|
||||
}
|
||||
|
||||
let mut result = String::with_capacity(arg.len() + 2);
|
||||
|
||||
if enclose {
|
||||
result.push('"');
|
||||
}
|
||||
|
||||
let chars: Vec<char> = arg.chars().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
if chars[i] == '\\' {
|
||||
let mut num_backslashes = 0;
|
||||
while i < chars.len() && chars[i] == '\\' {
|
||||
num_backslashes += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if i < chars.len() && chars[i] == '"' {
|
||||
// Backslashes followed by quote: double the backslashes and escape the quote
|
||||
for _ in 0..(num_backslashes * 2 + 1) {
|
||||
result.push('\\');
|
||||
}
|
||||
result.push('"');
|
||||
i += 1;
|
||||
} else if i >= chars.len() {
|
||||
// Trailing backslashes: double them (they precede the closing quote)
|
||||
for _ in 0..(num_backslashes * 2) {
|
||||
result.push('\\');
|
||||
}
|
||||
} else {
|
||||
// Backslashes not followed by quote: output as-is
|
||||
for _ in 0..num_backslashes {
|
||||
result.push('\\');
|
||||
}
|
||||
}
|
||||
} else if chars[i] == '"' {
|
||||
// Quote not preceded by backslash: escape it
|
||||
result.push('\\');
|
||||
result.push('"');
|
||||
i += 1;
|
||||
} else {
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if enclose {
|
||||
result.push('"');
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
|
||||
fn needs_quoting_powershell(s: &str) -> bool {
|
||||
s.is_empty()
|
||||
|| s.chars().any(|c| {
|
||||
c.is_whitespace()
|
||||
|| matches!(
|
||||
c,
|
||||
'"' | '`'
|
||||
| '$'
|
||||
| '&'
|
||||
| '|'
|
||||
| '<'
|
||||
| '>'
|
||||
| ';'
|
||||
| '('
|
||||
| ')'
|
||||
| '['
|
||||
| ']'
|
||||
| '{'
|
||||
| '}'
|
||||
| ','
|
||||
| '\''
|
||||
| '@'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn need_quotes_powershell(arg: &str) -> bool {
|
||||
let mut quote_count = 0;
|
||||
for c in arg.chars() {
|
||||
if c == '"' {
|
||||
quote_count += 1;
|
||||
} else if c.is_whitespace() && (quote_count % 2 == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn escape_powershell_quotes(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len() + 4);
|
||||
result.push('\'');
|
||||
for c in s.chars() {
|
||||
if c == '\'' {
|
||||
result.push('\'');
|
||||
}
|
||||
result.push(c);
|
||||
}
|
||||
result.push('\'');
|
||||
result
|
||||
}
|
||||
|
||||
pub fn quote_powershell(arg: &str) -> Cow<'_, str> {
|
||||
let ps_will_quote = Self::need_quotes_powershell(arg);
|
||||
let crt_quoted = Self::quote_windows(arg, !ps_will_quote);
|
||||
|
||||
if !Self::needs_quoting_powershell(arg) {
|
||||
return crt_quoted;
|
||||
}
|
||||
|
||||
Cow::Owned(Self::escape_powershell_quotes(&crt_quoted))
|
||||
}
|
||||
|
||||
pub fn quote_pwsh(arg: &str) -> Cow<'_, str> {
|
||||
if arg.is_empty() {
|
||||
return Cow::Borrowed("''");
|
||||
}
|
||||
|
||||
if !Self::needs_quoting_powershell(arg) {
|
||||
return Cow::Borrowed(arg);
|
||||
}
|
||||
|
||||
Cow::Owned(Self::escape_powershell_quotes(arg))
|
||||
}
|
||||
|
||||
pub fn quote_cmd(arg: &str) -> Cow<'_, str> {
|
||||
let crt_quoted = Self::quote_windows(arg, true);
|
||||
|
||||
let needs_cmd_escaping = crt_quoted.contains('"')
|
||||
|| crt_quoted.contains('%')
|
||||
|| crt_quoted
|
||||
.chars()
|
||||
.any(|c| matches!(c, '^' | '<' | '>' | '&' | '|' | '(' | ')'));
|
||||
|
||||
if !needs_cmd_escaping {
|
||||
return crt_quoted;
|
||||
}
|
||||
|
||||
let mut result = String::with_capacity(crt_quoted.len() * 2);
|
||||
for c in crt_quoted.chars() {
|
||||
match c {
|
||||
'^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => {
|
||||
result.push('^');
|
||||
result.push(c);
|
||||
}
|
||||
'%' => {
|
||||
result.push_str("%%cd:~,%");
|
||||
}
|
||||
_ => result.push(c),
|
||||
}
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
|
||||
/// Quotes the given argument if necessary, taking into account the command prefix.
|
||||
|
|
@ -538,7 +713,7 @@ impl ShellKind {
|
|||
match self {
|
||||
ShellKind::Cmd => "",
|
||||
ShellKind::Nushell => "overlay use",
|
||||
ShellKind::PowerShell => ".",
|
||||
ShellKind::PowerShell | ShellKind::Pwsh => ".",
|
||||
ShellKind::Fish
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
|
|
@ -558,6 +733,7 @@ impl ShellKind {
|
|||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::PowerShell
|
||||
| ShellKind::Pwsh
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Xonsh
|
||||
| ShellKind::Elvish => "clear",
|
||||
|
|
@ -576,6 +752,7 @@ impl ShellKind {
|
|||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::PowerShell
|
||||
| ShellKind::Pwsh
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Xonsh
|
||||
| ShellKind::Elvish => true,
|
||||
|
|
@ -605,7 +782,7 @@ mod tests {
|
|||
.try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
"\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest `\"test_foo.py::test_foo`\"\"".to_string()
|
||||
"'C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -617,7 +794,113 @@ mod tests {
|
|||
.try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
"\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"".to_string()
|
||||
"^\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\^\"test_foo.py::test_foo\\^\"^\"".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_quote_powershell_edge_cases() {
|
||||
let shell_kind = ShellKind::PowerShell;
|
||||
|
||||
// Empty string
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("").unwrap().into_owned(),
|
||||
"'\"\"'".to_string()
|
||||
);
|
||||
|
||||
// String without special characters (no quoting needed)
|
||||
assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
|
||||
|
||||
// String with spaces
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("hello world").unwrap().into_owned(),
|
||||
"'hello world'".to_string()
|
||||
);
|
||||
|
||||
// String with dollar signs
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("$variable").unwrap().into_owned(),
|
||||
"'$variable'".to_string()
|
||||
);
|
||||
|
||||
// String with backticks
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("test`command").unwrap().into_owned(),
|
||||
"'test`command'".to_string()
|
||||
);
|
||||
|
||||
// String with multiple special characters
|
||||
assert_eq!(
|
||||
shell_kind
|
||||
.try_quote("test `\"$var`\" end")
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
"'test `\\\"$var`\\\" end'".to_string()
|
||||
);
|
||||
|
||||
// String with backslashes and colon (path without spaces doesn't need quoting)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
|
||||
"C:\\path\\to\\file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_quote_cmd_edge_cases() {
|
||||
let shell_kind = ShellKind::Cmd;
|
||||
|
||||
// Empty string
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("").unwrap().into_owned(),
|
||||
"^\"^\"".to_string()
|
||||
);
|
||||
|
||||
// String without special characters (no quoting needed)
|
||||
assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
|
||||
|
||||
// String with spaces
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("hello world").unwrap().into_owned(),
|
||||
"^\"hello world^\"".to_string()
|
||||
);
|
||||
|
||||
// String with space and backslash (backslash not at end, so not doubled)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("path\\ test").unwrap().into_owned(),
|
||||
"^\"path\\ test^\"".to_string()
|
||||
);
|
||||
|
||||
// String ending with backslash (must be doubled before closing quote)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("test path\\").unwrap().into_owned(),
|
||||
"^\"test path\\\\^\"".to_string()
|
||||
);
|
||||
|
||||
// String ending with multiple backslashes (all doubled before closing quote)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("test path\\\\").unwrap().into_owned(),
|
||||
"^\"test path\\\\\\\\^\"".to_string()
|
||||
);
|
||||
|
||||
// String with embedded quote (quote is escaped, backslash before it is doubled)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("test\\\"quote").unwrap().into_owned(),
|
||||
"^\"test\\\\\\^\"quote^\"".to_string()
|
||||
);
|
||||
|
||||
// String with multiple backslashes before embedded quote (all doubled)
|
||||
assert_eq!(
|
||||
shell_kind
|
||||
.try_quote("test\\\\\"quote")
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
"^\"test\\\\\\\\\\^\"quote^\"".to_string()
|
||||
);
|
||||
|
||||
// String with backslashes not before quotes (path without spaces doesn't need quoting)
|
||||
assert_eq!(
|
||||
shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
|
||||
"C:\\path\\to\\file"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::shell::get_system_shell;
|
||||
use crate::shell::{Shell, ShellKind};
|
||||
|
||||
|
|
@ -42,7 +44,7 @@ impl ShellBuilder {
|
|||
self.program.clone()
|
||||
} else {
|
||||
match self.kind {
|
||||
ShellKind::PowerShell => {
|
||||
ShellKind::PowerShell | ShellKind::Pwsh => {
|
||||
format!("{} -C '{}'", self.program, command_to_use_in_label)
|
||||
}
|
||||
ShellKind::Cmd => {
|
||||
|
|
@ -78,11 +80,27 @@ impl ShellBuilder {
|
|||
task_args: &[String],
|
||||
) -> (String, Vec<String>) {
|
||||
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
|
||||
});
|
||||
let task_command = self.kind.prepend_command_prefix(&task_command);
|
||||
let task_command = if !task_args.is_empty() {
|
||||
match self.kind.try_quote_prefix_aware(&task_command) {
|
||||
Some(task_command) => task_command,
|
||||
None => task_command,
|
||||
}
|
||||
} else {
|
||||
task_command
|
||||
};
|
||||
let mut combined_command =
|
||||
task_args
|
||||
.iter()
|
||||
.fold(task_command.into_owned(), |mut command, arg| {
|
||||
command.push(' ');
|
||||
let shell_variable = self.kind.to_shell_variable(arg);
|
||||
command.push_str(&match self.kind.try_quote(&shell_variable) {
|
||||
Some(shell_variable) => shell_variable,
|
||||
None => Cow::Owned(shell_variable),
|
||||
});
|
||||
command
|
||||
});
|
||||
if self.redirect_stdin {
|
||||
match self.kind {
|
||||
ShellKind::Fish => {
|
||||
|
|
@ -99,7 +117,7 @@ impl ShellBuilder {
|
|||
combined_command.insert(0, '(');
|
||||
combined_command.push_str(") </dev/null");
|
||||
}
|
||||
ShellKind::PowerShell => {
|
||||
ShellKind::PowerShell | ShellKind::Pwsh => {
|
||||
combined_command.insert_str(0, "$null | & {");
|
||||
combined_command.push_str("}");
|
||||
}
|
||||
|
|
@ -115,6 +133,10 @@ impl ShellBuilder {
|
|||
|
||||
(self.program, self.args)
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ShellKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -144,7 +166,7 @@ mod test {
|
|||
vec![
|
||||
"-i",
|
||||
"-c",
|
||||
"echo $env.hello $env.world nothing --($env.something) $ ${test"
|
||||
"^echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -159,7 +181,7 @@ mod test {
|
|||
.build(Some("echo".into()), &["nothing".to_string()]);
|
||||
|
||||
assert_eq!(program, "nu");
|
||||
assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
|
||||
assert_eq!(args, vec!["-i", "-c", "(^echo nothing) </dev/null"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ async fn capture_windows(
|
|||
zed_path.display()
|
||||
),
|
||||
]),
|
||||
ShellKind::PowerShell => cmd.args([
|
||||
ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
|
|
|
|||
Loading…
Reference in a new issue