Skip to content

Commit

Permalink
ls: Implement --zero flag. (uutils#2929)
Browse files Browse the repository at this point in the history
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
 ============================================================================
```
  • Loading branch information
pimzero committed Jul 26, 2022
1 parent 4e72e28 commit 4f32e99
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 13 deletions.
120 changes: 107 additions & 13 deletions src/uu/ls/src/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
// overriden 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) {
Expand Down Expand Up @@ -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'
},
})
}
}
Expand Down Expand Up @@ -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 serarated 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
Expand Down Expand Up @@ -1884,7 +1973,12 @@ fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
.as_ref()
.map_or(0, |md| get_block_size(md, config));
}
writeln!(out, "total {}", display_size(total_size, config))?;
write!(
out,
"total {}{}",
display_size(total_size, config),
config.eol
)?;
Ok(())
}

Expand Down Expand Up @@ -1994,12 +2088,12 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
// Current col is never zero again if names have been printed.
// So we print a newline.
if current_col > 0 {
writeln!(out,)?;
write!(out, "{}", config.eol)?;
}
}
_ => {
for name in names {
writeln!(out, "{}", name.contents)?;
write!(out, "{}{}", name.contents, config.eol)?;
}
}
};
Expand Down Expand Up @@ -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 = {
Expand Down
61 changes: 61 additions & 0 deletions tests/by-util/test_ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,67 @@ 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("1\ntest-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\01\ntest-zero\02-test-zero\03-test-zero\0");

for opt in ignored_opts {
scene
.ucmd()
.args(&[opt, "--zero"])
.succeeds()
.stdout_only("0-test-zero\01\ntest-zero\02-test-zero\03-test-zero\0");
}

scene
.ucmd()
.args(&["--zero", "--quoting-style=c"])
.succeeds()
.stdout_only("\"0-test-zero\"\0\"1\\ntest-zero\"\0\"2-test-zero\"\0\"3-test-zero\"\0");

scene
.ucmd()
.args(&["--zero", "--color=always"])
.succeeds()
.stdout_only("\x1b[1;34m0-test-zero\x1b[0m\01\ntest-zero\02-test-zero\03-test-zero\0");

scene
.ucmd()
.args(&["--zero", "-m"])
.succeeds()
.stdout_only("0-test-zero, 1\ntest-zero, 2-test-zero, 3-test-zero\0");

scene
.ucmd()
.args(&["--zero", "--hide-control-chars"])
.succeeds()
.stdout_only("0-test-zero\01?test-zero\02-test-zero\03-test-zero\0");

scene
.ucmd()
.args(&["-l", "--zero"])
.succeeds()
.stdout_contains("total ");
}

#[test]
fn test_ls_commas_trailing() {
let scene = TestScenario::new(util_name!());
Expand Down

0 comments on commit 4f32e99

Please sign in to comment.