1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27: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"
version = "0.0.1"
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_procs 0.0.4",
]

View file

@ -15,7 +15,7 @@ edition = "2018"
path = "src/uniq.rs"
[dependencies]
getopts = "0.2.18"
clap = "2.33"
uucore = { version=">=0.0.4", package="uucore", path="../../uucore" }
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
// * file that was distributed with this source code.
extern crate getopts;
extern crate clap;
#[macro_use]
extern crate uucore;
use getopts::{Matches, Options};
use clap::{App, Arg, ArgMatches};
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path;
use std::str::FromStr;
static NAME: &str = "uniq";
static ABOUT: &str = "Report or omit repeated lines.";
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)]
enum Delimiters {
@ -194,113 +205,143 @@ impl Uniq {
}
}
fn opt_parsed<T: FromStr>(opt_name: &str, matches: &Matches) -> Option<T> {
matches.opt_str(opt_name).map(|arg_str| {
fn opt_parsed<T: FromStr>(opt_name: &str, matches: &ArgMatches) -> Option<T> {
matches.value_of(opt_name).map(|arg_str| {
let opt_val: Option<T> = arg_str.parse().ok();
opt_val.unwrap_or_else(|| crash!(1, "Invalid argument for {}: {}", opt_name, arg_str))
})
}
fn get_usage() -> String {
format!("{0} [OPTION]... [INPUT [OUTPUT]]...", executable!())
}
fn get_long_usage() -> String {
String::from(
"Filter adjacent matching lines from INPUT (or standard input),\n\
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",
)
}
pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args.collect_str();
let usage = get_usage();
let long_usage = get_long_usage();
let mut opts = Options::new();
let matches = App::new(executable!())
.version(VERSION)
.about(ABOUT)
.usage(&usage[..])
.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);
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 files: Vec<String> = matches
.values_of(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => crash!(1, "{}", f),
let (in_file_name, out_file_name) = match files.len() {
0 => ("-".to_owned(), "-".to_owned()),
1 => (files[0].clone(), "-".to_owned()),
2 => (files[0].clone(), files[1].clone()),
_ => {
// Cannot happen as clap will fail earlier
crash!(1, "Extra operand: {}", files[2]);
}
};
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\
writing to OUTPUT (or standard output)."
)
);
println!();
println!(
"Note: '{0}' does not detect repeated lines unless they are adjacent.\n\
You may want to sort the input first, or use 'sort -u' without '{0}'.\n",
NAME
);
} else if matches.opt_present("version") {
println!("{} {}", NAME, VERSION);
} else {
let (in_file_name, out_file_name) = match matches.free.len() {
0 => ("-".to_owned(), "-".to_owned()),
1 => (matches.free[0].clone(), "-".to_owned()),
2 => (matches.free[0].clone(), matches.free[1].clone()),
_ => {
crash!(1, "Extra operand: {}", matches.free[2]);
}
};
let uniq = Uniq {
repeats_only: matches.opt_present("repeated") || matches.opt_present("all-repeated"),
uniques_only: matches.opt_present("unique"),
all_repeated: matches.opt_present("all-repeated"),
delimiters: match matches.opt_default("all-repeated", "none") {
Some(ref opt_arg) if opt_arg != "none" => match &(*opt_arg.as_str()) {
"prepend" => Delimiters::Prepend,
"separate" => Delimiters::Separate,
_ => crash!(1, "Incorrect argument for all-repeated: {}", opt_arg),
},
_ => Delimiters::None,
let uniq = Uniq {
repeats_only: matches.is_present(OPT_REPEATED)
|| matches.occurrences_of(OPT_ALL_REPEATED) > 0,
uniques_only: matches.is_present(OPT_UNIQUE),
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()) {
"prepend" => Delimiters::Prepend,
"separate" => Delimiters::Separate,
_ => crash!(1, "Incorrect argument for all-repeated: {}", opt_arg),
},
show_counts: matches.opt_present("count"),
skip_fields: opt_parsed("skip-fields", &matches),
slice_start: opt_parsed("skip-chars", &matches),
slice_stop: opt_parsed("check-chars", &matches),
ignore_case: matches.opt_present("ignore-case"),
zero_terminated: matches.opt_present("zero-terminated"),
};
uniq.print_uniq(
&mut open_input_file(in_file_name),
&mut open_output_file(out_file_name),
);
}
_ => Delimiters::None,
},
show_counts: matches.is_present(OPT_COUNT),
skip_fields: opt_parsed(OPT_SKIP_FIELDS, &matches),
slice_start: opt_parsed(OPT_SKIP_CHARS, &matches),
slice_stop: opt_parsed(OPT_CHECK_CHARS, &matches),
ignore_case: matches.is_present(OPT_IGNORE_CASE),
zero_terminated: matches.is_present(OPT_ZERO_TERMINATED),
};
uniq.print_uniq(
&mut open_input_file(in_file_name),
&mut open_output_file(out_file_name),
);
0
}

View file

@ -1,6 +1,7 @@
use crate::common::util::*;
static INPUT: &'static str = "sorted.txt";
static OUTPUT: &'static str = "sorted-output.txt";
static SKIP_CHARS: &'static str = "skip-chars.txt";
static SKIP_FIELDS: &'static str = "skip-fields.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");
}
#[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]
fn test_stdin_counts() {
new_ucmd!()