diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 232250dcf..f4e347147 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -55,7 +55,7 @@ use uucore::libc::{dev_t, major, minor}; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::line_ending::LineEnding; -use uucore::quoting_style::{escape_name, QuotingStyle}; +use uucore::quoting_style::{escape_dir_name, escape_name, QuotingStyle}; use uucore::{ display::Quotable, error::{set_exit_code, UError, UResult}, @@ -2036,14 +2036,27 @@ impl PathData { } } +/// Show the directory name in the case where several arguments are given to ls +/// or the recursive flag is passed. +/// +/// ```no-exec +/// $ ls -R +/// .: <- This is printed by this function +/// dir1 file1 file2 +/// +/// dir1: <- This as well +/// file11 +/// ``` fn show_dir_name(path_data: &PathData, out: &mut BufWriter, config: &Config) { - if config.hyperlink && !config.dired { - let name = escape_name(path_data.p_buf.as_os_str(), &config.quoting_style); - let hyperlink = create_hyperlink(&name, path_data); - write!(out, "{hyperlink}:").unwrap(); + let escaped_name = escape_dir_name(path_data.p_buf.as_os_str(), &config.quoting_style); + + let name = if config.hyperlink && !config.dired { + create_hyperlink(&escaped_name, path_data) } else { - write!(out, "{}:", path_data.p_buf.display()).unwrap(); - } + escaped_name + }; + + write!(out, "{name}:").unwrap(); } #[allow(clippy::cognitive_complexity)] @@ -2327,9 +2340,10 @@ fn enter_directory( for e in entries .iter() .skip(if config.files == Files::All { 2 } else { 0 }) - .filter(|p| p.ft.get().is_some()) - .filter(|p| p.ft.get().unwrap().is_some()) - .filter(|p| p.ft.get().unwrap().unwrap().is_dir()) + .filter(|p| { + p.ft.get() + .is_some_and(|o_ft| o_ft.is_some_and(|ft| ft.is_dir())) + }) { match fs::read_dir(&e.p_buf) { Err(err) => { diff --git a/src/uucore/src/lib/features/quoting_style.rs b/src/uucore/src/lib/features/quoting_style.rs index cb98050a8..1efa6f746 100644 --- a/src/uucore/src/lib/features/quoting_style.rs +++ b/src/uucore/src/lib/features/quoting_style.rs @@ -12,7 +12,8 @@ use std::fmt; // These are characters with special meaning in the shell (e.g. bash). // The first const contains characters that only have a special meaning when they appear at the beginning of a name. const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#']; -const SPECIAL_SHELL_CHARS: &str = "`$&*()|[]{};\\'\"<>?! "; +// PR#6559 : Remove `]{}` from special shell chars. +const SPECIAL_SHELL_CHARS: &str = "`$&*()|[;\\'\"<>?! "; /// The quoting style to use when escaping a name. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -123,7 +124,7 @@ impl EscapedChar { } } - fn new_c(c: char, quotes: Quotes) -> Self { + fn new_c(c: char, quotes: Quotes, dirname: bool) -> Self { use EscapeState::*; let init_state = match c { '\x07' => Backslash('a'), @@ -142,10 +143,11 @@ impl EscapedChar { Quotes::Double => Backslash('"'), _ => Char('"'), }, - ' ' => match quotes { + ' ' if !dirname => match quotes { Quotes::None => Backslash(' '), _ => Char(' '), }, + ':' if dirname => Backslash(':'), _ if c.is_ascii_control() => Octal(EscapeOctal::from(c)), _ => Char(c), }; @@ -284,8 +286,29 @@ fn shell_with_escape(name: &str, quotes: Quotes) -> (String, bool) { (escaped_str, must_quote) } +/// Return a set of characters that implies quoting of the word in +/// shell-quoting mode. +fn shell_escaped_char_set(is_dirname: bool) -> &'static [char] { + const ESCAPED_CHARS: &[char] = &[ + // 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) + ':', + // Under this line are the control characters that should be + // quoted in shell mode in all cases. + '"', '`', '$', '\\', '^', '\n', '\t', '\r', '=', + ]; + + let start_index = if is_dirname { 0 } else { 1 }; + + &ESCAPED_CHARS[start_index..] +} + /// Escape a name according to the given quoting style. -pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { +/// +/// This inner function provides an additional flag `dirname` which +/// is meant for ls' directory name display. +fn escape_name_inner(name: &OsStr, style: &QuotingStyle, dirname: bool) -> String { match style { QuotingStyle::Literal { show_control } => { if *show_control { @@ -301,7 +324,7 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { let escaped_str: String = name .to_string_lossy() .chars() - .flat_map(|c| EscapedChar::new_c(c, *quotes)) + .flat_map(|c| EscapedChar::new_c(c, *quotes, dirname)) .collect(); match quotes { @@ -316,7 +339,8 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { show_control, } => { let name = name.to_string_lossy(); - let (quotes, must_quote) = if name.contains(&['"', '`', '$', '\\'][..]) { + + let (quotes, must_quote) = if name.contains(shell_escaped_char_set(dirname)) { (Quotes::Single, true) } else if name.contains('\'') { (Quotes::Double, true) @@ -341,6 +365,18 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { } } +/// Escape a filename with respect to the given style. +pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { + escape_name_inner(name, style, false) +} + +/// Escape a directory name with respect to the given style. +/// This is mainly meant to be used for ls' directory name printing and is not +/// likely to be used elsewhere. +pub fn escape_dir_name(dir_name: &OsStr, style: &QuotingStyle) -> String { + escape_name_inner(dir_name, style, true) +} + impl fmt::Display for QuotingStyle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { @@ -575,8 +611,8 @@ mod tests { ("one\ntwo", "literal-show"), ("one\\ntwo", "escape"), ("\"one\\ntwo\"", "c"), - ("one?two", "shell"), - ("one\ntwo", "shell-show"), + ("'one?two'", "shell"), + ("'one\ntwo'", "shell-show"), ("'one?two'", "shell-always"), ("'one\ntwo'", "shell-always-show"), ("'one'$'\\n''two'", "shell-escape"), @@ -619,9 +655,9 @@ mod tests { "\"\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017\"", "c", ), - ("????????????????", "shell"), + ("'????????????????'", "shell"), ( - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + "'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'", "shell-show", ), ("'????????????????'", "shell-always"), diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 90254d229..0c0d8e3a8 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -2197,6 +2197,464 @@ fn test_ls_recursive_1() { .stdout_is(out); } +/// The quoting module regroups tests that check the behavior of ls when +/// quoting and escaping special characters with different quoting styles. +#[cfg(unix)] +mod quoting { + use super::TestScenario; + + /// Create a directory with "dirname", then for each check, assert that the + /// output is correct. + fn check_quoting_dirname(dirname: &str, checks: &[(&str, &str, &str)], extra_args: &[&str]) { + for (qt_style, regular_mode, dir_mode) in checks { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir(dirname); + + let expected = format!( + "{}:\n{}\n\n{}:\n", + match *qt_style { + "shell-always" | "shell-escape-always" => "'.'", + "c" => "\".\"", + _ => ".", + }, + regular_mode, + dir_mode + ); + + scene + .ucmd() + .arg("-R") + .arg(format!("--quoting-style={qt_style}")) + .args(extra_args) + .succeeds() + .stdout_is(expected); + } + } + + #[test] + fn test_ls_quoting_simple() { + check_quoting_dirname( + // Control case + "dirname", + &[ + ("literal", "dirname", "./dirname"), + ("shell", "dirname", "./dirname"), + ("shell-always", "'dirname'", "'./dirname'"), + ("shell-escape", "dirname", "./dirname"), + ("shell-escape-always", "'dirname'", "'./dirname'"), + ("c", "\"dirname\"", "\"./dirname\""), + ("escape", "dirname", "./dirname"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_space() { + check_quoting_dirname( + // Space character + "dir name", + &[ + ("literal", "dir name", "./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'"), + ("c", "\"dir name\"", "\"./dir name\""), + ("escape", "dir\\ name", "./dir name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_dollar() { + check_quoting_dirname( + // Dollar character + "dir$name", + &[ + ("literal", "dir$name", "./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'"), + ("c", "\"dir$name\"", "\"./dir$name\""), + ("escape", "dir$name", "./dir$name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_single_quote() { + check_quoting_dirname( + // Single quote character + "dir'name", + &[ + ("literal", "dir'name", "./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\""), + ("c", "\"dir'name\"", "\"./dir'name\""), + ("escape", "dir'name", "./dir'name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_double_quote() { + check_quoting_dirname( + // Double quote character + "dir\"name", + &[ + ("literal", "dir\"name", "./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'"), + ("c", "\"dir\\\"name\"", "\"./dir\\\"name\""), + ("escape", "dir\"name", "./dir\"name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_colon() { + check_quoting_dirname( + // Colon character + "dir:name", + &[ + ("literal", "dir:name", "./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'"), + ("c", "\"dir:name\"", "\"./dir\\:name\""), + ("escape", "dir:name", "./dir\\:name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_backslash() { + check_quoting_dirname( + // Backslash character + "dir\\name", + &[ + ("literal", "dir\\name", "./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'"), + ("c", "\"dir\\\\name\"", "\"./dir\\\\name\""), + ("escape", "dir\\\\name", "./dir\\\\name"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_linefeed() { + check_quoting_dirname( + // Linefeed character + "dir\nname", + &[ + ("literal", "dir\nname", "./dir\nname"), + ("shell", "'dir\nname'", "'./dir\nname'"), + ("shell-always", "'dir\nname'", "'./dir\nname'"), + ("shell-escape", "'dir'$'\\n''name'", "'./dir'$'\\n''name'"), + ( + "shell-escape-always", + "'dir'$'\\n''name'", + "'./dir'$'\\n''name'", + ), + ("c", "\"dir\\nname\"", "\"./dir\\nname\""), + ("escape", "dir\\nname", "./dir\\nname"), + ], + &[], + ); + + check_quoting_dirname( + // Linefeed character WITH hide-control-chars + "dir\nname", + &[ + ("literal", "dir?name", "./dir?name"), + ("shell", "'dir?name'", "'./dir?name'"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\n''name'", "'./dir'$'\\n''name'"), + ( + "shell-escape-always", + "'dir'$'\\n''name'", + "'./dir'$'\\n''name'", + ), + ("c", "\"dir\\nname\"", "\"./dir\\nname\""), + ("escape", "dir\\nname", "./dir\\nname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_tabulation() { + check_quoting_dirname( + // Tabulation character + "dir\tname", + &[ + ("literal", "dir\tname", "./dir\tname"), + ("shell", "'dir\tname'", "'./dir\tname'"), + ("shell-always", "'dir\tname'", "'./dir\tname'"), + ("shell-escape", "'dir'$'\\t''name'", "'./dir'$'\\t''name'"), + ( + "shell-escape-always", + "'dir'$'\\t''name'", + "'./dir'$'\\t''name'", + ), + ("c", "\"dir\\tname\"", "\"./dir\\tname\""), + ("escape", "dir\\tname", "./dir\\tname"), + ], + &[], + ); + + check_quoting_dirname( + // Tabulation character + "dir\tname", + &[ + ("literal", "dir?name", "./dir?name"), + ("shell", "'dir?name'", "'./dir?name'"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\t''name'", "'./dir'$'\\t''name'"), + ( + "shell-escape-always", + "'dir'$'\\t''name'", + "'./dir'$'\\t''name'", + ), + ("c", "\"dir\\tname\"", "\"./dir\\tname\""), + ("escape", "dir\\tname", "./dir\\tname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_carriage_return() { + check_quoting_dirname( + // Carriage return character + "dir\rname", + &[ + ("literal", "dir?name", "./dir?name"), + ("shell", "'dir?name'", "'./dir?name'"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\r''name'", "'./dir'$'\\r''name'"), + ( + "shell-escape-always", + "'dir'$'\\r''name'", + "'./dir'$'\\r''name'", + ), + ("c", "\"dir\\rname\"", "\"./dir\\rname\""), + ("escape", "dir\\rname", "./dir\\rname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_bell() { + check_quoting_dirname( + // Bell character + "dir\x07name", + &[ + ("shell", "dir?name", "./dir?name"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\a''name'", "'./dir'$'\\a''name'"), + ( + "shell-escape-always", + "'dir'$'\\a''name'", + "'./dir'$'\\a''name'", + ), + ("c", "\"dir\\aname\"", "\"./dir\\aname\""), + ("escape", "dir\\aname", "./dir\\aname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_backspace() { + check_quoting_dirname( + // Backspace character + "dir\x08name", + &[ + ("shell", "dir?name", "./dir?name"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\b''name'", "'./dir'$'\\b''name'"), + ( + "shell-escape-always", + "'dir'$'\\b''name'", + "'./dir'$'\\b''name'", + ), + ("c", "\"dir\\bname\"", "\"./dir\\bname\""), + ("escape", "dir\\bname", "./dir\\bname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_vertical_tab() { + check_quoting_dirname( + // Vertical tab character + "dir\x0bname", + &[ + ("shell", "dir?name", "./dir?name"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\v''name'", "'./dir'$'\\v''name'"), + ( + "shell-escape-always", + "'dir'$'\\v''name'", + "'./dir'$'\\v''name'", + ), + ("c", "\"dir\\vname\"", "\"./dir\\vname\""), + ("escape", "dir\\vname", "./dir\\vname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_formfeed() { + check_quoting_dirname( + // Form feed character + "dir\x0cname", + &[ + ("shell", "dir?name", "./dir?name"), + ("shell-always", "'dir?name'", "'./dir?name'"), + ("shell-escape", "'dir'$'\\f''name'", "'./dir'$'\\f''name'"), + ( + "shell-escape-always", + "'dir'$'\\f''name'", + "'./dir'$'\\f''name'", + ), + ("c", "\"dir\\fname\"", "\"./dir\\fname\""), + ("escape", "dir\\fname", "./dir\\fname"), + ], + &["--hide-control-chars"], + ); + } + + #[test] + fn test_ls_quoting_open_bracket() { + check_quoting_dirname( + "[-open_bracket", + &[ + ("shell", "'[-open_bracket'", "'./[-open_bracket'"), + ("shell-always", "'[-open_bracket'", "'./[-open_bracket'"), + ("shell-escape", "'[-open_bracket'", "'./[-open_bracket'"), + ( + "shell-escape-always", + "'[-open_bracket'", + "'./[-open_bracket'", + ), + ("c", "\"[-open_bracket\"", "\"./[-open_bracket\""), + ("escape", "[-open_bracket", "./[-open_bracket"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_close_bracket() { + check_quoting_dirname( + "]-close_bracket", + &[ + ("shell", "]-close_bracket", "./]-close_bracket"), + ("shell-always", "']-close_bracket'", "'./]-close_bracket'"), + ("shell-escape", "]-close_bracket", "./]-close_bracket"), + ( + "shell-escape-always", + "']-close_bracket'", + "'./]-close_bracket'", + ), + ("c", "\"]-close_bracket\"", "\"./]-close_bracket\""), + ("escape", "]-close_bracket", "./]-close_bracket"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_open_brace() { + check_quoting_dirname( + "{-open_brace", + &[ + ("shell", "{-open_brace", "./{-open_brace"), + ("shell-always", "'{-open_brace'", "'./{-open_brace'"), + ("shell-escape", "{-open_brace", "./{-open_brace"), + ("shell-escape-always", "'{-open_brace'", "'./{-open_brace'"), + ("c", "\"{-open_brace\"", "\"./{-open_brace\""), + ("escape", "{-open_brace", "./{-open_brace"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_close_brace() { + check_quoting_dirname( + "}-close_brace", + &[ + ("shell", "}-close_brace", "./}-close_brace"), + ("shell-always", "'}-close_brace'", "'./}-close_brace'"), + ("shell-escape", "}-close_brace", "./}-close_brace"), + ( + "shell-escape-always", + "'}-close_brace'", + "'./}-close_brace'", + ), + ("c", "\"}-close_brace\"", "\"./}-close_brace\""), + ("escape", "}-close_brace", "./}-close_brace"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_caret() { + check_quoting_dirname( + "^-caret", + &[ + ("shell", "'^-caret'", "'./^-caret'"), + ("shell-always", "'^-caret'", "'./^-caret'"), + ("shell-escape", "'^-caret'", "'./^-caret'"), + ("shell-escape-always", "'^-caret'", "'./^-caret'"), + ("c", "\"^-caret\"", "\"./^-caret\""), + ("escape", "^-caret", "./^-caret"), + ], + &[], + ); + } + + #[test] + fn test_ls_quoting_equal() { + check_quoting_dirname( + "=-equal", + &[ + ("shell", "'=-equal'", "'./=-equal'"), + ("shell-always", "'=-equal'", "'./=-equal'"), + ("shell-escape", "'=-equal'", "'./=-equal'"), + ("shell-escape-always", "'=-equal'", "'./=-equal'"), + ("c", "\"=-equal\"", "\"./=-equal\""), + ("escape", "=-equal", "./=-equal"), + ], + &[], + ); + } +} + #[test] fn test_ls_color() { let scene = TestScenario::new(util_name!()); @@ -2738,7 +3196,7 @@ fn test_ls_quoting_style() { ("--quoting-style=shell-escape-always", "'one'$'\\n''two'"), ("--quoting-style=shell-escape-alway", "'one'$'\\n''two'"), ("--quoting-style=shell-escape-a", "'one'$'\\n''two'"), - ("--quoting-style=shell", "one?two"), + ("--quoting-style=shell", "'one?two'"), ("--quoting-style=shell-always", "'one?two'"), ("--quoting-style=shell-a", "'one?two'"), ] { @@ -2756,7 +3214,7 @@ fn test_ls_quoting_style() { ("-N", "one\ntwo"), ("--literal", "one\ntwo"), ("--l", "one\ntwo"), - ("--quoting-style=shell", "one\ntwo"), // FIXME: GNU ls quotes this case + ("--quoting-style=shell", "'one\ntwo'"), ("--quoting-style=shell-always", "'one\ntwo'"), ] { scene @@ -2981,8 +3439,8 @@ fn test_ls_align_unquoted() { .terminal_simulation(true) .succeeds() .stdout_only("\"'quoted'\" CAPS 'elf two' foobar\r\n"); - // ^ ^ ^ - // space no-space space + // ^ ^ ^ + // space no-space space // The same should happen with format columns/across // and shell quoting style, except for the `\r` at the end. @@ -2994,8 +3452,8 @@ fn test_ls_align_unquoted() { .arg("--quoting-style=shell") .succeeds() .stdout_only("\"'quoted'\" CAPS 'elf two' foobar\n"); - // ^ ^ ^ - // space no-space space + // ^ ^ ^ + // space no-space space } } diff --git a/util/build-gnu.sh b/util/build-gnu.sh index e882c2776..684187733 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -365,5 +365,8 @@ sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh # do that. So, it's okay to ignore the zero. sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh +# patching this because of the same reason as the last one. +sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh + # Slightly different error message sed -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh