mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 03:27:44 +00:00
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.
This commit is contained in:
parent
572a505119
commit
6a6875012e
4 changed files with 155 additions and 20 deletions
|
@ -20,12 +20,15 @@ use word_count::{TitledWordCount, WordCount};
|
||||||
use clap::{crate_version, App, AppSettings, Arg, ArgMatches};
|
use clap::{crate_version, App, AppSettings, Arg, ArgMatches};
|
||||||
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use uucore::display::{Quotable, Quoted};
|
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.
|
/// The minimum character width for formatting counts when reading from stdin.
|
||||||
const MINIMUM_WIDTH: usize = 7;
|
const MINIMUM_WIDTH: usize = 7;
|
||||||
|
@ -83,12 +86,14 @@ more than one FILE is specified.";
|
||||||
pub mod options {
|
pub mod options {
|
||||||
pub static BYTES: &str = "bytes";
|
pub static BYTES: &str = "bytes";
|
||||||
pub static CHAR: &str = "chars";
|
pub static CHAR: &str = "chars";
|
||||||
|
pub static FILES0_FROM: &str = "files0-from";
|
||||||
pub static LINES: &str = "lines";
|
pub static LINES: &str = "lines";
|
||||||
pub static MAX_LINE_LENGTH: &str = "max-line-length";
|
pub static MAX_LINE_LENGTH: &str = "max-line-length";
|
||||||
pub static WORDS: &str = "words";
|
pub static WORDS: &str = "words";
|
||||||
}
|
}
|
||||||
|
|
||||||
static ARG_FILES: &str = "files";
|
static ARG_FILES: &str = "files";
|
||||||
|
static STDIN_REPR: &str = "-";
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
format!(
|
format!(
|
||||||
|
@ -115,12 +120,22 @@ enum Input {
|
||||||
Stdin(StdinKind),
|
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 {
|
impl Input {
|
||||||
/// Converts input to title that appears in stats.
|
/// Converts input to title that appears in stats.
|
||||||
fn to_title(&self) -> Option<&Path> {
|
fn to_title(&self) -> Option<&Path> {
|
||||||
match self {
|
match self {
|
||||||
Input::Path(path) => Some(path),
|
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,
|
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]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let usage = usage();
|
let usage = usage();
|
||||||
|
|
||||||
let matches = uu_app().override_usage(&usage[..]).get_matches_from(args);
|
let matches = uu_app().override_usage(&usage[..]).get_matches_from(args);
|
||||||
|
|
||||||
let mut inputs: Vec<Input> = matches
|
let inputs = inputs(&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 settings = Settings::new(&matches);
|
let settings = Settings::new(&matches);
|
||||||
|
|
||||||
|
@ -179,6 +208,17 @@ pub fn uu_app<'a>() -> App<'a> {
|
||||||
.long(options::CHAR)
|
.long(options::CHAR)
|
||||||
.help("print the character counts"),
|
.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(
|
||||||
Arg::new(options::LINES)
|
Arg::new(options::LINES)
|
||||||
.short('l')
|
.short('l')
|
||||||
|
@ -205,6 +245,47 @@ pub fn uu_app<'a>() -> App<'a> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inputs(matches: &ArgMatches) -> UResult<Vec<Input>> {
|
||||||
|
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<Vec<Input>> {
|
||||||
|
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<T: WordCountable>(
|
fn word_count_from_reader<T: WordCountable>(
|
||||||
mut reader: T,
|
mut reader: T,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
|
|
|
@ -245,3 +245,57 @@ fn test_files_from_pseudo_filesystem() {
|
||||||
let result = new_ucmd!().arg("-c").arg("/proc/version").succeeds();
|
let result = new_ucmd!().arg("-c").arg("/proc/version").succeeds();
|
||||||
assert_ne!(result.stdout_str(), "0 /proc/version\n");
|
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("");
|
||||||
|
}
|
||||||
|
|
BIN
tests/fixtures/wc/files0_list.txt
vendored
Normal file
BIN
tests/fixtures/wc/files0_list.txt
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/wc/files0_list_with_stdin.txt
vendored
Normal file
BIN
tests/fixtures/wc/files0_list_with_stdin.txt
vendored
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue