diff --git a/src/uu/ls/src/quoting_style.rs b/src/uu/ls/src/quoting_style.rs index ceb54466c..49456fc22 100644 --- a/src/uu/ls/src/quoting_style.rs +++ b/src/uu/ls/src/quoting_style.rs @@ -1,6 +1,6 @@ use std::char::from_digit; -const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! "; +const SPECIAL_SHELL_CHARS: &str = "~`#$&*()|[]{};\\'\"<>?! "; pub(super) enum QuotingStyle { Shell { @@ -27,12 +27,10 @@ pub(super) enum Quotes { // This implementation is heavily inspired by the std::char::EscapeDefault implementation // in the Rust standard library. This custom implementation is needed because the // characters \a, \b, \e, \f & \v are not recognized by Rust. -#[derive(Clone, Debug)] struct EscapedChar { state: EscapeState, } -#[derive(Clone, Debug)] enum EscapeState { Done, Char(char), @@ -41,14 +39,12 @@ enum EscapeState { Octal(EscapeOctal), } -#[derive(Clone, Debug)] struct EscapeOctal { c: char, state: EscapeOctalState, idx: usize, } -#[derive(Clone, Debug)] enum EscapeOctalState { Done, Backslash, @@ -135,7 +131,6 @@ impl EscapedChar { '\x0B' => Backslash('v'), '\x0C' => Backslash('f'), '\r' => Backslash('r'), - '\\' => Backslash('\\'), '\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)), '\'' => match quotes { Quotes::Single => Backslash('\''), @@ -511,6 +506,23 @@ mod tests { ], ); + // A control character followed by a special shell character + check_names( + "one\n&two", + vec![ + ("one?&two", "literal"), + ("one\n&two", "literal-show"), + ("one\\n&two", "escape"), + ("\"one\\n&two\"", "c"), + ("'one?&two'", "shell"), + ("'one\n&two'", "shell-show"), + ("'one?&two'", "shell-always"), + ("'one\n&two'", "shell-always-show"), + ("'one'$'\\n''&two'", "shell-escape"), + ("'one'$'\\n''&two'", "shell-escape-always"), + ], + ); + // The first 16 control characters. NUL is also included, even though it is of // no importance for file names. check_names( @@ -627,4 +639,22 @@ mod tests { ], ); } + + #[test] + fn test_backslash() { + // Escaped in C-style, but not in Shell-style escaping + check_names( + "one\\two", + vec![ + ("one\\two", "literal"), + ("one\\two", "literal-show"), + ("one\\\\two", "escape"), + ("\"one\\\\two\"", "c"), + ("'one\\two'", "shell"), + ("\'one\\two\'", "shell-always"), + ("'one\\two'", "shell-escape"), + ("'one\\two'", "shell-escape-always"), + ], + ); + } } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index f87e64688..09e02f264 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -104,6 +104,12 @@ fn test_ls_width() { .stdout_only("test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n"); } + scene + .ucmd() + .arg("-w=bad") + .fails() + .stderr_contains("invalid line width"); + for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] { scene .ucmd() @@ -444,6 +450,39 @@ fn test_ls_deref() { assert!(!re.is_match(result.stdout_str().trim())); } +#[test] +fn test_ls_sort_none() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-3"); + at.touch("test-1"); + at.touch("test-2"); + + // Order is not specified so we just check that it doesn't + // give any errors. + scene.ucmd().arg("--sort=none").succeeds(); + scene.ucmd().arg("-U").succeeds(); +} + +#[test] +fn test_ls_sort_name() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-3"); + at.touch("test-1"); + at.touch("test-2"); + + let sep = if cfg!(unix) { "\n" } else { " " }; + + scene + .ucmd() + .arg("--sort=name") + .succeeds() + .stdout_is(["test-1", "test-2", "test-3\n"].join(sep)); +} + #[test] fn test_ls_order_size() { let scene = TestScenario::new(util_name!()); @@ -472,6 +511,18 @@ fn test_ls_order_size() { result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); #[cfg(windows)] result.stdout_only("test-1 test-2 test-3 test-4\n"); + + let result = scene.ucmd().arg("--sort=size").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + result.stdout_only("test-4 test-3 test-2 test-1\n"); + + let result = scene.ucmd().arg("--sort=size").arg("-r").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + result.stdout_only("test-1 test-2 test-3 test-4\n"); } #[test] @@ -480,13 +531,16 @@ fn test_ls_long_ctime() { let at = &scene.fixtures; at.touch("test-long-ctime-1"); - let result = scene.ucmd().arg("-lc").succeeds(); - // Should show the time on Unix, but question marks on windows. - #[cfg(unix)] - result.stdout_contains(":"); - #[cfg(not(unix))] - result.stdout_contains("???"); + for arg in &["-c", "--time=ctime", "--time=status"] { + let result = scene.ucmd().arg("-l").arg(arg).succeeds(); + + // Should show the time on Unix, but question marks on windows. + #[cfg(unix)] + result.stdout_contains(":"); + #[cfg(not(unix))] + result.stdout_contains("???"); + } } #[test] @@ -527,32 +581,46 @@ fn test_ls_order_time() { #[cfg(windows)] result.stdout_only("test-4 test-3 test-2 test-1\n"); + let result = scene.ucmd().arg("--sort=time").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + result.stdout_only("test-4 test-3 test-2 test-1\n"); + let result = scene.ucmd().arg("-tr").succeeds(); #[cfg(not(windows))] result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); #[cfg(windows)] result.stdout_only("test-1 test-2 test-3 test-4\n"); + let result = scene.ucmd().arg("--sort=time").arg("-r").succeeds(); + #[cfg(not(windows))] + result.stdout_only("test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + result.stdout_only("test-1 test-2 test-3 test-4\n"); + // 3 was accessed last in the read // So the order should be 2 3 4 1 - let result = scene.ucmd().arg("-tu").succeeds(); - let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap(); - let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); + for arg in &["-u", "--time=atime", "--time=access", "--time=use"] { + let result = scene.ucmd().arg("-t").arg(arg).succeeds(); + let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap(); + let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); - // It seems to be dependent on the platform whether the access time is actually set - if file3_access > file4_access { - if cfg!(not(windows)) { - result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); + // It seems to be dependent on the platform whether the access time is actually set + if file3_access > file4_access { + if cfg!(not(windows)) { + result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); + } else { + result.stdout_only("test-3 test-4 test-2 test-1\n"); + } } else { - result.stdout_only("test-3 test-4 test-2 test-1\n"); - } - } else { - // Access time does not seem to be set on Windows and some other - // systems so the order is 4 3 2 1 - if cfg!(not(windows)) { - result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); - } else { - result.stdout_only("test-4 test-3 test-2 test-1\n"); + // Access time does not seem to be set on Windows and some other + // systems so the order is 4 3 2 1 + if cfg!(not(windows)) { + result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); + } else { + result.stdout_only("test-4 test-3 test-2 test-1\n"); + } } } @@ -1059,9 +1127,11 @@ fn test_ls_quoting_style() { at.touch("one"); // It seems that windows doesn't allow \n in filenames. + // And it also doesn't like \, of course. #[cfg(unix)] { at.touch("one\ntwo"); + at.touch("one\\two"); // Default is shell-escape scene .ucmd() @@ -1123,6 +1193,42 @@ fn test_ls_quoting_style() { .succeeds() .stdout_only(format!("{}\n", correct)); } + + for (arg, correct) in &[ + ("--quoting-style=literal", "one\\two"), + ("-N", "one\\two"), + ("--quoting-style=c", "\"one\\\\two\""), + ("-Q", "\"one\\\\two\""), + ("--quote-name", "\"one\\\\two\""), + ("--quoting-style=escape", "one\\\\two"), + ("-b", "one\\\\two"), + ("--quoting-style=shell-escape", "'one\\two'"), + ("--quoting-style=shell-escape-always", "'one\\two'"), + ("--quoting-style=shell", "'one\\two'"), + ("--quoting-style=shell-always", "'one\\two'"), + ] { + scene + .ucmd() + .arg(arg) + .arg("one\\two") + .succeeds() + .stdout_only(format!("{}\n", correct)); + } + + // Tests for a character that forces quotation in shell-style escaping + // after a character in a dollar expression + at.touch("one\n&two"); + for (arg, correct) in &[ + ("--quoting-style=shell-escape", "'one'$'\\n''&two'"), + ("--quoting-style=shell-escape-always", "'one'$'\\n''&two'"), + ] { + scene + .ucmd() + .arg(arg) + .arg("one\n&two") + .succeeds() + .stdout_only(format!("{}\n", correct)); + } } scene @@ -1323,6 +1429,43 @@ fn test_ls_ignore_hide() { .stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); } +#[test] +fn test_ls_ignore_backups() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("somefile"); + at.touch("somebackup~"); + at.touch(".somehiddenfile"); + at.touch(".somehiddenbackup~"); + + scene.ucmd().arg("-B").succeeds().stdout_is("somefile\n"); + scene + .ucmd() + .arg("--ignore-backups") + .succeeds() + .stdout_is("somefile\n"); + + scene + .ucmd() + .arg("-aB") + .succeeds() + .stdout_contains(".somehiddenfile") + .stdout_contains("somefile") + .stdout_does_not_contain("somebackup") + .stdout_does_not_contain(".somehiddenbackup~"); + + scene + .ucmd() + .arg("-a") + .arg("--ignore-backups") + .succeeds() + .stdout_contains(".somehiddenfile") + .stdout_contains("somefile") + .stdout_does_not_contain("somebackup") + .stdout_does_not_contain(".somehiddenbackup~"); +} + #[test] fn test_ls_directory() { let scene = TestScenario::new(util_name!());