Skip to content
Closed
248 changes: 173 additions & 75 deletions src/bin/coreutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,107 +38,205 @@ fn usage<T>(utils: &UtilityMap<T>, 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 <PREFIX><UTIL_NAME> 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();

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, "<unknown binary name>");
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not too easy. We have this for BusyBox tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, interesting. I just saw the to do and removed it. Will implement it again.
-V is not GNU, but uutils standard, see extensions.md.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm considering to remove -h,-V extensions to avoid confliction in the future. (We already have some probrematic conflictions...)

// 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<Item = OsString>,
) -> (SelectedOption, Option<OsString>) {
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<usize> = 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()),
}
}
52 changes: 51 additions & 1 deletion tests/test_util_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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() {
Expand Down
Loading