From abf3895689f4795ff63daa9a3482ea98b46565c5 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:22:45 +0300 Subject: [PATCH] uucore: use single quotes for shell-quoted names containing '!' Interactive bash history-expands `!` inside double quotes, so a name with both `'` and `!` printed as a double-quoted string can run a command when pasted. Adding `!` to the characters that force single quotes fixes all four shell styles. Fixes #15812 --- .../src/lib/features/quoting_style/mod.rs | 10 ++++++++++ .../lib/features/quoting_style/shell_quoter.rs | 18 +++++++++++++++++- tests/by-util/test_ls.rs | 18 ++++++++++++++++++ tests/by-util/test_printf.rs | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/quoting_style/mod.rs b/src/uucore/src/lib/features/quoting_style/mod.rs index 75551692b5b..ac0034e65dc 100644 --- a/src/uucore/src/lib/features/quoting_style/mod.rs +++ b/src/uucore/src/lib/features/quoting_style/mod.rs @@ -1217,6 +1217,16 @@ mod tests { ("'can'\\''\\t'", "shell-escape-always"), ], ); + + check_names_both( + "can'!t", + &[ + ("'can'\\''!t'", "shell"), + ("'can'\\''!t'", "shell-always"), + ("'can'\\''!t'", "shell-escape"), + ("'can'\\''!t'", "shell-escape-always"), + ], + ); } #[test] diff --git a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs index 441f1e54042..41e2b14ec4d 100644 --- a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs +++ b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs @@ -247,7 +247,7 @@ fn bytes_start_with(bytes: &[u8], pattern: &[u8]) -> bool { /// Return a set of characters that implies quoting of the word in /// shell-quoting mode. fn shell_escaped_char_set(is_dirname: bool) -> &'static [u8] { - const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r="; + const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r=!"; // the ':' colon character only induce quoting in the // context of ls displaying a directory name before listing its content. // (e.g. with the recursive flag -R) @@ -299,6 +299,22 @@ mod tests { (ShellQuotes::Double, true) ); + // Simple quote uses double quotes in non-escape mode + assert_eq!( + initial_quoting(b"a'b", false, false, false), + (ShellQuotes::Double, true) + ); + + // `!` is history-expanded inside double quotes, so a quote with `!` uses single quotes + assert_eq!( + initial_quoting(b"a'!b", false, false, true), + (ShellQuotes::Single, true) + ); + assert_eq!( + initial_quoting(b"a'!b", false, false, false), + (ShellQuotes::Single, true) + ); + // Shell special chars force single quotes in escape mode assert_eq!( initial_quoting(b"test$var", false, false, true), diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 91445abd734..52dc6ac0029 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -3080,6 +3080,24 @@ mod quoting { ); } + #[test] + fn test_ls_quoting_single_quote_exclamation() { + check_quoting_dirname( + "dir'!name", + &[ + ("shell", "'dir'\\''!name'", "'./dir'\\''!name'"), + ("shell-always", "'dir'\\''!name'", "'./dir'\\''!name'"), + ("shell-escape", "'dir'\\''!name'", "'./dir'\\''!name'"), + ( + "shell-escape-always", + "'dir'\\''!name'", + "'./dir'\\''!name'", + ), + ], + &[], + ); + } + #[test] fn test_ls_quoting_double_quote() { check_quoting_dirname( diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index f598b6ab287..5f0f1e37cea 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -20,6 +20,7 @@ use uutests::new_ucmd; #[case::sub_q_string_non_printable(&["non-printable: %q", "\"$test\""], "non-printable: '\"$test\"'")] #[case::sub_q_string_special_non_printable(&["non-printable: %q", "test~"], "non-printable: test~")] #[case::sub_q_string_empty(&["%q", ""], "''")] +#[case::sub_q_string_quote_and_bang(&["%q", "a'!b"], "'a'\\''!b'")] #[case::sub_char(&["the letter %c", "A"], "the letter A")] #[case::sub_char_from_string(&["%c%c%c", "five", "%", "oval"], "f%o")] #[case::sub_num_int(&["twenty is %i", "20"], "twenty is 20")]