Merge pull request #2023 from ycd/cut

cut: move to clap, add gnu like error messages + tests
This commit is contained in:
Sylvestre Ledru 2021-04-04 14:47:15 +02:00 committed by GitHub
commit c196f4ae8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 34 deletions

1
Cargo.lock generated
View file

@ -1679,6 +1679,7 @@ dependencies = [
name = "uu_cut"
version = "0.0.6"
dependencies = [
"clap",
"uucore",
"uucore_procs",
]

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/cut.rs"
[dependencies]
clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,6 +10,7 @@
#[macro_use]
extern crate uucore;
use clap::{App, Arg};
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
use std::path::Path;
@ -20,6 +21,8 @@ use uucore::ranges::Range;
mod buffer;
mod searcher;
static NAME: &str = "cut";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static SYNTAX: &str =
"[-d] [-s] [-z] [--output-delimiter] ((-f|-b|-c) {{sequence}}) {{sourcefile}}+";
static SUMMARY: &str =
@ -398,8 +401,13 @@ fn cut_files(mut filenames: Vec<String>, mode: Mode) -> i32 {
} else {
let path = Path::new(&filename[..]);
if !path.exists() {
show_error!("{}", msg_args_nonexistent_file!(filename));
if path.is_dir() {
show_error!("{}: Is a directory", filename);
continue;
}
if !path.metadata().is_ok() {
show_error!("{}: No such file or directory", filename);
continue;
}
@ -422,34 +430,123 @@ fn cut_files(mut filenames: Vec<String>, mode: Mode) -> i32 {
exit_code
}
mod options {
pub const BYTES: &str = "bytes";
pub const CHARACTERS: &str = "characters";
pub const DELIMITER: &str = "delimiter";
pub const FIELDS: &str = "fields";
pub const ZERO_TERMINATED: &str = "zero-terminated";
pub const ONLY_DELIMITED: &str = "only-delimited";
pub const OUTPUT_DELIMITER: &str = "output-delimiter";
pub const COMPLEMENT: &str = "complement";
pub const FILE: &str = "file";
}
pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args.collect_str();
let matches = app!(SYNTAX, SUMMARY, LONG_HELP)
.optopt("b", "bytes", "filter byte columns from the input source", "sequence")
.optopt("c", "characters", "alias for character mode", "sequence")
.optopt("d", "delimiter", "specify the delimiter character that separates fields in the input source. Defaults to Tab.", "delimiter")
.optopt("f", "fields", "filter field columns from the input source", "sequence")
.optflag("n", "", "legacy option - has no effect.")
.optflag("", "complement", "invert the filter - instead of displaying only the filtered columns, display all but those columns")
.optflag("s", "only-delimited", "in field mode, only print lines which contain the delimiter")
.optflag("z", "zero-terminated", "instead of filtering columns based on line, filter columns based on \\0 (NULL character)")
.optopt("", "output-delimiter", "in field mode, replace the delimiter in output lines with this option's argument", "new delimiter")
.parse(args);
let complement = matches.opt_present("complement");
let matches = App::new(executable!())
.name(NAME)
.version(VERSION)
.usage(SYNTAX)
.about(SUMMARY)
.after_help(LONG_HELP)
.arg(
Arg::with_name(options::BYTES)
.short("b")
.long(options::BYTES)
.takes_value(true)
.help("filter byte columns from the input source")
.allow_hyphen_values(true)
.value_name("LIST")
.display_order(1),
)
.arg(
Arg::with_name(options::CHARACTERS)
.short("c")
.long(options::CHARACTERS)
.help("alias for character mode")
.takes_value(true)
.allow_hyphen_values(true)
.value_name("LIST")
.display_order(2),
)
.arg(
Arg::with_name(options::DELIMITER)
.short("d")
.long(options::DELIMITER)
.help("specify the delimiter character that separates fields in the input source. Defaults to Tab.")
.takes_value(true)
.value_name("DELIM")
.display_order(3),
)
.arg(
Arg::with_name(options::FIELDS)
.short("f")
.long(options::FIELDS)
.help("filter field columns from the input source")
.takes_value(true)
.allow_hyphen_values(true)
.value_name("LIST")
.display_order(4),
)
.arg(
Arg::with_name(options::COMPLEMENT)
.long(options::COMPLEMENT)
.help("invert the filter - instead of displaying only the filtered columns, display all but those columns")
.takes_value(false)
.display_order(5),
)
.arg(
Arg::with_name(options::ONLY_DELIMITED)
.short("s")
.long(options::ONLY_DELIMITED)
.help("in field mode, only print lines which contain the delimiter")
.takes_value(false)
.display_order(6),
)
.arg(
Arg::with_name(options::ZERO_TERMINATED)
.short("z")
.long(options::ZERO_TERMINATED)
.help("instead of filtering columns based on line, filter columns based on \\0 (NULL character)")
.takes_value(false)
.display_order(8),
)
.arg(
Arg::with_name(options::OUTPUT_DELIMITER)
.long(options::OUTPUT_DELIMITER)
.help("in field mode, replace the delimiter in output lines with this option's argument")
.takes_value(true)
.value_name("NEW_DELIM")
.display_order(7),
)
.arg(
Arg::with_name(options::FILE)
.hidden(true)
.multiple(true)
)
.get_matches_from(args);
let complement = matches.is_present(options::COMPLEMENT);
let mode_parse = match (
matches.opt_str("bytes"),
matches.opt_str("characters"),
matches.opt_str("fields"),
matches.value_of(options::BYTES),
matches.value_of(options::CHARACTERS),
matches.value_of(options::FIELDS),
) {
(Some(byte_ranges), None, None) => {
list_to_ranges(&byte_ranges[..], complement).map(|ranges| {
Mode::Bytes(
ranges,
Options {
out_delim: matches.opt_str("output-delimiter"),
zero_terminated: matches.opt_present("zero-terminated"),
out_delim: Some(
matches
.value_of(options::OUTPUT_DELIMITER)
.unwrap_or_default()
.to_owned(),
),
zero_terminated: matches.is_present(options::ZERO_TERMINATED),
},
)
})
@ -459,29 +556,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Mode::Characters(
ranges,
Options {
out_delim: matches.opt_str("output-delimiter"),
zero_terminated: matches.opt_present("zero-terminated"),
out_delim: Some(
matches
.value_of(options::OUTPUT_DELIMITER)
.unwrap_or_default()
.to_owned(),
),
zero_terminated: matches.is_present(options::ZERO_TERMINATED),
},
)
})
}
(None, None, Some(field_ranges)) => {
list_to_ranges(&field_ranges[..], complement).and_then(|ranges| {
let out_delim = match matches.opt_str("output-delimiter") {
let out_delim = match matches.value_of(options::OUTPUT_DELIMITER) {
Some(s) => {
if s.is_empty() {
Some("\0".to_owned())
} else {
Some(s)
Some(s.to_owned())
}
}
None => None,
};
let only_delimited = matches.opt_present("only-delimited");
let zero_terminated = matches.opt_present("zero-terminated");
let only_delimited = matches.is_present(options::ONLY_DELIMITED);
let zero_terminated = matches.is_present(options::ZERO_TERMINATED);
match matches.opt_str("delimiter") {
match matches.value_of(options::DELIMITER) {
Some(delim) => {
if delim.chars().count() > 1 {
Err(msg_opt_invalid_should_be!(
@ -494,7 +596,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let delim = if delim.is_empty() {
"\0".to_owned()
} else {
delim
delim.to_owned()
};
Ok(Mode::Fields(
@ -533,10 +635,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let mode_parse = match mode_parse {
Err(_) => mode_parse,
Ok(mode) => match mode {
Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("delimiter") => Err(
msg_opt_only_usable_if!("printing a sequence of fields", "--delimiter", "-d"),
),
Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("only-delimited") => {
Mode::Bytes(_, _) | Mode::Characters(_, _)
if matches.is_present(options::DELIMITER) =>
{
Err(msg_opt_only_usable_if!(
"printing a sequence of fields",
"--delimiter",
"-d"
))
}
Mode::Bytes(_, _) | Mode::Characters(_, _)
if matches.is_present(options::ONLY_DELIMITED) =>
{
Err(msg_opt_only_usable_if!(
"printing a sequence of fields",
"--only-delimited",
@ -547,8 +657,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
},
};
let files: Vec<String> = matches
.values_of(options::FILE)
.unwrap_or_default()
.map(str::to_owned)
.collect();
match mode_parse {
Ok(mode) => cut_files(matches.free, mode),
Ok(mode) => cut_files(files, mode),
Err(err_msg) => {
show_error!("{}", err_msg);
1

View file

@ -80,8 +80,7 @@ fn print_version() {
fn print_usage(opts: &Options) {
let brief = "Run COMMAND, with modified buffering operations for its standard streams\n \
Mandatory arguments to long options are mandatory for short options too.";
let explanation =
"If MODE is 'L' the corresponding stream will be line buffered.\n \
let explanation = "If MODE is 'L' the corresponding stream will be line buffered.\n \
This option is invalid with standard input.\n\n \
If MODE is '0' the corresponding stream will be unbuffered.\n\n \
Otherwise MODE is a number which may be followed by one of the following:\n\n \

View file

@ -139,3 +139,21 @@ fn test_zero_terminated_only_delimited() {
.succeeds()
.stdout_only("82\n7\0");
}
#[test]
fn test_directory_and_no_such_file() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("some");
ucmd.arg("-b1")
.arg("some")
.run()
.stderr_is("cut: error: some: Is a directory\n");
new_ucmd!()
.arg("-b1")
.arg("some")
.run()
.stderr_is("cut: error: some: No such file or directory\n");
}