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