Skip to content

Commit 96afff7

Browse files
author
Gunter Schmidt
committed
Feat: Allow shortened options
Allow --he for help or --l for list. This is based on PR uutils#11367
2 parents 8f90cdf + 58e59b8 commit 96afff7

3 files changed

Lines changed: 64 additions & 147 deletions

File tree

src/bin/coreutils.rs

Lines changed: 37 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use coreutils::validation;
88
use itertools::Itertools as _;
99
use std::cmp;
1010
use std::ffi::OsString;
11-
use std::io::{self, Write};
1211
use std::process;
1312
use uucore::Args;
1413

@@ -49,13 +48,9 @@ fn usage<T>(utils: &UtilityMap<T>, name: &str) {
4948
/// 'my_own_directory_service_ls' as long as the last letters match the utility.
5049
/// * coreutils arg: --list, --version, -V, --help, -h (or shortened long versions): \
5150
/// Output information about coreutils itself. \
52-
/// Multiple of these arguments, output limited to one, with help > version > list.
5351
/// * util name and any number of arguments: \
5452
/// Will get passed on to the selected utility. \
5553
/// Error if util name is not recognized.
56-
/// * --help or -h and a following util name: \
57-
/// Output help for that specific utility. \
58-
/// So 'coreutils sum --help' is the same as 'coreutils --help sum'.
5954
#[allow(clippy::cognitive_complexity)]
6055
fn main() {
6156
uucore::panic::mute_sigpipe_panic();
@@ -86,59 +81,49 @@ fn main() {
8681
validation::not_found(&OsString::from(binary_as_util));
8782
};
8883

89-
// 0th argument equals util name?
84+
// 0th/1st argument equals util name?
9085
if let Some(util_os) = util_name {
9186
let Some(util) = util_os.to_str() else {
87+
// Not UTF-8
9288
validation::not_found(&util_os)
9389
};
9490

95-
#[allow(clippy::single_match_else)]
96-
match utils.get(util) {
97-
Some(&(uumain, _)) => {
98-
// TODO: plug the deactivation of the translation
99-
// and load the English strings directly at compilation time in the
100-
// binary to avoid the load of the flt
101-
// Could be something like:
102-
// #[cfg(not(feature = "only_english"))]
103-
validation::setup_localization_or_exit(util);
104-
process::exit(uumain(vec![util_os].into_iter().chain(args)));
105-
}
106-
None => {
107-
let (option, help_util) = find_dominant_option(&util_os, &mut args);
108-
match option {
109-
SelectedOption::Help => match help_util {
110-
// see if they want help on a specific util and if it is valid
111-
Some(u_os) => match utils.get(&u_os.to_string_lossy()) {
112-
Some(&(uumain, _)) => {
113-
let code = uumain(
114-
vec![u_os, OsString::from("--help")]
115-
.into_iter()
116-
// Function requires a chain like in the Some case, but
117-
// the args are discarded as clap returns help immediately.
118-
.chain(args),
119-
);
120-
io::stdout().flush().expect("could not flush stdout");
121-
process::exit(code);
122-
}
123-
None => validation::not_found(&u_os),
124-
},
125-
// show coreutils help
126-
None => usage(&utils, binary_as_util),
127-
},
128-
SelectedOption::Version => {
129-
println!("{binary_as_util} {VERSION} (multi-call binary)");
130-
}
131-
SelectedOption::List => {
132-
let utils: Vec<_> = utils.keys().collect();
133-
for util in utils {
134-
println!("{util}");
135-
}
136-
}
137-
SelectedOption::Unrecognized(arg) => {
138-
// Argument looks like an option but wasn't recognized
139-
validation::unrecognized_option(binary_as_util, &arg);
140-
}
91+
// Util in known list?
92+
if let Some(&(uumain, _)) = utils.get(util) {
93+
// TODO: plug the deactivation of the translation
94+
// and load the English strings directly at compilation time in the
95+
// binary to avoid the load of the flt
96+
// Could be something like:
97+
// #[cfg(not(feature = "only_english"))]
98+
validation::setup_localization_or_exit(util);
99+
process::exit(uumain(vec![util_os].into_iter().chain(args)));
100+
} else {
101+
let l = util.len();
102+
// GNU coreutils --help string shows help for coreutils
103+
if util == "-h" || (l <= 6 && util[0..l] == "--help"[0..l]) {
104+
usage(&utils, binary_as_util);
105+
process::exit(0);
106+
// GNU coreutils --list string shows available utilities as list
107+
} else if l <= 6 && util[0..l] == "--list"[0..l] {
108+
// If --help is also present, show usage instead of list
109+
if args.any(|arg| arg == "--help" || arg == "-h") {
110+
usage(&utils, binary_as_util);
111+
process::exit(0);
112+
}
113+
let utils: Vec<_> = utils.keys().collect();
114+
for util in utils {
115+
println!("{util}");
141116
}
117+
process::exit(0);
118+
// GNU coreutils --version string shows version
119+
} else if util == "-V" || (l <= 9 && util[0..l] == "--version"[0..l]) {
120+
println!("{binary_as_util} {VERSION} (multi-call binary)");
121+
process::exit(0);
122+
} else if util.starts_with('-') {
123+
// Argument looks like an option but wasn't recognized
124+
validation::unrecognized_option(binary_as_util, &util_os);
125+
} else {
126+
validation::not_found(&util_os);
142127
}
143128
}
144129
} else {
@@ -147,91 +132,3 @@ fn main() {
147132
process::exit(0);
148133
}
149134
}
150-
151-
/// All defined coreutils options.
152-
// Important: when changing then adapt also [identify_option_from_partial_text]
153-
// as it works with the indices of this array.
154-
const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V"];
155-
156-
/// The dominant selected option.
157-
#[derive(Debug, Clone, PartialEq)]
158-
enum SelectedOption {
159-
Help,
160-
Version,
161-
List,
162-
Unrecognized(OsString),
163-
}
164-
165-
/// Coreutils only accepts one single option,
166-
/// if multiple are given, use the most dominant one.
167-
///
168-
/// Help > Version > List (e.g. 'coreutils --list --version' will return version)
169-
/// Unrecognized will return immediately.
170-
///
171-
/// # Returns
172-
/// (SelectedOption, Util for help request, if any)
173-
fn find_dominant_option(
174-
first_arg: &OsString,
175-
args: &mut impl Iterator<Item = OsString>,
176-
) -> (SelectedOption, Option<OsString>) {
177-
let mut sel = identify_option_from_partial_text(first_arg);
178-
match sel {
179-
SelectedOption::Help => return (SelectedOption::Help, args.next()),
180-
SelectedOption::Unrecognized(_) => {
181-
return (sel, None);
182-
}
183-
_ => {}
184-
}
185-
// check remaining options, allows multiple
186-
while let Some(arg) = args.next() {
187-
let so = identify_option_from_partial_text(&arg);
188-
match so {
189-
// most dominant, return directly
190-
SelectedOption::Help => {
191-
// if help is wanted, check if a tool was named
192-
return (so, args.next());
193-
}
194-
// best after help, can be set directly
195-
SelectedOption::Version => sel = SelectedOption::Version,
196-
SelectedOption::List => {
197-
if sel != SelectedOption::Version {
198-
sel = SelectedOption::List;
199-
}
200-
}
201-
// unrecognized is not allowed
202-
SelectedOption::Unrecognized(_) => {
203-
return (so, None);
204-
}
205-
}
206-
}
207-
208-
(sel, None)
209-
}
210-
211-
// Will identify one, SelectedOption::None cannot be returned.
212-
fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption {
213-
let mut option = &arg.to_string_lossy()[..];
214-
if let Some(p) = option.find('=') {
215-
option = &option[0..p];
216-
}
217-
let l = option.len();
218-
let possible_opts: Vec<usize> = COREUTILS_OPTIONS
219-
.iter()
220-
.enumerate()
221-
.filter(|(_, it)| it.len() >= l && &it[0..l] == option)
222-
.map(|(id, _)| id)
223-
.collect();
224-
225-
match possible_opts.len() {
226-
// exactly one hit
227-
1 => match &possible_opts[0] {
228-
// number represents index of [COREUTILS_OPTIONS]
229-
0 | 3 => SelectedOption::Help,
230-
1 => SelectedOption::List,
231-
2 | 4 => SelectedOption::Version,
232-
_ => SelectedOption::Help,
233-
},
234-
// None or more hits. The latter can not happen with the allowed options.
235-
_ => SelectedOption::Unrecognized(arg.clone()),
236-
}
237-
}

tests/test_util_name.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ fn init() {
2626
eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}");
2727
}
2828

29+
#[test]
30+
fn test_coreutils_help_ignore_args() {
31+
use std::process::Command;
32+
33+
let scenario = TestScenario::new("help_ignoring_args");
34+
if !scenario.bin_path.exists() {
35+
return;
36+
}
37+
38+
let output = Command::new(&scenario.bin_path)
39+
.arg("--help")
40+
.arg("---")
41+
.output()
42+
.unwrap();
43+
44+
assert_eq!(output.status.code(), Some(0));
45+
}
46+
2947
#[test]
3048
#[cfg(feature = "ls")]
3149
fn execution_phrase_double() {
@@ -246,7 +264,7 @@ fn util_arg_priority() {
246264
}
247265
let child = Command::new(&scenario.bin_path)
248266
.arg("--list")
249-
.arg("--version")
267+
.arg("--help")
250268
.stdout(Stdio::piped())
251269
.stderr(Stdio::piped())
252270
.spawn()
@@ -255,8 +273,8 @@ fn util_arg_priority() {
255273
assert_eq!(output.status.code(), Some(0));
256274
assert_eq!(output.stderr, b"");
257275
let output_str = String::from_utf8(output.stdout).unwrap();
258-
let ver = env::var("CARGO_PKG_VERSION").unwrap();
259-
assert_eq!(format!("coreutils {ver} (multi-call binary)\n"), output_str);
276+
assert!(output_str.contains("Usage: coreutils"));
277+
assert!(output_str.contains("lists all defined functions"));
260278
}
261279

262280
#[test]

util/fetch-gnu.sh

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ repo=https://github.com/coreutils/coreutils
44
curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf -
55

66
# TODO stop backporting tests from master at GNU coreutils > $ver
7-
# backport = ()
8-
# for f in ${backport[@]}
9-
# do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f
10-
# done
7+
backport=(
8+
misc/coreutils.sh
9+
)
10+
for f in "${backport[@]}"
11+
do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f
12+
done

0 commit comments

Comments
 (0)