diff --git a/Cargo.lock b/Cargo.lock index e5b870a2faa..00b5ea4d0e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4334,6 +4334,7 @@ dependencies = [ "num-traits", "number_prefix", "os_display", + "phf", "procfs", "selinux", "sha1", diff --git a/Cargo.toml b/Cargo.toml index acd1567ad82..df0848ca3f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ expensive_tests = [] # "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.) test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated -uudoc = ["zip", "dep:fluent-syntax"] +uudoc = ["clap_complete", "clap_mangen", "fluent-syntax", "zip", "feat_Tier1"] ## features ## Optional feature for stdbuf # "feat_external_libstdbuf" == use an external libstdbuf.so for stdbuf instead of embedding it @@ -408,14 +408,14 @@ uutests = { version = "0.3.0", package = "uutests", path = "tests/uutests" } [dependencies] clap.workspace = true -uucore.workspace = true -clap_complete.workspace = true -clap_mangen.workspace = true +clap_complete = { workspace = true, optional = true } +clap_mangen = { workspace = true, optional = true } +fluent-syntax = { workspace = true, optional = true } phf.workspace = true selinux = { workspace = true, optional = true } textwrap.workspace = true +uucore.workspace = true zip = { workspace = true, optional = true } -fluent-syntax = { workspace = true, optional = true } # * uutils diff --git a/GNUmakefile b/GNUmakefile index 1441944ad1c..89cfcbce1aa 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -302,6 +302,7 @@ TEST_PROGS := \ unexpand \ uniq \ unlink \ + uudoc \ wc \ who @@ -406,10 +407,13 @@ distclean: clean $(CARGO) clean $(CARGOFLAGS) && $(CARGO) update $(CARGOFLAGS) ifeq ($(MANPAGES),y) -manpages: build-coreutils +build-uudoc: + ${CARGO} build ${CARGOFLAGS} --bin uudoc --features "uudoc ${EXES}" ${PROFILE_CMD} --no-default-features + +manpages: build-coreutils build-uudoc mkdir -p $(BUILDDIR)/man/ $(foreach prog, $(INSTALLEES) $(HASHSUM_PROGS), \ - $(BUILDDIR)/coreutils manpage $(prog) > $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1 $(newline) \ + $(BUILDDIR)/uudoc manpage $(prog) > $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1 $(newline) \ ) install-manpages: manpages @@ -422,12 +426,12 @@ install-manpages: endif ifeq ($(COMPLETIONS),y) -completions: build-coreutils +completions: build-coreutils build-uudoc mkdir -p $(BUILDDIR)/completions/zsh $(BUILDDIR)/completions/bash $(BUILDDIR)/completions/fish $(foreach prog, $(INSTALLEES) $(HASHSUM_PROGS) , \ - $(BUILDDIR)/coreutils completion $(prog) zsh > $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog) $(newline) \ - $(BUILDDIR)/coreutils completion $(prog) bash > $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog) $(newline) \ - $(BUILDDIR)/coreutils completion $(prog) fish > $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish $(newline) \ + $(BUILDDIR)/uudoc completion $(prog) zsh > $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog) $(newline) \ + $(BUILDDIR)/uudoc completion $(prog) bash > $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog) $(newline) \ + $(BUILDDIR)/uudoc completion $(prog) fish > $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish $(newline) \ ) install-completions: completions @@ -520,4 +524,4 @@ endif rm -f $(addprefix $(DESTDIR)$(DATAROOTDIR)/fish/vendor_completions.d/$(PROG_PREFIX),$(addsuffix .fish,$(PROGS))) rm -f $(addprefix $(DESTDIR)$(DATAROOTDIR)/man/man1/$(PROG_PREFIX),$(addsuffix .1,$(PROGS))) -.PHONY: all build build-coreutils build-pkgs test distclean clean busytest install uninstall +.PHONY: all build build-coreutils build-pkgs build-uudoc test distclean clean busytest install uninstall diff --git a/README.md b/README.md index 26b1e4d0d7d..c056f9e2344 100644 --- a/README.md +++ b/README.md @@ -252,19 +252,23 @@ The `coreutils` binary can generate completions for the `bash`, `elvish`, The syntax is: ```shell -cargo run completion +# Install uudoc first +cargo install --bin uudoc --features uudoc --path . + +# Then use the installed binary +uudoc completion ``` So, to install completions for `ls` on `bash` to `/usr/local/share/bash-completion/completions/ls`, run: ```shell -cargo run completion ls bash > /usr/local/share/bash-completion/completions/ls +uudoc completion ls bash > /usr/local/share/bash-completion/completions/ls ``` Completion for prefixed `cp` with `uu-` on `zsh` is generated by ```shell -env PROG_PREFIX=uu- cargo run completion cp zsh +env PROG_PREFIX=uu- uudoc completion cp zsh ``` ### Manually install manpages @@ -272,13 +276,17 @@ env PROG_PREFIX=uu- cargo run completion cp zsh To generate manpages, the syntax is: ```bash -cargo run manpage +# Install uudoc first +cargo install --bin uudoc --features uudoc --path . + +# Then use the installed binary +uudoc manpage ``` So, to install the manpage for `ls` to `/usr/local/share/man/man1/ls.1` run: ```bash -cargo run manpage ls > /usr/local/share/man/man1/ls.1 +uudoc manpage ls > /usr/local/share/man/man1/ls.1 ``` ## Un-installation diff --git a/build.rs b/build.rs index 1a7bba8dc0a..aba8a95ee3d 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) krate +// spell-checker:ignore (vars) krate mangen use std::env; use std::fs::File; @@ -32,7 +32,8 @@ pub fn main() { // Allow this as we have a bunch of info in the comments #[allow(clippy::match_same_arms)] match krate.as_ref() { - "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names + "default" | "macos" | "unix" | "windows" | "selinux" | "zip" | "clap_complete" + | "clap_mangen" | "fluent_syntax" => continue, // common/standard feature names "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { continue; } // crate-local custom features diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index d7477fde6d1..069b68dc3db 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1081,6 +1081,25 @@ dependencies = [ "winnow", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1367,6 +1386,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "sm3" version = "0.4.2" @@ -1733,6 +1758,7 @@ dependencies = [ "num-traits", "number_prefix", "os_display", + "phf", "procfs", "sha1", "sha2", diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 64a79a3fd1e..05eeae26fdb 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -3,18 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore manpages mangen prefixcat testcat - -use clap::{Arg, Command}; -use clap_complete::Shell; use std::cmp; -use std::ffi::OsStr; use std::ffi::OsString; use std::io::{self, Write}; -use std::path::{Path, PathBuf}; use std::process; -use uucore::display::Quotable; -use uucore::locale; + +use clap::Command; + +use coreutils::validation; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -38,61 +34,6 @@ fn usage(utils: &UtilityMap, name: &str) { ); } -/// # Panics -/// Panics if the binary path cannot be determined -fn binary_path(args: &mut impl Iterator) -> PathBuf { - match args.next() { - Some(ref s) if !s.is_empty() => PathBuf::from(s), - _ => std::env::current_exe().unwrap(), - } -} - -fn name(binary_path: &Path) -> Option<&str> { - binary_path.file_stem()?.to_str() -} - -fn get_canonical_util_name(util_name: &str) -> &str { - match util_name { - // uu_test aliases - '[' is an alias for test - "[" => "test", - - // hashsum aliases - all these hash commands are aliases for hashsum - "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" - | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" - | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", - - "dir" => "ls", // dir is an alias for ls - - // Default case - return the util name as is - _ => util_name, - } -} - -fn find_prefixed_util<'a>( - binary_name: &str, - mut util_keys: impl Iterator, -) -> Option<&'a str> { - util_keys.find(|util| { - binary_name.ends_with(*util) - && binary_name.len() > util.len() // Ensure there's actually a prefix - && !binary_name[..binary_name.len() - (*util).len()] - .ends_with(char::is_alphanumeric) - }) -} - -fn setup_localization_or_exit(util_name: &str) { - locale::setup_localization(get_canonical_util_name(util_name)).unwrap_or_else(|err| { - match err { - uucore::locale::LocalizationError::ParseResource { - error: err_msg, - snippet, - } => eprintln!("Localization parse error at {snippet}: {err_msg}"), - other => eprintln!("Could not init the localization system: {other}"), - } - process::exit(99) - }); -} - #[allow(clippy::cognitive_complexity)] fn main() { uucore::panic::mute_sigpipe_panic(); @@ -100,44 +41,38 @@ fn main() { let utils = util_map(); let mut args = uucore::args_os(); - let binary = binary_path(&mut args); - let binary_as_util = name(&binary).unwrap_or_else(|| { + let binary = validation::binary_path(&mut args); + let binary_as_util = validation::name(&binary).unwrap_or_else(|| { usage(&utils, ""); process::exit(0); }); // binary name equals util name? if let Some(&(uumain, _)) = utils.get(binary_as_util) { - setup_localization_or_exit(binary_as_util); + validation::setup_localization_or_exit(binary_as_util); process::exit(uumain(vec![binary.into()].into_iter().chain(args))); } // binary name equals prefixed util name? // * prefix/stem may be any string ending in a non-alphanumeric character // For example, if the binary is named `uu_test`, it will match `test` as a utility. - let util_name = if let Some(util) = find_prefixed_util(binary_as_util, utils.keys().copied()) { - // prefixed util => replace 0th (aka, executable name) argument - Some(OsString::from(util)) - } else { - // unmatched binary name => regard as multi-binary container and advance argument list - uucore::set_utility_is_second_arg(); - args.next() - }; + let util_name = + if let Some(util) = validation::find_prefixed_util(binary_as_util, utils.keys().copied()) { + // prefixed util => replace 0th (aka, executable name) argument + Some(OsString::from(util)) + } else { + // unmatched binary name => regard as multi-binary container and advance argument list + uucore::set_utility_is_second_arg(); + args.next() + }; // 0th argument equals util name? if let Some(util_os) = util_name { - fn not_found(util: &OsStr) -> ! { - println!("{}: function/utility not found", util.maybe_quote()); - process::exit(1); - } - let Some(util) = util_os.to_str() else { - not_found(&util_os) + validation::not_found(&util_os) }; match util { - "completion" => gen_completions(args, &utils), - "manpage" => gen_manpage(args, &utils), "--list" => { let mut utils: Vec<_> = utils.keys().collect(); utils.sort(); @@ -161,7 +96,7 @@ fn main() { // binary to avoid the load of the flt // Could be something like: // #[cfg(not(feature = "only_english"))] - setup_localization_or_exit(util); + validation::setup_localization_or_exit(util); process::exit(uumain(vec![util_os].into_iter().chain(args))); } None => { @@ -169,7 +104,7 @@ fn main() { // see if they want help on a specific util if let Some(util_os) = args.next() { let Some(util) = util_os.to_str() else { - not_found(&util_os) + validation::not_found(&util_os) }; match utils.get(util) { @@ -182,13 +117,13 @@ fn main() { io::stdout().flush().expect("could not flush stdout"); process::exit(code); } - None => not_found(&util_os), + None => validation::not_found(&util_os), } } usage(&utils, binary_as_util); process::exit(0); } else { - not_found(&util_os); + validation::not_found(&util_os); } } } @@ -198,163 +133,3 @@ fn main() { process::exit(0); } } - -/// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout -/// # Panics -/// Panics if the utility map is empty -fn gen_completions( - args: impl Iterator, - util_map: &UtilityMap, -) -> ! { - let all_utilities: Vec<_> = std::iter::once("coreutils") - .chain(util_map.keys().copied()) - .collect(); - - let matches = Command::new("completion") - .about("Prints completions to stdout") - .arg( - Arg::new("utility") - .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) - .required(true), - ) - .arg( - Arg::new("shell") - .value_parser(clap::builder::EnumValueParser::::new()) - .required(true), - ) - .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); - - let utility = matches.get_one::("utility").unwrap(); - let shell = *matches.get_one::("shell").unwrap(); - - let mut command = if utility == "coreutils" { - gen_coreutils_app(util_map) - } else { - util_map.get(utility).unwrap().1() - }; - let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; - - clap_complete::generate(shell, &mut command, bin_name, &mut io::stdout()); - io::stdout().flush().unwrap(); - process::exit(0); -} - -/// Generate the manpage for the utility in the first parameter -/// # Panics -/// Panics if the utility map is empty -fn gen_manpage( - args: impl Iterator, - util_map: &UtilityMap, -) -> ! { - let all_utilities: Vec<_> = std::iter::once("coreutils") - .chain(util_map.keys().copied()) - .collect(); - - let matches = Command::new("manpage") - .about("Prints manpage to stdout") - .arg( - Arg::new("utility") - .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) - .required(true), - ) - .get_matches_from(std::iter::once(OsString::from("manpage")).chain(args)); - - let utility = matches.get_one::("utility").unwrap(); - - let command = if utility == "coreutils" { - gen_coreutils_app(util_map) - } else { - setup_localization_or_exit(utility); - util_map.get(utility).unwrap().1() - }; - - let man = clap_mangen::Man::new(command); - man.render(&mut io::stdout()) - .expect("Man page generation failed"); - io::stdout().flush().unwrap(); - process::exit(0); -} - -/// # Panics -/// Panics if the utility map is empty -fn gen_coreutils_app(util_map: &UtilityMap) -> Command { - let mut command = Command::new("coreutils"); - for (name, (_, sub_app)) in util_map { - // Recreate a small subcommand with only the relevant info - // (name & short description) - let about = sub_app() - .get_about() - .expect("Could not get the 'about'") - .to_string(); - let sub_app = Command::new(name).about(about); - command = command.subcommand(sub_app); - } - command -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::Path; - - #[test] - fn test_get_canonical_util_name() { - // Test a few key aliases - assert_eq!(get_canonical_util_name("["), "test"); - assert_eq!(get_canonical_util_name("md5sum"), "hashsum"); - assert_eq!(get_canonical_util_name("dir"), "ls"); - - // Test passthrough case - assert_eq!(get_canonical_util_name("cat"), "cat"); - } - - #[test] - fn test_name() { - // Test normal executable name - assert_eq!(name(Path::new("/usr/bin/ls")), Some("ls")); - assert_eq!(name(Path::new("cat")), Some("cat")); - assert_eq!( - name(Path::new("./target/debug/coreutils")), - Some("coreutils") - ); - - // Test with extensions - assert_eq!(name(Path::new("program.exe")), Some("program")); - assert_eq!(name(Path::new("/path/to/utility.bin")), Some("utility")); - - // Test edge cases - assert_eq!(name(Path::new("")), None); - assert_eq!(name(Path::new("/")), None); - } - - #[test] - fn test_find_prefixed_util() { - let utils = ["test", "cat", "ls", "cp"]; - - // Test exact prefixed matches - assert_eq!( - find_prefixed_util("uu_test", utils.iter().copied()), - Some("test") - ); - assert_eq!( - find_prefixed_util("my-cat", utils.iter().copied()), - Some("cat") - ); - assert_eq!( - find_prefixed_util("prefix_ls", utils.iter().copied()), - Some("ls") - ); - - // Test non-alphanumeric separator requirement - assert_eq!(find_prefixed_util("prefixcat", utils.iter().copied()), None); // no separator - assert_eq!(find_prefixed_util("testcat", utils.iter().copied()), None); // no separator - - // Test no match - assert_eq!(find_prefixed_util("unknown", utils.iter().copied()), None); - assert_eq!(find_prefixed_util("", utils.iter().copied()), None); - - // Test exact util name (should not match as prefixed) - assert_eq!(find_prefixed_util("test", utils.iter().copied()), None); - assert_eq!(find_prefixed_util("cat", utils.iter().copied()), None); - } -} diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 8212a8a0b73..49d6f16bce7 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -2,23 +2,155 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore tldr uuhelp -use clap::Command; +// spell-checker:ignore mangen tldr + +use std::{ + collections::HashMap, + ffi::OsString, + fs::File, + io::{self, Read, Seek, Write}, + process, +}; + +use clap::{Arg, Command}; +use clap_complete::Shell; +use clap_mangen::Man; use fluent_syntax::ast::{Entry, Message, Pattern}; use fluent_syntax::parser; -use std::collections::HashMap; -use std::ffi::OsString; -use std::fs::File; -use std::io::{self, Read, Seek, Write}; +use textwrap::{fill, indent, termwidth}; use zip::ZipArchive; +use coreutils::validation; +use uucore::Args; + include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); +/// Print usage information for uudoc +fn usage(utils: &UtilityMap) { + println!("uudoc - Documentation generator for uutils coreutils"); + println!(); + println!("Usage: uudoc [command] [args]"); + println!(); + println!("Commands:"); + println!(" (no command) Generate mdbook documentation (default)"); + println!(" manpage Generate manpage for a utility"); + println!(" completion Generate shell completions for a utility"); + println!(); + println!("Available utilities:"); + let all_utilities = validation::get_all_utilities(utils); + let display_list = all_utilities.join(", "); + let width = std::cmp::min(termwidth(), 100) - 4 * 2; + println!("{}", indent(&fill(&display_list, width), " ")); +} + +/// Generates the coreutils app for the utility map +fn gen_coreutils_app(util_map: &UtilityMap) -> clap::Command { + let mut command = clap::Command::new("coreutils"); + for (name, (_, sub_app)) in util_map { + // Recreate a small subcommand with only the relevant info + // (name & short description) + let about = sub_app() + .get_about() + .expect("Could not get the 'about'") + .to_string(); + let sub_app = clap::Command::new(name).about(about); + command = command.subcommand(sub_app); + } + command +} + +/// Generate the manpage for the utility in the first parameter +fn gen_manpage(args: impl Iterator, util_map: &UtilityMap) -> ! { + let all_utilities = validation::get_all_utilities(util_map); + + let matches = Command::new("manpage") + .about("Prints manpage to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(&all_utilities)) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("manpage")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + let command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + validation::setup_localization_or_exit(utility); + util_map.get(utility).unwrap().1() + }; + + let man = Man::new(command); + man.render(&mut io::stdout()) + .expect("Man page generation failed"); + io::stdout().flush().unwrap(); + process::exit(0); +} + +/// Generate shell completions for the utility in the first parameter +fn gen_completions(args: impl Iterator, util_map: &UtilityMap) -> ! { + let all_utilities = validation::get_all_utilities(util_map); + + let matches = Command::new("completion") + .about("Prints completions to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(&all_utilities)) + .required(true), + ) + .arg( + Arg::new("shell") + .value_parser(clap::builder::EnumValueParser::::new()) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + let shell = *matches.get_one::("shell").unwrap(); + + let mut command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + validation::setup_localization_or_exit(utility); + util_map.get(utility).unwrap().1() + }; + let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; + + clap_complete::generate(shell, &mut command, bin_name, &mut io::stdout()); + io::stdout().flush().unwrap(); + process::exit(0); +} + /// # Errors /// Returns an error if the writer fails. #[allow(clippy::too_many_lines)] fn main() -> io::Result<()> { + let args: Vec = uucore::args_os_filtered().collect(); + + // Check for manpage/completion commands first + if args.len() > 1 { + let command = args.get(1).and_then(|s| s.to_str()).unwrap_or_default(); + match command { + "manpage" => { + let args_iter = args.into_iter().skip(2); + gen_manpage(args_iter, &util_map::>>()); + } + "completion" => { + let args_iter = args.into_iter().skip(2); + gen_completions(args_iter, &util_map::>>()); + } + "--help" | "-h" => { + usage(&util_map::>>()); + process::exit(0); + } + _ => { + eprintln!("Unknown command: {command}"); + eprintln!("Use 'uudoc --help' for usage information."); + process::exit(1); + } + } + } let mut tldr_zip = File::open("docs/tldr.zip") .ok() .and_then(|f| ZipArchive::new(f).ok()); diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 00000000000..2342b52dbf1 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,6 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +pub mod validation; diff --git a/src/common/validation.rs b/src/common/validation.rs new file mode 100644 index 00000000000..1715ad4bf68 --- /dev/null +++ b/src/common/validation.rs @@ -0,0 +1,157 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore prefixcat testcat + +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; +use std::process; + +use uucore::Args; +use uucore::display::Quotable; +use uucore::locale; + +/// Gets all available utilities including "coreutils" +#[allow(clippy::type_complexity)] +pub fn get_all_utilities( + util_map: &phf::OrderedMap<&'static str, (fn(T) -> i32, fn() -> clap::Command)>, +) -> Vec<&'static str> { + std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect() +} + +/// Prints a "utility not found" error and exits +pub fn not_found(util: &OsStr) -> ! { + println!("{}: function/utility not found", util.maybe_quote()); + process::exit(1); +} + +/// Sets up localization for a utility with proper error handling +pub fn setup_localization_or_exit(util_name: &str) { + let util_name = get_canonical_util_name(util_name); + locale::setup_localization(util_name).unwrap_or_else(|err| { + match err { + locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + process::exit(99) + }); +} + +/// Gets the canonical utility name, resolving aliases +fn get_canonical_util_name(util_name: &str) -> &str { + match util_name { + // uu_test aliases - '[' is an alias for test + "[" => "test", + + // hashsum aliases - all these hash commands are aliases for hashsum + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" + | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" + | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", + + "dir" => "ls", // dir is an alias for ls + + // Default case - return the util name as is + _ => util_name, + } +} + +/// Finds a utility with a prefix (e.g., "uu_test" -> "test") +pub fn find_prefixed_util<'a>( + binary_name: &str, + mut util_keys: impl Iterator, +) -> Option<&'a str> { + util_keys.find(|util| { + binary_name.ends_with(*util) + && binary_name.len() > util.len() // Ensure there's actually a prefix + && !binary_name[..binary_name.len() - (*util).len()] + .ends_with(char::is_alphanumeric) + }) +} + +/// Gets the binary path from command line arguments +/// # Panics +/// Panics if the binary path cannot be determined +pub fn binary_path(args: &mut impl Iterator) -> PathBuf { + match args.next() { + Some(ref s) if !s.is_empty() => PathBuf::from(s), + _ => std::env::current_exe().unwrap(), + } +} + +/// Extracts the binary name from a path +pub fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_canonical_util_name() { + // Test a few key aliases + assert_eq!(get_canonical_util_name("["), "test"); + assert_eq!(get_canonical_util_name("md5sum"), "hashsum"); + assert_eq!(get_canonical_util_name("dir"), "ls"); + + // Test passthrough case + assert_eq!(get_canonical_util_name("cat"), "cat"); + } + + #[test] + fn test_name() { + // Test normal executable name + assert_eq!(name(Path::new("/usr/bin/ls")), Some("ls")); + assert_eq!(name(Path::new("cat")), Some("cat")); + assert_eq!( + name(Path::new("./target/debug/coreutils")), + Some("coreutils") + ); + + // Test with extensions + assert_eq!(name(Path::new("program.exe")), Some("program")); + assert_eq!(name(Path::new("/path/to/utility.bin")), Some("utility")); + + // Test edge cases + assert_eq!(name(Path::new("")), None); + assert_eq!(name(Path::new("/")), None); + } + + #[test] + fn test_find_prefixed_util() { + let utils = ["test", "cat", "ls", "cp"]; + + // Test exact prefixed matches + assert_eq!( + find_prefixed_util("uu_test", utils.iter().copied()), + Some("test") + ); + assert_eq!( + find_prefixed_util("my-cat", utils.iter().copied()), + Some("cat") + ); + assert_eq!( + find_prefixed_util("prefix_ls", utils.iter().copied()), + Some("ls") + ); + + // Test non-alphanumeric separator requirement + assert_eq!(find_prefixed_util("prefixcat", utils.iter().copied()), None); // no separator + assert_eq!(find_prefixed_util("testcat", utils.iter().copied()), None); // no separator + + // Test no match + assert_eq!(find_prefixed_util("unknown", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("", utils.iter().copied()), None); + + // Test exact util name (should not match as prefixed) + assert_eq!(find_prefixed_util("test", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("cat", utils.iter().copied()), None); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000000..df2e538e734 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +pub mod common; + +pub use common::validation; diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index d5def4b0901..34b31b4b651 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -27,6 +27,7 @@ chrono = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } number_prefix = { workspace = true } +phf = { workspace = true } dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 47da8296faf..5c406356bac 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -377,6 +377,13 @@ pub fn args_os() -> impl Iterator { ARGV.iter().cloned() } +/// Returns an iterator over the command line arguments as `OsString`s, filtering out empty arguments. +/// This is useful for handling cases where extra whitespace or empty arguments are present. +/// args_os_filtered() can be expensive to call +pub fn args_os_filtered() -> impl Iterator { + ARGV.iter().filter(|arg| !arg.is_empty()).cloned() +} + /// Read a line from stdin and check whether the first character is `'y'` or `'Y'` pub fn read_yes() -> bool { let mut s = String::new(); diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index 17b46ab29e0..b46b8e0b163 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -5,7 +5,6 @@ #[cfg(target_os = "linux")] use uutests::at_and_ucmd; use uutests::new_ucmd; -use uutests::util::TestScenario; #[test] #[cfg(target_os = "linux")] @@ -236,42 +235,3 @@ cyBvdmVyIHRoZSBsYXp5IGRvZy4= // cSpell:enable ); } - -// Prevent regression to: -// -// ❯ coreutils manpage base64 | rg --fixed-strings -- 'base32' -// The data are encoded as described for the base32 alphabet in RFC 4648. -// to the bytes of the formal base32 alphabet. Use \-\-ignore\-garbage -// The data are encoded as described for the base32 alphabet in RFC 4648. -// to the bytes of the formal base32 alphabet. Use \-\-ignore\-garbage -#[test] -fn test_manpage() { - use std::process::{Command, Stdio}; - unsafe { - // force locale to english to avoid issues with manpage output - std::env::set_var("LANG", "C"); - } - - let test_scenario = TestScenario::new(""); - - let child = Command::new(&test_scenario.bin_path) - .arg("manpage") - .arg("base64") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); - - let output = child.wait_with_output().unwrap(); - - assert_eq!(output.status.code().unwrap(), 0); - - assert!(output.stderr.is_empty()); - - let stdout_str = std::str::from_utf8(&output.stdout).unwrap(); - - assert!(stdout_str.contains("base64 alphabet")); - - assert!(!stdout_str.to_ascii_lowercase().contains("base32")); -} diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 99b7d688778..caf900db80a 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -202,62 +202,6 @@ fn util_invalid_name_invalid_command() { ); } -#[test] -#[cfg(feature = "true")] -fn util_completion() { - use std::process::{Command, Stdio}; - - let scenario = TestScenario::new("completion"); - if !scenario.bin_path.exists() { - println!("Skipping test: Binary not found at {:?}", scenario.bin_path); - return; - } - - let child = Command::new(&scenario.bin_path) - .arg("completion") - .arg("true") - .arg("powershell") - .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("using namespace System.Management.Automation"), - "{output_str:?}" - ); -} - -#[test] -#[cfg(feature = "true")] -fn util_manpage() { - use std::process::{Command, Stdio}; - - let scenario = TestScenario::new("completion"); - if !scenario.bin_path.exists() { - println!("Skipping test: Binary not found at {:?}", scenario.bin_path); - return; - } - - let child = Command::new(&scenario.bin_path) - .arg("manpage") - .arg("true") - .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("\n.TH true 1 "), "{output_str:?}"); -} - #[test] fn util_version() { use std::process::{Command, Stdio}; diff --git a/tests/tests.rs b/tests/tests.rs index 90d7c6e553a..9ffdfd4a312 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -400,6 +400,10 @@ mod test_unlink; #[path = "by-util/test_uptime.rs"] mod test_uptime; +#[cfg(feature = "uudoc")] +#[path = "uudoc/mod.rs"] +mod test_uudoc; + #[cfg(feature = "users")] #[path = "by-util/test_users.rs"] mod test_users; diff --git a/tests/uudoc/mod.rs b/tests/uudoc/mod.rs new file mode 100644 index 00000000000..33cb27a46b7 --- /dev/null +++ b/tests/uudoc/mod.rs @@ -0,0 +1,135 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::{env, path::PathBuf, process::Command, sync::OnceLock}; + +static UUDOC_BINARY_PATH: OnceLock = OnceLock::new(); + +fn get_uudoc_command() -> Command { + let uudoc_binary = UUDOC_BINARY_PATH.get_or_init(|| { + let coreutils_binary = PathBuf::from(env!("CARGO_BIN_EXE_coreutils")); + coreutils_binary.parent().unwrap().join("uudoc") + }); + Command::new(uudoc_binary) +} + +#[test] +fn test_manpage_generation() { + let output = get_uudoc_command() + .arg("manpage") + .arg("ls") + .output() + .expect("Failed to execute command"); + + assert!( + output.status.success(), + "Command failed with status: {}", + output.status + ); + assert!( + output.stderr.is_empty(), + "stderr should be empty but got: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let output_str = String::from_utf8_lossy(&output.stdout); + assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains('1'), "{output_str}"); +} + +#[test] +fn test_manpage_coreutils() { + let output = get_uudoc_command() + .arg("manpage") + .arg("coreutils") + .output() + .expect("Failed to execute command"); + + assert!( + output.status.success(), + "Command failed with status: {}", + output.status + ); + assert!( + output.stderr.is_empty(), + "stderr should be empty but got: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let output_str = String::from_utf8_lossy(&output.stdout); + assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains("coreutils"), "{output_str}"); +} + +#[test] +fn test_manpage_invalid_utility() { + let output = get_uudoc_command() + .arg("manpage") + .arg("nonexistent_utility") + .output() + .expect("Failed to execute command"); + + // Should fail for invalid utility + assert!(!output.status.success(), "Command should have failed"); +} + +#[test] +fn test_completion_generation() { + let output = get_uudoc_command() + .arg("completion") + .arg("ls") + .arg("powershell") + .output() + .expect("Failed to execute command"); + + assert!( + output.status.success(), + "Command failed with status: {}", + output.status + ); + assert!( + output.stderr.is_empty(), + "stderr should be empty but got: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let output_str = String::from_utf8_lossy(&output.stdout); + assert!( + output_str.contains("using namespace System.Management.Automation"), + "{output_str}" + ); +} + +// Prevent regression to: +// +// ❯ uudoc manpage base64 | rg --fixed-strings -- 'base32' +// The data are encoded as described for the base32 alphabet in RFC 4648. +// to the bytes of the formal base32 alphabet. Use \-\-ignore\-garbage +// The data are encoded as described for the base32 alphabet in RFC 4648. +// to the bytes of the formal base32 alphabet. Use \-\-ignore\-garbage +#[test] +fn test_manpage_base64() { + let output = get_uudoc_command() + .arg("manpage") + .arg("base64") + .env("LANG", "C") // force locale to english to avoid issues with manpage output + .output() + .expect("Failed to execute command"); + + assert!( + output.status.success(), + "Command failed with status: {}", + output.status + ); + assert!( + output.stderr.is_empty(), + "stderr should be empty but got: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let output_str = String::from_utf8_lossy(&output.stdout); + assert!(output_str.contains("base64 alphabet")); + assert!(!output_str.to_ascii_lowercase().contains("base32")); +}