mirror of
https://github.com/uutils/coreutils
synced 2024-10-06 16:09:08 +00:00
Merge pull request #2023 from ycd/cut
cut: move to clap, add gnu like error messages + tests
This commit is contained in:
commit
c196f4ae8b
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1679,6 +1679,7 @@ dependencies = [
|
|||
name = "uu_cut"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
]
|
||||
|
|
|
@ -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" }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue