diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index b4a151d81..ccec22d54 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -129,6 +129,176 @@ jobs: echo "::notice::All Fluent files passed Mozilla Fluent Linter validation" + l10n_clap_error_localization: + name: L10n/Clap Error Localization Test + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + sudo locale-gen --keep-existing fr_FR.UTF-8 + locale -a | grep -i fr || exit 1 + - name: Build coreutils with clap localization support + shell: bash + run: | + cargo build --features feat_os_unix --bin coreutils + - name: Test English clap error localization + shell: bash + run: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + # Test invalid argument error - should show colored error message + echo "Testing invalid argument error..." + error_output=$(cargo run --features feat_os_unix --bin coreutils -- cp --invalid-arg 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + # Check for expected English clap error patterns + english_errors_found=0 + + if echo "$error_output" | grep -q "error.*unexpected argument"; then + echo "✓ Found English clap error message pattern" + english_errors_found=$((english_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Usage:"; then + echo "✓ Found English usage pattern" + english_errors_found=$((english_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "For more information.*--help"; then + echo "✓ Found English help suggestion" + english_errors_found=$((english_errors_found + 1)) + fi + + # Test typo suggestion + echo "Testing typo suggestion..." + typo_output=$(cargo run --features feat_os_unix --bin coreutils -- ls --verbos 2>&1 || echo "Expected error occurred") + echo "Typo output: $typo_output" + + if echo "$typo_output" | grep -q "similar.*verbose"; then + echo "✓ Found English typo suggestion" + english_errors_found=$((english_errors_found + 1)) + fi + + echo "English clap errors found: $english_errors_found" + if [ "$english_errors_found" -ge 2 ]; then + echo "✓ SUCCESS: English clap error localization working" + else + echo "✗ ERROR: English clap error localization not working properly" + exit 1 + fi + env: + RUST_BACKTRACE: "1" + + - name: Test French clap error localization + shell: bash + run: | + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + # Test invalid argument error - should show French colored error message + echo "Testing invalid argument error in French..." + error_output=$(cargo run --features feat_os_unix --bin coreutils -- cp --invalid-arg 2>&1 || echo "Expected error occurred") + echo "French error output: $error_output" + + # Check for expected French clap error patterns + french_errors_found=0 + + if echo "$error_output" | grep -q "erreur.*argument inattendu"; then + echo "✓ Found French clap error message: 'erreur: argument inattendu'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "conseil.*pour passer.*comme valeur"; then + echo "✓ Found French tip message: 'conseil: pour passer ... comme valeur'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Utilisation:"; then + echo "✓ Found French usage pattern: 'Utilisation:'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Pour plus d'informations.*--help"; then + echo "✓ Found French help suggestion: 'Pour plus d'informations'" + french_errors_found=$((french_errors_found + 1)) + fi + + # Test typo suggestion in French + echo "Testing typo suggestion in French..." + typo_output=$(cargo run --features feat_os_unix --bin coreutils -- ls --verbos 2>&1 || echo "Expected error occurred") + echo "French typo output: $typo_output" + + if echo "$typo_output" | grep -q "conseil.*similaire.*verbose"; then + echo "✓ Found French typo suggestion with 'conseil'" + french_errors_found=$((french_errors_found + 1)) + fi + + echo "French clap errors found: $french_errors_found" + if [ "$french_errors_found" -ge 2 ]; then + echo "✓ SUCCESS: French clap error localization working - found $french_errors_found French patterns" + else + echo "✗ ERROR: French clap error localization not working properly" + echo "Note: This might be expected if French common locale files are not available" + # Don't fail the build - French clap localization might not be fully set up yet + echo "::warning::French clap error localization not working, but continuing" + fi + + # Test that colors are working (ANSI escape codes) + echo "Testing ANSI color codes in error output..." + if echo "$error_output" | grep -q $'\x1b\[3[0-7]m'; then + echo "✓ Found ANSI color codes in error output" + else + echo "✗ No ANSI color codes found - colors may not be working" + echo "::warning::ANSI color codes not detected in clap error output" + fi + env: + RUST_BACKTRACE: "1" + + - name: Test clap localization with multiple utilities + shell: bash + run: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + utilities_to_test=("ls" "cat" "touch" "cp" "mv") + utilities_passed=0 + + for util in "${utilities_to_test[@]}"; do + echo "Testing $util with invalid argument..." + util_error=$(cargo run --features feat_os_unix --bin coreutils -- "$util" --nonexistent-flag 2>&1 || echo "Expected error occurred") + + if echo "$util_error" | grep -q "error.*unexpected argument"; then + echo "✓ $util: clap localization working" + utilities_passed=$((utilities_passed + 1)) + else + echo "✗ $util: clap localization not working" + echo "Output: $util_error" + fi + done + + echo "Utilities with working clap localization: $utilities_passed/${#utilities_to_test[@]}" + if [ "$utilities_passed" -ge 3 ]; then + echo "✓ SUCCESS: Clap localization working across multiple utilities" + else + echo "✗ ERROR: Clap localization not working for enough utilities" + exit 1 + fi + env: + RUST_BACKTRACE: "1" + l10n_french_integration: name: L10n/French Integration Test runs-on: ubuntu-latest diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 199830c2d..51dd4a30c 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -27,7 +27,8 @@ "src/uu/dd/test-resources/**", "vendor/**", "**/*.svg", - "src/uu/*/locales/*.ftl" + "src/uu/*/locales/*.ftl", + "src/uucore/locales/*.ftl" ], "enableGlobDot": true, diff --git a/GNUmakefile b/GNUmakefile index 54aea865f..20dc731d3 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -418,6 +418,14 @@ endif ifeq ($(LOCALES),y) locales: + @# Copy uucore common locales + @if [ -d "$(BASEDIR)/src/uucore/locales" ]; then \ + mkdir -p "$(BUILDDIR)/locales/uucore"; \ + for locale_file in "$(BASEDIR)"/src/uucore/locales/*.ftl; do \ + $(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/uucore/"; \ + done; \ + fi; \ + # Copy utility-specific locales @for prog in $(INSTALLEES); do \ if [ -d "$(BASEDIR)/src/uu/$$prog/locales" ]; then \ mkdir -p "$(BUILDDIR)/locales/$$prog"; \ diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 29ad9d273..8fe80b602 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -6,12 +6,13 @@ use platform_info::*; use clap::Command; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError}; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from(args)?; + uu_app().get_matches_from_localized(args); let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, translate!("cannot-get-system")))?; @@ -23,6 +24,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("arch-about")) .after_help(translate!("arch-after-help")) .infer_long_args(true) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index b7d1124ac..db1e97016 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -9,12 +9,12 @@ use clap::{Arg, ArgAction, Command}; use std::fs::File; use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::encoding::{ - BASE2LSBF, BASE2MSBF, Format, Z85Wrapper, + BASE2LSBF, BASE2MSBF, EncodingWrapper, Format, SupportsFastDecodeAndEncode, Z85Wrapper, for_base_common::{BASE32, BASE32HEX, BASE64, BASE64_NOPAD, BASE64URL, HEXUPPER_PERMISSIVE}, }; -use uucore::encoding::{EncodingWrapper, SupportsFastDecodeAndEncode}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::translate; @@ -100,12 +100,14 @@ pub fn parse_base_cmd_args( usage: &str, ) -> UResult { let command = base_app(about, usage); - Config::from(&command.try_get_matches_from(args)?) + let matches = command.get_matches_from_localized(args); + Config::from(&matches) } pub fn base_app(about: &'static str, usage: &str) -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(usage)) .infer_long_args(true) diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 255ff6110..61ac69289 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -15,6 +15,7 @@ use uucore::error::{UResult, UUsageError}; use uucore::format_usage; use uucore::line_ending::LineEnding; +use uucore::LocalizedCommand; use uucore::translate; pub mod options { @@ -29,7 +30,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // // Argument parsing // - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); @@ -81,6 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("basename-about")) .override_usage(format_usage(&translate!("basename-usage"))) .infer_long_args(true) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index c7f975952..f03c7c3b9 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -22,6 +22,7 @@ use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use std::os::unix::net::UnixStream; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::UResult; #[cfg(not(target_os = "windows"))] @@ -230,7 +231,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) { NumberingMode::NonEmpty @@ -286,6 +287,7 @@ pub fn uu_app() -> Command { .version(uucore::crate_version!()) .override_usage(format_usage(&translate!("cat-usage"))) .about(translate!("cat-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index ca65cf4a7..25c2d099e 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -7,6 +7,7 @@ #![allow(clippy::upper_case_acronyms)] use clap::builder::ValueParser; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::translate; use uucore::{display::Quotable, format_usage, show_error, show_warning}; @@ -156,6 +157,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("chcon-about")) .override_usage(format_usage(&translate!("chcon-usage"))) .infer_long_args(true) @@ -303,7 +305,7 @@ struct Options { } fn parse_command_line(config: Command, args: impl uucore::Args) -> Result { - let matches = config.try_get_matches_from(args)?; + let matches = config.get_matches_from_localized(args); let verbose = matches.get_flag(options::VERBOSE); diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index 9df217fd0..aca69d103 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -99,6 +99,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("chgrp-about")) .override_usage(format_usage(&translate!("chgrp-usage"))) .infer_long_args(true) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index f2d2bd47a..92ea6024e 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -11,6 +11,7 @@ use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code}; use uucore::fs::display_permissions_unix; @@ -112,7 +113,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); // skip binary name let matches = uu_app() .after_help(translate!("chmod-after-help")) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); @@ -177,6 +178,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("chmod-about")) .override_usage(format_usage(&translate!("chmod-usage"))) .args_override_self(true) diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index bd5872d51..ceff36b59 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -77,6 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("chown-about")) .override_usage(format_usage(&translate!("chown-usage"))) .infer_long_args(true) diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 3d8ea589b..52db47e36 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -236,6 +236,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("chroot-about")) .override_usage(format_usage(&translate!("chroot-usage"))) .infer_long_args(true) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 6dc28040e..01e3da55f 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -20,6 +20,7 @@ use uucore::checksum::{ }; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{ encoding, error::{FromIo, UResult, USimpleError}, @@ -236,7 +237,7 @@ fn handle_tag_text_binary_flags>( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let check = matches.get_flag(options::CHECK); @@ -343,6 +344,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("cksum-about")) .override_usage(format_usage(&translate!("cksum-usage"))) .infer_long_args(true) diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index e55d181d6..b77c549d7 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -8,6 +8,7 @@ use std::cmp::Ordering; use std::fs::{File, metadata}; use std::io::{self, BufRead, BufReader, Read, Stdin, stdin}; +use uucore::LocalizedCommand; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::fs::paths_refer_to_same_file; @@ -280,7 +281,7 @@ fn open_file(name: &str, line_ending: LineEnding) -> io::Result { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); let filename1 = matches.get_one::(options::FILE_1).unwrap(); let filename2 = matches.get_one::(options::FILE_2).unwrap(); @@ -315,6 +316,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("comm-about")) .override_usage(format_usage(&translate!("comm-usage"))) .infer_long_args(true) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index e826ac3c4..41cded5f0 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -13,6 +13,7 @@ use std::fs::{self, Metadata, OpenOptions, Permissions}; use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf, StripPrefixError}; use std::{fmt, io}; +use uucore::LocalizedCommand; #[cfg(all(unix, not(target_os = "android")))] use uucore::fsxattr::copy_xattrs; use uucore::translate; @@ -520,6 +521,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) .about(translate!("cp-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("cp-usage"))) .after_help(format!( "{}\n\n{}", @@ -778,7 +780,7 @@ pub fn uu_app() -> Command { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let options = Options::from_matches(&matches)?; diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index ba58479b2..fc18b97da 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -25,6 +25,7 @@ mod split_name; use crate::csplit_error::CsplitError; use crate::split_name::SplitName; +use uucore::LocalizedCommand; use uucore::translate; mod options { @@ -604,7 +605,7 @@ where #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); // get the file to split let file_name = matches.get_one::(options::FILE).unwrap(); @@ -629,6 +630,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("csplit-about")) .override_usage(format_usage(&translate!("csplit-usage"))) .args_override_self(true) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 4bf323507..57f0e61d1 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -18,6 +18,7 @@ use uucore::os_str_as_bytes; use self::searcher::Searcher; use matcher::{ExactMatcher, Matcher, WhitespaceMatcher}; +use uucore::LocalizedCommand; use uucore::ranges::Range; use uucore::translate; use uucore::{format_usage, show_error, show_if_err}; @@ -482,7 +483,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }) .collect(); - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let complement = matches.get_flag(options::COMPLEMENT); let only_delimited = matches.get_flag(options::ONLY_DELIMITED); @@ -594,6 +595,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("cut-usage"))) .about(translate!("cut-about")) .after_help(translate!("cut-after-help")) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 6d5a418c2..1685844d9 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -21,6 +21,7 @@ use uucore::{format_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; +use uucore::LocalizedCommand; use uucore::parser::shortcut_value_parser::ShortcutValueParser; // Options @@ -111,7 +112,7 @@ impl From<&str> for Rfc3339Format { #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let format = if let Some(form) = matches.get_one::(OPT_FORMAT) { if !form.starts_with('+') { @@ -264,6 +265,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("date-about")) .override_usage(format_usage(&translate!("date-usage"))) .infer_long_args(true) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 4d77dcfe7..0ddeefeb4 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -55,6 +55,7 @@ use nix::{ errno::Errno, fcntl::{PosixFadviseAdvice, posix_fadvise}, }; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; #[cfg(unix)] @@ -1415,7 +1416,7 @@ fn is_fifo(filename: &str) -> bool { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let settings: Settings = Parser::new().parse( matches @@ -1442,6 +1443,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("dd-about")) .override_usage(format_usage(&translate!("dd-usage"))) .after_help(translate!("dd-after-help")) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 6c8a1ee7a..3a7a10f92 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -11,6 +11,7 @@ mod table; use blocks::HumanReadable; use clap::builder::ValueParser; use table::HeaderMode; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError, get_exit_code}; use uucore::fsext::{MountInfo, read_fs_list}; @@ -406,7 +407,7 @@ impl UError for DfError { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); #[cfg(windows)] { @@ -462,6 +463,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("df-about")) .override_usage(format_usage(&translate!("df-usage"))) .after_help(translate!("df-after-help")) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index b76d49f67..87a459f29 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -18,6 +18,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, parser::parse_glob}; mod options { @@ -120,7 +121,7 @@ fn generate_ls_colors(fmt: &OutputFmt, sep: &str) -> String { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files = matches .get_many::(options::FILE) @@ -238,6 +239,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("dircolors-about")) .after_help(translate!("dircolors-after-help")) .override_usage(format_usage(&translate!("dircolors-usage"))) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 6d7a4a5a5..aac8e57f3 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -5,6 +5,7 @@ use clap::{Arg, ArgAction, Command}; use std::path::Path; +use uucore::LocalizedCommand; use uucore::display::print_verbatim; use uucore::error::{UResult, UUsageError}; use uucore::format_usage; @@ -21,7 +22,7 @@ mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(translate!("dirname-after-help")) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); @@ -63,6 +64,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .about(translate!("dirname-about")) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("dirname-usage"))) .args_override_self(true) .infer_long_args(true) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index f39d257de..64a7662bb 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -25,6 +25,7 @@ use uucore::fsext::{MetadataTimeField, metadata_get_time}; use uucore::line_ending::LineEnding; use uucore::translate; +use uucore::LocalizedCommand; use uucore::parser::parse_glob; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; @@ -580,7 +581,7 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let summarize = matches.get_flag(options::SUMMARIZE); @@ -804,6 +805,7 @@ fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("du-about")) .after_help(translate!("du-after-help")) .override_usage(format_usage(&translate!("du-usage"))) diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 6c61ec7d8..4bbff02e9 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -193,6 +193,7 @@ pub fn uu_app() -> Command { .about(translate!("echo-about")) .after_help(translate!("echo-after-help")) .override_usage(format_usage(&translate!("echo-usage"))) + .help_template(uucore::localized_help_template(uucore::util_name())) .arg( Arg::new(options::NO_NEWLINE) .short('n') diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 9e42b1694..52e3bfa22 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -225,6 +225,7 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { pub fn uu_app() -> Command { Command::new(crate_name!()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("env-about")) .override_usage(format_usage(&translate!("env-usage"))) .after_help(translate!("env-after-help")) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 8d768522d..cd528316e 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -251,6 +251,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("expand-about")) .after_help(LONG_HELP) .override_usage(format_usage(&translate!("expand-usage"))) diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index ba980357c..75ce8620d 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -73,6 +73,7 @@ impl UError for ExprError { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("expr-about")) .override_usage(format_usage(&translate!("expr-usage"))) .after_help(translate!("expr-after-help")) diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 776a98536..8516b1eb6 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -12,6 +12,7 @@ use std::io::{self, Write, stdin, stdout}; use clap::{Arg, ArgAction, Command}; use num_bigint::BigUint; use num_traits::FromPrimitive; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; use uucore::translate; @@ -79,7 +80,7 @@ fn write_result( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); // If matches find --exponents flag than variable print_exponents is true and p^e output format will be used. let print_exponents = matches.get_flag(options::EXPONENTS); @@ -121,6 +122,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("factor-about")) .override_usage(format_usage(&translate!("factor-usage"))) .infer_long_args(true) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 92128f5f1..108349031 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -24,6 +24,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } if let Err(e) = command.try_get_matches_from_mut(args) { + // For the false command, we don't want to show any error messages for UnknownArgument + // since false should produce no output and just exit with code 1 let error = match e.kind() { clap::error::ErrorKind::DisplayHelp => command.print_help(), clap::error::ErrorKind::DisplayVersion => { @@ -45,6 +47,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("false-about")) // We provide our own help and version options, to ensure maximum compatibility with GNU. .disable_help_flag(true) diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index f6e555055..46f9d547f 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -12,6 +12,7 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::format_usage; use linebreak::break_lines; @@ -334,7 +335,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let matches = uu_app().try_get_matches_from(&args)?; + let matches = uu_app().get_matches_from_localized(&args); let files = extract_files(&matches)?; @@ -352,6 +353,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("fmt-about")) .override_usage(format_usage(&translate!("fmt-usage"))) .infer_long_args(true) diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 318e3875b..22fc6df23 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; use std::fs::File; use std::io::{BufRead, BufReader, Read, Write, stdin, stdout}; use std::path::Path; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -31,7 +32,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); let (args, obs_width) = handle_obsolete(&args[..]); - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let bytes = matches.get_flag(options::BYTES); let spaces = matches.get_flag(options::SPACES); @@ -61,6 +62,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("fold-usage"))) .about(translate!("fold-about")) .infer_long_args(true) diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index 498c72802..17624ffde 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -14,6 +14,7 @@ use uucore::{ }; use clap::{Arg, ArgAction, Command}; +use uucore::LocalizedCommand; use uucore::translate; mod options { @@ -47,7 +48,7 @@ fn infallible_gid2grp(gid: &u32) -> String { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let users: Vec = matches .get_many::(options::USERS) @@ -81,6 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("groups-about")) .override_usage(format_usage(&translate!("groups-usage"))) .infer_long_args(true) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index a6fc13e23..0fc23e35f 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -15,6 +15,7 @@ use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; +use uucore::LocalizedCommand; use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumOptions; use uucore::checksum::ChecksumVerbose; @@ -181,7 +182,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // causes "error: " to be printed twice (once from crash!() and once from clap). With // the current setup, the name of the utility is not printed, but I think this is at // least somewhat better from a user's perspective. - let matches = command.try_get_matches_from(args)?; + let matches = command.get_matches_from_localized(args); let input_length: Option<&usize> = if binary_name == "b2sum" { matches.get_one::(options::LENGTH) @@ -311,6 +312,7 @@ mod options { pub fn uu_app_common() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("hashsum-about")) .override_usage(format_usage(&translate!("hashsum-usage"))) .infer_long_args(true) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index b7e4334c4..818062ba9 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -14,6 +14,7 @@ use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::{AsRawFd, FromRawFd}; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; use uucore::line_ending::LineEnding; @@ -71,6 +72,7 @@ type HeadResult = Result; pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("head-about")) .override_usage(format_usage(&translate!("head-usage"))) .infer_long_args(true) @@ -549,7 +551,8 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; + let args_vec: Vec<_> = arg_iterate(args)?.collect(); + let matches = uu_app().get_matches_from_localized(args_vec); let args = match HeadOptions::get_from(&matches) { Ok(o) => o, Err(s) => { diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index f5054389b..8a4f5efd8 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -9,11 +9,12 @@ use clap::Command; use libc::{c_long, gethostid}; use uucore::{error::UResult, format_usage}; +use uucore::LocalizedCommand; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from(args)?; + uu_app().get_matches_from_localized(args); hostid(); Ok(()) } @@ -21,6 +22,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("hostid-about")) .override_usage(format_usage(&translate!("hostid-usage"))) .infer_long_args(true) diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 558c6aff1..acefbf732 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -12,6 +12,7 @@ use std::{collections::hash_set::HashSet, ffi::OsString}; use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; +use uucore::LocalizedCommand; #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] use dns_lookup::lookup_host; @@ -60,7 +61,7 @@ mod wsa { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); #[cfg(windows)] let _handle = wsa::start().map_err_context(|| translate!("hostname-error-winsock"))?; @@ -76,6 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("hostname-about")) .override_usage(format_usage(&translate!("hostname-usage"))) .infer_long_args(true) diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 41e926450..0852d9927 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -44,6 +44,7 @@ use uucore::libc::{getlogin, uid_t}; use uucore::line_ending::LineEnding; use uucore::translate; +use uucore::LocalizedCommand; use uucore::process::{getegid, geteuid, getgid, getuid}; use uucore::{format_usage, show_error}; @@ -121,7 +122,7 @@ struct State { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(translate!("id-after-help")) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let users: Vec = matches .get_many::(options::ARG_USERS) @@ -349,6 +350,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("id-about")) .override_usage(format_usage(&translate!("id-usage"))) .infer_long_args(true) diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 4f434485e..dc249b5d0 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true } filetime = { workspace = true } file_diff = { workspace = true } thiserror = { workspace = true } -uucore = { workspace = true, features = [ +uucore = { workspace = true, default-features = true, features = [ "backup-control", "buf-copy", "fs", diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index ac1948c51..66e80b90d 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -16,6 +16,7 @@ use std::fs::{self, metadata}; use std::path::{MAIN_SEPARATOR, Path, PathBuf}; use std::process; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::backup_control::{self, BackupMode}; use uucore::buf_copy::copy_stream; use uucore::display::Quotable; @@ -165,7 +166,7 @@ static ARG_FILES: &str = "files"; /// #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let paths: Vec = matches .get_many::(ARG_FILES) @@ -183,6 +184,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("install-about")) .override_usage(format_usage(&translate!("install-usage"))) .infer_long_args(true) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 3dec9bef1..dd1323506 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -16,6 +16,7 @@ use std::num::IntErrorKind; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; use uucore::format_usage; @@ -821,7 +822,7 @@ fn parse_settings(matches: &clap::ArgMatches) -> UResult { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let settings = parse_settings(&matches)?; @@ -854,6 +855,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("join-about")) .override_usage(format_usage(&translate!("join-usage"))) .infer_long_args(true) diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 148489b2d..e749e982b 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -13,6 +13,7 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::signals::{ALL_SIGNALS, signal_by_name_or_value, signal_name_by_value}; use uucore::{format_usage, show}; @@ -40,7 +41,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut args = args.collect_ignore(); let obs_signal = handle_obsolete(&mut args); - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mode = if matches.get_flag(options::TABLE) { Mode::Table @@ -99,6 +100,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("kill-about")) .override_usage(format_usage(&translate!("kill-usage"))) .infer_long_args(true) diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 20528a700..773d52d20 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -8,6 +8,7 @@ use clap::{Arg, Command}; use std::ffi::OsString; use std::fs::hard_link; use std::path::Path; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; use uucore::format_usage; @@ -19,7 +20,7 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(options::FILES) .unwrap_or_default() @@ -36,6 +37,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("link-about")) .override_usage(format_usage(&translate!("link-usage"))) .infer_long_args(true) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index df53b16b8..8b21bd093 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -23,6 +23,7 @@ use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::backup_control::{self, BackupMode}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; @@ -94,7 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { backup_control::BACKUP_CONTROL_LONG_HELP ); - let matches = uu_app().after_help(after_help).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(after_help) + .get_matches_from_localized(args); /* the list of files */ @@ -141,6 +144,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { 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"))) .infer_long_args(true) diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 77d8fe15c..1cf1872a8 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -7,6 +7,7 @@ use clap::Command; use std::ffi::CStr; +use uucore::LocalizedCommand; use uucore::translate; use uucore::{error::UResult, show_error}; @@ -23,7 +24,7 @@ fn get_userlogin() -> Option { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _ = uu_app().try_get_matches_from(args)?; + let _ = uu_app().get_matches_from_localized(args); match get_userlogin() { Some(userlogin) => println!("{userlogin}"), @@ -36,6 +37,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(uucore::util_name()) .about(translate!("logname-about")) .infer_long_args(true) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f4c685863..b25924c3f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -57,7 +57,7 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::libc::{dev_t, major, minor}; use uucore::{ display::Quotable, - error::{UError, UResult, USimpleError, set_exit_code}, + error::{UError, UResult, set_exit_code}, format::human::{SizeFormat, human_readable}, format_usage, fs::FileInformation, @@ -1103,22 +1103,11 @@ impl Config { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let command = uu_app(); - - let matches = match command.try_get_matches_from(args) { - // clap successfully parsed the arguments: + let matches = match uu_app().try_get_matches_from(args) { Ok(matches) => matches, - // --help, --version, etc.: - Err(e) if e.exit_code() == 0 => { - return Err(e.into()); - } - // Errors in argument *values* cause exit code 1: - Err(e) if e.kind() == clap::error::ErrorKind::InvalidValue => { - return Err(USimpleError::new(1, e.to_string())); - } - // All other argument parsing errors cause exit code 2: Err(e) => { - return Err(USimpleError::new(2, e.to_string())); + // Use localization handler for all clap errors + uucore::clap_localization::handle_clap_error_with_exit_code(e, "ls", 2); } }; @@ -1136,6 +1125,7 @@ pub fn uu_app() -> Command { .version(uucore::crate_version!()) .override_usage(format_usage(&translate!("ls-usage"))) .about(translate!("ls-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index b2c349337..fb82d963c 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -15,6 +15,7 @@ use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; #[cfg(not(windows))] use uucore::mode; use uucore::{display::Quotable, fs::dir_strip_dot_for_creation}; @@ -81,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // " of each created directory to CTX"), let matches = uu_app() .after_help(translate!("mkdir-after-help")) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let dirs = matches .get_many::(options::DIRS) @@ -111,6 +112,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("mkdir-about")) .override_usage(format_usage(&translate!("mkdir-usage"))) .infer_long_args(true) diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 33d842d92..5cae85cc8 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -12,6 +12,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, show}; mod options { @@ -23,7 +24,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mode = calculate_mode(matches.get_one::(options::MODE)) .map_err(|e| USimpleError::new(1, translate!("mkfifo-error-invalid-mode", "error" => e)))?; @@ -83,6 +84,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("mkfifo-usage"))) .about(translate!("mkfifo-about")) .infer_long_args(true) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 5bd79ade9..d922c3b82 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -10,6 +10,7 @@ use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOT use libc::{dev_t, mode_t}; use std::ffi::CString; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; use uucore::format_usage; @@ -111,7 +112,7 @@ fn mknod(file_name: &str, config: Config) -> i32 { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let file_type = matches.get_one::("type").unwrap(); @@ -170,6 +171,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("mknod-usage"))) .after_help(translate!("mknod-after-help")) .about(translate!("mknod-about")) diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 90ac7c875..23d22d361 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -333,6 +333,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = match uu_app().try_get_matches_from(&args) { Ok(m) => m, Err(e) => { + use uucore::clap_localization::handle_clap_error_with_exit_code; + if e.kind() == clap::error::ErrorKind::UnknownArgument { + handle_clap_error_with_exit_code(e, uucore::util_name(), 1); + } if e.kind() == clap::error::ErrorKind::TooManyValues && e.context().any(|(kind, val)| { kind == clap::error::ContextKind::InvalidArg @@ -393,6 +397,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("mktemp-about")) .override_usage(format_usage(&translate!("mktemp-usage"))) .infer_long_args(true) diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 4bd2e80df..8aa6b7729 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -26,6 +26,7 @@ use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::{display::Quotable, show}; +use uucore::LocalizedCommand; use uucore::translate; #[derive(Debug)] @@ -151,7 +152,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { print!("\r"); println!("{panic_info}"); })); - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mut options = Options::from(&matches); if let Some(files) = matches.get_many::(options::FILES) { let length = files.len(); @@ -212,6 +213,7 @@ pub fn uu_app() -> Command { .about(translate!("more-about")) .override_usage(format_usage(&translate!("more-usage"))) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .infer_long_args(true) .arg( Arg::new(options::SILENT) diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 948e486c9..b89290149 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -10,7 +10,8 @@ mod error; mod hardlink; use clap::builder::ValueParser; -use clap::{Arg, ArgAction, ArgMatches, Command, error::ErrorKind}; +use clap::error::ErrorKind; +use clap::{Arg, ArgAction, ArgMatches, Command}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] @@ -51,6 +52,7 @@ use uucore::update_control; // These are exposed for projects (e.g. nushell) that want to create an `Options` value, which // requires these enums +use uucore::LocalizedCommand; pub use uucore::{backup_control::BackupMode, update_control::UpdateMode}; use uucore::{format_usage, prompt_yes, show}; @@ -151,8 +153,7 @@ static OPT_SELINUX: &str = "selinux"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let mut app = uu_app(); - let matches = app.try_get_matches_from_mut(args)?; + let matches = uu_app().get_matches_from_localized(args); let files: Vec = matches .get_many::(ARG_FILES) @@ -161,11 +162,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect(); if files.len() == 1 && !matches.contains_id(OPT_TARGET_DIRECTORY) { - app.error( + let err = uu_app().error( ErrorKind::TooFewValues, translate!("mv-error-insufficient-arguments", "arg_files" => ARG_FILES), - ) - .exit(); + ); + uucore::clap_localization::handle_clap_error_with_exit_code(err, uucore::util_name(), 1); } let overwrite_mode = determine_overwrite_mode(&matches); @@ -225,6 +226,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) .about(translate!("mv-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("mv-usage"))) .after_help(format!( "{}\n\n{}", diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 776332797..5e9b555e8 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -185,6 +185,7 @@ pub fn uu_app() -> Command { .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .arg( Arg::new(options::ADJUSTMENT) .short('n') diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 1e5eb3d72..890de1e86 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -10,6 +10,7 @@ use std::path::Path; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, show_error}; mod helper; @@ -176,7 +177,7 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mut settings = Settings::default(); @@ -229,6 +230,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .about(translate!("nl-about")) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("nl-usage"))) .after_help(translate!("nl-after-help")) .infer_long_args(true) diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 92dc510b5..f20ec29af 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -86,6 +86,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("nohup-about")) .after_help(translate!("nohup-after-help")) .override_usage(format_usage(&translate!("nohup-usage"))) diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index f7be033bd..dd53e81b9 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -7,6 +7,7 @@ use clap::{Arg, ArgAction, Command}; use std::{env, thread}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; @@ -26,7 +27,7 @@ static OPT_IGNORE: &str = "ignore"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let ignore = match matches.get_one::(OPT_IGNORE) { Some(numstr) => match numstr.trim().parse::() { @@ -92,6 +93,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("nproc-about")) .override_usage(format_usage(&translate!("nproc-usage"))) .infer_long_args(true) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index f8dddd8c6..e5cec7692 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -13,6 +13,7 @@ use std::result::Result as StdResult; use std::str::FromStr; use units::{IEC_BASES, SI_BASES}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::UResult; use uucore::translate; @@ -254,7 +255,7 @@ fn parse_options(args: &ArgMatches) -> Result { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; @@ -279,6 +280,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("numfmt-about")) .after_help(translate!("numfmt-after-help")) .override_usage(format_usage(&translate!("numfmt-usage"))) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 7db53b6b6..e63a29a00 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -46,6 +46,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::parser::parse_size::ParseSizeError; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::{format_usage, show_error, show_warning}; @@ -220,7 +221,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let clap_opts = uu_app(); - let clap_matches = clap_opts.try_get_matches_from(&args)?; + let clap_matches = clap_opts.get_matches_from_localized(&args); let od_options = OdOptions::new(&clap_matches, &args)?; @@ -251,6 +252,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("od-about")) .override_usage(format_usage(&translate!("od-usage"))) .after_help(translate!("od-after-help")) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index b6b9b59a8..82a03f93b 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -10,6 +10,7 @@ use std::io::{BufRead, BufReader, Stdin, Write, stdin, stdout}; use std::iter::Cycle; use std::rc::Rc; use std::slice::Iter; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; use uucore::line_ending::LineEnding; @@ -24,7 +25,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let serial = matches.get_flag(options::SERIAL); let delimiters = matches.get_one::(options::DELIMITER).unwrap(); @@ -41,6 +42,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("paste-about")) .override_usage(format_usage(&translate!("paste-usage"))) .infer_long_args(true) diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 3b7a3c164..c46ed39ae 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -8,6 +8,7 @@ use clap::{Arg, ArgAction, Command}; use std::fs; use std::io::{ErrorKind, Write}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{UResult, UUsageError, set_exit_code}; use uucore::format_usage; @@ -34,7 +35,7 @@ const POSIX_NAME_MAX: usize = 14; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); // set working mode let is_posix = matches.get_flag(options::POSIX); @@ -81,6 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("pathchk-about")) .override_usage(format_usage(&translate!("pathchk-usage"))) .infer_long_args(true) diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index e494ba555..c2d303e26 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -36,6 +36,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(&translate!("pinky-usage"))) .infer_long_args(true) diff --git a/src/uu/pinky/src/platform/openbsd.rs b/src/uu/pinky/src/platform/openbsd.rs index fb7cd155b..d8dcbc928 100644 --- a/src/uu/pinky/src/platform/openbsd.rs +++ b/src/uu/pinky/src/platform/openbsd.rs @@ -5,11 +5,12 @@ // Specific implementation for OpenBSD: tool unsupported (utmpx not supported) use crate::uu_app; +use uucore::LocalizedCommand; use uucore::error::UResult; use uucore::translate; pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from(args)?; + let _matches = uu_app().get_matches_from_localized(args); println!("{}", translate!("pinky-unsupported-openbsd")); Ok(()) } diff --git a/src/uu/pinky/src/platform/unix.rs b/src/uu/pinky/src/platform/unix.rs index 59c226046..3581cedba 100644 --- a/src/uu/pinky/src/platform/unix.rs +++ b/src/uu/pinky/src/platform/unix.rs @@ -9,6 +9,7 @@ use crate::Capitalize; use crate::options; use crate::uu_app; +use uucore::LocalizedCommand; use uucore::entries::{Locate, Passwd}; use uucore::error::{FromIo, UResult}; use uucore::libc::S_IWGRP; @@ -34,7 +35,7 @@ fn get_long_usage() -> String { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(get_long_usage()) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let users: Vec = matches .get_many::(options::USER) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index fbad8c93a..75fea4be1 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -16,6 +16,7 @@ use std::os::unix::fs::FileTypeExt; use std::time::SystemTime; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::UResult; use uucore::format_usage; @@ -158,6 +159,7 @@ enum PrError { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("pr-about")) .after_help(translate!("pr-after-help")) .override_usage(format_usage(&translate!("pr-usage"))) @@ -315,8 +317,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let opt_args = recreate_arguments(&args); - let mut command = uu_app(); - let matches = command.try_get_matches_from_mut(opt_args)?; + let command = uu_app(); + let matches = command.get_matches_from_mut_localized(opt_args); let mut files = matches .get_many::(options::FILES) diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 063be33ff..3ae77a77e 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -14,7 +14,10 @@ static ARG_VARIABLES: &str = "variables"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let matches = uu_app().try_get_matches_from(args).unwrap_or_else(|e| { + use uucore::clap_localization::handle_clap_error_with_exit_code; + handle_clap_error_with_exit_code(e, uucore::util_name(), 2) + }); let variables: Vec = matches .get_many::(ARG_VARIABLES) @@ -54,6 +57,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("printenv-about")) .override_usage(format_usage(&translate!("printenv-usage"))) .infer_long_args(true) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 2c536bcb6..e48d253f5 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -6,6 +6,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::io::stdout; use std::ops::ControlFlow; +use uucore::LocalizedCommand; use uucore::error::{UResult, UUsageError}; use uucore::format::{FormatArgument, FormatArguments, FormatItem, parse_spec_and_escape}; use uucore::translate; @@ -21,7 +22,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let matches = uu_app().get_matches_from_localized(args); let format = matches .get_one::(options::FORMAT) @@ -84,6 +85,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .allow_hyphen_values(true) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("printf-about")) .after_help(translate!("printf-after-help")) .override_usage(format_usage(&translate!("printf-usage"))) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 78ec37a47..b8352e06f 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -15,6 +15,7 @@ use std::num::ParseIntError; use clap::{Arg, ArgAction, Command}; use regex::Regex; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::format_usage; @@ -728,7 +729,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let config = get_config(&matches)?; let input_files; @@ -770,6 +771,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .about(translate!("ptx-about")) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("ptx-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 39dad5f9e..fccf8c7b3 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -13,6 +13,7 @@ use uucore::format_usage; use uucore::display::println_verbatim; use uucore::error::{FromIo, UResult}; +use uucore::LocalizedCommand; use uucore::translate; const OPT_LOGICAL: &str = "logical"; const OPT_PHYSICAL: &str = "physical"; @@ -109,7 +110,7 @@ fn logical_path() -> io::Result { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); // if POSIXLY_CORRECT is set, we want to a logical resolution. // This produces a different output when doing mkdir -p a/b && ln -s a/b c && cd c && pwd // We should get c in this case instead of a/b at the end of the path @@ -141,6 +142,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("pwd-about")) .override_usage(format_usage(&translate!("pwd-usage"))) .infer_long_args(true) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 8f61d9ab2..f634c970a 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; use std::fs; use std::io::{Write, stdout}; use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; @@ -29,7 +30,7 @@ const ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mut no_trailing_delimiter = matches.get_flag(OPT_NO_NEWLINE); let use_zero = matches.get_flag(OPT_ZERO); @@ -107,6 +108,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("readlink-about")) .override_usage(format_usage(&translate!("readlink-usage"))) .infer_long_args(true) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index d5d77cc2c..1b4ea6028 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -86,6 +86,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("realpath-about")) .override_usage(format_usage(&translate!("realpath-usage"))) .infer_long_args(true) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index f7971aa80..9e3fa9b39 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -23,6 +23,7 @@ use uucore::error::{FromIo, UError, UResult}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error}; #[derive(Debug, Error)] @@ -143,7 +144,7 @@ static ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(ARG_FILES) @@ -226,6 +227,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) .about(translate!("rm-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("rm-usage"))) .after_help(translate!("rm-after-help")) .infer_long_args(true) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 34346c0d6..d2c2ac105 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -15,6 +15,7 @@ use uucore::display::Quotable; use uucore::error::{UResult, set_exit_code, strip_errno}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, show_error, util_name}; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; @@ -25,7 +26,7 @@ static ARG_DIRS: &str = "dirs"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let opts = Opts { ignore: matches.get_flag(OPT_IGNORE_FAIL_NON_EMPTY), @@ -170,6 +171,7 @@ struct Opts { pub fn uu_app() -> Command { Command::new(util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(util_name())) .about(translate!("rmdir-about")) .override_usage(format_usage(&translate!("rmdir-usage"))) .infer_long_args(true) diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 3dfa5cc0c..f6fee9158 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -87,6 +87,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("runcon-about")) .after_help(translate!("runcon-after-help")) .override_usage(format_usage(&translate!("runcon-usage"))) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 6e343f534..5ab029b1a 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -221,6 +221,7 @@ pub fn uu_app() -> Command { .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("seq-about")) .override_usage(format_usage(&translate!("seq-usage"))) .arg( diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index a34797835..c158678a4 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -14,6 +14,7 @@ use std::io::{self, Read, Seek, Write}; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::parser::parse_size::parse_size_u64; @@ -238,7 +239,7 @@ impl<'a> BytesWriter<'a> { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); if !matches.contains_id(options::FILE) { return Err(UUsageError::new( @@ -315,6 +316,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("shred-about")) .after_help(translate!("shred-after-help")) .override_usage(format_usage(&translate!("shred-usage"))) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 254875eaa..45c1e4ece 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -17,6 +17,7 @@ use std::io::{BufWriter, Error, Read, Write, stdin, stdout}; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; use std::str::FromStr; +use uucore::LocalizedCommand; use uucore::display::{OsWrite, Quotable}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; @@ -51,7 +52,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mode = if matches.get_flag(options::ECHO) { Mode::Echo( @@ -145,6 +146,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .about(translate!("shuf-about")) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("shuf-usage"))) .infer_long_args(true) .arg( diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 1731c2af1..4cbaa6e6f 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -6,6 +6,7 @@ use clap::{Arg, ArgAction, Command}; use std::thread; use std::time::Duration; +use uucore::LocalizedCommand; use uucore::translate; use uucore::{ error::{UResult, USimpleError, UUsageError}, @@ -20,7 +21,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let numbers = matches .get_many::(options::NUMBER) @@ -39,6 +40,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("sleep-about")) .after_help(translate!("sleep-after-help")) .override_usage(format_usage(&translate!("sleep-usage"))) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 74b6253fb..f9224f865 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -43,7 +43,7 @@ use std::str::Utf8Error; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, strip_errno}; -use uucore::error::{UError, UResult, USimpleError, UUsageError, set_exit_code}; +use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::extendedbigdecimal::ExtendedBigDecimal; use uucore::format_usage; use uucore::line_ending::LineEnding; @@ -1050,11 +1050,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // nor return with a non-zero exit code in this case (we should print to stdout and return 0). // This logic is similar to the code in clap, but we return 2 as the exit code in case of real failure // (clap returns 1). - e.print().unwrap(); - if e.use_stderr() { - set_exit_code(2); - } - return Ok(()); + use uucore::clap_localization::handle_clap_error_with_exit_code; + // Always use the localization handler for proper error and help handling + handle_clap_error_with_exit_code(e, uucore::util_name(), 2); } }; @@ -1345,6 +1343,7 @@ pub fn uu_app() -> Command { .about(translate!("sort-about")) .after_help(translate!("sort-after-help")) .override_usage(format_usage(&translate!("sort-usage"))) + .help_template(uucore::localized_help_template(uucore::util_name())) .infer_long_args(true) .disable_help_flag(true) .disable_version_flag(true) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index b8351c31e..70c475782 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -26,6 +26,7 @@ use uucore::translate; use uucore::parser::parse_size::parse_size_u64; +use uucore::LocalizedCommand; use uucore::format_usage; use uucore::uio_error; @@ -51,7 +52,7 @@ static ARG_PREFIX: &str = "prefix"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (args, obs_lines) = handle_obsolete(args); - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); match Settings::from(&matches, obs_lines.as_deref()) { Ok(settings) => split(&settings), @@ -227,6 +228,7 @@ fn handle_preceding_options( pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("split-about")) .after_help(translate!("split-after-help")) .override_usage(format_usage(&translate!("split-usage"))) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 6bc0aa0b3..4d5dde949 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -27,6 +27,7 @@ use std::path::Path; use std::{env, fs}; use thiserror::Error; +use uucore::LocalizedCommand; use uucore::time::{FormatSystemTimeFallback, format_system_time, system_time_to_sec}; #[derive(Debug, Error)] @@ -1220,7 +1221,7 @@ impl Stater { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(translate!("stat-after-help")) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let stater = Stater::new(&matches)?; let exit_status = stater.exec(); @@ -1234,6 +1235,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("stat-about")) .override_usage(format_usage(&translate!("stat-usage"))) .infer_long_args(true) diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index e4ccd55e0..c2c6beab8 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -234,6 +234,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("stdbuf-about")) .after_help(translate!("stdbuf-after-help")) .override_usage(format_usage(&translate!("stdbuf-usage"))) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 5de8f9e36..69400c855 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -29,6 +29,7 @@ use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; +use uucore::LocalizedCommand; use uucore::error::{UError, UResult, USimpleError}; use uucore::format_usage; use uucore::translate; @@ -242,7 +243,7 @@ ioctl_write_ptr_bad!( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let opts = Options::from(&matches)?; @@ -1006,6 +1007,7 @@ fn get_sane_control_char(cc_index: S) -> u8 { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("stty-usage"))) .about(translate!("stty-about")) .infer_long_args(true) diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 8359ec002..2366a59d3 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -13,6 +13,7 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{format_usage, show}; fn bsd_sum(mut reader: impl Read) -> std::io::Result<(usize, u16)> { @@ -98,7 +99,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), @@ -137,6 +138,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("sum-usage"))) .about(translate!("sum-about")) .infer_long_args(true) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 95af36c80..d58463591 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -13,6 +13,7 @@ use nix::fcntl::{OFlag, open}; #[cfg(any(target_os = "linux", target_os = "android"))] use nix::sys::stat::Mode; use std::path::Path; +use uucore::LocalizedCommand; use uucore::display::Quotable; #[cfg(any(target_os = "linux", target_os = "android"))] use uucore::error::FromIo; @@ -173,7 +174,7 @@ mod platform { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files: Vec = matches .get_many::(ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) @@ -226,6 +227,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("sync-about")) .override_usage(format_usage(&translate!("sync-usage"))) .infer_long_args(true) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 52d885bf4..799e65571 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -21,6 +21,7 @@ use uucore::{format_usage, show}; use crate::error::TacError; +use uucore::LocalizedCommand; use uucore::translate; mod options { @@ -32,7 +33,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let before = matches.get_flag(options::BEFORE); let regex = matches.get_flag(options::REGEX); @@ -56,6 +57,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("tac-usage"))) .about(translate!("tac-about")) .infer_long_args(true) diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 7e2a20bcb..ef53b3943 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -461,6 +461,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("tail-about")) .override_usage(format_usage(&translate!("tail-usage"))) .infer_long_args(true) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index d7d4b6704..5c6a120e7 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -17,6 +17,7 @@ use uucore::{format_usage, show_error}; // spell-checker:ignore nopipe +use uucore::LocalizedCommand; #[cfg(unix)] use uucore::signals::{enable_pipe_errors, ignore_interrupts}; @@ -51,7 +52,7 @@ enum OutputErrorMode { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let append = matches.get_flag(options::APPEND); let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS); @@ -94,6 +95,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("tee-about")) .override_usage(format_usage(&translate!("tee-usage"))) .after_help(translate!("tee-after-help")) diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index c1a680093..c553e1f8b 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -15,6 +15,7 @@ use std::ffi::{OsStr, OsString}; use std::fs; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; @@ -35,6 +36,7 @@ pub fn uu_app() -> Command { // since we don't recognize -h and -v as help/version flags. Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("test-about")) .override_usage(format_usage(&translate!("test-usage"))) .after_help(translate!("test-after-help")) @@ -49,7 +51,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { if binary_name.ends_with('[') { // If invoked as [ we should recognize --help and --version (but not -h or -v) if args.len() == 1 && (args[0] == "--help" || args[0] == "--version") { - uu_app().get_matches_from(std::iter::once(program).chain(args.into_iter())); + uu_app().get_matches_from_localized(std::iter::once(program).chain(args.into_iter())); return Ok(()); } // If invoked via name '[', matching ']' must be in the last arg diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 755f593bf..dded6bad6 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -121,6 +121,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new("timeout") .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("timeout-about")) .override_usage(format_usage(&translate!("timeout-usage"))) .arg( diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index fde4cdc1b..451b5b560 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -20,6 +20,7 @@ use std::ffi::OsString; use std::fs::{self, File}; use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; @@ -186,7 +187,7 @@ fn shr2(s: &str) -> String { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mut filenames: Vec<&String> = matches .get_many::(ARG_FILES) @@ -254,6 +255,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("touch-about")) .override_usage(format_usage(&translate!("touch-usage"))) .infer_long_args(true) diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 5e5316dbc..709cf227b 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -13,6 +13,7 @@ use operation::{ }; use std::ffi::OsString; use std::io::{Write, stdin, stdout}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::is_stdin_directory; @@ -40,7 +41,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let delete_flag = matches.get_flag(options::DELETE); let complement_flag = matches.get_flag(options::COMPLEMENT); @@ -170,6 +171,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("tr-about")) .override_usage(format_usage(&translate!("tr-usage"))) .after_help(translate!("tr-after-help")) diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 788d23067..e19b26408 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -42,6 +42,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("true-about")) // We provide our own help and version options, to ensure maximum compatibility with GNU. .disable_help_flag(true) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index b11796cc0..0ad207b7a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -118,6 +118,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("truncate-about")) .override_usage(format_usage(&translate!("truncate-usage"))) .infer_long_args(true) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 646303bab..33822a474 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -11,6 +11,7 @@ use uucore::display::Quotable; use uucore::error::{UError, UResult}; use uucore::{format_usage, show}; +use uucore::LocalizedCommand; use uucore::translate; mod options { @@ -43,7 +44,7 @@ impl UError for TsortError {} #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let input = matches .get_one::(options::FILE) @@ -76,6 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index d5c843fcd..1ac6ed362 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -18,7 +18,10 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let matches = uu_app().try_get_matches_from(args).unwrap_or_else(|e| { + use uucore::clap_localization::handle_clap_error_with_exit_code; + handle_clap_error_with_exit_code(e, uucore::util_name(), 2) + }); let silent = matches.get_flag(options::SILENT); @@ -55,6 +58,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("tty-about")) .override_usage(format_usage(&translate!("tty-usage"))) .infer_long_args(true) diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index 092ec1a64..d23d8b7fc 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -7,6 +7,7 @@ use clap::{Arg, ArgAction, Command}; use platform_info::*; +use uucore::LocalizedCommand; use uucore::translate; use uucore::{ error::{UResult, USimpleError}, @@ -120,7 +121,7 @@ pub struct Options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let options = Options { all: matches.get_flag(options::ALL), @@ -141,6 +142,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("uname-about")) .override_usage(format_usage(&translate!("uname-usage"))) .infer_long_args(true) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index f0b8924b2..e4a3a9964 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -155,6 +155,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .override_usage(format_usage(&translate!("unexpand-usage"))) .about(translate!("unexpand-about")) .infer_long_args(true) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 1a2a3c7d9..cb9f9d198 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -592,6 +592,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("uniq-about")) .override_usage(format_usage(&translate!("uniq-usage"))) .infer_long_args(true) diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 47d6e04d0..14602cf38 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -9,6 +9,7 @@ use std::path::Path; use clap::builder::ValueParser; use clap::{Arg, Command}; +use uucore::LocalizedCommand; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; use uucore::format_usage; @@ -18,7 +19,7 @@ static OPT_PATH: &str = "FILE"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let path: &Path = matches.get_one::(OPT_PATH).unwrap().as_ref(); @@ -29,6 +30,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("unlink-about")) .override_usage(format_usage(&translate!("unlink-usage"))) .infer_long_args(true) diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 41d0f6464..6e028fbd6 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -17,6 +17,7 @@ use uucore::uptime::*; use clap::{Arg, ArgAction, Command, ValueHint, builder::ValueParser}; +use uucore::LocalizedCommand; use uucore::format_usage; #[cfg(unix)] @@ -47,7 +48,7 @@ impl UError for UptimeError { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); #[cfg(unix)] let file_path = matches.get_one::(options::PATH); @@ -71,6 +72,7 @@ pub fn uu_app() -> Command { let cmd = Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(&translate!("uptime-usage"))) .infer_long_args(true) diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 67761ca06..ca63e527e 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -16,6 +16,7 @@ use uucore::translate; #[cfg(target_os = "openbsd")] use utmp_classic::{UtmpEntry, parse_from_path}; +use uucore::LocalizedCommand; #[cfg(not(target_os = "openbsd"))] use uucore::utmpx::{self, Utmpx}; @@ -37,7 +38,7 @@ fn get_long_usage() -> String { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(get_long_usage()) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let maybe_file: Option<&Path> = matches.get_one::(ARG_FILE).map(AsRef::as_ref); @@ -89,6 +90,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(&translate!("users-usage"))) .infer_long_args(true) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 920f4602f..fa111e4e4 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -26,6 +26,7 @@ use unicode_width::UnicodeWidthChar; use utf8::{BufReadDecoder, BufReadDecoderError}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::{ error::{FromIo, UError, UResult}, format_usage, @@ -376,7 +377,7 @@ impl UError for WcError { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let settings = Settings::new(&matches); let inputs = Inputs::new(&matches)?; @@ -387,6 +388,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("wc-about")) .override_usage(format_usage(&translate!("wc-usage"))) .infer_long_args(true) diff --git a/src/uu/who/src/platform/openbsd.rs b/src/uu/who/src/platform/openbsd.rs index 8e0bbd3b9..4a2954d27 100644 --- a/src/uu/who/src/platform/openbsd.rs +++ b/src/uu/who/src/platform/openbsd.rs @@ -7,11 +7,12 @@ use crate::uu_app; +use uucore::LocalizedCommand; use uucore::error::UResult; use uucore::translate; pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from(args)?; + let _matches = uu_app().get_matches_from_localized(args); println!("{}", translate!("who-unsupported-openbsd")); Ok(()) } diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index f9f322a5d..7f7f5dc91 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -13,6 +13,7 @@ use uucore::error::{FromIo, UResult}; use uucore::libc::{S_IWGRP, STDIN_FILENO, ttyname}; use uucore::translate; +use uucore::LocalizedCommand; use uucore::utmpx::{self, Utmpx, time}; use std::borrow::Cow; @@ -28,7 +29,7 @@ fn get_long_usage() -> String { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(get_long_usage()) - .try_get_matches_from(args)?; + .get_matches_from_localized(args); let files: Vec = matches .get_many::(options::FILE) diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 1a139f20b..2a91b4cf6 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -47,6 +47,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(&translate!("who-usage"))) .infer_long_args(true) diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 928e81edf..f65dfd078 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -5,6 +5,7 @@ use clap::Command; use std::ffi::OsString; +use uucore::LocalizedCommand; use uucore::display::println_verbatim; use uucore::error::{FromIo, UResult}; use uucore::translate; @@ -13,7 +14,7 @@ mod platform; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from(args)?; + uu_app().get_matches_from_localized(args); let username = whoami()?; println_verbatim(username).map_err_context(|| translate!("whoami-error-failed-to-print"))?; Ok(()) @@ -27,6 +28,7 @@ pub fn whoami() -> UResult { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("whoami-about")) .override_usage(uucore::util_name()) .infer_long_args(true) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 6b2bc6495..028f6ba7e 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command, builder::ValueParser}; use std::error::Error; use std::ffi::OsString; use std::io::{self, Write}; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; #[cfg(unix)] @@ -21,7 +22,7 @@ const BUF_SIZE: usize = 16 * 1024; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let mut buffer = Vec::with_capacity(BUF_SIZE); args_into_buffer(&mut buffer, matches.get_many::("STRING")).unwrap(); @@ -40,6 +41,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(translate!("yes-about")) .override_usage(format_usage(&translate!("yes-usage"))) .arg( diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl new file mode 100644 index 000000000..da3dbaf3c --- /dev/null +++ b/src/uucore/locales/en-US.ftl @@ -0,0 +1,37 @@ +# Common strings shared across all uutils commands +# Mostly clap + +# Generic words +common-error = error +common-tip = tip +common-usage = Usage +common-help = help +common-version = version + +# Common clap error messages +clap-error-unexpected-argument = { $error_word }: unexpected argument '{ $arg }' found +clap-error-similar-argument = { $tip_word }: a similar argument exists: '{ $suggestion }' +clap-error-pass-as-value = { $tip_word }: to pass '{ $arg }' as a value, use '{ $tip_command }' +clap-error-invalid-value = { $error_word }: invalid value '{ $value }' for '{ $option }' +clap-error-value-required = { $error_word }: a value is required for '{ $option }' but none was supplied +clap-error-possible-values = possible values +clap-error-help-suggestion = For more information, try '{ $command } --help'. +common-help-suggestion = For more information, try '--help'. + +# Common help text patterns +help-flag-help = Print help information +help-flag-version = Print version information + +# Common error contexts +error-io = I/O error +error-permission-denied = Permission denied +error-file-not-found = No such file or directory +error-invalid-argument = Invalid argument + +# Common actions +action-copying = copying +action-moving = moving +action-removing = removing +action-creating = creating +action-reading = reading +action-writing = writing diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl new file mode 100644 index 000000000..b6c5a7bcf --- /dev/null +++ b/src/uucore/locales/fr-FR.ftl @@ -0,0 +1,37 @@ +# Chaînes communes partagées entre toutes les commandes uutils +# Principalement pour clap + +# Mots génériques +common-error = erreur +common-tip = conseil +common-usage = Utilisation +common-help = aide +common-version = version + +# Messages d'erreur clap communs +clap-error-unexpected-argument = { $error_word } : argument inattendu '{ $arg }' trouvé +clap-error-similar-argument = { $tip_word } : un argument similaire existe : '{ $suggestion }' +clap-error-pass-as-value = { $tip_word } : pour passer '{ $arg }' comme valeur, utilisez '{ $tip_command }' +clap-error-invalid-value = { $error_word } : valeur invalide '{ $value }' pour '{ $option }' +clap-error-value-required = { $error_word } : une valeur est requise pour '{ $option }' mais aucune n'a été fournie +clap-error-possible-values = valeurs possibles +clap-error-help-suggestion = Pour plus d'informations, essayez '{ $command } --help'. +common-help-suggestion = Pour plus d'informations, essayez '--help'. + +# Modèles de texte d'aide communs +help-flag-help = Afficher les informations d'aide +help-flag-version = Afficher les informations de version + +# Contextes d'erreur communs +error-io = Erreur E/S +error-permission-denied = Permission refusée +error-file-not-found = Aucun fichier ou répertoire de ce type +error-invalid-argument = Argument invalide + +# Actions communes +action-copying = copie +action-moving = déplacement +action-removing = suppression +action-creating = création +action-reading = lecture +action-writing = écriture diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 72fb0b27c..bddce9cf2 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -11,16 +11,17 @@ use crate::display::Quotable; use crate::error::{UResult, USimpleError, strip_errno}; pub use crate::features::entries; use crate::show_error; + use clap::{Arg, ArgMatches, Command}; + use libc::{gid_t, uid_t}; use options::traverse; use walkdir::WalkDir; -use std::io::Error as IOError; -use std::io::Result as IOResult; - use std::ffi::CString; use std::fs::Metadata; +use std::io::Error as IOError; +use std::io::Result as IOResult; use std::os::unix::fs::MetadataExt; use std::os::unix::ffi::OsStrExt; diff --git a/src/uucore/src/lib/features/proc_info.rs b/src/uucore/src/lib/features/proc_info.rs index 3f5334d10..8345e7e09 100644 --- a/src/uucore/src/lib/features/proc_info.rs +++ b/src/uucore/src/lib/features/proc_info.rs @@ -30,6 +30,7 @@ #![allow(dead_code)] use crate::features::tty::Teletype; + use std::hash::Hash; use std::{ collections::HashMap, @@ -38,6 +39,7 @@ use std::{ path::PathBuf, rc::Rc, }; + use walkdir::{DirEntry, WalkDir}; /// State or process diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index ab28d657b..af0e039d0 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -5,7 +5,7 @@ //! library ~ (core/bundler file) // #![deny(missing_docs)] //TODO: enable this // -// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal +// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil // * feature-gated external crates (re-shared as public internal modules) #[cfg(feature = "libc")] @@ -22,6 +22,8 @@ mod mods; // core cross-platform modules pub use uucore_procs::*; // * cross-platform modules +pub use crate::mods::clap_localization; +pub use crate::mods::clap_localization::LocalizedCommand; pub use crate::mods::display; pub use crate::mods::error; #[cfg(feature = "fs")] @@ -226,6 +228,41 @@ pub fn format_usage(s: &str) -> String { s.replace("{}", crate::execution_phrase()) } +/// Creates a localized help template for clap commands. +/// +/// This function returns a help template that uses the localized +/// "Usage:" label from the translation files. This ensures consistent +/// localization across all utilities. +/// +/// Note: We avoid using clap's `{usage-heading}` placeholder because it is +/// hardcoded to "Usage:" and cannot be localized. Instead, we manually +/// construct the usage line with the localized label. +/// +/// # Parameters +/// - `util_name`: The name of the utility (for localization setup) +/// +/// # Example +/// ```no_run +/// use clap::Command; +/// use uucore::localized_help_template; +/// +/// let app = Command::new("myutil") +/// .help_template(localized_help_template("myutil")); +/// ``` +pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { + // Ensure localization is initialized for this utility + let _ = crate::locale::setup_localization(util_name); + + let usage_label = crate::locale::translate!("common-usage"); + + // Create a template that avoids clap's hardcoded {usage-heading} + let template = format!( + "{{before-help}}{{about-with-newline}}\n{usage_label}: {{usage}}\n\n{{all-args}}{{after-help}}" + ); + + clap::builder::StyledStr::from(template) +} + /// Used to check if the utility is the second argument. /// Used to check if we were called as a multicall binary (`coreutils `) pub fn get_utility_is_second_arg() -> bool { diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 7af54ff5a..e33bf0319 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // mods ~ cross-platforms modules (core/bundler file) +pub mod clap_localization; pub mod display; pub mod error; #[cfg(feature = "fs")] diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs new file mode 100644 index 000000000..c2b665830 --- /dev/null +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -0,0 +1,388 @@ +// 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 (path) osrelease + +//! Helper clap functions to localize error handling and options +//! +//! This module provides utilities for handling clap errors with localization support. +//! It uses clap's error context API to extract structured information from errors +//! instead of parsing error strings, providing a more robust solution. +//! + +use crate::locale::translate; + +use clap::error::{ContextKind, ErrorKind}; +use clap::{ArgMatches, Command, Error}; + +use std::error::Error as StdError; +use std::ffi::OsString; + +/// Determines if a clap error should show simple help instead of full usage +/// Based on clap's own design patterns and error categorization +fn should_show_simple_help_for_clap_error(kind: ErrorKind) -> bool { + match kind { + // Show simple help + ErrorKind::InvalidValue + | ErrorKind::InvalidSubcommand + | ErrorKind::ValueValidation + | ErrorKind::InvalidUtf8 + | ErrorKind::ArgumentConflict + | ErrorKind::NoEquals => true, + + // Argument count and structural errors need special formatting + ErrorKind::TooFewValues + | ErrorKind::TooManyValues + | ErrorKind::WrongNumberOfValues + | ErrorKind::MissingSubcommand => false, + + // MissingRequiredArgument needs different handling + ErrorKind::MissingRequiredArgument => false, + + // Special cases - handle their own display + ErrorKind::DisplayHelp + | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand + | ErrorKind::DisplayVersion => false, + + // UnknownArgument gets special handling elsewhere, so mark as false here + ErrorKind::UnknownArgument => false, + + // System errors - keep simple + ErrorKind::Io | ErrorKind::Format => true, + + // Default for any new ErrorKind variants - be conservative and show simple help + _ => true, + } +} + +/// Color enum for consistent styling +#[derive(Debug, Clone, Copy)] +pub enum Color { + Red, + Yellow, + Green, +} + +impl Color { + fn code(self) -> &'static str { + match self { + Color::Red => "31", + Color::Yellow => "33", + Color::Green => "32", + } + } +} + +/// Apply color to text using ANSI escape codes +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() + } + }; + + 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 + let usage_label = translate!("common-usage"); + let localized_help = help_text.replace("Usage:", &format!("{usage_label}:")); + + print!("{}", localized_help); + std::process::exit(0); + } + ErrorKind::DisplayVersion => { + // For version, use clap's built-in formatting and exit with 0 + // Output to stdout as expected by tests + 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, + }; + + // 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(); + + // 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 + 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}"); + 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!(); + } + } + } + + // 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")); + + 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); + } + } + // 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); + } + } + + // For other simple validation errors, use the same simple format as other errors + let lines: Vec<&str> = rendered_str.lines().collect(); + if let Some(main_error_line) = lines.first() { + // Keep the "error: " prefix for test compatibility + eprintln!("{}", main_error_line); + eprintln!(); + // Use the execution phrase for the help suggestion to match test expectations + eprintln!("{}", translate!("common-help-suggestion")); + } else { + // Fallback to original rendering if we can't parse + eprint!("{}", err.render()); + } + + // InvalidValue errors should exit with code 1 for all utilities + let actual_exit_code = if matches!(kind, ErrorKind::InvalidValue) { + 1 + } else { + exit_code + }; + + std::process::exit(actual_exit_code); + } + _ => { + // For MissingRequiredArgument, use the full clap error as it includes proper usage + if matches!(err.kind(), ErrorKind::MissingRequiredArgument) { + eprint!("{}", err.render()); + std::process::exit(exit_code); + } + + // For TooFewValues and similar structural errors, use the full clap error + if matches!( + err.kind(), + ErrorKind::TooFewValues | ErrorKind::TooManyValues | ErrorKind::WrongNumberOfValues + ) { + eprint!("{}", err.render()); + std::process::exit(exit_code); + } + + // For other errors, show just the error and help suggestion + let rendered_str = err.render().to_string(); + let lines: Vec<&str> = rendered_str.lines().collect(); + + // Print error message (first line) + if let Some(first_line) = lines.first() { + eprintln!("{}", first_line); + } + + // For other errors, just show help suggestion + eprintln!(); + eprintln!("{}", translate!("common-help-suggestion")); + + std::process::exit(exit_code); + } + } +} + +/// Trait extension to provide localized clap error handling +/// This provides a cleaner API than wrapping with macros +pub trait LocalizedCommand { + /// Get matches with localized error handling + fn get_matches_localized(self) -> ArgMatches + where + Self: Sized; + + /// Get matches from args with localized error handling + fn get_matches_from_localized(self, itr: I) -> ArgMatches + where + Self: Sized, + I: IntoIterator, + T: Into + Clone; + + /// Get matches from mutable args with localized error handling + fn get_matches_from_mut_localized(self, itr: I) -> ArgMatches + where + Self: Sized, + I: IntoIterator, + T: Into + Clone; +} + +impl LocalizedCommand for Command { + fn get_matches_localized(self) -> ArgMatches { + self.try_get_matches() + .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) + } + + fn get_matches_from_localized(self, itr: I) -> ArgMatches + where + I: IntoIterator, + T: Into + Clone, + { + self.try_get_matches_from(itr) + .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) + } + + fn get_matches_from_mut_localized(mut self, itr: I) -> ArgMatches + where + I: IntoIterator, + T: Into + Clone, + { + self.try_get_matches_from_mut(itr) + .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) + } +} diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 7b86af32c..58a45dc0e 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -5,12 +5,15 @@ // spell-checker:disable use crate::error::UError; + use fluent::{FluentArgs, FluentBundle, FluentResource}; use fluent_syntax::parser::ParserError; + use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::OnceLock; + use thiserror::Error; use unic_langid::LanguageIdentifier; @@ -107,7 +110,68 @@ thread_local! { static LOCALIZER: OnceLock = const { OnceLock::new() }; } -/// Initialize localization with a specific locale and config +/// 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 + let normalized_dir = utility_locales_dir + .canonicalize() + .unwrap_or_else(|_| utility_locales_dir.to_path_buf()); + + // Walk up: locales -> printenv -> uu -> src + let uucore_locales = normalized_dir + .parent()? // printenv + .parent()? // uu + .parent()? // src + .join("uucore") + .join("locales"); + + // Only return if the directory actually exists + uucore_locales.exists().then_some(uucore_locales) +} + +/// Create a bundle that combines common and utility-specific strings +fn create_bundle( + locale: &LanguageIdentifier, + locales_dir: &Path, + util_name: &str, +) -> Result, LocalizationError> { + let mut bundle = FluentBundle::new(vec![locale.clone()]); + + // Disable Unicode directional isolate characters + bundle.set_use_isolating(false); + + // Load common strings from uucore locales directory + if let Some(common_dir) = find_uucore_locales_dir(locales_dir) { + let common_locale_path = common_dir.join(format!("{locale}.ftl")); + if let Ok(common_ftl) = fs::read_to_string(&common_locale_path) { + if let Ok(common_resource) = FluentResource::try_new(common_ftl) { + bundle.add_resource_overriding(common_resource); + } + } + } + + // Then, try to load utility-specific strings from the utility's locale directory + let util_locales_dir = get_locales_dir(util_name).ok(); + if let Some(util_dir) = util_locales_dir { + let util_locale_path = util_dir.join(format!("{locale}.ftl")); + if let Ok(util_ftl) = fs::read_to_string(&util_locale_path) { + if let Ok(util_resource) = FluentResource::try_new(util_ftl) { + bundle.add_resource_overriding(util_resource); + } + } + } + + // If we have at least one resource, return the bundle + if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) { + Ok(bundle) + } else { + Err(LocalizationError::LocalesDirNotFound(format!( + "No localization strings found for {locale} and utility {util_name}" + ))) + } +} + +/// Initialize localization with common strings in addition to utility-specific strings fn init_localization( locale: &LanguageIdentifier, locales_dir: &Path, @@ -116,22 +180,18 @@ fn init_localization( let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); - // Try to load English from embedded resources first, then fall back to filesystem. - // This ensures consistent behavior and faster loading since embedded resources - // are immediately available. The filesystem fallback allows for development - // and testing scenarios where locale files might be present in the filesystem. - let english_bundle = - create_english_bundle_from_embedded(&default_locale, util_name).or_else(|_| { - // Try filesystem as fallback (useful for development/testing) - create_bundle(&default_locale, locales_dir) - })?; + // Try to create a bundle that combines common and utility-specific strings + let english_bundle = create_bundle(&default_locale, locales_dir, util_name).or_else(|_| { + // Fallback to embedded utility-specific and common strings + create_english_bundle_from_embedded(&default_locale, util_name) + })?; let loc = if locale == &default_locale { // If requesting English, just use English as primary (no fallback needed) Localizer::new(english_bundle) } else { - // Try to load the requested locale - if let Ok(primary_bundle) = create_bundle(locale, locales_dir) { + // Try to load the requested locale with common strings + if let Ok(primary_bundle) = create_bundle(locale, locales_dir, util_name) { // Successfully loaded requested locale, load English as fallback Localizer::new(primary_bundle).with_fallback(english_bundle) } else { @@ -147,53 +207,29 @@ fn init_localization( Ok(()) } -/// Create a bundle for a specific locale -fn create_bundle( - locale: &LanguageIdentifier, - locales_dir: &Path, -) -> Result, LocalizationError> { - let locale_path = locales_dir.join(format!("{locale}.ftl")); - - let ftl_file = fs::read_to_string(&locale_path).map_err(|e| LocalizationError::Io { - source: e, - path: locale_path.clone(), - })?; - - let resource = FluentResource::try_new(ftl_file.clone()).map_err( - |(_partial_resource, mut errs): (FluentResource, Vec)| { - let first_err = errs.remove(0); - // Attempt to extract the snippet from the original ftl_file - let snippet = if let Some(range) = first_err.slice.clone() { - ftl_file.get(range).unwrap_or("").to_string() +/// Helper function to parse FluentResource from content string +fn parse_fluent_resource(content: &str) -> Result { + FluentResource::try_new(content.to_string()).map_err( + |(_partial_resource, errs): (FluentResource, Vec)| { + if let Some(first_err) = errs.into_iter().next() { + let snippet = first_err + .slice + .clone() + .and_then(|range| content.get(range)) + .unwrap_or("") + .to_string(); + LocalizationError::ParseResource { + error: first_err, + snippet, + } } else { - String::new() - }; - LocalizationError::ParseResource { - error: first_err, - snippet, + LocalizationError::LocalesDirNotFound("Parse error without details".to_string()) } }, - )?; - - let mut bundle = FluentBundle::new(vec![locale.clone()]); - - // Disable Unicode directional isolate characters (U+2068, U+2069) - // By default, Fluent wraps variables for security - // and proper text rendering in mixed-script environments (Arabic + English). - // Disabling gives cleaner output: "Welcome, Alice!" but reduces protection - // against bidirectional text attacks. Safe for English-only applications. - bundle.set_use_isolating(false); - - bundle.add_resource(resource).map_err(|errs| { - LocalizationError::Bundle(format!( - "Failed to add resource to bundle for {locale}: {errs:?}", - )) - })?; - - Ok(bundle) + ) } -/// Create a bundle from embedded English locale files +/// Create a bundle from embedded English locale files with common uucore strings fn create_english_bundle_from_embedded( locale: &LanguageIdentifier, util_name: &str, @@ -206,41 +242,31 @@ fn create_english_bundle_from_embedded( } let embedded_locales = get_embedded_locales(); - let locale_key = format!("{util_name}/en-US.ftl"); - - let ftl_content = embedded_locales.get(locale_key.as_str()).ok_or_else(|| { - LocalizationError::LocalesDirNotFound(format!("No embedded locale found for {util_name}")) - })?; - - let resource = FluentResource::try_new(ftl_content.to_string()).map_err( - |(_partial_resource, errs): (FluentResource, Vec)| { - if let Some(first_err) = errs.into_iter().next() { - let snippet = first_err - .slice - .clone() - .and_then(|range| ftl_content.get(range)) - .unwrap_or("") - .to_string(); - LocalizationError::ParseResource { - error: first_err, - snippet, - } - } else { - LocalizationError::LocalesDirNotFound("Parse error without details".to_string()) - } - }, - )?; - let mut bundle = FluentBundle::new(vec![locale.clone()]); bundle.set_use_isolating(false); - bundle.add_resource(resource).map_err(|errs| { - LocalizationError::Bundle(format!( - "Failed to add embedded resource to bundle for {locale}: {errs:?}", - )) - })?; + // First, try to load common uucore strings + let uucore_key = "uucore/en-US.ftl"; + if let Some(uucore_content) = embedded_locales.get(uucore_key) { + let uucore_resource = parse_fluent_resource(uucore_content)?; + bundle.add_resource_overriding(uucore_resource); + } - Ok(bundle) + // Then, try to load utility-specific strings + let locale_key = format!("{util_name}/en-US.ftl"); + if let Some(ftl_content) = embedded_locales.get(locale_key.as_str()) { + let resource = parse_fluent_resource(ftl_content)?; + bundle.add_resource_overriding(resource); + } + + // Return the bundle if we have either common strings or utility-specific strings + if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) { + Ok(bundle) + } else { + Err(LocalizationError::LocalesDirNotFound(format!( + "No embedded locale found for {util_name} and no common strings found" + ))) + } } fn get_message_internal(id: &str, args: Option) -> String { @@ -328,6 +354,7 @@ fn detect_system_locale() -> Result { } /// Sets up localization using the system locale with English fallback. +/// Always loads common strings in addition to utility-specific strings. /// /// This function initializes the localization system based on the system's locale /// preferences (via the LANG environment variable) or falls back to English @@ -368,13 +395,14 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") }); - // Try to find the locales directory. If found, use init_localization which - // will prioritize embedded resources but can also load from filesystem. - // If no locales directory exists, directly use embedded English resources. + // Load common strings along with utility-specific strings match get_locales_dir(p) { - Ok(locales_dir) => init_localization(&locale, &locales_dir, p), + Ok(locales_dir) => { + // Load both utility-specific and common strings + init_localization(&locale, &locales_dir, p) + } Err(_) => { - // No locales directory found, use embedded English directly + // No locales directory found, use embedded English with common strings directly let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?; @@ -537,6 +565,62 @@ mod tests { use std::path::PathBuf; use tempfile::TempDir; + /// Test-specific helper function to create a bundle from test directory only + #[cfg(test)] + fn create_test_bundle( + locale: &LanguageIdentifier, + test_locales_dir: &Path, + ) -> Result, LocalizationError> { + let mut bundle = FluentBundle::new(vec![locale.clone()]); + bundle.set_use_isolating(false); + + // Only load from the test directory - no common strings or utility-specific paths + let locale_path = test_locales_dir.join(format!("{locale}.ftl")); + if let Ok(ftl_content) = fs::read_to_string(&locale_path) { + let resource = parse_fluent_resource(&ftl_content)?; + bundle.add_resource_overriding(resource); + return Ok(bundle); + } + + Err(LocalizationError::LocalesDirNotFound(format!( + "No localization strings found for {locale} in {}", + test_locales_dir.display() + ))) + } + + /// Test-specific initialization function for test directories + #[cfg(test)] + fn init_test_localization( + locale: &LanguageIdentifier, + test_locales_dir: &Path, + ) -> Result<(), LocalizationError> { + let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) + .expect("Default locale should always be valid"); + + // Create English bundle from test directory + let english_bundle = create_test_bundle(&default_locale, test_locales_dir)?; + + let loc = if locale == &default_locale { + // If requesting English, just use English as primary + Localizer::new(english_bundle) + } else { + // Try to load the requested locale from test directory + if let Ok(primary_bundle) = create_test_bundle(locale, test_locales_dir) { + // Successfully loaded requested locale, load English as fallback + Localizer::new(primary_bundle).with_fallback(english_bundle) + } else { + // Failed to load requested locale, just use English as primary + Localizer::new(english_bundle) + } + }; + + LOCALIZER.with(|lock| { + lock.set(loc) + .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) + })?; + Ok(()) + } + /// Helper function to create a temporary directory with test locale files fn create_test_locales_dir() -> TempDir { let temp_dir = TempDir::new().expect("Failed to create temp directory"); @@ -602,31 +686,12 @@ invalid-syntax = This is { $missing temp_dir } - #[test] - fn test_localization_error_from_io_error() { - let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); - let loc_error = LocalizationError::from(io_error); - - match loc_error { - LocalizationError::Io { source: _, path } => { - assert_eq!(path, PathBuf::from("")); - } - _ => panic!("Expected IO error variant"), - } - } - - #[test] - fn test_localization_error_uerror_impl() { - let error = LocalizationError::Bundle("some error".to_string()); - assert_eq!(error.code(), 1); - } - #[test] fn test_create_bundle_success() { let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("en-US").unwrap(); - let result = create_bundle(&locale, temp_dir.path()); + let result = create_test_bundle(&locale, temp_dir.path()); assert!(result.is_ok()); let bundle = result.unwrap(); @@ -638,13 +703,13 @@ invalid-syntax = This is { $missing let temp_dir = TempDir::new().unwrap(); let locale = LanguageIdentifier::from_str("de-DE").unwrap(); - let result = create_bundle(&locale, temp_dir.path()); + let result = create_test_bundle(&locale, temp_dir.path()); assert!(result.is_err()); - if let Err(LocalizationError::Io { source: _, path }) = result { - assert!(path.to_string_lossy().contains("de-DE.ftl")); + if let Err(LocalizationError::LocalesDirNotFound(_)) = result { + // Expected - no localization strings found } else { - panic!("Expected IO error"); + panic!("Expected LocalesDirNotFound error"); } } @@ -653,24 +718,29 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("es-ES").unwrap(); - let result = create_bundle(&locale, temp_dir.path()); - assert!(result.is_err()); + let result = create_test_bundle(&locale, temp_dir.path()); - if let Err(LocalizationError::ParseResource { - error: _parser_err, - snippet: _, - }) = result - { - // Expected ParseResource variant - } else { - panic!("Expected ParseResource error"); + // The result should be an error due to invalid syntax + match result { + Err(LocalizationError::ParseResource { + error: _parser_err, + snippet: _, + }) => { + // Expected ParseResource variant - test passes + } + Ok(_) => { + panic!("Expected ParseResource error, but bundle was created successfully"); + } + Err(other) => { + panic!("Expected ParseResource error, but got: {other:?}"); + } } } #[test] fn test_localizer_format_primary_bundle() { let temp_dir = create_test_locales_dir(); - let en_bundle = create_bundle( + let en_bundle = create_test_bundle( &LanguageIdentifier::from_str("en-US").unwrap(), temp_dir.path(), ) @@ -685,7 +755,7 @@ invalid-syntax = This is { $missing fn test_localizer_format_with_args() { use fluent::FluentArgs; let temp_dir = create_test_locales_dir(); - let en_bundle = create_bundle( + let en_bundle = create_test_bundle( &LanguageIdentifier::from_str("en-US").unwrap(), temp_dir.path(), ) @@ -702,12 +772,12 @@ invalid-syntax = This is { $missing #[test] fn test_localizer_fallback_to_english() { let temp_dir = create_test_locales_dir(); - let fr_bundle = create_bundle( + let fr_bundle = create_test_bundle( &LanguageIdentifier::from_str("fr-FR").unwrap(), temp_dir.path(), ) .unwrap(); - let en_bundle = create_bundle( + let en_bundle = create_test_bundle( &LanguageIdentifier::from_str("en-US").unwrap(), temp_dir.path(), ) @@ -727,7 +797,7 @@ invalid-syntax = This is { $missing #[test] fn test_localizer_format_message_not_found() { let temp_dir = create_test_locales_dir(); - let en_bundle = create_bundle( + let en_bundle = create_test_bundle( &LanguageIdentifier::from_str("en-US").unwrap(), temp_dir.path(), ) @@ -745,10 +815,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("en-US").unwrap(); - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); - if let Err(e) = &result { - eprintln!("Init localization failed: {e}"); - } + let result = init_test_localization(&locale, temp_dir.path()); assert!(result.is_ok()); // Test that we can get messages @@ -765,7 +832,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); + let result = init_test_localization(&locale, temp_dir.path()); assert!(result.is_ok()); // Test French message @@ -786,7 +853,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); + let result = init_test_localization(&locale, temp_dir.path()); assert!(result.is_ok()); // Should use English as primary since German failed to load @@ -804,11 +871,11 @@ invalid-syntax = This is { $missing let locale = LanguageIdentifier::from_str("en-US").unwrap(); // Initialize once - let result1 = init_localization(&locale, temp_dir.path(), "test"); + let result1 = init_test_localization(&locale, temp_dir.path()); assert!(result1.is_ok()); // Try to initialize again - should fail - let result2 = init_localization(&locale, temp_dir.path(), "test"); + let result2 = init_test_localization(&locale, temp_dir.path()); assert!(result2.is_err()); match result2 { @@ -828,7 +895,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); - init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap(); + init_test_localization(&locale, temp_dir.path()).unwrap(); let message = get_message("greeting"); assert_eq!(message, "Bonjour, le monde!"); @@ -837,16 +904,6 @@ invalid-syntax = This is { $missing .unwrap(); } - #[test] - fn test_get_message_not_initialized() { - std::thread::spawn(|| { - let message = get_message("greeting"); - assert_eq!(message, "greeting"); // Should return the ID itself - }) - .join() - .unwrap(); - } - #[test] fn test_get_message_with_args() { use fluent::FluentArgs; @@ -854,7 +911,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("en-US").unwrap(); - init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap(); + init_test_localization(&locale, temp_dir.path()).unwrap(); let mut args = FluentArgs::new(); args.set("name".to_string(), "Bob".to_string()); @@ -873,7 +930,7 @@ invalid-syntax = This is { $missing let temp_dir = create_test_locales_dir(); let locale = LanguageIdentifier::from_str("en-US").unwrap(); - init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap(); + init_test_localization(&locale, temp_dir.path()).unwrap(); // Test singular let mut args1 = FluentArgs::new(); @@ -891,6 +948,269 @@ invalid-syntax = This is { $missing .unwrap(); } + #[test] + fn test_thread_local_isolation() { + use std::thread; + + let temp_dir = create_test_locales_dir(); + + // Initialize in main thread with French + let temp_path_main = temp_dir.path().to_path_buf(); + let main_handle = thread::spawn(move || { + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + init_test_localization(&locale, &temp_path_main).unwrap(); + let main_message = get_message("greeting"); + assert_eq!(main_message, "Bonjour, le monde!"); + }); + main_handle.join().unwrap(); + + // Test in a different thread - should not be initialized + let temp_path = temp_dir.path().to_path_buf(); + let handle = thread::spawn(move || { + // This thread should have its own uninitialized LOCALIZER + let thread_message = get_message("greeting"); + assert_eq!(thread_message, "greeting"); // Returns ID since not initialized + + // Initialize in this thread with English + let en_locale = LanguageIdentifier::from_str("en-US").unwrap(); + init_test_localization(&en_locale, &temp_path).unwrap(); + let thread_message_after_init = get_message("greeting"); + assert_eq!(thread_message_after_init, "Hello, world!"); + }); + + handle.join().unwrap(); + + // Test another thread to verify French doesn't persist across threads + let final_handle = thread::spawn(move || { + // Should be uninitialized again + let final_message = get_message("greeting"); + assert_eq!(final_message, "greeting"); + }); + final_handle.join().unwrap(); + } + + #[test] + fn test_japanese_localization() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ja-JP").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Japanese greeting + let message = get_message("greeting"); + assert_eq!(message, "こんにちは、世界!"); + + // Test Japanese with arguments + let mut args = FluentArgs::new(); + args.set("name".to_string(), "田中".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "ようこそ、田中さん!"); + + // Test Japanese count (no pluralization) + let mut count_args = FluentArgs::new(); + count_args.set("count".to_string(), "5".to_string()); + let count_message = get_message_with_args("count-items", count_args); + assert_eq!(count_message, "5個のアイテムがあります"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_arabic_localization() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic greeting (RTL text) + let message = get_message("greeting"); + assert_eq!(message, "أهلاً بالعالم!"); + + // Test Arabic with arguments + let mut args = FluentArgs::new(); + args.set("name", "أحمد".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); + + // Test Arabic pluralization (zero case) + let mut args_zero = FluentArgs::new(); + args_zero.set("count", 0); + let message_zero = get_message_with_args("count-items", args_zero); + assert_eq!(message_zero, "لديك لا عناصر"); + + // Test Arabic pluralization (one case) + let mut args_one = FluentArgs::new(); + args_one.set("count", 1); + let message_one = get_message_with_args("count-items", args_one); + assert_eq!(message_one, "لديك عنصر واحد"); + + // Test Arabic pluralization (two case) + let mut args_two = FluentArgs::new(); + args_two.set("count", 2); + let message_two = get_message_with_args("count-items", args_two); + assert_eq!(message_two, "لديك عنصران"); + + // Test Arabic pluralization (few case - 3-10) + let mut args_few = FluentArgs::new(); + args_few.set("count", 5); + let message_few = get_message_with_args("count-items", args_few); + assert_eq!(message_few, "لديك 5 عناصر"); + + // Test Arabic pluralization (other case - 11+) + let mut args_many = FluentArgs::new(); + args_many.set("count", 15); + let message_many = get_message_with_args("count-items", args_many); + assert_eq!(message_many, "لديك 15 عنصر"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_arabic_localization_with_macro() { + std::thread::spawn(|| { + use self::translate; + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic greeting (RTL text) + let message = translate!("greeting"); + assert_eq!(message, "أهلاً بالعالم!"); + + // Test Arabic with arguments + let welcome = translate!("welcome", "name" => "أحمد"); + assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); + + // Test Arabic pluralization (zero case) + let message_zero = translate!("count-items", "count" => 0); + assert_eq!(message_zero, "لديك لا عناصر"); + + // Test Arabic pluralization (one case) + let message_one = translate!("count-items", "count" => 1); + assert_eq!(message_one, "لديك عنصر واحد"); + + // Test Arabic pluralization (two case) + let message_two = translate!("count-items", "count" => 2); + assert_eq!(message_two, "لديك عنصران"); + + // Test Arabic pluralization (few case - 3-10) + let message_few = translate!("count-items", "count" => 5); + assert_eq!(message_few, "لديك 5 عناصر"); + + // Test Arabic pluralization (other case - 11+) + let message_many = translate!("count-items", "count" => 15); + assert_eq!(message_many, "لديك 15 عنصر"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_mixed_script_fallback() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic message exists + let arabic_message = get_message("greeting"); + assert_eq!(arabic_message, "أهلاً بالعالم!"); + + // Test fallback to English for missing message + let fallback_message = get_message("missing-in-other"); + assert_eq!(fallback_message, "This message only exists in English"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_unicode_directional_isolation_disabled() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + init_test_localization(&locale, temp_dir.path()).unwrap(); + + // Test that Latin script names are NOT isolated in RTL context + // since we disabled Unicode directional isolation + let mut args = FluentArgs::new(); + args.set("name".to_string(), "John Smith".to_string()); + let message = get_message_with_args("welcome", args); + + // The Latin name should NOT be wrapped in directional isolate characters + assert!(!message.contains("\u{2068}John Smith\u{2069}")); + assert_eq!(message, "أهلاً وسهلاً، John Smith!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_parse_resource_error_includes_snippet() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("es-ES").unwrap(); + + let result = create_test_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); + + if let Err(LocalizationError::ParseResource { + error: _err, + snippet, + }) = result + { + // The snippet should contain exactly the invalid text from es-ES.ftl + assert!( + snippet.contains("This is { $missing"), + "snippet was `{snippet}` but did not include the invalid text" + ); + } else { + panic!("Expected LocalizationError::ParseResource with snippet"); + } + } + + #[test] + fn test_localization_error_from_io_error() { + let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); + let loc_error = LocalizationError::from(io_error); + + match loc_error { + LocalizationError::Io { source: _, path } => { + assert_eq!(path, PathBuf::from("")); + } + _ => panic!("Expected IO error variant"), + } + } + + #[test] + fn test_localization_error_uerror_impl() { + let error = LocalizationError::Bundle("some error".to_string()); + assert_eq!(error.code(), 1); + } + + #[test] + fn test_get_message_not_initialized() { + std::thread::spawn(|| { + let message = get_message("greeting"); + assert_eq!(message, "greeting"); // Should return the ID itself + }) + .join() + .unwrap(); + } + #[test] fn test_detect_system_locale_from_lang_env() { // Test locale parsing logic directly instead of relying on environment variables @@ -939,20 +1259,19 @@ invalid-syntax = This is { $missing #[test] fn test_setup_localization_success() { std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - // Save current LANG value let original_lang = env::var("LANG").ok(); unsafe { - env::set_var("LANG", "fr-FR.UTF-8"); + env::set_var("LANG", "en-US.UTF-8"); // Use English since we have embedded resources for "test" } - let result = setup_localization(temp_dir.path().to_str().unwrap()); + let result = setup_localization("test"); assert!(result.is_ok()); - // Test that French is loaded - let message = get_message("greeting"); - assert_eq!(message, "Bonjour, le monde!"); + // Test that we can get messages (should use embedded English for "test" utility) + let message = get_message("test-about"); + // Since we're using embedded resources, we should get the expected message + assert!(!message.is_empty()); // Restore original LANG value if let Some(val) = original_lang { @@ -972,20 +1291,18 @@ invalid-syntax = This is { $missing #[test] fn test_setup_localization_falls_back_to_english() { std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - // Save current LANG value let original_lang = env::var("LANG").ok(); unsafe { - env::set_var("LANG", "de-DE.UTF-8"); - } // German file doesn't exist + env::set_var("LANG", "de-DE.UTF-8"); // German file doesn't exist, should fallback + } - let result = setup_localization(temp_dir.path().to_str().unwrap()); + let result = setup_localization("test"); assert!(result.is_ok()); - // Should fall back to English - let message = get_message("greeting"); - assert_eq!(message, "Hello, world!"); + // Should fall back to English embedded resources + let message = get_message("test-about"); + assert!(!message.is_empty()); // Should get something, not just the key // Restore original LANG value if let Some(val) = original_lang { @@ -1026,218 +1343,6 @@ invalid-syntax = This is { $missing .unwrap(); } - #[test] - fn test_thread_local_isolation() { - use std::thread; - - let temp_dir = create_test_locales_dir(); - - // Initialize in main thread with French - let temp_path_main = temp_dir.path().to_path_buf(); - let main_handle = thread::spawn(move || { - let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); - init_localization(&locale, &temp_path_main, "nonexistent_test_util").unwrap(); - let main_message = get_message("greeting"); - assert_eq!(main_message, "Bonjour, le monde!"); - }); - main_handle.join().unwrap(); - - // Test in a different thread - should not be initialized - let temp_path = temp_dir.path().to_path_buf(); - let handle = thread::spawn(move || { - // This thread should have its own uninitialized LOCALIZER - let thread_message = get_message("greeting"); - assert_eq!(thread_message, "greeting"); // Returns ID since not initialized - - // Initialize in this thread with English - let en_locale = LanguageIdentifier::from_str("en-US").unwrap(); - init_localization(&en_locale, &temp_path, "nonexistent_test_util").unwrap(); - let thread_message_after_init = get_message("greeting"); - assert_eq!(thread_message_after_init, "Hello, world!"); - }); - - handle.join().unwrap(); - - // Test another thread to verify French doesn't persist across threads - let final_handle = thread::spawn(move || { - // Should be uninitialized again - let final_message = get_message("greeting"); - assert_eq!(final_message, "greeting"); - }); - final_handle.join().unwrap(); - } - - #[test] - fn test_japanese_localization() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ja-JP").unwrap(); - - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); - assert!(result.is_ok()); - - // Test Japanese greeting - let message = get_message("greeting"); - assert_eq!(message, "こんにちは、世界!"); - - // Test Japanese with arguments - let mut args = FluentArgs::new(); - args.set("name".to_string(), "田中".to_string()); - let welcome = get_message_with_args("welcome", args); - assert_eq!(welcome, "ようこそ、田中さん!"); - - // Test Japanese count (no pluralization) - let mut count_args = FluentArgs::new(); - count_args.set("count".to_string(), "5".to_string()); - let count_message = get_message_with_args("count-items", count_args); - assert_eq!(count_message, "5個のアイテムがあります"); - }) - .join() - .unwrap(); - } - - #[test] - fn test_arabic_localization() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); - assert!(result.is_ok()); - - // Test Arabic greeting (RTL text) - let message = get_message("greeting"); - assert_eq!(message, "أهلاً بالعالم!"); - - // Test Arabic with arguments - let mut args = FluentArgs::new(); - args.set("name", "أحمد".to_string()); - let welcome = get_message_with_args("welcome", args); - - assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); - - // Test Arabic pluralization (zero case) - let mut args_zero = FluentArgs::new(); - args_zero.set("count", 0); - let message_zero = get_message_with_args("count-items", args_zero); - assert_eq!(message_zero, "لديك لا عناصر"); - - // Test Arabic pluralization (one case) - let mut args_one = FluentArgs::new(); - args_one.set("count", 1); - let message_one = get_message_with_args("count-items", args_one); - assert_eq!(message_one, "لديك عنصر واحد"); - - // Test Arabic pluralization (two case) - let mut args_two = FluentArgs::new(); - args_two.set("count", 2); - let message_two = get_message_with_args("count-items", args_two); - assert_eq!(message_two, "لديك عنصران"); - - // Test Arabic pluralization (few case - 3-10) - let mut args_few = FluentArgs::new(); - args_few.set("count", 5); - let message_few = get_message_with_args("count-items", args_few); - assert_eq!(message_few, "لديك 5 عناصر"); - - // Test Arabic pluralization (other case - 11+) - let mut args_many = FluentArgs::new(); - args_many.set("count", 15); - let message_many = get_message_with_args("count-items", args_many); - assert_eq!(message_many, "لديك 15 عنصر"); - }) - .join() - .unwrap(); - } - - #[test] - fn test_arabic_localization_with_macro() { - std::thread::spawn(|| { - use self::translate; - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); - assert!(result.is_ok()); - - // Test Arabic greeting (RTL text) - let message = translate!("greeting"); - assert_eq!(message, "أهلاً بالعالم!"); - - // Test Arabic with arguments - let welcome = translate!("welcome", "name" => "أحمد"); - assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); - - // Test Arabic pluralization (zero case) - let message_zero = translate!("count-items", "count" => 0); - assert_eq!(message_zero, "لديك لا عناصر"); - - // Test Arabic pluralization (one case) - let message_one = translate!("count-items", "count" => 1); - assert_eq!(message_one, "لديك عنصر واحد"); - - // Test Arabic pluralization (two case) - let message_two = translate!("count-items", "count" => 2); - assert_eq!(message_two, "لديك عنصران"); - - // Test Arabic pluralization (few case - 3-10) - let message_few = translate!("count-items", "count" => 5); - assert_eq!(message_few, "لديك 5 عناصر"); - - // Test Arabic pluralization (other case - 11+) - let message_many = translate!("count-items", "count" => 15); - assert_eq!(message_many, "لديك 15 عنصر"); - }) - .join() - .unwrap(); - } - - #[test] - fn test_mixed_script_fallback() { - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_localization(&locale, temp_dir.path(), "nonexistent_test_util"); - assert!(result.is_ok()); - - // Test Arabic message exists - let arabic_message = get_message("greeting"); - assert_eq!(arabic_message, "أهلاً بالعالم!"); - - // Test fallback to English for missing message - let fallback_message = get_message("missing-in-other"); - assert_eq!(fallback_message, "This message only exists in English"); - }) - .join() - .unwrap(); - } - - #[test] - fn test_unicode_directional_isolation_disabled() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - init_localization(&locale, temp_dir.path(), "nonexistent_test_util").unwrap(); - - // Test that Latin script names are NOT isolated in RTL context - // since we disabled Unicode directional isolation - let mut args = FluentArgs::new(); - args.set("name".to_string(), "John Smith".to_string()); - let message = get_message_with_args("welcome", args); - - // The Latin name should NOT be wrapped in directional isolate characters - assert!(!message.contains("\u{2068}John Smith\u{2069}")); - assert_eq!(message, "أهلاً وسهلاً، John Smith!"); - }) - .join() - .unwrap(); - } - #[test] fn test_error_display() { let io_error = LocalizationError::Io { @@ -1254,26 +1359,43 @@ invalid-syntax = This is { $missing } #[test] - fn test_parse_resource_error_includes_snippet() { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("es-ES").unwrap(); + fn test_clap_localization_fallbacks() { + std::thread::spawn(|| { + // Test the scenario where localization isn't properly initialized + // and we need fallbacks for clap error handling - let result = create_bundle(&locale, temp_dir.path()); - assert!(result.is_err()); + // First, test when localizer is not initialized + let error_msg = get_message("common-error"); + assert_eq!(error_msg, "common-error"); // Should return key when not initialized - if let Err(LocalizationError::ParseResource { - error: _err, - snippet, - }) = result - { - // The snippet should contain exactly the invalid text from es-ES.ftl - assert!( - snippet.contains("This is { $missing"), - "snippet was `{snippet}` but did not include the invalid text" - ); - } else { - panic!("Expected LocalizationError::ParseResource with snippet"); - } + let tip_msg = get_message("common-tip"); + assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized + + // Now initialize with setup_localization + let result = setup_localization("comm"); + if result.is_err() { + // If setup fails (e.g., no embedded locales for comm), try with a known utility + let _ = setup_localization("test"); + } + + // Test that common strings are available after initialization + let error_after_init = get_message("common-error"); + // Should either be translated or return the key (but not panic) + assert!(!error_after_init.is_empty()); + + let tip_after_init = get_message("common-tip"); + assert!(!tip_after_init.is_empty()); + + // Test that clap error keys work with fallbacks + let unknown_arg_key = get_message("clap-error-unexpected-argument"); + assert!(!unknown_arg_key.is_empty()); + + // Test usage key fallback + let usage_key = get_message("common-usage"); + assert!(!usage_key.is_empty()); + }) + .join() + .unwrap(); } } diff --git a/tests/by-util/test_base32.rs b/tests/by-util/test_base32.rs index 8bdf19ed0..252256668 100644 --- a/tests/by-util/test_base32.rs +++ b/tests/by-util/test_base32.rs @@ -112,13 +112,12 @@ fn test_wrap() { #[test] fn test_wrap_no_arg() { - let expected_stderr = "a value is required for '--wrap ' but none was supplied"; - for wrap_param in ["-w", "--wrap"] { new_ucmd!() .arg(wrap_param) .fails() - .stderr_contains(expected_stderr) + .stderr_contains("error: a value is required for '--wrap ' but none was supplied") + .stderr_contains("For more information, try '--help'.") .no_stdout(); } } diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index 2feb6ceff..ad0d1c2b1 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -147,7 +147,8 @@ fn test_wrap_no_arg() { new_ucmd!() .arg(wrap_param) .fails() - .stderr_contains("a value is required for '--wrap ' but none was supplied") + .stderr_contains("error: a value is required for '--wrap ' but none was supplied") + .stderr_contains("For more information, try '--help'.") .no_stdout(); } } diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index 0177216c7..9c04f39c6 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -572,3 +572,18 @@ fn test_both_inputs_out_of_order_but_identical() { .stdout_is("\t\t2\n\t\t1\n\t\t0\n") .no_stderr(); } + +#[test] +fn test_comm_extra_arg_error() { + let scene = TestScenario::new(util_name!()); + + // Test extra argument error case from GNU test + scene + .ucmd() + .args(&["a", "b", "no-such"]) + .fails() + .code_is(1) + .stderr_contains("error: unexpected argument 'no-such' found") + .stderr_contains("Usage: comm [OPTION]... FILE1 FILE2") + .stderr_contains("For more information, try '--help'."); +} diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index e7e23b9ca..74701e962 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1617,7 +1617,7 @@ fn test_reading_partial_blocks_from_fifo() { .args(["dd", "ibs=3", "obs=3", &format!("if={fifoname}")]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .env("LANG", "C") + .env("LC_ALL", "C") .spawn() .unwrap(); @@ -1662,7 +1662,7 @@ fn test_reading_partial_blocks_from_fifo_unbuffered() { .args(["dd", "bs=3", "ibs=1", "obs=1", &format!("if={fifoname}")]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .env("LANG", "C") + .env("LC_ALL", "C") .spawn() .unwrap(); diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 6c4191a20..86c358724 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -856,6 +856,18 @@ fn test_du_invalid_threshold() { ts.ucmd().arg(format!("--threshold={threshold}")).fails(); } +#[test] +fn test_du_threshold_error_handling() { + // Test missing threshold value - the specific case from GNU test + new_ucmd!() + .arg("--threshold") + .fails() + .stderr_contains( + "error: a value is required for '--threshold ' but none was supplied", + ) + .stderr_contains("For more information, try '--help'."); +} + #[test] fn test_du_apparent_size() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 2324da2a0..f06e7a616 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -28,6 +28,19 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } +#[test] +fn test_invalid_negative_arg_shows_tip() { + // Test that factor shows a tip when given an invalid negative argument + // This replicates the GNU test issue where "-1" was interpreted as an invalid option + new_ucmd!() + .arg("-1") + .fails() + .code_is(1) + .stderr_contains("unexpected argument '-1' found") + .stderr_contains("tip: to pass '-1' as a value, use '-- -1'") + .stderr_contains("Usage: factor"); +} + #[test] fn test_valid_arg_exponents() { new_ucmd!().arg("-h").succeeds(); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index beb1262a2..72b4b770a 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -83,6 +83,42 @@ fn test_invalid_value_returns_1() { } } +/* spellchecker: disable */ +#[test] +fn test_localized_possible_values() { + let test_cases = vec![ + ( + "en_US.UTF-8", + vec![ + "error: invalid value 'invalid_test_value' for '--color", + "[possible values:", + ], + ), + ( + "fr_FR.UTF-8", + vec![ + "erreur : valeur invalide 'invalid_test_value' pour '--color", + "[valeurs possibles:", + ], + ), + ]; + + for (locale, expected_strings) in test_cases { + let result = new_ucmd!() + .env("LANG", locale) + .env("LC_ALL", locale) + .arg("--color=invalid_test_value") + .fails(); + + result.code_is(1); + let stderr = result.stderr_str(); + for expected in expected_strings { + assert!(stderr.contains(expected)); + } + } +} +/* spellchecker: enable */ + #[test] fn test_invalid_value_returns_2() { // Invalid values to flags *sometimes* result in error code 2: diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 8da94e864..23d7315fb 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -2547,3 +2547,26 @@ fn test_mv_selinux_context() { let _ = std::fs::remove_file(at.plus_as_string(src)); } } + +#[test] +fn test_mv_error_usage_display_missing_arg() { + new_ucmd!() + .arg("--target-directory=.") + .fails() + .code_is(1) + .stderr_contains("error: the following required arguments were not provided:") + .stderr_contains("...") + .stderr_contains("Usage: mv [OPTION]... [-T] SOURCE DEST") + .stderr_contains("For more information, try '--help'."); +} + +#[test] +fn test_mv_error_usage_display_too_few() { + new_ucmd!() + .arg("file1") + .fails() + .code_is(1) + .stderr_contains("requires at least 2 values, but only 1 was provided") + .stderr_contains("Usage: mv [OPTION]... [-T] SOURCE DEST") + .stderr_contains("For more information, try '--help'."); +} diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 4f01e526d..0bfc71f07 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -28,3 +28,15 @@ fn test_ignore_equal_var() { // tested by gnu/tests/misc/printenv.sh new_ucmd!().env("a=b", "c").arg("a=b").fails().no_stdout(); } + +#[test] +fn test_invalid_option_exit_code() { + // printenv should return exit code 2 for invalid options + // This matches GNU printenv behavior and the GNU tests expectation + new_ucmd!() + .arg("-/") + .fails() + .code_is(2) + .stderr_contains("unexpected argument") + .stderr_contains("For more information, try '--help'"); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 657a3addd..5c1bb1570 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1568,3 +1568,156 @@ fn test_g_float_hex() { .succeeds() .stdout_is(output); } + +/* spell-checker: disable */ +#[test] +fn test_french_translations() { + // Test that French translations work for clap error messages + // Set LANG to French and test with an invalid argument + let result = new_ucmd!() + .env("LANG", "fr_FR.UTF-8") + .env("LC_ALL", "fr_FR.UTF-8") + .arg("--invalid-arg") + .fails(); + + let stderr = result.stderr_str(); + assert!(stderr.contains("erreur")); + assert!(stderr.contains("argument inattendu")); + assert!(stderr.contains("trouvé")); +} + +#[test] +fn test_argument_suggestion() { + let test_cases = vec![ + ("en_US.UTF-8", vec!["tip", "similar", "--reverse"]), + ("fr_FR.UTF-8", vec!["conseil", "similaire", "--reverse"]), + ]; + + for (locale, expected_strings) in test_cases { + let result = new_ucmd!() + .env("LANG", locale) + .env("LC_ALL", locale) + .arg("--revrse") // Typo + .fails(); + + let stderr = result.stderr_str(); + for expected in expected_strings { + assert!(stderr.contains(expected)); + } + } +} + +#[test] +fn test_clap_localization_unknown_argument() { + let test_cases = vec![ + ( + "en_US.UTF-8", + vec![ + "error: unexpected argument '--unknown-option' found", + "Usage:", + "For more information, try '--help'.", + ], + ), + ( + "fr_FR.UTF-8", + vec![ + "erreur : argument inattendu '--unknown-option' trouvé", + "Utilisation:", + "Pour plus d'informations, essayez '--help'.", + ], + ), + ]; + + for (locale, expected_strings) in test_cases { + let result = new_ucmd!() + .env("LANG", locale) + .env("LC_ALL", locale) + .arg("--unknown-option") + .fails(); + + result.code_is(2); // sort uses exit code 2 for invalid options + let stderr = result.stderr_str(); + for expected in expected_strings { + assert!(stderr.contains(expected)); + } + } +} + +#[test] +fn test_clap_localization_help_message() { + // Test help message in English + let result_en = new_ucmd!() + .env("LANG", "en_US.UTF-8") + .env("LC_ALL", "en_US.UTF-8") + .arg("--help") + .succeeds(); + + let stdout_en = result_en.stdout_str(); + assert!(stdout_en.contains("Usage:")); + assert!(stdout_en.contains("Options:")); + + // Test help message in French + let result_fr = new_ucmd!() + .env("LANG", "fr_FR.UTF-8") + .env("LC_ALL", "fr_FR.UTF-8") + .arg("--help") + .succeeds(); + + let stdout_fr = result_fr.stdout_str(); + assert!(stdout_fr.contains("Utilisation:")); + assert!(stdout_fr.contains("Options:")); +} + +#[test] +fn test_clap_localization_missing_required_argument() { + // Test missing required argument + let result_en = new_ucmd!().env("LC_ALL", "en_US.UTF-8").arg("-k").fails(); + + let stderr_en = result_en.stderr_str(); + assert!(stderr_en.contains(" a value is required for '--key ' but none was supplied")); + assert!(stderr_en.contains("-k")); +} + +#[test] +fn test_clap_localization_invalid_value() { + let test_cases = vec![ + ("en_US.UTF-8", "sort: failed to parse key 'invalid'"), + ("fr_FR.UTF-8", "sort: échec d'analyse de la clé 'invalid'"), + ]; + + for (locale, expected_message) in test_cases { + let result = new_ucmd!() + .env("LANG", locale) + .env("LC_ALL", locale) + .arg("-k") + .arg("invalid") + .fails(); + + let stderr = result.stderr_str(); + assert!(stderr.contains(expected_message)); + } +} + +#[test] +fn test_clap_localization_tip_for_value_with_dash() { + let test_cases = vec![ + ("en_US.UTF-8", vec!["tip:", "-- --file-with-dash"]), + ("fr_FR.UTF-8", vec!["tip:", "-- --file-with-dash"]), // TODO: fix French translation + ]; + + for (locale, expected_strings) in test_cases { + let result = new_ucmd!() + .env("LANG", locale) + .env("LC_ALL", locale) + .arg("--output") + .arg("--file-with-dash") + .fails(); + + let stderr = result.stderr_str(); + for expected in expected_strings { + assert!(stderr.contains(expected)); + } + } +} + +/* spell-checker: enable */ diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index a91ffc8ff..34b24d84d 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1935,9 +1935,8 @@ fn test_split_separator_no_value() { .ignore_stdin_write_error() .pipe_in("a\n") .fails() - .stderr_contains( - "error: a value is required for '--separator ' but none was supplied", - ); + .stderr_contains("error: a value is required for '--separator ' but none was supplied") + .stderr_contains("For more information, try '--help'."); } #[test] diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index a9fbb29d9..23dbccfe7 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -36,6 +36,7 @@ fn execution_phrase_double() { let output = Command::new(&scenario.bin_path) .arg("ls") .arg("--some-invalid-arg") + .env("LANG", "en_US.UTF-8") .output() .unwrap(); assert!(