From 7e68fc9b34b95b0c2079f5802a83d3e3622cf2d0 Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 16:25:27 +0100 Subject: [PATCH 1/7] Fix: coreutils now allows options with incomplete names Addresses issue #10269. * Allow options with --l, --li or similar for --list * Prioritize options: -V --list --help will return help -V --list will return version * Added tests * Selected option now is returned as Enum for better code readability. * No functional changes * first arg needs to be the binary/executable. \ This is usually coreutils, but can be the util name itself, e.g. 'ls'. \ The util name will be checked against the list of enabled utils, where * the name exactly matches the name of an applet/util or * the name matches pattern, e.g. 'my_own_directory_service_ls' as long as the last letters match the utility. * coreutils arg: --list, --version, -V, --help, -h (or shortened long versions): \ Output information about coreutils itself. \ Multiple of these arguments, output limited to one, with help > version > list. * util name and any number of arguments: \ Will get passed on to the selected utility. \ Error if util name is not recognized. * --help or -h and a following util name: \ Output help for that specific utility. \ So 'coreutils sum --help' is the same as 'coreutils --help sum'. --- src/bin/coreutils.rs | 248 ++++++++++++++++++++++++++++------------ tests/test_util_name.rs | 52 ++++++++- 2 files changed, 224 insertions(+), 76 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 59634849a6b..400bdd108bd 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -38,6 +38,27 @@ fn usage(utils: &UtilityMap, name: &str) { ); } +/// all defined coreutils options +const COREUTILS_OPTIONS: [&'static str; 5] = ["--list", "-V", "--version", "-h", "--help"]; + +/// Entry into Coreutils +/// +/// # Arguments +/// * first arg needs to be the binary/executable. \ +/// This is usually coreutils, but can be the util name itself, e.g. 'ls'. \ +/// The util name will be checked against the list of enabled utils, where +/// * the name exactly matches the name of an applet/util or +/// * the name matches pattern, e.g. +/// 'my_own_directory_service_ls' as long as the last letters match the utility. +/// * coreutils arg: --list, --version, -V, --help, -h (or shortened long versions): \ +/// Output information about coreutils itself. \ +/// Multiple of these arguments, output limited to one, with help > version > list. +/// * util name and any number of arguments: \ +/// Will get passed on to the selected utility. \ +/// Error if util name is not recognized. +/// * --help or -h and a following util name: \ +/// Output help for that specific utility. \ +/// So 'coreutils sum --help' is the same as 'coreutils --help sum'. #[allow(clippy::cognitive_complexity)] fn main() { uucore::panic::mute_sigpipe_panic(); @@ -45,100 +66,177 @@ fn main() { let utils = util_map(); let mut args = uucore::args_os(); + // get binary which is always the first argument and remove it from args let binary = validation::binary_path(&mut args); let binary_as_util = validation::name(&binary).unwrap_or_else(|| { + // non UTF-8 name usage(&utils, ""); process::exit(0); }); - // binary name ends with util name? - let is_coreutils = binary_as_util.ends_with("utils"); - let matched_util = utils - .keys() - .filter(|&&u| binary_as_util.ends_with(u) && !is_coreutils) - .max_by_key(|u| u.len()); //Prefer stty more than tty. *utils is not ls - - let util_name = if let Some(&util) = matched_util { - Some(OsString::from(util)) - } else if is_coreutils || binary_as_util.ends_with("box") { - // todo: Remove support of "*box" from binary + // get the called util + let util_os = if binary_as_util.ends_with("utils") { + // coreutils uucore::set_utility_is_second_arg(); - args.next() + match args.next() { + Some(u) => u, + None => { + // no arguments provided + usage(&utils, binary_as_util); + process::exit(0); + } + } } else { - validation::not_found(&OsString::from(binary_as_util)); + // Is the binary name a prefixed util name? + // Prefer stty more than tty. *utils is not ls + let name = if let Some(matched_util) = utils + .keys() + .filter(|&&util_name| binary_as_util.ends_with(util_name)) + .max_by_key(|u| u.len()) + { + *matched_util + } else { + binary_as_util + }; + + OsString::from(name) }; - // 0th argument equals util name? - if let Some(util_os) = util_name { - let Some(util) = util_os.to_str() else { - validation::not_found(&util_os) - }; + let Some(util) = util_os.to_str() else { + // non-UTF-8 name + validation::not_found(&util_os) + }; - match util { - "--list" => { - // If --help is also present, show usage instead of list - if args.any(|arg| arg == "--help" || arg == "-h") { - usage(&utils, binary_as_util); - process::exit(0); + match utils.get(util) { + Some(&(uumain, _)) => { + // TODO: plug the deactivation of the translation + // and load the English strings directly at compilation time in the + // binary to avoid the load of the flt + // Could be something like: + // #[cfg(not(feature = "only_english"))] + validation::setup_localization_or_exit(util); + process::exit(uumain(vec![util_os].into_iter().chain(args))); + } + None => { + let (option, help_util) = find_dominant_option(&util_os, &mut args); + match option { + SelectedOption::Help => match help_util { + // see if they want help on a specific util and if it is valid + Some(u_os) => match utils.get(&u_os.to_string_lossy()) { + Some(&(uumain, _)) => { + let code = uumain( + vec![u_os, OsString::from("--help")] + .into_iter() + // Function requires a chain like in the Some case, but + // the args are discarded as clap returns help immediately. + .chain(args), + ); + io::stdout().flush().expect("could not flush stdout"); + process::exit(code); + } + None => validation::not_found(&u_os), + }, + // show coreutils help + None => usage(&utils, binary_as_util), + }, + SelectedOption::Version => { + println!("{binary_as_util} {VERSION} (multi-call binary)"); } - let utils: Vec<_> = utils.keys().collect(); - for util in utils { - println!("{util}"); + SelectedOption::List => { + let utils: Vec<_> = utils.keys().collect(); + for util in utils { + println!("{util}"); + } + } + SelectedOption::Unrecognized(arg) => { + // Argument looks like an option but wasn't recognized + validation::unrecognized_option(binary_as_util, &arg); } - process::exit(0); - } - "--version" | "-V" => { - println!("{binary_as_util} {VERSION} (multi-call binary)"); - process::exit(0); } - // Not a special command: fallthrough to calling a util - _ => {} + // process::exit(0); } + } +} + +/// The dominant selected option. +#[derive(Debug, Clone, PartialEq)] +enum SelectedOption { + Help, + Version, + List, + Unrecognized(OsString), +} - match utils.get(util) { - Some(&(uumain, _)) => { - // TODO: plug the deactivation of the translation - // and load the English strings directly at compilation time in the - // binary to avoid the load of the flt - // Could be something like: - // #[cfg(not(feature = "only_english"))] - validation::setup_localization_or_exit(util); - process::exit(uumain(vec![util_os].into_iter().chain(args))); +/// Coreutils only accepts one single option, +/// if multiple are given, use the most dominant one. +/// +/// Help > Version > List (e.g. 'coreutils --list --version' will return version) +/// Unrecognized will return immediately. +/// +/// # Returns +/// (SelectedOption, Util for help request, if any) +fn find_dominant_option( + first_arg: &OsString, + args: &mut impl Iterator, +) -> (SelectedOption, Option) { + let mut sel = identify_option_from_partial_text(first_arg); + match sel { + SelectedOption::Help => return (SelectedOption::Help, args.next()), + SelectedOption::Unrecognized(_) => { + return (sel, None); + } + _ => {} + }; + // check remaining options, allows multiple + while let Some(arg) = args.next() { + let so = identify_option_from_partial_text(&arg); + match so { + // most dominant, return directly + SelectedOption::Help => { + // if help is wanted, check if a tool was named + return (so, args.next()); } - None => { - if util == "--help" || util == "-h" { - // see if they want help on a specific util - if let Some(util_os) = args.next() { - let Some(util) = util_os.to_str() else { - validation::not_found(&util_os) - }; - - match utils.get(util) { - Some(&(uumain, _)) => { - let code = uumain( - vec![util_os, OsString::from("--help")] - .into_iter() - .chain(args), - ); - io::stdout().flush().expect("could not flush stdout"); - process::exit(code); - } - None => validation::not_found(&util_os), - } - } - usage(&utils, binary_as_util); - process::exit(0); - } else if util.starts_with('-') { - // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &util_os); - } else { - validation::not_found(&util_os); + // best after help, can be set directly + SelectedOption::Version => sel = SelectedOption::Version, + SelectedOption::List => { + if sel != SelectedOption::Version { + sel = SelectedOption::List } } + // unrecognized is not allowed + SelectedOption::Unrecognized(_) => { + return (so, None); + } } - } else { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); + } + + (sel, None) +} + +// Will identify one, SelectedOption::None cannot be returned. +fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { + let mut option = &arg.to_string_lossy()[..]; + if let Some(p) = option.find('=') { + option = &option[0..p]; + } + // // don't care about hyphens, -h and --h(elp) are identical + // let option = option.replace("-", ""); + let l = option.len(); + let possible_opts: Vec = COREUTILS_OPTIONS + .iter() + .enumerate() + .filter(|(_, it)| it.len() >= l && &it[0..l] == option) + .map(|(id, _)| id) + .collect(); + + match possible_opts.len() { + // exactly one hit + 1 => match &possible_opts[0] { + 0 => SelectedOption::List, + 1 | 2 => SelectedOption::Version, + _ => SelectedOption::Help, + }, + // None or more hits. The latter can not happen with the allowed options. + _ => SelectedOption::Unrecognized(arg.to_os_string()), } } diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 17b9dc36e5e..8b6d26d3c99 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -192,7 +192,7 @@ fn util_version() { println!("Skipping test: Binary not found at {:?}", scenario.bin_path); return; } - for arg in ["-V", "--version"] { + for arg in ["-V", "--version", "--ver"] { let child = Command::new(&scenario.bin_path) .arg(arg) .stdin(Stdio::piped()) @@ -209,6 +209,56 @@ fn util_version() { } } +#[test] +fn util_help() { + use std::process::{Command, Stdio}; + + let scenario = TestScenario::new("--version"); + if !scenario.bin_path.exists() { + println!("Skipping test: Binary not found at {:?}", scenario.bin_path); + return; + } + for arg in ["-h", "--help", "--he"] { + let child = Command::new(&scenario.bin_path) + .arg(arg) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let output = child.wait_with_output().unwrap(); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stderr, b""); + let output_str = String::from_utf8(output.stdout).unwrap(); + assert!(output_str.contains("Usage: coreutils")); + assert!(output_str.contains("lists all defined functions")); + } +} + +#[test] +fn util_arg_priority() { + use std::process::{Command, Stdio}; + + let scenario = TestScenario::new("--version"); + if !scenario.bin_path.exists() { + println!("Skipping test: Binary not found at {:?}", scenario.bin_path); + return; + } + let child = Command::new(&scenario.bin_path) + .arg("--list") + .arg("--version") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let output = child.wait_with_output().unwrap(); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stderr, b""); + let output_str = String::from_utf8(output.stdout).unwrap(); + let ver = env::var("CARGO_PKG_VERSION").unwrap(); + assert_eq!(format!("coreutils {ver} (multi-call binary)\n"), output_str); +} + #[test] #[cfg(target_env = "musl")] fn test_musl_no_dynamic_deps() { From 726911c2888eca3c5a496509e5f9a725bc167c08 Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 17:34:12 +0100 Subject: [PATCH 2/7] Fix: added end_with("box") again for busy_box tests Fixed some clippy warnings. --- src/bin/coreutils.rs | 111 +++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 400bdd108bd..5d8d75ba0bc 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -39,7 +39,7 @@ fn usage(utils: &UtilityMap, name: &str) { } /// all defined coreutils options -const COREUTILS_OPTIONS: [&'static str; 5] = ["--list", "-V", "--version", "-h", "--help"]; +const COREUTILS_OPTIONS: [&str; 5] = ["--list", "-V", "--version", "-h", "--help"]; /// Entry into Coreutils /// @@ -49,7 +49,7 @@ const COREUTILS_OPTIONS: [&'static str; 5] = ["--list", "-V", "--version", "-h", /// The util name will be checked against the list of enabled utils, where /// * the name exactly matches the name of an applet/util or /// * the name matches pattern, e.g. -/// 'my_own_directory_service_ls' as long as the last letters match the utility. +/// 'my_own_directory_service_ls' as long as the last letters match the utility. /// * coreutils arg: --list, --version, -V, --help, -h (or shortened long versions): \ /// Output information about coreutils itself. \ /// Multiple of these arguments, output limited to one, with help > version > list. @@ -75,16 +75,16 @@ fn main() { }); // get the called util - let util_os = if binary_as_util.ends_with("utils") { + let util_os = if binary_as_util.ends_with("utils") || binary_as_util.ends_with("box") { + // todo: Remove support of "*box" from binary, but required for busy_box tests // coreutils uucore::set_utility_is_second_arg(); - match args.next() { - Some(u) => u, - None => { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); - } + if let Some(u_name) = args.next() { + u_name + } else { + // no arguments provided + usage(&utils, binary_as_util); + process::exit(0); } } else { // Is the binary name a prefixed util name? @@ -107,54 +107,51 @@ fn main() { validation::not_found(&util_os) }; - match utils.get(util) { - Some(&(uumain, _)) => { - // TODO: plug the deactivation of the translation - // and load the English strings directly at compilation time in the - // binary to avoid the load of the flt - // Could be something like: - // #[cfg(not(feature = "only_english"))] - validation::setup_localization_or_exit(util); - process::exit(uumain(vec![util_os].into_iter().chain(args))); - } - None => { - let (option, help_util) = find_dominant_option(&util_os, &mut args); - match option { - SelectedOption::Help => match help_util { - // see if they want help on a specific util and if it is valid - Some(u_os) => match utils.get(&u_os.to_string_lossy()) { - Some(&(uumain, _)) => { - let code = uumain( - vec![u_os, OsString::from("--help")] - .into_iter() - // Function requires a chain like in the Some case, but - // the args are discarded as clap returns help immediately. - .chain(args), - ); - io::stdout().flush().expect("could not flush stdout"); - process::exit(code); - } - None => validation::not_found(&u_os), - }, - // show coreutils help - None => usage(&utils, binary_as_util), - }, - SelectedOption::Version => { - println!("{binary_as_util} {VERSION} (multi-call binary)"); - } - SelectedOption::List => { - let utils: Vec<_> = utils.keys().collect(); - for util in utils { - println!("{util}"); + if let Some(&(uumain, _)) = utils.get(util) { + // TODO: plug the deactivation of the translation + // and load the English strings directly at compilation time in the + // binary to avoid the load of the flt + // Could be something like: + // #[cfg(not(feature = "only_english"))] + validation::setup_localization_or_exit(util); + process::exit(uumain(vec![util_os].into_iter().chain(args))); + } else { + let (option, help_util) = find_dominant_option(&util_os, &mut args); + match option { + SelectedOption::Help => match help_util { + // see if they want help on a specific util and if it is valid + Some(u_os) => match utils.get(&u_os.to_string_lossy()) { + Some(&(uumain, _)) => { + let code = uumain( + vec![u_os, OsString::from("--help")] + .into_iter() + // Function requires a chain like in the Some case, but + // the args are discarded as clap returns help immediately. + .chain(args), + ); + io::stdout().flush().expect("could not flush stdout"); + process::exit(code); } - } - SelectedOption::Unrecognized(arg) => { - // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &arg); + None => validation::not_found(&u_os), + }, + // show coreutils help + None => usage(&utils, binary_as_util), + }, + SelectedOption::Version => { + println!("{binary_as_util} {VERSION} (multi-call binary)"); + } + SelectedOption::List => { + let utils: Vec<_> = utils.keys().collect(); + for util in utils { + println!("{util}"); } } - // process::exit(0); + SelectedOption::Unrecognized(arg) => { + // Argument looks like an option but wasn't recognized + validation::unrecognized_option(binary_as_util, &arg); + } } + // process::exit(0); } } @@ -186,7 +183,7 @@ fn find_dominant_option( return (sel, None); } _ => {} - }; + } // check remaining options, allows multiple while let Some(arg) = args.next() { let so = identify_option_from_partial_text(&arg); @@ -200,7 +197,7 @@ fn find_dominant_option( SelectedOption::Version => sel = SelectedOption::Version, SelectedOption::List => { if sel != SelectedOption::Version { - sel = SelectedOption::List + sel = SelectedOption::List; } } // unrecognized is not allowed @@ -237,6 +234,6 @@ fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { _ => SelectedOption::Help, }, // None or more hits. The latter can not happen with the allowed options. - _ => SelectedOption::Unrecognized(arg.to_os_string()), + _ => SelectedOption::Unrecognized(arg.clone()), } } From e19e238ba98df501d807c3f063072b287247b384 Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 18:49:02 +0100 Subject: [PATCH 3/7] Version with less changes The previous commit had some additional code restructuring which made diff checking difficult. Here the same functionality is implemented, but now it is clearer that only the None path of "match utils.get" has changed. This only affects coreutils own option parsing. --- src/bin/coreutils.rs | 152 +++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 5d8d75ba0bc..b189bde4aea 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -38,9 +38,6 @@ fn usage(utils: &UtilityMap, name: &str) { ); } -/// all defined coreutils options -const COREUTILS_OPTIONS: [&str; 5] = ["--list", "-V", "--version", "-h", "--help"]; - /// Entry into Coreutils /// /// # Arguments @@ -66,95 +63,96 @@ fn main() { let utils = util_map(); let mut args = uucore::args_os(); - // get binary which is always the first argument and remove it from args let binary = validation::binary_path(&mut args); let binary_as_util = validation::name(&binary).unwrap_or_else(|| { - // non UTF-8 name usage(&utils, ""); process::exit(0); }); - // get the called util - let util_os = if binary_as_util.ends_with("utils") || binary_as_util.ends_with("box") { - // todo: Remove support of "*box" from binary, but required for busy_box tests - // coreutils + // binary name ends with util name? + let is_coreutils = binary_as_util.ends_with("utils"); + let matched_util = utils + .keys() + .filter(|&&u| binary_as_util.ends_with(u) && !is_coreutils) + .max_by_key(|u| u.len()); //Prefer stty more than tty. *utils is not ls + + let util_name = if let Some(&util) = matched_util { + Some(OsString::from(util)) + } else if is_coreutils || binary_as_util.ends_with("box") { + // todo: Remove support of "*box" from binary uucore::set_utility_is_second_arg(); - if let Some(u_name) = args.next() { - u_name - } else { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); - } + args.next() } else { - // Is the binary name a prefixed util name? - // Prefer stty more than tty. *utils is not ls - let name = if let Some(matched_util) = utils - .keys() - .filter(|&&util_name| binary_as_util.ends_with(util_name)) - .max_by_key(|u| u.len()) - { - *matched_util - } else { - binary_as_util - }; - - OsString::from(name) + validation::not_found(&OsString::from(binary_as_util)); }; - let Some(util) = util_os.to_str() else { - // non-UTF-8 name - validation::not_found(&util_os) - }; + // 0th argument equals util name? + if let Some(util_os) = util_name { + let Some(util) = util_os.to_str() else { + validation::not_found(&util_os) + }; - if let Some(&(uumain, _)) = utils.get(util) { - // TODO: plug the deactivation of the translation - // and load the English strings directly at compilation time in the - // binary to avoid the load of the flt - // Could be something like: - // #[cfg(not(feature = "only_english"))] - validation::setup_localization_or_exit(util); - process::exit(uumain(vec![util_os].into_iter().chain(args))); - } else { - let (option, help_util) = find_dominant_option(&util_os, &mut args); - match option { - SelectedOption::Help => match help_util { - // see if they want help on a specific util and if it is valid - Some(u_os) => match utils.get(&u_os.to_string_lossy()) { - Some(&(uumain, _)) => { - let code = uumain( - vec![u_os, OsString::from("--help")] - .into_iter() - // Function requires a chain like in the Some case, but - // the args are discarded as clap returns help immediately. - .chain(args), - ); - io::stdout().flush().expect("could not flush stdout"); - process::exit(code); - } - None => validation::not_found(&u_os), - }, - // show coreutils help - None => usage(&utils, binary_as_util), - }, - SelectedOption::Version => { - println!("{binary_as_util} {VERSION} (multi-call binary)"); + #[allow(clippy::single_match_else)] + match utils.get(util) { + Some(&(uumain, _)) => { + // TODO: plug the deactivation of the translation + // and load the English strings directly at compilation time in the + // binary to avoid the load of the flt + // Could be something like: + // #[cfg(not(feature = "only_english"))] + validation::setup_localization_or_exit(util); + process::exit(uumain(vec![util_os].into_iter().chain(args))); } - SelectedOption::List => { - let utils: Vec<_> = utils.keys().collect(); - for util in utils { - println!("{util}"); + None => { + let (option, help_util) = find_dominant_option(&util_os, &mut args); + match option { + SelectedOption::Help => match help_util { + // see if they want help on a specific util and if it is valid + Some(u_os) => match utils.get(&u_os.to_string_lossy()) { + Some(&(uumain, _)) => { + let code = uumain( + vec![u_os, OsString::from("--help")] + .into_iter() + // Function requires a chain like in the Some case, but + // the args are discarded as clap returns help immediately. + .chain(args), + ); + io::stdout().flush().expect("could not flush stdout"); + process::exit(code); + } + None => validation::not_found(&u_os), + }, + // show coreutils help + None => usage(&utils, binary_as_util), + }, + SelectedOption::Version => { + println!("{binary_as_util} {VERSION} (multi-call binary)"); + } + SelectedOption::List => { + let utils: Vec<_> = utils.keys().collect(); + for util in utils { + println!("{util}"); + } + } + SelectedOption::Unrecognized(arg) => { + // Argument looks like an option but wasn't recognized + validation::unrecognized_option(binary_as_util, &arg); + } } } - SelectedOption::Unrecognized(arg) => { - // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &arg); - } } - // process::exit(0); + } else { + // no arguments provided + usage(&utils, binary_as_util); + process::exit(0); } } +/// All defined coreutils options. +// Important: when changing then adapt also [identify_option_from_partial_text] +// as it works with the indexes of this array. +const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V"]; + /// The dominant selected option. #[derive(Debug, Clone, PartialEq)] enum SelectedOption { @@ -229,8 +227,10 @@ fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { match possible_opts.len() { // exactly one hit 1 => match &possible_opts[0] { - 0 => SelectedOption::List, - 1 | 2 => SelectedOption::Version, + // number represents index of [COREUTILS_OPTIONS] + 0 | 3 => SelectedOption::Help, + 1 => SelectedOption::List, + 2 | 4 => SelectedOption::Version, _ => SelectedOption::Help, }, // None or more hits. The latter can not happen with the allowed options. From 08b497c59232fa6bb4962e0a83dda58342e0922b Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 19:23:13 +0100 Subject: [PATCH 4/7] replaced SelectedOption Enum with Strings --- src/bin/coreutils.rs | 67 +++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index b189bde4aea..8b0a491478b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -105,8 +105,8 @@ fn main() { } None => { let (option, help_util) = find_dominant_option(&util_os, &mut args); - match option { - SelectedOption::Help => match help_util { + match option.as_str() { + "--help" => match help_util { // see if they want help on a specific util and if it is valid Some(u_os) => match utils.get(&u_os.to_string_lossy()) { Some(&(uumain, _)) => { @@ -125,18 +125,18 @@ fn main() { // show coreutils help None => usage(&utils, binary_as_util), }, - SelectedOption::Version => { + "--version" => { println!("{binary_as_util} {VERSION} (multi-call binary)"); } - SelectedOption::List => { + "--list" => { let utils: Vec<_> = utils.keys().collect(); for util in utils { println!("{util}"); } } - SelectedOption::Unrecognized(arg) => { + _ => { // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &arg); + validation::unrecognized_option(binary_as_util, &OsString::from(option)); } } } @@ -153,15 +153,6 @@ fn main() { // as it works with the indexes of this array. const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V"]; -/// The dominant selected option. -#[derive(Debug, Clone, PartialEq)] -enum SelectedOption { - Help, - Version, - List, - Unrecognized(OsString), -} - /// Coreutils only accepts one single option, /// if multiple are given, use the most dominant one. /// @@ -173,33 +164,33 @@ enum SelectedOption { fn find_dominant_option( first_arg: &OsString, args: &mut impl Iterator, -) -> (SelectedOption, Option) { +) -> (String, Option) { let mut sel = identify_option_from_partial_text(first_arg); - match sel { - SelectedOption::Help => return (SelectedOption::Help, args.next()), - SelectedOption::Unrecognized(_) => { + match sel.as_str() { + "--help" => return (sel, args.next()), + "--list" | "--version" => {} // fall through + _ => { return (sel, None); } - _ => {} } // check remaining options, allows multiple while let Some(arg) = args.next() { let so = identify_option_from_partial_text(&arg); - match so { + match so.as_str() { // most dominant, return directly - SelectedOption::Help => { + "--help" => { // if help is wanted, check if a tool was named return (so, args.next()); } // best after help, can be set directly - SelectedOption::Version => sel = SelectedOption::Version, - SelectedOption::List => { - if sel != SelectedOption::Version { - sel = SelectedOption::List; + "--version" => sel = so, + "--list" => { + if sel != "--version" { + sel = so; } } // unrecognized is not allowed - SelectedOption::Unrecognized(_) => { + _ => { return (so, None); } } @@ -208,14 +199,12 @@ fn find_dominant_option( (sel, None) } -// Will identify one, SelectedOption::None cannot be returned. -fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { +// Will identify the matching option and return it. +fn identify_option_from_partial_text(arg: &OsString) -> String { let mut option = &arg.to_string_lossy()[..]; if let Some(p) = option.find('=') { option = &option[0..p]; } - // // don't care about hyphens, -h and --h(elp) are identical - // let option = option.replace("-", ""); let l = option.len(); let possible_opts: Vec = COREUTILS_OPTIONS .iter() @@ -224,16 +213,18 @@ fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { .map(|(id, _)| id) .collect(); - match possible_opts.len() { + let sel_opt = match possible_opts.len() { // exactly one hit 1 => match &possible_opts[0] { // number represents index of [COREUTILS_OPTIONS] - 0 | 3 => SelectedOption::Help, - 1 => SelectedOption::List, - 2 | 4 => SelectedOption::Version, - _ => SelectedOption::Help, + 0 | 3 => "--help", + 1 => "--list", + 2 | 4 => "--version", + _ => "--help", }, // None or more hits. The latter can not happen with the allowed options. - _ => SelectedOption::Unrecognized(arg.clone()), - } + _ => &arg.to_string_lossy(), + }; + + sel_opt.to_string() } From e35265b7be6d0b5628b60cdb1bdd23174b1388ed Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 19:31:16 +0100 Subject: [PATCH 5/7] Update comment --- src/bin/coreutils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 8b0a491478b..11433a431ad 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -160,7 +160,7 @@ const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V /// Unrecognized will return immediately. /// /// # Returns -/// (SelectedOption, Util for help request, if any) +/// (String with option, e.g. "--list", Util for help request, if any) fn find_dominant_option( first_arg: &OsString, args: &mut impl Iterator, From 7c1b167cd23d13170f15410832fc4b1cd386f3ae Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Mon, 16 Mar 2026 23:06:26 +0100 Subject: [PATCH 6/7] Reverting to Enums Enums are technically superior: Easier to maintain, safer to work with and more performant. --- src/bin/coreutils.rs | 69 ++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 11433a431ad..094738ad915 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -105,8 +105,8 @@ fn main() { } None => { let (option, help_util) = find_dominant_option(&util_os, &mut args); - match option.as_str() { - "--help" => match help_util { + match option { + SelectedOption::Help => match help_util { // see if they want help on a specific util and if it is valid Some(u_os) => match utils.get(&u_os.to_string_lossy()) { Some(&(uumain, _)) => { @@ -125,18 +125,18 @@ fn main() { // show coreutils help None => usage(&utils, binary_as_util), }, - "--version" => { + SelectedOption::Version => { println!("{binary_as_util} {VERSION} (multi-call binary)"); } - "--list" => { + SelectedOption::List => { let utils: Vec<_> = utils.keys().collect(); for util in utils { println!("{util}"); } } - _ => { + SelectedOption::Unrecognized(arg) => { // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &OsString::from(option)); + validation::unrecognized_option(binary_as_util, &arg); } } } @@ -150,9 +150,18 @@ fn main() { /// All defined coreutils options. // Important: when changing then adapt also [identify_option_from_partial_text] -// as it works with the indexes of this array. +// as it works with the indices of this array. const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V"]; +/// The dominant selected option. +#[derive(Debug, Clone, PartialEq)] +enum SelectedOption { + Help, + Version, + List, + Unrecognized(OsString), +} + /// Coreutils only accepts one single option, /// if multiple are given, use the most dominant one. /// @@ -160,37 +169,37 @@ const COREUTILS_OPTIONS: [&str; 5] = ["--help", "--list", "--version", "-h", "-V /// Unrecognized will return immediately. /// /// # Returns -/// (String with option, e.g. "--list", Util for help request, if any) +/// (SelectedOption, Util for help request, if any) fn find_dominant_option( first_arg: &OsString, args: &mut impl Iterator, -) -> (String, Option) { +) -> (SelectedOption, Option) { let mut sel = identify_option_from_partial_text(first_arg); - match sel.as_str() { - "--help" => return (sel, args.next()), - "--list" | "--version" => {} // fall through - _ => { + match sel { + SelectedOption::Help => return (SelectedOption::Help, args.next()), + SelectedOption::Unrecognized(_) => { return (sel, None); } + _ => {} } // check remaining options, allows multiple while let Some(arg) = args.next() { let so = identify_option_from_partial_text(&arg); - match so.as_str() { + match so { // most dominant, return directly - "--help" => { + SelectedOption::Help => { // if help is wanted, check if a tool was named return (so, args.next()); } // best after help, can be set directly - "--version" => sel = so, - "--list" => { - if sel != "--version" { - sel = so; + SelectedOption::Version => sel = SelectedOption::Version, + SelectedOption::List => { + if sel != SelectedOption::Version { + sel = SelectedOption::List; } } // unrecognized is not allowed - _ => { + SelectedOption::Unrecognized(_) => { return (so, None); } } @@ -199,8 +208,8 @@ fn find_dominant_option( (sel, None) } -// Will identify the matching option and return it. -fn identify_option_from_partial_text(arg: &OsString) -> String { +// Will identify one, SelectedOption::None cannot be returned. +fn identify_option_from_partial_text(arg: &OsString) -> SelectedOption { let mut option = &arg.to_string_lossy()[..]; if let Some(p) = option.find('=') { option = &option[0..p]; @@ -213,18 +222,16 @@ fn identify_option_from_partial_text(arg: &OsString) -> String { .map(|(id, _)| id) .collect(); - let sel_opt = match possible_opts.len() { + match possible_opts.len() { // exactly one hit 1 => match &possible_opts[0] { // number represents index of [COREUTILS_OPTIONS] - 0 | 3 => "--help", - 1 => "--list", - 2 | 4 => "--version", - _ => "--help", + 0 | 3 => SelectedOption::Help, + 1 => SelectedOption::List, + 2 | 4 => SelectedOption::Version, + _ => SelectedOption::Help, }, // None or more hits. The latter can not happen with the allowed options. - _ => &arg.to_string_lossy(), - }; - - sel_opt.to_string() + _ => SelectedOption::Unrecognized(arg.clone()), + } } From 77ed6fc18b49c0a83badf1af4af9037864465df2 Mon Sep 17 00:00:00 2001 From: Gunter Schmidt Date: Wed, 18 Mar 2026 13:46:46 +0100 Subject: [PATCH 7/7] Fix: return arg is ambiguous When a completion of a long argument results in multiple arguments, then a list of options is returned in the error message --- src/uucore/locales/en-US.ftl | 3 ++ src/uucore/locales/fr-FR.ftl | 3 ++ src/uucore/src/lib/mods/clap_localization.rs | 41 +++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 15896209526..03fdcd50834 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -19,6 +19,9 @@ clap-error-missing-required-arguments = { $error_word }: the following required clap-error-possible-values = possible values clap-error-help-suggestion = For more information, try '{ $command } --help'. common-help-suggestion = For more information, try '--help'. +# For clap_localization +clap-error-ambiguous-argument=Error: Argument '{ $arg }' is ambiguous. + Did you mean one of these? # Common help text patterns help-flag-help = Print help information diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index e9ff4abe475..c94bd4138b2 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -19,6 +19,9 @@ clap-error-missing-required-arguments = { $error_word } : les arguments requis s clap-error-possible-values = valeurs possibles clap-error-help-suggestion = Pour plus d'informations, essayez '{ $command } --help'. common-help-suggestion = Pour plus d'informations, essayez '--help'. +# For clap_localization +clap-error-ambiguous-argument=Error: L'argument '{ $arg }' est ambigu. + Tu parlais d'un de ceux-ci? # Modèles de texte d'aide communs help-flag-help = Afficher les informations d'aide diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index fc4f838c216..7ad969a760d 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -432,7 +432,7 @@ where /// let result = handle_clap_result_with_exit_code(cmd, args, 125); /// ``` pub fn handle_clap_result_with_exit_code( - cmd: Command, + mut cmd: Command, itr: I, exit_code: i32, ) -> UResult @@ -440,10 +440,47 @@ where I: IntoIterator, T: Into + Clone, { - cmd.try_get_matches_from(itr).map_err(|e| { + // cloning args for double use in error case + let args = itr.into_iter().collect::>(); + let itr = args.clone(); + // using mut to avoid cloning cmd + cmd.try_get_matches_from_mut(itr).map_err(|e| { if e.exit_code() == 0 { e.into() // Preserve help/version } else { + if e.kind() == ErrorKind::UnknownArgument || e.kind() == ErrorKind::InvalidSubcommand { + // find ambiguous options + // Find the string the user actually typed (e.g., "--de") + // for arg in &itr {} + let args_str: Vec = args + .into_iter() + .map(|t| { + let o: OsString = t.into(); + o.to_string_lossy().to_string() + }) + .collect(); + if let Some(provided) = args_str.iter().find(|a| a.starts_with("--")) { + let search_term = provided.trim_start_matches("--"); + + // Manually filter all defined long arguments + let mut matches: Vec<_> = cmd + .get_arguments() + .filter_map(|arg| arg.get_long()) + .filter(|l| l.starts_with(search_term)) + .collect(); + + if matches.len() > 1 { + let mut msg = + translate!("clap-error-ambiguous-argument", "arg" => provided); + matches.sort(); + for m in matches { + msg.push_str(&format!("\n --{}", m)); + } + return USimpleError::new(exit_code, msg); + } + } + } + let formatter = ErrorFormatter::new(crate::util_name()); let code = formatter.print_error(&e, exit_code); USimpleError::new(code, "")