1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

refactor(uniq): Move to clap + add a test (#1626)

This commit is contained in:
Sylvestre Ledru 2020-11-21 09:52:40 +01:00 committed by GitHub
parent 7bbb4c98e8
commit 41ba5ed913
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 100 deletions

2
Cargo.lock generated
View file

@ -2237,7 +2237,7 @@ dependencies = [
name = "uu_uniq" name = "uu_uniq"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)",
"uucore 0.0.4", "uucore 0.0.4",
"uucore_procs 0.0.4", "uucore_procs 0.0.4",
] ]

View file

@ -15,7 +15,7 @@ edition = "2018"
path = "src/uniq.rs" path = "src/uniq.rs"
[dependencies] [dependencies]
getopts = "0.2.18" clap = "2.33"
uucore = { version=">=0.0.4", package="uucore", path="../../uucore" } uucore = { version=">=0.0.4", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.4", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.4", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,19 +5,30 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
extern crate getopts; extern crate clap;
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use getopts::{Matches, Options}; use clap::{App, Arg, ArgMatches};
use std::fs::File; use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
static NAME: &str = "uniq"; static ABOUT: &str = "Report or omit repeated lines.";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static OPT_ALL_REPEATED: &str = "all-repeated";
static OPT_CHECK_CHARS: &str = "check-chars";
static OPT_COUNT: &str = "count";
static OPT_IGNORE_CASE: &str = "ignore-case";
static OPT_REPEATED: &str = "repeated";
static OPT_SKIP_FIELDS: &str = "skip-fields";
static OPT_SKIP_CHARS: &str = "skip-chars";
static OPT_UNIQUE: &str = "unique";
static OPT_ZERO_TERMINATED: &str = "zero-terminated";
static ARG_FILES: &str = "files";
#[derive(PartialEq)] #[derive(PartialEq)]
enum Delimiters { enum Delimiters {
@ -194,94 +205,124 @@ impl Uniq {
} }
} }
fn opt_parsed<T: FromStr>(opt_name: &str, matches: &Matches) -> Option<T> { fn opt_parsed<T: FromStr>(opt_name: &str, matches: &ArgMatches) -> Option<T> {
matches.opt_str(opt_name).map(|arg_str| { matches.value_of(opt_name).map(|arg_str| {
let opt_val: Option<T> = arg_str.parse().ok(); let opt_val: Option<T> = arg_str.parse().ok();
opt_val.unwrap_or_else(|| crash!(1, "Invalid argument for {}: {}", opt_name, arg_str)) opt_val.unwrap_or_else(|| crash!(1, "Invalid argument for {}: {}", opt_name, arg_str))
}) })
} }
pub fn uumain(args: impl uucore::Args) -> i32 { fn get_usage() -> String {
let args = args.collect_str(); format!("{0} [OPTION]... [INPUT [OUTPUT]]...", executable!())
}
let mut opts = Options::new(); fn get_long_usage() -> String {
String::from(
opts.optflag("c", "count", "prefix lines by the number of occurrences");
opts.optflag("d", "repeated", "only print duplicate lines");
opts.optflagopt(
"D",
"all-repeated",
"print all duplicate lines delimit-method={none(default),prepend,separate} Delimiting is done with blank lines",
"delimit-method"
);
opts.optopt(
"f",
"skip-fields",
"avoid comparing the first N fields",
"N",
);
opts.optopt(
"s",
"skip-chars",
"avoid comparing the first N characters",
"N",
);
opts.optopt(
"w",
"check-chars",
"compare no more than N characters in lines",
"N",
);
opts.optflag(
"i",
"ignore-case",
"ignore differences in case when comparing",
);
opts.optflag("u", "unique", "only print unique lines");
opts.optflag("z", "zero-terminated", "end lines with 0 byte, not newline");
opts.optflag("h", "help", "display this help and exit");
opts.optflag("V", "version", "output version information and exit");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => crash!(1, "{}", f),
};
if matches.opt_present("help") {
println!("{} {}", NAME, VERSION);
println!();
println!("Usage:");
println!(" {0} [OPTION]... [FILE]...", NAME);
println!();
print!(
"{}",
opts.usage(
"Filter adjacent matching lines from INPUT (or standard input),\n\ "Filter adjacent matching lines from INPUT (or standard input),\n\
writing to OUTPUT (or standard output)." writing to OUTPUT (or standard output).
Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\
You may want to sort the input first, or use 'sort -u' without 'uniq'.\n",
) )
); }
println!();
println!( pub fn uumain(args: impl uucore::Args) -> i32 {
"Note: '{0}' does not detect repeated lines unless they are adjacent.\n\ let usage = get_usage();
You may want to sort the input first, or use 'sort -u' without '{0}'.\n", let long_usage = get_long_usage();
NAME
); let matches = App::new(executable!())
} else if matches.opt_present("version") { .version(VERSION)
println!("{} {}", NAME, VERSION); .about(ABOUT)
} else { .usage(&usage[..])
let (in_file_name, out_file_name) = match matches.free.len() { .after_help(&long_usage[..])
.arg(
Arg::with_name(OPT_ALL_REPEATED)
.short("D")
.long(OPT_ALL_REPEATED)
.possible_values(&["none", "prepend", "separate"])
.help("print all duplicate lines. Delimiting is done with blank lines")
.value_name("delimit-method")
.default_value("none"),
)
.arg(
Arg::with_name(OPT_CHECK_CHARS)
.short("w")
.long(OPT_CHECK_CHARS)
.help("compare no more than N characters in lines")
.value_name("N"),
)
.arg(
Arg::with_name(OPT_COUNT)
.short("c")
.long(OPT_COUNT)
.help("prefix lines by the number of occurrences"),
)
.arg(
Arg::with_name(OPT_IGNORE_CASE)
.short("i")
.long(OPT_IGNORE_CASE)
.help("ignore differences in case when comparing"),
)
.arg(
Arg::with_name(OPT_REPEATED)
.short("d")
.long(OPT_REPEATED)
.help("only print duplicate lines"),
)
.arg(
Arg::with_name(OPT_SKIP_CHARS)
.short("s")
.long(OPT_SKIP_CHARS)
.help("avoid comparing the first N characters")
.value_name("N"),
)
.arg(
Arg::with_name(OPT_SKIP_FIELDS)
.short("f")
.long(OPT_SKIP_FIELDS)
.help("avoid comparing the first N fields")
.value_name("N"),
)
.arg(
Arg::with_name(OPT_UNIQUE)
.short("u")
.long(OPT_UNIQUE)
.help("only print unique lines"),
)
.arg(
Arg::with_name(OPT_ZERO_TERMINATED)
.short("z")
.long(OPT_ZERO_TERMINATED)
.help("end lines with 0 byte, not newline"),
)
.arg(
Arg::with_name(ARG_FILES)
.multiple(true)
.takes_value(true)
.max_values(2),
)
.get_matches_from(args);
let files: Vec<String> = matches
.values_of(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let (in_file_name, out_file_name) = match files.len() {
0 => ("-".to_owned(), "-".to_owned()), 0 => ("-".to_owned(), "-".to_owned()),
1 => (matches.free[0].clone(), "-".to_owned()), 1 => (files[0].clone(), "-".to_owned()),
2 => (matches.free[0].clone(), matches.free[1].clone()), 2 => (files[0].clone(), files[1].clone()),
_ => { _ => {
crash!(1, "Extra operand: {}", matches.free[2]); // Cannot happen as clap will fail earlier
crash!(1, "Extra operand: {}", files[2]);
} }
}; };
let uniq = Uniq { let uniq = Uniq {
repeats_only: matches.opt_present("repeated") || matches.opt_present("all-repeated"), repeats_only: matches.is_present(OPT_REPEATED)
uniques_only: matches.opt_present("unique"), || matches.occurrences_of(OPT_ALL_REPEATED) > 0,
all_repeated: matches.opt_present("all-repeated"), uniques_only: matches.is_present(OPT_UNIQUE),
delimiters: match matches.opt_default("all-repeated", "none") { all_repeated: matches.occurrences_of(OPT_ALL_REPEATED) > 0,
delimiters: match matches.value_of(OPT_ALL_REPEATED).map(String::from) {
Some(ref opt_arg) if opt_arg != "none" => match &(*opt_arg.as_str()) { Some(ref opt_arg) if opt_arg != "none" => match &(*opt_arg.as_str()) {
"prepend" => Delimiters::Prepend, "prepend" => Delimiters::Prepend,
"separate" => Delimiters::Separate, "separate" => Delimiters::Separate,
@ -289,18 +330,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}, },
_ => Delimiters::None, _ => Delimiters::None,
}, },
show_counts: matches.opt_present("count"), show_counts: matches.is_present(OPT_COUNT),
skip_fields: opt_parsed("skip-fields", &matches), skip_fields: opt_parsed(OPT_SKIP_FIELDS, &matches),
slice_start: opt_parsed("skip-chars", &matches), slice_start: opt_parsed(OPT_SKIP_CHARS, &matches),
slice_stop: opt_parsed("check-chars", &matches), slice_stop: opt_parsed(OPT_CHECK_CHARS, &matches),
ignore_case: matches.opt_present("ignore-case"), ignore_case: matches.is_present(OPT_IGNORE_CASE),
zero_terminated: matches.opt_present("zero-terminated"), zero_terminated: matches.is_present(OPT_ZERO_TERMINATED),
}; };
uniq.print_uniq( uniq.print_uniq(
&mut open_input_file(in_file_name), &mut open_input_file(in_file_name),
&mut open_output_file(out_file_name), &mut open_output_file(out_file_name),
); );
}
0 0
} }

