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:
parent
7bbb4c98e8
commit
41ba5ed913
4 changed files with 151 additions and 100 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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!()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue