mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +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
5 changed files with 169 additions and 34 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1679,6 +1679,7 @@ dependencies = [
|
||||||
name = "uu_cut"
|
name = "uu_cut"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
"uucore",
|
"uucore",
|
||||||
"uucore_procs",
|
"uucore_procs",
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,6 +15,7 @@ edition = "2018"
|
||||||
path = "src/cut.rs"
|
path = "src/cut.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = "2.33"
|
||||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
|
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
|
||||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
use clap::{App, Arg};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
|
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -20,6 +21,8 @@ use uucore::ranges::Range;
|
||||||
mod buffer;
|
mod buffer;
|
||||||
mod searcher;
|
mod searcher;
|
||||||
|
|
||||||
|
static NAME: &str = "cut";
|
||||||
|
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
static SYNTAX: &str =
|
static SYNTAX: &str =
|
||||||
"[-d] [-s] [-z] [--output-delimiter] ((-f|-b|-c) {{sequence}}) {{sourcefile}}+";
|
"[-d] [-s] [-z] [--output-delimiter] ((-f|-b|-c) {{sequence}}) {{sourcefile}}+";
|
||||||
static SUMMARY: &str =
|
static SUMMARY: &str =
|
||||||
|
@ -398,8 +401,13 @@ fn cut_files(mut filenames: Vec<String>, mode: Mode) -> i32 {
|
||||||
} else {
|
} else {
|
||||||
let path = Path::new(&filename[..]);
|
let path = Path::new(&filename[..]);
|
||||||
|
|
||||||
if !path.exists() {
|
if path.is_dir() {
|
||||||
show_error!("{}", msg_args_nonexistent_file!(filename));
|
show_error!("{}: Is a directory", filename);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.metadata().is_ok() {
|
||||||
|
show_error!("{}: No such file or directory", filename);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,34 +430,123 @@ fn cut_files(mut filenames: Vec<String>, mode: Mode) -> i32 {
|
||||||
exit_code
|
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 {
|
pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let args = args.collect_str();
|
let args = args.collect_str();
|
||||||
|
|
||||||
let matches = app!(SYNTAX, SUMMARY, LONG_HELP)
|
let matches = App::new(executable!())
|
||||||
.optopt("b", "bytes", "filter byte columns from the input source", "sequence")
|
.name(NAME)
|
||||||
.optopt("c", "characters", "alias for character mode", "sequence")
|
.version(VERSION)
|
||||||
.optopt("d", "delimiter", "specify the delimiter character that separates fields in the input source. Defaults to Tab.", "delimiter")
|
.usage(SYNTAX)
|
||||||
.optopt("f", "fields", "filter field columns from the input source", "sequence")
|
.about(SUMMARY)
|
||||||
.optflag("n", "", "legacy option - has no effect.")
|
.after_help(LONG_HELP)
|
||||||
.optflag("", "complement", "invert the filter - instead of displaying only the filtered columns, display all but those columns")
|
.arg(
|
||||||
.optflag("s", "only-delimited", "in field mode, only print lines which contain the delimiter")
|
Arg::with_name(options::BYTES)
|
||||||
.optflag("z", "zero-terminated", "instead of filtering columns based on line, filter columns based on \\0 (NULL character)")
|
.short("b")
|
||||||
.optopt("", "output-delimiter", "in field mode, replace the delimiter in output lines with this option's argument", "new delimiter")
|
.long(options::BYTES)
|
||||||
.parse(args);
|
.takes_value(true)
|
||||||
let complement = matches.opt_present("complement");
|
.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 (
|
let mode_parse = match (
|
||||||
matches.opt_str("bytes"),
|
matches.value_of(options::BYTES),
|
||||||
matches.opt_str("characters"),
|
matches.value_of(options::CHARACTERS),
|
||||||
matches.opt_str("fields"),
|
matches.value_of(options::FIELDS),
|
||||||
) {
|
) {
|
||||||
(Some(byte_ranges), None, None) => {
|
(Some(byte_ranges), None, None) => {
|
||||||
list_to_ranges(&byte_ranges[..], complement).map(|ranges| {
|
list_to_ranges(&byte_ranges[..], complement).map(|ranges| {
|
||||||
Mode::Bytes(
|
Mode::Bytes(
|
||||||
ranges,
|
ranges,
|
||||||
Options {
|
Options {
|
||||||
out_delim: matches.opt_str("output-delimiter"),
|
out_delim: Some(
|
||||||
zero_terminated: matches.opt_present("zero-terminated"),
|
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(
|
Mode::Characters(
|
||||||
ranges,
|
ranges,
|
||||||
Options {
|
Options {
|
||||||
out_delim: matches.opt_str("output-delimiter"),
|
out_delim: Some(
|
||||||
zero_terminated: matches.opt_present("zero-terminated"),
|
matches
|
||||||
|
.value_of(options::OUTPUT_DELIMITER)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
zero_terminated: matches.is_present(options::ZERO_TERMINATED),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
(None, None, Some(field_ranges)) => {
|
(None, None, Some(field_ranges)) => {
|
||||||
list_to_ranges(&field_ranges[..], complement).and_then(|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) => {
|
Some(s) => {
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
Some("\0".to_owned())
|
Some("\0".to_owned())
|
||||||
} else {
|
} else {
|
||||||
Some(s)
|
Some(s.to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let only_delimited = matches.opt_present("only-delimited");
|
let only_delimited = matches.is_present(options::ONLY_DELIMITED);
|
||||||
let zero_terminated = matches.opt_present("zero-terminated");
|
let zero_terminated = matches.is_present(options::ZERO_TERMINATED);
|
||||||
|
|
||||||
match matches.opt_str("delimiter") {
|
match matches.value_of(options::DELIMITER) {
|
||||||
Some(delim) => {
|
Some(delim) => {
|
||||||
if delim.chars().count() > 1 {
|
if delim.chars().count() > 1 {
|
||||||
Err(msg_opt_invalid_should_be!(
|
Err(msg_opt_invalid_should_be!(
|
||||||
|
@ -494,7 +596,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let delim = if delim.is_empty() {
|
let delim = if delim.is_empty() {
|
||||||
"\0".to_owned()
|
"\0".to_owned()
|
||||||
} else {
|
} else {
|
||||||
delim
|
delim.to_owned()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Mode::Fields(
|
Ok(Mode::Fields(
|
||||||
|
@ -533,10 +635,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let mode_parse = match mode_parse {
|
let mode_parse = match mode_parse {
|
||||||
Err(_) => mode_parse,
|
Err(_) => mode_parse,
|
||||||
Ok(mode) => match mode {
|
Ok(mode) => match mode {
|
||||||
Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("delimiter") => Err(
|
Mode::Bytes(_, _) | Mode::Characters(_, _)
|
||||||
msg_opt_only_usable_if!("printing a sequence of fields", "--delimiter", "-d"),
|
if matches.is_present(options::DELIMITER) =>
|
||||||
),
|
{
|
||||||
Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("only-delimited") => {
|
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!(
|
Err(msg_opt_only_usable_if!(
|
||||||
"printing a sequence of fields",
|
"printing a sequence of fields",
|
||||||
"--only-delimited",
|
"--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 {
|
match mode_parse {
|
||||||
Ok(mode) => cut_files(matches.free, mode),
|
Ok(mode) => cut_files(files, mode),
|
||||||
Err(err_msg) => {
|
Err(err_msg) => {
|
||||||
show_error!("{}", err_msg);
|
show_error!("{}", err_msg);
|
||||||
1
|
1
|
||||||
|
|
|
@ -80,8 +80,7 @@ fn print_version() {
|
||||||
fn print_usage(opts: &Options) {
|
fn print_usage(opts: &Options) {
|
||||||
let brief = "Run COMMAND, with modified buffering operations for its standard streams\n \
|
let brief = "Run COMMAND, with modified buffering operations for its standard streams\n \
|
||||||
Mandatory arguments to long options are mandatory for short options too.";
|
Mandatory arguments to long options are mandatory for short options too.";
|
||||||
let explanation =
|
let explanation = "If MODE is 'L' the corresponding stream will be line buffered.\n \
|
||||||
"If MODE is 'L' the corresponding stream will be line buffered.\n \
|
|
||||||
This option is invalid with standard input.\n\n \
|
This option is invalid with standard input.\n\n \
|
||||||
If MODE is '0' the corresponding stream will be unbuffered.\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 \
|
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()
|
.succeeds()
|
||||||
.stdout_only("82\n7\0");
|
.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…
Add table
Add a link
Reference in a new issue