View file

@ -1,6 +1,7 @@
use crate::common::util::*; use crate::common::util::*;
static INPUT: &'static str = "sorted.txt"; static INPUT: &'static str = "sorted.txt";
static OUTPUT: &'static str = "sorted-output.txt";
static SKIP_CHARS: &'static str = "skip-chars.txt"; static SKIP_CHARS: &'static str = "skip-chars.txt";
static SKIP_FIELDS: &'static str = "skip-fields.txt"; static SKIP_FIELDS: &'static str = "skip-fields.txt";
static SORTED_ZERO_TERMINATED: &'static str = "sorted-zero-terminated.txt"; static SORTED_ZERO_TERMINATED: &'static str = "sorted-zero-terminated.txt";
@ -21,6 +22,15 @@ fn test_single_default() {
.stdout_is_fixture("sorted-simple.expected"); .stdout_is_fixture("sorted-simple.expected");
} }
#[test]
fn test_single_default_output() {
let (at, mut ucmd) = at_and_ucmd!();
let expected = at.read("sorted-simple.expected");
ucmd.args(&[INPUT, OUTPUT]).run();
let found = at.read(OUTPUT);
assert_eq!(found, expected);
}
#[test] #[test]
fn test_stdin_counts() { fn test_stdin_counts() {
new_ucmd!() new_ucmd!()