Skip to content

Commit 435984b

Browse files
committed
feat: add Preprae::with_quoted_command() (#1799)
That way it's possible to execute shell commands with spaces in their paths, for example.
1 parent cc7b614 commit 435984b

File tree

4 files changed

+72
-0
lines changed

4 files changed

+72
-0
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-command/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ doctest = false
1717
[dependencies]
1818
gix-trace = { version = "^0.1.12", path = "../gix-trace" }
1919
gix-path = { version = "^0.10.14", path = "../gix-path" }
20+
gix-quote = { version = "^0.4.15", path = "../gix-quote" }
2021

2122
bstr = { version = "1.5.0", default-features = false, features = ["std", "unicode"] }
2223
shell-words = "1.0"

gix-command/src/lib.rs

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub struct Prepare {
2828
pub env: Vec<(OsString, OsString)>,
2929
/// If `true`, we will use `shell_program` or `sh` to execute the `command`.
3030
pub use_shell: bool,
31+
/// If `true`, `command` is assumed to be a command or path to the program to execute, and it will be shell-quoted
32+
/// to assure it will be executed as is and without splitting across whitespace.
33+
pub quote_command: bool,
3134
/// The name or path to the shell program to use instead of `sh`.
3235
pub shell_program: Option<OsString>,
3336
/// If `true` (default `true` on windows and `false` everywhere else)
@@ -119,6 +122,15 @@ mod prepare {
119122
self.use_shell = true;
120123
self
121124
}
125+
126+
/// If [`with_shell()`](Self::with_shell()) is set, then quote the command to assure its path is left intact.
127+
///
128+
/// Note that this should not be used if the command is a script - quoting is only the right choice if it's known to be a program path.
129+
pub fn with_quoted_command(mut self) -> Self {
130+
self.quote_command = true;
131+
self
132+
}
133+
122134
/// Set the name or path to the shell `program` to use if a shell is to be used, to avoid using the default shell which is `sh`.
123135
pub fn with_shell_program(mut self, program: impl Into<OsString>) -> Self {
124136
self.shell_program = Some(program.into());
@@ -236,6 +248,11 @@ mod prepare {
236248
cmd.arg("-c");
237249
if !prep.args.is_empty() {
238250
if prep.command.to_str().map_or(true, |cmd| !cmd.contains("$@")) {
251+
if prep.quote_command {
252+
if let Ok(command) = gix_path::os_str_into_bstr(&prep.command) {
253+
prep.command = gix_path::from_bstring(gix_quote::single(command)).into();
254+
}
255+
}
239256
prep.command.push(" \"$@\"");
240257
} else {
241258
gix_trace::debug!(
@@ -458,6 +475,7 @@ pub fn prepare(cmd: impl Into<OsString>) -> Prepare {
458475
args: Vec::new(),
459476
env: Vec::new(),
460477
use_shell: false,
478+
quote_command: false,
461479
allow_manual_arg_splitting: cfg!(windows),
462480
}
463481
}

gix-command/tests/command.rs

+52
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,44 @@ mod prepare {
351351
);
352352
}
353353

354+
#[test]
355+
fn quoted_command_without_argument_splitting() {
356+
let cmd = std::process::Command::from(
357+
gix_command::prepare("ls")
358+
.arg("--foo=a b")
359+
.command_may_be_shell_script_disallow_manual_argument_splitting()
360+
.with_shell()
361+
.with_quoted_command(),
362+
);
363+
assert_eq!(
364+
format!("{cmd:?}"),
365+
quoted(&[SH, "-c", "\\'ls\\' \\\"$@\\\"", "--", "--foo=a b"]),
366+
"looks strange thanks to debug printing, but is the right amount of quotes actually"
367+
);
368+
}
369+
370+
#[test]
371+
fn quoted_windows_command_without_argument_splitting() {
372+
let cmd = std::process::Command::from(
373+
gix_command::prepare("C:\\Users\\O'Shaughnessy\\with space.exe")
374+
.arg("--foo='a b'")
375+
.command_may_be_shell_script_disallow_manual_argument_splitting()
376+
.with_shell()
377+
.with_quoted_command(),
378+
);
379+
assert_eq!(
380+
format!("{cmd:?}"),
381+
quoted(&[
382+
SH,
383+
"-c",
384+
"\\'C:\\\\Users\\\\O\\'\\\\\\'\\'Shaughnessy\\\\with space.exe\\' \\\"$@\\\"",
385+
"--",
386+
"--foo=\\'a b\\'"
387+
]),
388+
"again, a lot of extra backslashes, but it's correct outside of the debug formatting"
389+
);
390+
}
391+
354392
#[test]
355393
fn single_and_complex_arguments_will_not_auto_split_on_special_characters() {
356394
let cmd = std::process::Command::from(
@@ -388,6 +426,20 @@ mod prepare {
388426
We deal with it by not doubling the '$@' argument, which seems more flexible."
389427
);
390428
}
429+
430+
#[test]
431+
fn script_with_dollar_at_has_no_quoting() {
432+
let cmd = std::process::Command::from(
433+
gix_command::prepare("echo \"$@\" >&2")
434+
.command_may_be_shell_script()
435+
.with_quoted_command()
436+
.arg("store"),
437+
);
438+
assert_eq!(
439+
format!("{cmd:?}"),
440+
format!(r#""{SH}" "-c" "echo \"$@\" >&2" "--" "store""#)
441+
);
442+
}
391443
}
392444

393445
mod spawn {

0 commit comments

Comments
 (0)