From fe791ce026c0a6f5963e71a0df9b9cca8f92a11b Mon Sep 17 00:00:00 2001 From: Pierre Marsais Date: Sun, 24 Jul 2022 13:49:05 +0100 Subject: [PATCH] ls: Implement --zero flag. (#2929) This flag can be used to provide a easy machine parseable output from ls, as discussed in the GNU bug report https://debbugs.gnu.org/cgi/bugreport.cgi?bug=49716. There are some peculiarities with this flag: - Current implementation of GNU ls of the `--zero` flag implies some other flags. Those can be overridden by setting those flags after `--zero` in the command line. - This flag is not compatible with `--dired`. This patch is not 100% compliant with GNU ls: GNU ls `--zero` will fail if `--dired` and `-l` are set, while with this patch only `--dired` is needed for the command to fail. We also add `--dired` flag to the parser, with no additional behaviour change. Testing done: ``` $ bash util/build-gnu.sh [...] $ bash util/run-gnu-test.sh tests/ls/zero-option.sh [...] PASS: tests/ls/zero-option.sh ============================================================================ Testsuite summary for GNU coreutils 9.1.36-8ec11 ============================================================================ # TOTAL: 1 # PASS: 1 # SKIP: 0 # XFAIL: 0 # FAIL: 0 # XPASS: 0 # ERROR: 0 ============================================================================ ``` --- src/uu/ls/src/ls.rs | 122 ++++++++++++++++++++++++++++++++++----- tests/by-util/test_ls.rs | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 14 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5da64abe542..0b8e74729aa 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -5,7 +5,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize +// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize dired #[macro_use] extern crate uucore; @@ -137,6 +137,8 @@ pub mod options { pub static IGNORE: &str = "ignore"; pub static CONTEXT: &str = "context"; pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; + pub static ZERO: &str = "zero"; + pub static DIRED: &str = "dired"; } const DEFAULT_TERM_WIDTH: u16 = 80; @@ -337,6 +339,7 @@ pub struct Config { context: bool, selinux_supported: bool, group_directories_first: bool, + eol: char, } // Fields that can be removed or added to the long format @@ -481,7 +484,7 @@ impl Config { Time::Modification }; - let needs_color = match options.value_of(options::COLOR) { + let mut needs_color = match options.value_of(options::COLOR) { None => options.is_present(options::COLOR), Some(val) => match val { "" | "always" | "yes" | "force" => true, @@ -490,12 +493,6 @@ impl Config { }, }; - let color = if needs_color { - Some(LsColors::from_env().unwrap_or_default()) - } else { - None - }; - let cmd_line_bs = options.value_of(options::size::BLOCK_SIZE); let opt_si = cmd_line_bs.is_some() && options @@ -607,7 +604,7 @@ impl Config { }; #[allow(clippy::needless_bool)] - let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { + let mut show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { false } else if options.is_present(options::SHOW_CONTROL_CHARS) { true @@ -619,7 +616,7 @@ impl Config { .value_of(options::QUOTING_STYLE) .map(|cmd_line_qs| cmd_line_qs.to_owned()); - let quoting_style = if let Some(style) = opt_quoting_style { + let mut quoting_style = if let Some(style) = opt_quoting_style { match style.as_str() { "literal" => QuotingStyle::Literal { show_control }, "shell" => QuotingStyle::Shell { @@ -750,6 +747,81 @@ impl Config { } } + // According to ls info page, `--zero` implies the following flags: + // - `--show-control-chars` + // - `--format=single-column` + // - `--color=none` + // - `--quoting-style=literal` + // Current GNU ls implementation allows `--zero` behaviour to be + // overridden by later flags. + let zero_formats_opts = [ + options::format::ACROSS, + options::format::COLUMNS, + options::format::COMMAS, + options::format::LONG, + options::format::LONG_NO_GROUP, + options::format::LONG_NO_OWNER, + options::format::LONG_NUMERIC_UID_GID, + options::format::ONE_LINE, + options::FORMAT, + ]; + let zero_colors_opts = [options::COLOR]; + let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; + let zero_quoting_style_opts = [ + options::QUOTING_STYLE, + options::quoting::C, + options::quoting::ESCAPE, + options::quoting::LITERAL, + ]; + let get_last = + |flag: &str| -> usize { options.indices_of(flag).and_then(|x| x.last()).unwrap_or(0) }; + if get_last(options::ZERO) + > zero_formats_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + format = if format == Format::Long { + format + } else { + Format::OneLine + }; + } + if get_last(options::ZERO) + > zero_colors_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + needs_color = false; + } + if get_last(options::ZERO) + > zero_show_control_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + show_control = true; + } + if get_last(options::ZERO) + > zero_quoting_style_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + quoting_style = QuotingStyle::Literal { show_control }; + } + + let color = if needs_color { + Some(LsColors::from_env().unwrap_or_default()) + } else { + None + }; + let dereference = if options.is_present(options::dereference::ALL) { Dereference::All } else if options.is_present(options::dereference::ARGS) { @@ -798,6 +870,11 @@ impl Config { } }, group_directories_first: options.is_present(options::GROUP_DIRECTORIES_FIRST), + eol: if options.is_present(options::ZERO) { + '\0' + } else { + '\n' + }, }) } } @@ -913,6 +990,18 @@ pub fn uu_app<'a>() -> Command<'a> { options::format::COLUMNS, ]), ) + .arg( + Arg::new(options::ZERO) + .long(options::ZERO) + .conflicts_with(options::DIRED) + .help("List entries separated by ASCII NUL characters."), + ) + .arg( + Arg::new(options::DIRED) + .long(options::DIRED) + .short('D') + .hide(true), + ) // The next four arguments do not override with the other format // options, see the comment in Config::from for the reason. // Ideally, they would use Arg::override_with, with their own name @@ -1884,7 +1973,12 @@ fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter 0 { - writeln!(out,)?; + write!(out, "{}", config.eol)?; } } _ => { for name in names { - writeln!(out, "{}", name.contents)?; + write!(out, "{}{}", name.contents, config.eol)?; } } }; @@ -2199,7 +2293,7 @@ fn display_item_long( let dfn = display_file_name(item, config, None, "".to_owned(), out).contents; - writeln!(out, " {} {}", display_date(md, config), dfn)?; + write!(out, " {} {}{}", display_date(md, config), dfn, config.eol)?; } else { #[cfg(unix)] let leading_char = { diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index dc6ce3bd1cd..b8fa41cc5fe 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -796,6 +796,116 @@ fn test_ls_commas() { } } +#[test] +fn test_ls_zero() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("0-test-zero"); + at.touch(&at.plus_as_string("2-test-zero")); + at.touch(&at.plus_as_string("3-test-zero")); + + let ignored_opts = [ + "--quoting-style=c", + "--color=always", + "-m", + "--hide-control-chars", + ]; + + scene + .ucmd() + .arg("--zero") + .succeeds() + .stdout_only("0-test-zero\x002-test-zero\x003-test-zero\x00"); + + for opt in ignored_opts { + scene + .ucmd() + .args(&[opt, "--zero"]) + .succeeds() + .stdout_only("0-test-zero\x002-test-zero\x003-test-zero\x00"); + } + + scene + .ucmd() + .args(&["--zero", "--quoting-style=c"]) + .succeeds() + .stdout_only("\"0-test-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00"); + + scene + .ucmd() + .args(&["--zero", "--color=always"]) + .succeeds() + .stdout_only("\x1b[1;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00"); + + scene + .ucmd() + .args(&["--zero", "-m"]) + .succeeds() + .stdout_only("0-test-zero, 2-test-zero, 3-test-zero\x00"); + + scene + .ucmd() + .args(&["--zero", "--hide-control-chars"]) + .succeeds() + .stdout_only("0-test-zero\x002-test-zero\x003-test-zero\x00"); + + #[cfg(unix)] + { + at.touch(&at.plus_as_string("1\ntest-zero")); + + let ignored_opts = [ + "--quoting-style=c", + "--color=always", + "-m", + "--hide-control-chars", + ]; + + scene + .ucmd() + .arg("--zero") + .succeeds() + .stdout_only("0-test-zero\x001\ntest-zero\x002-test-zero\x003-test-zero\x00"); + + for opt in ignored_opts { + scene + .ucmd() + .args(&[opt, "--zero"]) + .succeeds() + .stdout_only("0-test-zero\x001\ntest-zero\x002-test-zero\x003-test-zero\x00"); + } + + scene + .ucmd() + .args(&["--zero", "--quoting-style=c"]) + .succeeds() + .stdout_only("\"0-test-zero\"\x00\"1\\ntest-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00"); + + scene + .ucmd() + .args(&["--zero", "--color=always"]) + .succeeds() + .stdout_only("\x1b[1;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00"); + + scene + .ucmd() + .args(&["--zero", "-m"]) + .succeeds() + .stdout_only("0-test-zero, 1\ntest-zero, 2-test-zero, 3-test-zero\x00"); + + scene + .ucmd() + .args(&["--zero", "--hide-control-chars"]) + .succeeds() + .stdout_only("0-test-zero\x001?test-zero\x002-test-zero\x003-test-zero\x00"); + } + + scene + .ucmd() + .args(&["-l", "--zero"]) + .succeeds() + .stdout_contains("total "); +} + #[test] fn test_ls_commas_trailing() { let scene = TestScenario::new(util_name!());