From e2baf1ccebaa44ad0e42ae938880fe5847cd6a16 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Tue, 9 Dec 2025 13:14:10 +0000 Subject: [PATCH] Adding example of how the clap parser can be setup --- src/uu/ln/src/ln.rs | 86 ++++++++++------ src/uucore/src/lib/lib.rs | 17 +-- src/uucore/src/lib/mods/clap_localization.rs | 103 +++++++++++++++++++ src/uucore/src/lib/mods/locale.rs | 22 ++++ 4 files changed, 185 insertions(+), 43 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 094106383b1..3e5efa7143d 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -10,7 +10,8 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; use uucore::fs::{make_path_relative_to, paths_refer_to_same_file}; use uucore::translate; -use uucore::{format_usage, prompt_yes, show_error}; +use uucore::{prompt_yes, show_error}; +use uucore::clap_localization::localize_command; use std::borrow::Cow; use std::collections::HashSet; @@ -91,7 +92,36 @@ static ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let cmd = uu_app(); + let matches = cmd.clone().try_get_matches_from(args); + + let matches = match matches { + Ok(m) => { + if m.get_flag("help") { + localize_command(cmd).print_help().unwrap(); + println!(); + return Ok(()); + } + m + } + Err(e) => { + use clap::error::ErrorKind; + match e.kind() { + ErrorKind::DisplayHelp => { + localize_command(cmd).print_help().unwrap(); + println!(); + return Ok(()); + } + ErrorKind::DisplayVersion => { + print!("{}", e.render()); + return Ok(()); + } + _ => { + return Err(uucore::clap_localization::clap_error_to_uerror(e)); + } + } + } + }; /* the list of files */ @@ -135,71 +165,65 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { exec(&paths[..], &settings) } +/// Build the clap Command with translation keys (not translated strings). +/// Translation only happens when help is actually displayed. pub fn uu_app() -> Command { - let after_help = format!( - "{}\n\n{}", - translate!("ln-after-help"), - backup_control::BACKUP_CONTROL_LONG_HELP - ); - Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .about(translate!("ln-about")) - .override_usage(format_usage(&translate!("ln-usage"))) + .disable_help_flag(true) + .about("ln-about") // key, not translated + .after_help("ln-after-help") // key .infer_long_args(true) - .after_help(after_help) + .arg( + Arg::new("help") + .short('h') + .long("help") + .help("help-help") // key + .action(ArgAction::SetTrue), + ) .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) - /*.arg( - Arg::new(options::DIRECTORY) - .short('d') - .long(options::DIRECTORY) - .help("allow users with appropriate privileges to attempt to make hard links to directories") - )*/ .arg( Arg::new(options::FORCE) .short('f') .long(options::FORCE) - .help(translate!("ln-help-force")) + .help("ln-help-force") // key .action(ArgAction::SetTrue), ) .arg( Arg::new(options::INTERACTIVE) .short('i') .long(options::INTERACTIVE) - .help(translate!("ln-help-interactive")) + .help("ln-help-interactive") // key .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_DEREFERENCE) .short('n') .long(options::NO_DEREFERENCE) - .help(translate!("ln-help-no-dereference")) + .help("ln-help-no-dereference") // key .action(ArgAction::SetTrue), ) .arg( Arg::new(options::LOGICAL) .short('L') .long(options::LOGICAL) - .help(translate!("ln-help-logical")) + .help("ln-help-logical") // key .overrides_with(options::PHYSICAL) .action(ArgAction::SetTrue), ) .arg( - // Not implemented yet Arg::new(options::PHYSICAL) .short('P') .long(options::PHYSICAL) - .help(translate!("ln-help-physical")) + .help("ln-help-physical") // key .action(ArgAction::SetTrue), ) .arg( Arg::new(options::SYMBOLIC) .short('s') .long(options::SYMBOLIC) - .help(translate!("ln-help-symbolic")) - // override added for https://github.com/uutils/coreutils/issues/2359 + .help("ln-help-symbolic") // key .overrides_with(options::SYMBOLIC) .action(ArgAction::SetTrue), ) @@ -208,7 +232,7 @@ pub fn uu_app() -> Command { Arg::new(options::TARGET_DIRECTORY) .short('t') .long(options::TARGET_DIRECTORY) - .help(translate!("ln-help-target-directory")) + .help("ln-help-target-directory") // key .value_name("DIRECTORY") .value_hint(clap::ValueHint::DirPath) .value_parser(clap::value_parser!(OsString)) @@ -218,14 +242,14 @@ pub fn uu_app() -> Command { Arg::new(options::NO_TARGET_DIRECTORY) .short('T') .long(options::NO_TARGET_DIRECTORY) - .help(translate!("ln-help-no-target-directory")) + .help("ln-help-no-target-directory") // key .action(ArgAction::SetTrue), ) .arg( Arg::new(options::RELATIVE) .short('r') .long(options::RELATIVE) - .help(translate!("ln-help-relative")) + .help("ln-help-relative") // key .requires(options::SYMBOLIC) .action(ArgAction::SetTrue), ) @@ -233,7 +257,7 @@ pub fn uu_app() -> Command { Arg::new(options::VERBOSE) .short('v') .long(options::VERBOSE) - .help(translate!("ln-help-verbose")) + .help("ln-help-verbose") // key .action(ArgAction::SetTrue), ) .arg( @@ -241,7 +265,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .value_hint(clap::ValueHint::AnyPath) .value_parser(clap::value_parser!(OsString)) - .required(true) + .required_unless_present("help") .num_args(1..), ) } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 40632ae983b..d65465bf0e8 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -190,20 +190,13 @@ macro_rules! bin { ($util:ident) => { pub fn main() { use std::io::Write; - use uucore::locale; // suppress extraneous error output for SIGPIPE failures/panics uucore::panic::mute_sigpipe_panic(); - locale::setup_localization(uucore::get_canonical_util_name(stringify!($util))) - .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}"), - } - std::process::exit(99) - }); + + // Store util name for lazy localization initialization + uucore::locale::set_util_name_for_lazy_init( + uucore::get_canonical_util_name(stringify!($util)), + ); // execute utility code let code = $util::uumain(uucore::args_os()); diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 5a54bf7c302..e28e2377e02 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -561,6 +561,109 @@ pub fn configure_localized_command(mut cmd: Command) -> Command { cmd } +/// Localizes a clap `Command` by translating all keys to actual strings. +/// +/// This function is the "layer between clap and translations". It takes a Command +/// that was built with translation keys (not translated strings) and converts +/// those keys to actual translated strings just before displaying help. +/// +/// # Arguments +/// +/// * `cmd` - The clap `Command` with translation keys in about/help fields +/// +/// # Returns +/// +/// A new `Command` with all keys translated and proper formatting applied. +/// +/// # Example +/// +/// ```no_run +/// use clap::{Arg, ArgAction, Command}; +/// use uucore::clap_localization::localize_command; +/// +/// // Build command with keys (not translated strings) +/// let cmd = Command::new("myutil") +/// .about("myutil-about") // key, not translated +/// .arg(Arg::new("verbose") +/// .short('v') +/// .help("myutil-help-verbose")); // key +/// +/// // Only translate when actually displaying help +/// if user_requested_help { +/// localize_command(cmd).print_help().unwrap(); +/// } +/// ``` +pub fn localize_command(mut cmd: Command) -> Command { + let util_name = crate::util_name(); + + // Translate about text + if let Some(about) = cmd.get_about().map(|s| s.to_string()) { + cmd = cmd.about(translate!(&about)); + } + + // Translate after_help text + if let Some(after_help) = cmd.get_after_help().map(|s| s.to_string()) { + cmd = cmd.after_help(translate!(&after_help)); + } + + // Set localized usage + let usage_key = format!("{util_name}-usage"); + cmd = cmd.override_usage(crate::format_usage(&translate!(&usage_key))); + + // Translate arg help texts + let arg_ids: Vec<_> = cmd.get_arguments().map(|a| a.get_id().clone()).collect(); + for id in arg_ids { + cmd = cmd.mut_arg(&id, |arg| { + let mut arg = arg; + if let Some(help_key) = arg.get_help().map(|s| s.to_string()) { + arg = arg.help(translate!(&help_key)); + } + if let Some(long_help_key) = arg.get_long_help().map(|s| s.to_string()) { + arg = arg.long_help(translate!(&long_help_key)); + } + arg + }); + } + + // Apply color and help template + configure_localized_command(cmd) +} + +/// Converts a clap Error to a UError for use in UResult return types. +/// +/// This is useful when manually handling clap parsing and needing to +/// return errors through the UResult system. +pub fn clap_error_to_uerror(err: Error) -> Box { + Box::new(ClapErrorWrapper(err)) +} + +/// Wrapper to convert clap::Error into UError +struct ClapErrorWrapper(Error); + +impl std::fmt::Display for ClapErrorWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for ClapErrorWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl std::error::Error for ClapErrorWrapper {} + +impl crate::error::UError for ClapErrorWrapper { + fn code(&self) -> i32 { + if self.0.exit_code() == 0 { 0 } else { 1 } + } + + fn usage(&self) -> bool { + true + } +} + /* spell-checker: disable */ #[cfg(test)] mod tests { diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 559dc72ef14..e6ed4240bf7 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -110,6 +110,27 @@ thread_local! { static LOCALIZER: OnceLock = const { OnceLock::new() }; } +// Store util name for lazy initialization +static UTIL_NAME_FOR_LAZY_INIT: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Store the utility name for lazy localization initialization. +/// Called from bin! macro before uumain. +pub fn set_util_name_for_lazy_init(name: &str) { + let _ = UTIL_NAME_FOR_LAZY_INIT.set(name.to_string()); +} + +/// Ensure localization is initialized (lazy initialization). +/// Called automatically by translate! macro when needed. +fn ensure_initialized() { + LOCALIZER.with(|lock| { + if lock.get().is_none() { + if let Some(util_name) = UTIL_NAME_FOR_LAZY_INIT.get() { + let _ = setup_localization(util_name); + } + } + }); +} + /// Helper function to find the uucore locales directory from a utility's locales directory fn find_uucore_locales_dir(utility_locales_dir: &Path) -> Option { // Normalize the path to get absolute path @@ -262,6 +283,7 @@ fn create_english_bundle_from_embedded( } fn get_message_internal(id: &str, args: Option) -> String { + ensure_initialized(); LOCALIZER.with(|lock| { lock.get() .map(|loc| loc.format(id, args.as_ref()))