diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 59aea1542..97ea26b8b 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 { + Self::Stdin(StdinKind::Explicit) + } else { + Self::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 000000000..5c7af28f0 Binary files /dev/null and b/tests/fixtures/wc/files0_list.txt differ 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 000000000..b938a7867 Binary files /dev/null and b/tests/fixtures/wc/files0_list_with_stdin.txt differ