Skip to content

Commit 667de43

Browse files
committed
fix!: rename with_shell() to command_may_be_shell_script(), add with_shell() to enforce using a shell.
That way it's clear the shell will only be used if the command actually is a shell script. This also renames `with_shell_*` variants to `command_may_be_shell_script_*` variants, without a respective `with_shell_*` alternative.
1 parent 7c803fa commit 667de43

File tree

2 files changed

+85
-33
lines changed

2 files changed

+85
-33
lines changed

gix-command/src/lib.rs

+30-14
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,41 @@ mod prepare {
9393

9494
/// Builder
9595
impl Prepare {
96-
/// If called, the command will not be executed directly, but with `sh`, but only if the
97-
/// command passed to [`prepare`](super::prepare()) requires this.
96+
/// If called, the command will be checked for characters that are typical for shell scripts, and if found will use `sh` to execute it
97+
/// or whatever is set as [`with_shell_program()`](Self::with_shell_program()).
98+
/// If the command isn't valid UTF-8, a shell will always be used.
9899
///
99-
/// This also allows to pass shell scripts as command, or use commands that contain arguments which are subsequently
100-
/// parsed by `sh`.
101-
pub fn with_shell(mut self) -> Self {
100+
/// If a shell is used, then arguments given here with [arg()](Self::arg) or [args()](Self::args) will be substituted via `"$@"` if it's not
101+
/// already present in the command.
102+
///
103+
/// If this method is not called, commands are always executed verbatim, without the use of a shell.
104+
pub fn command_may_be_shell_script(mut self) -> Self {
102105
self.use_shell = self.command.to_str().map_or(true, |cmd| {
103106
cmd.as_bytes().find_byteset(b"|&;<>()$`\\\"' \t\n*?[#~=%").is_some()
104107
});
105108
self
106109
}
107110

108-
/// Set the name or path to the shell `program` to use, to avoid using the default shell which is `sh`.
111+
/// If called, unconditionally use a shell to execute the command and its arguments, and `sh` to execute it,
112+
/// or whatever is set as [`with_shell_program()`](Self::with_shell_program()).
113+
///
114+
/// If a shell is used, then arguments given here with [arg()](Self::arg) or [args()](Self::args) will be substituted via `"$@"` if it's not
115+
/// already present in the command.
116+
///
117+
/// If this method is not called, commands are always executed verbatim, without the use of a shell.
118+
pub fn with_shell(mut self) -> Self {
119+
self.use_shell = true;
120+
self
121+
}
122+
/// 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`.
109123
pub fn with_shell_program(mut self, program: impl Into<OsString>) -> Self {
110124
self.shell_program = Some(program.into());
111125
self
112126
}
113127

114128
/// Unconditionally turn off using the shell when spawning the command.
115129
/// Note that not using the shell is the default so an effective use of this method
116-
/// is some time after [`with_shell()`][Prepare::with_shell()] was called.
130+
/// is some time after [`command_may_be_shell_script()`](Prepare::command_may_be_shell_script()) was called.
117131
pub fn without_shell(mut self) -> Self {
118132
self.use_shell = false;
119133
self
@@ -128,18 +142,20 @@ mod prepare {
128142
self
129143
}
130144

131-
/// Use a shell, but try to split arguments by hand if this can be safely done without a shell.
145+
/// Like [`command_may_be_shell_script()`](Prepare::command_may_be_shell_script()), but try to split arguments by hand if this can be safely done without a shell.
132146
///
133-
/// If that's not the case, use a shell instead.
134-
pub fn with_shell_allow_manual_argument_splitting(mut self) -> Self {
147+
/// This is useful on platforms where spawning processes is slow, or where many processes have to be spawned in a raw which should be sped up.
148+
/// Manual argument splitting is enabled by default on Windows only.
149+
pub fn command_may_be_shell_script_allow_manual_argument_splitting(mut self) -> Self {
135150
self.allow_manual_arg_splitting = true;
136-
self.with_shell()
151+
self.command_may_be_shell_script()
137152
}
138153

139-
/// Use a shell, but prohibit splitting arguments by hand even if this could be safely done without a shell.
140-
pub fn with_shell_disallow_manual_argument_splitting(mut self) -> Self {
154+
/// Like [`command_may_be_shell_script()`](Prepare::command_may_be_shell_script()), but don't allow to bypass the shell even if manual argument splitting
155+
/// can be performed safely.
156+
pub fn command_may_be_shell_script_disallow_manual_argument_splitting(mut self) -> Self {
141157
self.allow_manual_arg_splitting = false;
142-
self.with_shell()
158+
self.command_may_be_shell_script()
143159
}
144160

145161
/// Configure the process to use `stdio` for _stdin.

gix-command/tests/command.rs

+55-19
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ mod prepare {
246246
#[test]
247247
fn multiple_arguments_in_one_line_with_auto_split() {
248248
let cmd = std::process::Command::from(
249-
gix_command::prepare("echo first second third").with_shell_allow_manual_argument_splitting(),
249+
gix_command::prepare("echo first second third")
250+
.command_may_be_shell_script_allow_manual_argument_splitting(),
250251
);
251252
assert_eq!(
252253
format!("{cmd:?}"),
@@ -267,7 +268,8 @@ mod prepare {
267268

268269
#[test]
269270
fn single_and_multiple_arguments_as_part_of_command_with_shell() {
270-
let cmd = std::process::Command::from(gix_command::prepare("ls first second third").with_shell());
271+
let cmd =
272+
std::process::Command::from(gix_command::prepare("ls first second third").command_may_be_shell_script());
271273
assert_eq!(
272274
format!("{cmd:?}"),
273275
if cfg!(windows) {
@@ -283,7 +285,7 @@ mod prepare {
283285
fn single_and_multiple_arguments_as_part_of_command_with_given_shell() {
284286
let cmd = std::process::Command::from(
285287
gix_command::prepare("ls first second third")
286-
.with_shell()
288+
.command_may_be_shell_script()
287289
.with_shell_program("/somepath/to/bash"),
288290
);
289291
assert_eq!(
@@ -299,7 +301,11 @@ mod prepare {
299301

300302
#[test]
301303
fn single_and_complex_arguments_as_part_of_command_with_shell() {
302-
let cmd = std::process::Command::from(gix_command::prepare("ls --foo \"a b\"").arg("additional").with_shell());
304+
let cmd = std::process::Command::from(
305+
gix_command::prepare("ls --foo \"a b\"")
306+
.arg("additional")
307+
.command_may_be_shell_script(),
308+
);
303309
assert_eq!(
304310
format!("{cmd:?}"),
305311
if cfg!(windows) {
@@ -314,7 +320,7 @@ mod prepare {
314320
#[test]
315321
fn single_and_complex_arguments_with_auto_split() {
316322
let cmd = std::process::Command::from(
317-
gix_command::prepare("ls --foo=\"a b\"").with_shell_allow_manual_argument_splitting(),
323+
gix_command::prepare("ls --foo=\"a b\"").command_may_be_shell_script_allow_manual_argument_splitting(),
318324
);
319325
assert_eq!(
320326
format!("{cmd:?}"),
@@ -326,15 +332,29 @@ mod prepare {
326332
#[test]
327333
fn single_and_complex_arguments_without_auto_split() {
328334
let cmd = std::process::Command::from(
329-
gix_command::prepare("ls --foo=\"a b\"").with_shell_disallow_manual_argument_splitting(),
335+
gix_command::prepare("ls --foo=\"a b\"").command_may_be_shell_script_disallow_manual_argument_splitting(),
330336
);
331337
assert_eq!(format!("{cmd:?}"), quoted(&[SH, "-c", r#"ls --foo=\"a b\""#, "--"]));
332338
}
333339

340+
#[test]
341+
fn single_and_simple_arguments_without_auto_split_with_shell() {
342+
let cmd = std::process::Command::from(
343+
gix_command::prepare("ls")
344+
.arg("--foo=a b")
345+
.command_may_be_shell_script_disallow_manual_argument_splitting()
346+
.with_shell(),
347+
);
348+
assert_eq!(
349+
format!("{cmd:?}"),
350+
quoted(&[SH, "-c", "ls \\\"$@\\\"", "--", "--foo=a b"])
351+
);
352+
}
353+
334354
#[test]
335355
fn single_and_complex_arguments_will_not_auto_split_on_special_characters() {
336356
let cmd = std::process::Command::from(
337-
gix_command::prepare("ls --foo=~/path").with_shell_allow_manual_argument_splitting(),
357+
gix_command::prepare("ls --foo=~/path").command_may_be_shell_script_allow_manual_argument_splitting(),
338358
);
339359
assert_eq!(
340360
format!("{cmd:?}"),
@@ -345,7 +365,8 @@ mod prepare {
345365

346366
#[test]
347367
fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() {
348-
let cmd = std::process::Command::from(gix_command::prepare("~/bin/exe --foo \"a b\"").with_shell());
368+
let cmd =
369+
std::process::Command::from(gix_command::prepare("~/bin/exe --foo \"a b\"").command_may_be_shell_script());
349370
assert_eq!(
350371
format!("{cmd:?}"),
351372
format!(r#""{SH}" "-c" "~/bin/exe --foo \"a b\"" "--""#),
@@ -355,7 +376,11 @@ mod prepare {
355376

356377
#[test]
357378
fn script_with_dollar_at() {
358-
let cmd = std::process::Command::from(gix_command::prepare("echo \"$@\" >&2").with_shell().arg("store"));
379+
let cmd = std::process::Command::from(
380+
gix_command::prepare("echo \"$@\" >&2")
381+
.command_may_be_shell_script()
382+
.arg("store"),
383+
);
359384
assert_eq!(
360385
format!("{cmd:?}"),
361386
format!(r#""{SH}" "-c" "echo \"$@\" >&2" "--" "store""#),
@@ -374,7 +399,7 @@ mod spawn {
374399
let out = gix_command::prepare("echo $FIRST $SECOND")
375400
.env("FIRST", "first")
376401
.env("SECOND", "second")
377-
.with_shell()
402+
.command_may_be_shell_script()
378403
.spawn()?
379404
.wait_with_output()?;
380405
assert_eq!(out.stdout.as_bstr(), "first second\n");
@@ -385,12 +410,15 @@ mod spawn {
385410
#[cfg(unix)]
386411
fn disallow_shell() -> crate::Result {
387412
let out = gix_command::prepare("PATH= echo hi")
388-
.with_shell()
413+
.command_may_be_shell_script()
389414
.spawn()?
390415
.wait_with_output()?;
391416
assert_eq!(out.stdout.as_bstr(), "hi\n");
392417

393-
let mut cmd: std::process::Command = gix_command::prepare("echo hi").with_shell().without_shell().into();
418+
let mut cmd: std::process::Command = gix_command::prepare("echo hi")
419+
.command_may_be_shell_script()
420+
.without_shell()
421+
.into();
394422
assert!(
395423
cmd.env_remove("PATH").spawn().is_err(),
396424
"no command named 'echo hi' exists"
@@ -400,9 +428,13 @@ mod spawn {
400428

401429
#[test]
402430
fn script_with_dollar_at() -> crate::Result {
403-
let out = std::process::Command::from(gix_command::prepare("echo \"$@\"").with_shell().arg("arg"))
404-
.spawn()?
405-
.wait_with_output()?;
431+
let out = std::process::Command::from(
432+
gix_command::prepare("echo \"$@\"")
433+
.command_may_be_shell_script()
434+
.arg("arg"),
435+
)
436+
.spawn()?
437+
.wait_with_output()?;
406438
assert_eq!(
407439
out.stdout.to_str_lossy().trim(),
408440
"arg",
@@ -433,7 +465,7 @@ mod spawn {
433465
#[test]
434466
fn command_in_path_with_args() -> crate::Result {
435467
assert!(gix_command::prepare(if cfg!(unix) { "ls -l" } else { "dir.exe -a" })
436-
.with_shell()
468+
.command_may_be_shell_script()
437469
.spawn()?
438470
.wait()?
439471
.success());
@@ -442,14 +474,18 @@ mod spawn {
442474

443475
#[test]
444476
fn sh_shell_specific_script_code() -> crate::Result {
445-
assert!(gix_command::prepare(":;:;:").with_shell().spawn()?.wait()?.success());
477+
assert!(gix_command::prepare(":;:;:")
478+
.command_may_be_shell_script()
479+
.spawn()?
480+
.wait()?
481+
.success());
446482
Ok(())
447483
}
448484

449485
#[test]
450486
fn sh_shell_specific_script_code_with_single_extra_arg() -> crate::Result {
451487
let out = gix_command::prepare("printf")
452-
.with_shell()
488+
.command_may_be_shell_script()
453489
.arg("1")
454490
.spawn()?
455491
.wait_with_output()?;
@@ -461,7 +497,7 @@ mod spawn {
461497
#[test]
462498
fn sh_shell_specific_script_code_with_multiple_extra_args() -> crate::Result {
463499
let out = gix_command::prepare("printf")
464-
.with_shell()
500+
.command_may_be_shell_script()
465501
.arg("%s")
466502
.arg("arg")
467503
.spawn()?

0 commit comments

Comments
 (0)