Skip to content

Commit d83fe8d

Browse files
committed
unix: Relax escaping in Debug impl on Command
The Debug output of Command is very useful for showing to the user the command that was executed when something went wrong. This is done for example by rustc when invoking an external tool like the linker fails. It is also overly verbose, since everything is quoted, which makes it harder to read. Instead, we now first check if we're reasonably sure that an argument is simple enough that using it in the shell wouldn't need quoting, and then output it without quotes if possible. Before and example output could look like this: PATH="/a:/b" "cc" "foo.o" "-target" "arm64-apple-darwin11.0" Now it looks like this: PATH=/a:/b cc foo.o -target arm64-apple-darwin11.0
1 parent 67a8c64 commit d83fe8d

File tree

7 files changed

+114
-28
lines changed

7 files changed

+114
-28
lines changed

library/std/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
#![feature(cfg_target_thread_local)]
286286
#![feature(cfi_encoding)]
287287
#![feature(concat_idents)]
288+
#![feature(debug_closure_helpers)]
288289
#![feature(decl_macro)]
289290
#![feature(deprecated_suggestion)]
290291
#![feature(doc_cfg)]

library/std/src/process/tests.rs

+6-12
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ fn debug_print() {
552552

553553
let mut command = Command::new("some-boring-name");
554554

555-
assert_eq!(format!("{command:?}"), format!(r#""some-boring-name""#));
555+
assert_eq!(format!("{command:?}"), format!("some-boring-name"));
556556

557557
assert_eq!(
558558
format!("{command:#?}"),
@@ -568,7 +568,7 @@ fn debug_print() {
568568

569569
command.args(&["1", "2", "3"]);
570570

571-
assert_eq!(format!("{command:?}"), format!(r#""some-boring-name" "1" "2" "3""#));
571+
assert_eq!(format!("{command:?}"), format!("some-boring-name 1 2 3"));
572572

573573
assert_eq!(
574574
format!("{command:#?}"),
@@ -587,10 +587,7 @@ fn debug_print() {
587587

588588
crate::os::unix::process::CommandExt::arg0(&mut command, "exciting-name");
589589

590-
assert_eq!(
591-
format!("{command:?}"),
592-
format!(r#"["some-boring-name"] "exciting-name" "1" "2" "3""#)
593-
);
590+
assert_eq!(format!("{command:?}"), format!("[some-boring-name] exciting-name 1 2 3"));
594591

595592
assert_eq!(
596593
format!("{command:#?}"),
@@ -609,10 +606,7 @@ fn debug_print() {
609606

610607
let mut command_with_env_and_cwd = Command::new("boring-name");
611608
command_with_env_and_cwd.current_dir("/some/path").env("FOO", "bar");
612-
assert_eq!(
613-
format!("{command_with_env_and_cwd:?}"),
614-
r#"cd "/some/path" && FOO="bar" "boring-name""#
615-
);
609+
assert_eq!(format!("{command_with_env_and_cwd:?}"), "cd /some/path && FOO=bar boring-name");
616610
assert_eq!(
617611
format!("{command_with_env_and_cwd:#?}"),
618612
format!(
@@ -638,7 +632,7 @@ fn debug_print() {
638632

639633
let mut command_with_removed_env = Command::new("boring-name");
640634
command_with_removed_env.env_remove("FOO").env_remove("BAR");
641-
assert_eq!(format!("{command_with_removed_env:?}"), r#"env -u BAR -u FOO "boring-name""#);
635+
assert_eq!(format!("{command_with_removed_env:?}"), "env -u BAR -u FOO boring-name");
642636
assert_eq!(
643637
format!("{command_with_removed_env:#?}"),
644638
format!(
@@ -660,7 +654,7 @@ fn debug_print() {
660654

661655
let mut command_with_cleared_env = Command::new("boring-name");
662656
command_with_cleared_env.env_clear().env("BAR", "val").env_remove("FOO");
663-
assert_eq!(format!("{command_with_cleared_env:?}"), r#"env -i BAR="val" "boring-name""#);
657+
assert_eq!(format!("{command_with_cleared_env:?}"), "env -i BAR=val boring-name");
664658
assert_eq!(
665659
format!("{command_with_cleared_env:#?}"),
666660
format!(

library/std/src/sys/pal/unix/process/process_common.rs

+60-6
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,56 @@ impl fmt::Debug for Command {
580580

581581
debug_command.finish()
582582
} else {
583+
/// Only escape arguments when necessary, otherwise output it
584+
/// unescaped to make the output easier to read.
585+
///
586+
/// NOTE: This is best effort only!
587+
fn relaxed_escaping(bytes: &[u8], is_arg: bool) -> impl fmt::Display + use<'_> {
588+
// Don't escape if all the characters are likely
589+
// to not be a problem when copied to the shell.
590+
let can_print_safely = move |&c| {
591+
// See: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
592+
match c {
593+
// The documentation linked above says that `=`:
594+
// > may need to be quoted under certain circumstances
595+
//
596+
// Because they may be parsed as a variable assignment.
597+
// But in argument position to a command, they
598+
// shouldn't need escaping.
599+
b'=' if is_arg => true,
600+
// As per documentation linked above:
601+
// > The application shall quote the following characters
602+
b'|' | b'&' | b';' | b'<' | b'>' | b'(' | b')' | b'$' | b'`' | b'\\'
603+
| b'"' | b'\'' | b' ' | b'\t' | b'\n' => false,
604+
// As per documentation linked above:
605+
// > may need to be quoted under certain circumstances
606+
b'*' | b'?' | b'[' | b'#' | b'~' | b'=' | b'%' => false,
607+
// It'd be weird to quote `[` and not quote `]`.
608+
b']' => false,
609+
// ! does history expansion in Bash, require quoting for that as well.
610+
//
611+
// NOTE: This doesn't currently work properly, we'd need to backslash-escape
612+
// `!` as well (which `escape_ascii` doesn't do).
613+
b'!' => false,
614+
// Assume all other printable characters may be output.
615+
0x20..0x7e => true,
616+
// Unprintable or not ASCII.
617+
_ => false,
618+
}
619+
};
620+
fmt::from_fn(move |f| {
621+
if !bytes.is_empty() && bytes.iter().all(can_print_safely) {
622+
// Unwrap is fine, we've checked above that the bytes only contains ascii.
623+
let ascii = crate::str::from_utf8(bytes).unwrap();
624+
write!(f, "{ascii}")
625+
} else {
626+
write!(f, "\"{}\"", bytes.escape_ascii())
627+
}
628+
})
629+
}
630+
583631
if let Some(ref cwd) = self.cwd {
584-
write!(f, "cd {cwd:?} && ")?;
632+
write!(f, "cd {} && ", relaxed_escaping(cwd.to_bytes(), true))?;
585633
}
586634
if self.env.does_clear() {
587635
write!(f, "env -i ")?;
@@ -595,23 +643,29 @@ impl fmt::Debug for Command {
595643
write!(f, "env ")?;
596644
any_removed = true;
597645
}
598-
write!(f, "-u {} ", key.to_string_lossy())?;
646+
write!(f, "-u {} ", relaxed_escaping(key.as_encoded_bytes(), false))?;
599647
}
600648
}
601649
}
602650
// Altered env vars can just be added in front of the program.
603651
for (key, value_opt) in self.get_envs() {
604652
if let Some(value) = value_opt {
605-
write!(f, "{}={value:?} ", key.to_string_lossy())?;
653+
write!(
654+
f,
655+
"{}={} ",
656+
relaxed_escaping(key.as_encoded_bytes(), false),
657+
relaxed_escaping(value.as_encoded_bytes(), false)
658+
)?;
606659
}
607660
}
608661
if self.program != self.args[0] {
609-
write!(f, "[{:?}] ", self.program)?;
662+
write!(f, "[{}] ", relaxed_escaping(self.program.to_bytes(), false))?;
610663
}
611-
write!(f, "{:?}", self.args[0])?;
664+
665+
write!(f, "{}", relaxed_escaping(self.args[0].to_bytes(), false))?;
612666

613667
for arg in &self.args[1..] {
614-
write!(f, " {:?}", arg)?;
668+
write!(f, " {}", relaxed_escaping(arg.to_bytes(), true))?;
615669
}
616670
Ok(())
617671
}

library/std/src/sys/pal/unix/process/process_common/tests.rs

+35
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,38 @@ fn unix_exit_statuses() {
190190
}
191191
}
192192
}
193+
194+
#[test]
195+
fn command_debug_escaping() {
196+
#[track_caller]
197+
fn check(arg: impl AsRef<OsStr>, expected: &str) {
198+
let cmd = Command::new(arg.as_ref());
199+
assert_eq!(format!("{cmd:?}"), expected);
200+
}
201+
202+
// Escaped.
203+
check("", r#""""#);
204+
check("é", r#""\xc3\xa9""#);
205+
check("a'", r#""a\'""#);
206+
check("a\"", r#""a\"""#);
207+
check("'\"", r#""\'\"""#);
208+
check(" ", r#"" ""#);
209+
check(
210+
"/Users/The user's name/.cargo/bin/cargo",
211+
r#""/Users/The user\'s name/.cargo/bin/cargo""#,
212+
);
213+
check("!a", r#""!a""#);
214+
215+
// Simple enough that we don't escape them.
216+
check("a", "a");
217+
check("/my/simple/path", "/my/simple/path");
218+
check("abc-defg_1234", "abc-defg_1234");
219+
220+
// Real-world use-case, immediately clear that two of the arguments are passed as one.
221+
let mut cmd = crate::process::Command::new("ld");
222+
cmd.arg("my/file.o");
223+
cmd.arg("-macos_version_min");
224+
cmd.arg("13.0");
225+
cmd.arg("-arch arm64");
226+
assert_eq!(format!("{cmd:?}"), r#"ld my/file.o -macos_version_min 13.0 "-arch arm64""#);
227+
}

tests/run-make/link-args-order/rmake.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ fn main() {
1313
.linker_flavor(linker)
1414
.link_arg("a")
1515
.link_args("b c")
16-
.link_args("d e")
17-
.link_arg("f")
16+
.link_arg("d e")
17+
.link_args("f g")
18+
.link_arg("h")
1819
.run_fail()
19-
.assert_stderr_contains(r#""a" "b" "c" "d" "e" "f""#);
20+
.assert_stderr_contains(r#"a b c "d e" f g h"#);
2021
rustc()
2122
.input("empty.rs")
2223
.linker_flavor(linker)
2324
.arg("-Zpre-link-arg=a")
2425
.arg("-Zpre-link-args=b c")
25-
.arg("-Zpre-link-args=d e")
26-
.arg("-Zpre-link-arg=f")
26+
.arg("-Zpre-link-arg=d e")
27+
.arg("-Zpre-link-args=f g")
28+
.arg("-Zpre-link-arg=h")
2729
.run_fail()
28-
.assert_stderr_contains(r#""a" "b" "c" "d" "e" "f""#);
30+
.assert_stderr_contains(r#"a b c "d e" f g h"#);
2931
}

tests/run-make/link-dedup/rmake.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ fn needle_from_libs(libs: &[&str]) -> String {
3131
let mut needle = String::new();
3232
for lib in libs {
3333
if is_msvc() {
34-
let _ = needle.write_fmt(format_args!(r#""{lib}.lib" "#));
34+
let _ = needle.write_fmt(format_args!("{lib}.lib "));
3535
} else {
36-
let _ = needle.write_fmt(format_args!(r#""-l{lib}" "#));
36+
let _ = needle.write_fmt(format_args!("-l{lib} "));
3737
}
3838
}
3939
needle.pop(); // remove trailing space

tests/run-make/pass-linker-flags-flavor/rmake.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ fn main() {
7272
.stdout_utf8();
7373

7474
let no_verbatim = regex::Regex::new("l1.*-Wl,a1.*l2.*-Wl,a2.*d1.*-Wl,a3").unwrap();
75-
let one_verbatim = regex::Regex::new(r#"l1.*"a1".*l2.*-Wl,a2.*d1.*-Wl,a3"#).unwrap();
76-
let ld = regex::Regex::new(r#"l1.*"a1".*l2.*"a2".*d1.*"a3""#).unwrap();
75+
let one_verbatim = regex::Regex::new("l1.*a1.*l2.*-Wl,a2.*d1.*-Wl,a3").unwrap();
76+
let ld = regex::Regex::new("l1.*a1.*l2.*a2.*d1.*a3").unwrap();
7777

7878
assert!(no_verbatim.is_match(&out_gnu));
7979
assert!(no_verbatim.is_match(&out_att_gnu));

0 commit comments

Comments
 (0)