diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index c2b6658303f..b179a6cf44f 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -79,32 +79,14 @@ fn colorize(text: &str, color: Color) -> String { format!("\x1b[{}m{text}\x1b[0m", color.code()) } -pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: i32) -> ! { - // Ensure localization is initialized for this utility (always with common strings) - let _ = crate::locale::setup_localization(util_name); - - // Check if colors are enabled by examining clap's rendered output - let rendered_str = err.render().to_string(); - let colors_enabled = rendered_str.contains("\x1b["); - - // Helper function to conditionally colorize text - let maybe_colorize = |text: &str, color: Color| -> String { - if colors_enabled { - colorize(text, color) - } else { - text.to_string() - } - }; - +/// Handle DisplayHelp and DisplayVersion errors +fn handle_display_errors(err: Error) -> ! { match err.kind() { ErrorKind::DisplayHelp => { // For help messages, we use the localized help template // The template should already have the localized usage label, // but we also replace any remaining "Usage:" instances for fallback - // Ensure localization is initialized - let _ = crate::locale::setup_localization(util_name); - let help_text = err.render().to_string(); // Replace any remaining "Usage:" with localized version as fallback @@ -120,167 +102,185 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: print!("{}", err.render()); std::process::exit(0); } - ErrorKind::UnknownArgument => { - // Force localization initialization - ignore any previous failures - crate::locale::setup_localization(util_name).ok(); - - // Choose exit code based on utility name - let exit_code = match util_name { - // These utilities expect exit code 2 for invalid options - "ls" | "dir" | "vdir" | "sort" | "tty" | "printenv" => 2, - // Most utilities expect exit code 1 - _ => 1, - }; + _ => unreachable!("handle_display_errors called with non-display error"), + } +} +/// Handle UnknownArgument errors with localization and suggestions +fn handle_unknown_argument_error( + err: Error, + util_name: &str, + maybe_colorize: impl Fn(&str, Color) -> String, +) -> ! { + if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { + let arg_str = invalid_arg.to_string(); + // Get localized error word with fallback + let error_word = translate!("common-error"); + + let colored_arg = maybe_colorize(&arg_str, Color::Yellow); + let colored_error_word = maybe_colorize(&error_word, Color::Red); + + // Print main error message with fallback + let error_msg = translate!( + "clap-error-unexpected-argument", + "arg" => colored_arg.clone(), + "error_word" => colored_error_word.clone() + ); + eprintln!("{error_msg}"); + eprintln!(); + + // Show suggestion if available + if let Some(suggested_arg) = err.get(ContextKind::SuggestedArg) { + let tip_word = translate!("common-tip"); + let colored_tip_word = maybe_colorize(&tip_word, Color::Green); + let colored_suggestion = maybe_colorize(&suggested_arg.to_string(), Color::Green); + let suggestion_msg = translate!( + "clap-error-similar-argument", + "tip_word" => colored_tip_word.clone(), + "suggestion" => colored_suggestion.clone() + ); + eprintln!("{suggestion_msg}"); + eprintln!(); + } else { // For UnknownArgument, we need to preserve clap's built-in tips (like using -- for values) // while still allowing localization of the main error message let rendered_str = err.render().to_string(); - let _lines: Vec<&str> = rendered_str.lines().collect(); - if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { - let arg_str = invalid_arg.to_string(); + // Look for other clap tips (like "-- --file-with-dash") that aren't suggestions + // These usually start with " tip:" and contain useful information + for line in rendered_str.lines() { + if line.trim_start().starts_with("tip:") && !line.contains("similar argument") { + eprintln!("{line}"); + eprintln!(); + } + } + } - // Get localized error word with fallback - let error_word = translate!("common-error"); + // Show usage information for unknown arguments + let usage_key = format!("{util_name}-usage"); + let usage_text = translate!(&usage_key); + let formatted_usage = crate::format_usage(&usage_text); + let usage_label = translate!("common-usage"); + eprintln!("{usage_label}: {formatted_usage}"); + eprintln!(); + eprintln!("{}", translate!("common-help-suggestion")); + } else { + // Generic fallback case + let error_word = translate!("common-error"); + let colored_error_word = maybe_colorize(&error_word, Color::Red); + eprintln!("{colored_error_word}: unexpected argument"); + } + // Choose exit code based on utility name + let exit_code = match util_name { + // These utilities expect exit code 2 for invalid options + "ls" | "dir" | "vdir" | "sort" | "tty" | "printenv" => 2, + // Most utilities expect exit code 1 + _ => 1, + }; - let colored_arg = maybe_colorize(&arg_str, Color::Yellow); - let colored_error_word = maybe_colorize(&error_word, Color::Red); + std::process::exit(exit_code); +} - // Print main error message with fallback - let error_msg = translate!( - "clap-error-unexpected-argument", - "arg" => colored_arg.clone(), - "error_word" => colored_error_word.clone() - ); - eprintln!("{error_msg}"); - eprintln!(); +/// Handle InvalidValue and ValueValidation errors with localization +fn handle_invalid_value_error(err: Error, maybe_colorize: impl Fn(&str, Color) -> String) -> ! { + // Extract value and option from error context using clap's context API + // This is much more robust than parsing the error string + let invalid_arg = err.get(ContextKind::InvalidArg); + let invalid_value = err.get(ContextKind::InvalidValue); + + if let (Some(arg), Some(value)) = (invalid_arg, invalid_value) { + let option = arg.to_string(); + let value = value.to_string(); + + // Check if this is actually a missing value (empty string) + if value.is_empty() { + // This is the case where no value was provided for an option that requires one + let error_word = translate!("common-error"); + eprintln!( + "{}", + translate!("clap-error-value-required", "error_word" => error_word, "option" => option) + ); + } else { + // Get localized error word and prepare message components outside conditionals + let error_word = translate!("common-error"); + let colored_error_word = maybe_colorize(&error_word, Color::Red); + let colored_value = maybe_colorize(&value, Color::Yellow); + let colored_option = maybe_colorize(&option, Color::Green); + + let error_msg = translate!( + "clap-error-invalid-value", + "error_word" => colored_error_word, + "value" => colored_value, + "option" => colored_option + ); + + // For ValueValidation errors, include the validation error in the message + match err.source() { + Some(source) if matches!(err.kind(), ErrorKind::ValueValidation) => { + eprintln!("{error_msg}: {source}"); + } + _ => eprintln!("{error_msg}"), + } + } + + // For ValueValidation errors, include the validation error details + // Note: We don't print these separately anymore as they're part of the main message - // Show suggestion if available - let suggestion = err.get(ContextKind::SuggestedArg); - if let Some(suggested_arg) = suggestion { - let tip_word = translate!("common-tip"); - let colored_tip_word = maybe_colorize(&tip_word, Color::Green); - let colored_suggestion = - maybe_colorize(&suggested_arg.to_string(), Color::Green); - let suggestion_msg = translate!( - "clap-error-similar-argument", - "tip_word" => colored_tip_word.clone(), - "suggestion" => colored_suggestion.clone() - ); - eprintln!("{suggestion_msg}"); + // Show possible values if available (for InvalidValue errors) + if matches!(err.kind(), ErrorKind::InvalidValue) { + if let Some(valid_values) = err.get(ContextKind::ValidValue) { + if !valid_values.to_string().is_empty() { + // Don't show possible values if they are empty eprintln!(); - } else { - // Look for other clap tips (like "-- --file-with-dash") that aren't suggestions - // These usually start with " tip:" and contain useful information - for line in _lines.iter() { - if line.trim().starts_with("tip:") && !line.contains("similar argument") { - eprintln!("{}", line); - eprintln!(); - } - } + let possible_values_label = translate!("clap-error-possible-values"); + eprintln!(" [{possible_values_label}: {valid_values}]"); } + } + } - // Show usage information for unknown arguments - let usage_key = format!("{util_name}-usage"); - let usage_text = translate!(&usage_key); - let formatted_usage = crate::format_usage(&usage_text); - let usage_label = translate!("common-usage"); - eprintln!("{}: {}", usage_label, formatted_usage); - eprintln!(); - eprintln!("{}", translate!("common-help-suggestion")); + eprintln!(); + eprintln!("{}", translate!("common-help-suggestion")); + } else { + // Fallback if we can't extract context - use clap's default formatting + let rendered_str = err.render().to_string(); + let lines: Vec<&str> = rendered_str.lines().collect(); + if let Some(main_error_line) = lines.first() { + eprintln!("{main_error_line}"); + eprintln!(); + eprintln!("{}", translate!("common-help-suggestion")); + } else { + eprint!("{}", err.render()); + } + } + std::process::exit(1); +} - std::process::exit(exit_code); - } else { - // Generic fallback case - let error_word = translate!("common-error"); - let colored_error_word = maybe_colorize(&error_word, Color::Red); - eprintln!("{colored_error_word}: unexpected argument"); - std::process::exit(exit_code); - } +pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: i32) -> ! { + // Check if colors are enabled by examining clap's rendered output + let rendered_str = err.render().to_string(); + let colors_enabled = rendered_str.contains("\x1b["); + + // Helper function to conditionally colorize text + let maybe_colorize = |text: &str, color: Color| -> String { + if colors_enabled { + colorize(text, color) + } else { + text.to_string() + } + }; + + match err.kind() { + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { + handle_display_errors(err); + } + ErrorKind::UnknownArgument => { + handle_unknown_argument_error(err, util_name, maybe_colorize); } // Check if this is a simple validation error that should show simple help kind if should_show_simple_help_for_clap_error(kind) => { // Special handling for InvalidValue and ValueValidation to provide localized error if matches!(kind, ErrorKind::InvalidValue | ErrorKind::ValueValidation) { - // Force localization initialization - crate::locale::setup_localization(util_name).ok(); - - // Extract value and option from error context using clap's context API - // This is much more robust than parsing the error string - let invalid_arg = err.get(ContextKind::InvalidArg); - let invalid_value = err.get(ContextKind::InvalidValue); - - if let (Some(arg), Some(value)) = (invalid_arg, invalid_value) { - let option = arg.to_string(); - let value = value.to_string(); - - // Check if this is actually a missing value (empty string) - if value.is_empty() { - // This is the case where no value was provided for an option that requires one - let error_word = translate!("common-error"); - eprintln!( - "{}", - translate!("clap-error-value-required", "error_word" => error_word, "option" => option) - ); - eprintln!(); - eprintln!("{}", translate!("common-help-suggestion")); - std::process::exit(1); - } else { - // Get localized error word and prepare message components outside conditionals - let error_word = translate!("common-error"); - let colored_error_word = maybe_colorize(&error_word, Color::Red); - let colored_value = maybe_colorize(&value, Color::Yellow); - let colored_option = maybe_colorize(&option, Color::Green); - - let error_msg = translate!( - "clap-error-invalid-value", - "error_word" => colored_error_word, - "value" => colored_value, - "option" => colored_option - ); - - // For ValueValidation errors, include the validation error in the message - if matches!(kind, ErrorKind::ValueValidation) { - if let Some(source) = err.source() { - // Print error with validation detail on same line - eprintln!("{error_msg}: {}", source); - } else { - // Print localized error message - eprintln!("{error_msg}"); - } - } else { - // Print localized error message - eprintln!("{error_msg}"); - } - } - - // For ValueValidation errors, include the validation error details - // Note: We don't print these separately anymore as they're part of the main message - - // Show possible values if available (for InvalidValue errors) - if matches!(kind, ErrorKind::InvalidValue) { - if let Some(valid_values) = err.get(ContextKind::ValidValue) { - eprintln!(); - let possible_values_label = translate!("clap-error-possible-values"); - eprintln!(" [{}: {}]", possible_values_label, valid_values); - } - } - - eprintln!(); - eprintln!("{}", translate!("common-help-suggestion")); - std::process::exit(1); - } else { - // Fallback if we can't extract context - use clap's default formatting - let lines: Vec<&str> = rendered_str.lines().collect(); - if let Some(main_error_line) = lines.first() { - eprintln!("{}", main_error_line); - eprintln!(); - eprintln!("{}", translate!("common-help-suggestion")); - } else { - eprint!("{}", err.render()); - } - std::process::exit(1); - } + handle_invalid_value_error(err, maybe_colorize); } // For other simple validation errors, use the same simple format as other errors @@ -386,3 +386,332 @@ impl LocalizedCommand for Command { .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) } } + +/* spell-checker: disable */ +#[cfg(test)] +mod tests { + use super::*; + use clap::{Arg, Command}; + use std::ffi::OsString; + + #[test] + fn test_color_codes() { + assert_eq!(Color::Red.code(), "31"); + assert_eq!(Color::Yellow.code(), "33"); + assert_eq!(Color::Green.code(), "32"); + } + + #[test] + fn test_colorize() { + let red_text = colorize("error", Color::Red); + assert_eq!(red_text, "\x1b[31merror\x1b[0m"); + + let yellow_text = colorize("warning", Color::Yellow); + assert_eq!(yellow_text, "\x1b[33mwarning\x1b[0m"); + + let green_text = colorize("success", Color::Green); + assert_eq!(green_text, "\x1b[32msuccess\x1b[0m"); + } + + fn create_test_command() -> Command { + Command::new("test") + .arg( + Arg::new("input") + .short('i') + .long("input") + .value_name("FILE") + .help("Input file"), + ) + .arg( + Arg::new("output") + .short('o') + .long("output") + .value_name("FILE") + .help("Output file"), + ) + .arg( + Arg::new("format") + .long("format") + .value_parser(["json", "xml", "csv"]) + .help("Output format"), + ) + } + + #[test] + fn test_get_matches_from_localized_with_valid_args() { + let result = std::panic::catch_unwind(|| { + let cmd = create_test_command(); + let matches = cmd.get_matches_from_localized(vec!["test", "--input", "file.txt"]); + matches.get_one::("input").unwrap().clone() + }); + + if let Ok(input_value) = result { + assert_eq!(input_value, "file.txt"); + } + } + + #[test] + fn test_get_matches_from_localized_with_osstring_args() { + let args: Vec = vec!["test".into(), "--input".into(), "test.txt".into()]; + + let result = std::panic::catch_unwind(|| { + let cmd = create_test_command(); + let matches = cmd.get_matches_from_localized(args); + matches.get_one::("input").unwrap().clone() + }); + + if let Ok(input_value) = result { + assert_eq!(input_value, "test.txt"); + } + } + + #[test] + fn test_localized_command_from_mut() { + let args: Vec = vec!["test".into(), "--output".into(), "result.txt".into()]; + + let result = std::panic::catch_unwind(|| { + let cmd = create_test_command(); + let matches = cmd.get_matches_from_mut_localized(args); + matches.get_one::("output").unwrap().clone() + }); + + if let Ok(output_value) = result { + assert_eq!(output_value, "result.txt"); + } + } + + fn create_unknown_argument_error() -> Error { + let cmd = create_test_command(); + cmd.try_get_matches_from(vec!["test", "--unknown-arg"]) + .unwrap_err() + } + + fn create_invalid_value_error() -> Error { + let cmd = create_test_command(); + cmd.try_get_matches_from(vec!["test", "--format", "invalid"]) + .unwrap_err() + } + + fn create_help_error() -> Error { + let cmd = create_test_command(); + cmd.try_get_matches_from(vec!["test", "--help"]) + .unwrap_err() + } + + fn create_version_error() -> Error { + let cmd = Command::new("test").version("1.0.0"); + cmd.try_get_matches_from(vec!["test", "--version"]) + .unwrap_err() + } + + #[test] + fn test_error_kind_detection() { + let unknown_err = create_unknown_argument_error(); + assert_eq!(unknown_err.kind(), ErrorKind::UnknownArgument); + + let invalid_value_err = create_invalid_value_error(); + assert_eq!(invalid_value_err.kind(), ErrorKind::InvalidValue); + + let help_err = create_help_error(); + assert_eq!(help_err.kind(), ErrorKind::DisplayHelp); + + let version_err = create_version_error(); + assert_eq!(version_err.kind(), ErrorKind::DisplayVersion); + } + + #[test] + fn test_context_extraction() { + let unknown_err = create_unknown_argument_error(); + let invalid_arg = unknown_err.get(ContextKind::InvalidArg); + assert!(invalid_arg.is_some()); + assert!(invalid_arg.unwrap().to_string().contains("unknown-arg")); + + let invalid_value_err = create_invalid_value_error(); + let invalid_value = invalid_value_err.get(ContextKind::InvalidValue); + assert!(invalid_value.is_some()); + assert_eq!(invalid_value.unwrap().to_string(), "invalid"); + } + + fn test_maybe_colorize_helper(colors_enabled: bool) { + let maybe_colorize = |text: &str, color: Color| -> String { + if colors_enabled { + colorize(text, color) + } else { + text.to_string() + } + }; + + let result = maybe_colorize("test", Color::Red); + if colors_enabled { + assert!(result.contains("\x1b[31m")); + assert!(result.contains("\x1b[0m")); + } else { + assert_eq!(result, "test"); + } + } + + #[test] + fn test_maybe_colorize_with_colors() { + test_maybe_colorize_helper(true); + } + + #[test] + fn test_maybe_colorize_without_colors() { + test_maybe_colorize_helper(false); + } + + #[test] + fn test_simple_help_classification() { + let simple_help_kinds = [ + ErrorKind::InvalidValue, + ErrorKind::ValueValidation, + ErrorKind::InvalidSubcommand, + ErrorKind::InvalidUtf8, + ErrorKind::ArgumentConflict, + ErrorKind::NoEquals, + ErrorKind::Io, + ErrorKind::Format, + ]; + + let non_simple_help_kinds = [ + ErrorKind::TooFewValues, + ErrorKind::TooManyValues, + ErrorKind::WrongNumberOfValues, + ErrorKind::MissingSubcommand, + ErrorKind::MissingRequiredArgument, + ErrorKind::DisplayHelp, + ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, + ErrorKind::DisplayVersion, + ErrorKind::UnknownArgument, + ]; + + for kind in &simple_help_kinds { + assert!( + should_show_simple_help_for_clap_error(*kind), + "Expected {:?} to show simple help", + kind + ); + } + + for kind in &non_simple_help_kinds { + assert!( + !should_show_simple_help_for_clap_error(*kind), + "Expected {:?} to NOT show simple help", + kind + ); + } + } + + #[test] + fn test_localization_setup() { + use crate::locale::{get_message, setup_localization}; + + let _ = setup_localization("test"); + + let common_keys = [ + "common-error", + "common-usage", + "common-help-suggestion", + "clap-error-unexpected-argument", + "clap-error-invalid-value", + ]; + for key in &common_keys { + let message = get_message(key); + assert_ne!(message, *key, "Translation not found for key: {}", key); + } + } + + #[test] + fn test_localization_with_args() { + use crate::locale::{get_message_with_args, setup_localization}; + use fluent::FluentArgs; + + let _ = setup_localization("test"); + + let mut args = FluentArgs::new(); + args.set("error_word", "ERROR"); + args.set("arg", "--test"); + + let message = get_message_with_args("clap-error-unexpected-argument", args); + assert_ne!( + message, "clap-error-unexpected-argument", + "Translation not found for key: clap-error-unexpected-argument" + ); + } + + #[test] + fn test_french_localization() { + use crate::locale::{get_message, setup_localization}; + use std::env; + + let original_lang = env::var("LANG").unwrap_or_default(); + + unsafe { + env::set_var("LANG", "fr-FR"); + } + let result = setup_localization("test"); + + if result.is_ok() { + let error_word = get_message("common-error"); + assert_eq!(error_word, "erreur"); + + let usage_word = get_message("common-usage"); + assert_eq!(usage_word, "Utilisation"); + + let tip_word = get_message("common-tip"); + assert_eq!(tip_word, "conseil"); + } + + unsafe { + if original_lang.is_empty() { + env::remove_var("LANG"); + } else { + env::set_var("LANG", original_lang); + } + } + } + + #[test] + fn test_french_clap_error_messages() { + use crate::locale::{get_message_with_args, setup_localization}; + use fluent::FluentArgs; + use std::env; + + let original_lang = env::var("LANG").unwrap_or_default(); + + unsafe { + env::set_var("LANG", "fr-FR"); + } + let result = setup_localization("test"); + + if result.is_ok() { + let mut args = FluentArgs::new(); + args.set("error_word", "erreur"); + args.set("arg", "--inconnu"); + + let unexpected_msg = get_message_with_args("clap-error-unexpected-argument", args); + assert!(unexpected_msg.contains("erreur")); + assert!(unexpected_msg.contains("--inconnu")); + assert!(unexpected_msg.contains("inattendu")); + + let mut value_args = FluentArgs::new(); + value_args.set("error_word", "erreur"); + value_args.set("value", "invalide"); + value_args.set("option", "--format"); + + let invalid_msg = get_message_with_args("clap-error-invalid-value", value_args); + assert!(invalid_msg.contains("erreur")); + assert!(invalid_msg.contains("invalide")); + assert!(invalid_msg.contains("--format")); + } + + unsafe { + if original_lang.is_empty() { + env::remove_var("LANG"); + } else { + env::set_var("LANG", original_lang); + } + } + } +} +/* spell-checker: enable */ diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 86c358724ee..ffde10303b7 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1430,3 +1430,12 @@ fn test_du_inodes_total_text() { assert!(parts[0].parse::().is_ok()); } + +#[test] +fn test_du_threshold_no_suggested_values() { + // tested by tests/du/threshold + let ts = TestScenario::new(util_name!()); + + let result = ts.ucmd().arg("--threshold").fails(); + assert!(!result.stderr_str().contains("[possible values: ]")); +}