uudoc: move manpage generation away from the binary

This commit is contained in:
Aaron Ang
2025-10-18 21:25:23 -07:00
parent 905629b680
commit 2919027841
17 changed files with 536 additions and 367 deletions
Generated
+1
View File
@@ -4334,6 +4334,7 @@ dependencies = [
"num-traits",
"number_prefix",
"os_display",
"phf",
"procfs",
"selinux",
"sha1",
+5 -5
View File
@@ -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
+11 -7
View File
@@ -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
+13 -5
View File
@@ -252,19 +252,23 @@ The `coreutils` binary can generate completions for the `bash`, `elvish`,
The syntax is:
```shell
cargo run completion <utility> <shell>
# Install uudoc first
cargo install --bin uudoc --features uudoc --path .
# Then use the installed binary
uudoc completion <utility> <shell>
```
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 <utility>
# Install uudoc first
cargo install --bin uudoc --features uudoc --path .
# Then use the installed binary
uudoc manpage <utility>
```
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
+3 -2
View File
@@ -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
+26
View File
@@ -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",
+21 -246
View File
@@ -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<T>(utils: &UtilityMap<T>, name: &str) {
);
}
/// # Panics
/// Panics if the binary path cannot be determined
fn binary_path(args: &mut impl Iterator<Item = OsString>) -> 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<Item = &'a str>,
) -> 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, "<unknown binary name>");
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<T: uucore::Args>(
args: impl Iterator<Item = OsString>,
util_map: &UtilityMap<T>,
) -> ! {
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::<Shell>::new())
.required(true),
)
.get_matches_from(std::iter::once(OsString::from("completion")).chain(args));
let utility = matches.get_one::<String>("utility").unwrap();
let shell = *matches.get_one::<Shell>("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<T: uucore::Args>(
args: impl Iterator<Item = OsString>,
util_map: &UtilityMap<T>,
) -> ! {
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::<String>("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<T: uucore::Args>(util_map: &UtilityMap<T>) -> 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);
}
}
+138 -6
View File
@@ -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<T: Args>(utils: &UtilityMap<T>) {
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 <utility> Generate manpage for a utility");
println!(" completion <utility> <shell> 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<T: Args>(util_map: &UtilityMap<T>) -> 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<T: Args>(args: impl Iterator<Item = OsString>, util_map: &UtilityMap<T>) -> ! {
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::<String>("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<T: Args>(args: impl Iterator<Item = OsString>, util_map: &UtilityMap<T>) -> ! {
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::<Shell>::new())
.required(true),
)
.get_matches_from(std::iter::once(OsString::from("completion")).chain(args));
let utility = matches.get_one::<String>("utility").unwrap();
let shell = *matches.get_one::<Shell>("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<OsString> = 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::<Box<dyn Iterator<Item = OsString>>>());
}
"completion" => {
let args_iter = args.into_iter().skip(2);
gen_completions(args_iter, &util_map::<Box<dyn Iterator<Item = OsString>>>());
}
"--help" | "-h" => {
usage(&util_map::<Box<dyn Iterator<Item = OsString>>>());
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());
+6
View File
@@ -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;
+157
View File
@@ -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<T: Args>(
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<Item = &'a str>,
) -> 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<Item = OsString>) -> 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);
}
}
+8
View File
@@ -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;
+1
View File
@@ -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"
+7
View File
@@ -377,6 +377,13 @@ pub fn args_os() -> impl Iterator<Item = OsString> {
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<Item = OsString> {
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();
-40
View File
@@ -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"));
}
-56
View File
@@ -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};
+4
View File
@@ -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;
+135
View File
@@ -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<PathBuf> = 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"));
}