From 6a6875012eed330e922e2d5566016a9427f6b1ef Mon Sep 17 00:00:00 2001 From: Allan Silva Date: Sun, 30 Jan 2022 01:18:32 -0300 Subject: [PATCH] wc: implement files0-from option When this option is present, the files argument is not processed. This option processes the file list from provided file, splitting them by the ascii NUL (\0) character. When files0-from is '-', the file list is processed from stdin. --- src/uu/wc/src/wc.rs | 121 ++++++++++++++++--- tests/by-util/test_wc.rs | 54 +++++++++ tests/fixtures/wc/files0_list.txt | Bin 0 -> 53 bytes tests/fixtures/wc/files0_list_with_stdin.txt | Bin 0 -> 31 bytes 4 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/wc/files0_list.txt create mode 100644 tests/fixtures/wc/files0_list_with_stdin.txt diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 59aea1542..2afbe4e21 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -20,12 +20,15 @@ use word_count::{TitledWordCount, WordCount}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::cmp::max; +use std::error::Error; +use std::ffi::OsStr; +use std::fmt::Display; use std::fs::{self, File}; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use uucore::display::{Quotable, Quoted}; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UError, UResult, USimpleError}; /// The minimum character width for formatting counts when reading from stdin. const MINIMUM_WIDTH: usize = 7; @@ -83,12 +86,14 @@ more than one FILE is specified."; pub mod options { pub static BYTES: &str = "bytes"; pub static CHAR: &str = "chars"; + pub static FILES0_FROM: &str = "files0-from"; pub static LINES: &str = "lines"; pub static MAX_LINE_LENGTH: &str = "max-line-length"; pub static WORDS: &str = "words"; } static ARG_FILES: &str = "files"; +static STDIN_REPR: &str = "-"; fn usage() -> String { format!( @@ -115,12 +120,22 @@ enum Input { Stdin(StdinKind), } +impl From<&OsStr> for Input { + fn from(input: &OsStr) -> Self { + if input == STDIN_REPR { + Input::Stdin(StdinKind::Explicit) + } else { + Input::Path(input.into()) + } + } +} + impl Input { /// Converts input to title that appears in stats. fn to_title(&self) -> Option<&Path> { match self { Input::Path(path) => Some(path), - Input::Stdin(StdinKind::Explicit) => Some("-".as_ref()), + Input::Stdin(StdinKind::Explicit) => Some(STDIN_REPR.as_ref()), Input::Stdin(StdinKind::Implicit) => None, } } @@ -133,29 +148,43 @@ impl Input { } } +#[derive(Debug)] +enum WcError { + FilesDisabled(String), + StdinReprNotAllowed(String), +} + +impl UError for WcError { + fn code(&self) -> i32 { + match self { + WcError::FilesDisabled(_) | WcError::StdinReprNotAllowed(_) => 1, + } + } + + fn usage(&self) -> bool { + matches!(self, WcError::FilesDisabled(_)) + } +} + +impl Error for WcError {} + +impl Display for WcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WcError::FilesDisabled(message) | WcError::StdinReprNotAllowed(message) => { + write!(f, "{}", message) + } + } + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); let matches = uu_app().override_usage(&usage[..]).get_matches_from(args); - let mut inputs: Vec = matches - .values_of_os(ARG_FILES) - .map(|v| { - v.map(|i| { - if i == "-" { - Input::Stdin(StdinKind::Explicit) - } else { - Input::Path(i.into()) - } - }) - .collect() - }) - .unwrap_or_default(); - - if inputs.is_empty() { - inputs.push(Input::Stdin(StdinKind::Implicit)); - } + let inputs = inputs(&matches)?; let settings = Settings::new(&matches); @@ -179,6 +208,17 @@ pub fn uu_app<'a>() -> App<'a> { .long(options::CHAR) .help("print the character counts"), ) + .arg( + Arg::new(options::FILES0_FROM) + .long(options::FILES0_FROM) + .takes_value(true) + .value_name("F") + .help( + "read input from the files specified by + NUL-terminated names in file F; + If F is - then read names from standard input", + ), + ) .arg( Arg::new(options::LINES) .short('l') @@ -205,6 +245,47 @@ pub fn uu_app<'a>() -> App<'a> { ) } +fn inputs(matches: &ArgMatches) -> UResult> { + match matches.values_of_os(ARG_FILES) { + Some(os_values) => { + if matches.is_present(options::FILES0_FROM) { + return Err(WcError::FilesDisabled( + "file operands cannot be combined with --files0-from".into(), + ) + .into()); + } + + Ok(os_values.map(Input::from).collect()) + } + None => match matches.value_of(options::FILES0_FROM) { + Some(files_0_from) => create_paths_from_files0(files_0_from), + None => Ok(vec![Input::Stdin(StdinKind::Implicit)]), + }, + } +} + +fn create_paths_from_files0(files_0_from: &str) -> UResult> { + let mut paths = String::new(); + let read_from_stdin = files_0_from == STDIN_REPR; + + if read_from_stdin { + io::stdin().lock().read_to_string(&mut paths)?; + } else { + File::open(files_0_from)?.read_to_string(&mut paths)?; + } + + let paths: Vec<&str> = paths.split_terminator('\0').collect(); + + if read_from_stdin && paths.contains(&STDIN_REPR) { + return Err(WcError::StdinReprNotAllowed( + "when reading file names from stdin, no file name of '-' allowed".into(), + ) + .into()); + } + + Ok(paths.iter().map(OsStr::new).map(Input::from).collect()) +} + fn word_count_from_reader( mut reader: T, settings: &Settings, diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 5c4763f99..39689afc9 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -245,3 +245,57 @@ fn test_files_from_pseudo_filesystem() { let result = new_ucmd!().arg("-c").arg("/proc/version").succeeds(); assert_ne!(result.stdout_str(), "0 /proc/version\n"); } + +#[test] +fn test_files0_disabled_files_argument() { + const MSG: &str = "file operands cannot be combined with --files0-from"; + new_ucmd!() + .args(&["--files0-from=files0_list.txt"]) + .arg("lorem_ipsum.txt") + .fails() + .stderr_contains(MSG) + .stdout_is(""); +} + +#[test] +fn test_files0_from() { + new_ucmd!() + .args(&["--files0-from=files0_list.txt"]) + .run() + .stdout_is( + " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ + alice_in_wonderland.txt\n 36 370 2189 total\n", + ); +} + +#[test] +fn test_files0_from_with_stdin() { + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in("lorem_ipsum.txt") + .run() + .stdout_is(" 13 109 772 lorem_ipsum.txt\n"); +} + +#[test] +fn test_files0_from_with_stdin_in_file() { + new_ucmd!() + .args(&["--files0-from=files0_list_with_stdin.txt"]) + .pipe_in_fixture("alice_in_wonderland.txt") + .run() + .stdout_is( + " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ + -\n 36 370 2189 total\n", + ); +} + +#[test] +fn test_files0_from_with_stdin_try_read_from_stdin() { + const MSG: &str = "when reading file names from stdin, no file name of '-' allowed"; + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in("-") + .fails() + .stderr_contains(MSG) + .stdout_is(""); +} diff --git a/tests/fixtures/wc/files0_list.txt b/tests/fixtures/wc/files0_list.txt new file mode 100644 index 0000000000000000000000000000000000000000..5c7af28f0d7f4d299f800105b5031cc0bc1f2fe2 GIT binary patch literal 53 zcmd1FFG|gg&nze|&DATZC}GIWPpXVh$xO}$^AdA1lT+g}^Ww|%^HNfaauV}WK;i%~ CJQT?Q literal 0 HcmV?d00001 diff --git a/tests/fixtures/wc/files0_list_with_stdin.txt b/tests/fixtures/wc/files0_list_with_stdin.txt new file mode 100644 index 0000000000000000000000000000000000000000..b938a78677a223a828e066f8d3e565e9c8488e0f GIT binary patch literal 31 icmd1FFG|gg&nze|&DATZC}GIWPpXVh$xO}$^K=2lObe#~ literal 0 HcmV?d00001