From 863a5aba000cecbfef7e5d9450a5d277d3a8fe7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:03:13 +0000 Subject: [PATCH 01/83] chore(deps): update rust crate terminal_size to v0.4.3 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c1545784..0517e772f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,7 +940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1532,7 +1532,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2394,7 +2394,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2674,17 +2674,17 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4313,7 +4313,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From 7ca85a067890b4031993b9fcf545c9e6721acdfe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:25:57 +0000 Subject: [PATCH 02/83] chore(deps): update rust crate clap to v4.5.44 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0517e772f..db8fec128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,18 +372,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.43" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" +checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.43" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", From 3028d116c6f262b189108a7c6ea6fdc05c37fe4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:26:03 +0000 Subject: [PATCH 03/83] chore(deps): update rust crate clap_complete to v4.5.57 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db8fec128..a0576d1a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,9 +394,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e4efcbb5da11a92e8a609233aa1e8a7d91e38de0be865f016d14700d45a7fd" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] From 1684e2b5242d99f09e0cc562ace4f169852447fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 01:06:14 +0000 Subject: [PATCH 04/83] chore(deps): update rust crate thiserror to v2.0.14 --- Cargo.lock | 94 +++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0576d1a0..924eccdc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1054,7 +1054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2442,7 +2442,7 @@ dependencies = [ "once_cell", "parking_lot", "selinux-sys", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2710,11 +2710,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.14", ] [[package]] @@ -2730,9 +2730,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -2998,7 +2998,7 @@ dependencies = [ "memchr", "nix", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "winapi-util", "windows-sys 0.60.2", @@ -3013,7 +3013,7 @@ dependencies = [ "fts-sys", "libc", "selinux", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3032,7 +3032,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3051,7 +3051,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3086,7 +3086,7 @@ dependencies = [ "libc", "linux-raw-sys 0.10.0", "selinux", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "walkdir", "xattr", @@ -3099,7 +3099,7 @@ dependencies = [ "clap", "fluent", "regex", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3138,7 +3138,7 @@ dependencies = [ "libc", "nix", "signal-hook", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3149,7 +3149,7 @@ dependencies = [ "clap", "fluent", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -3188,7 +3188,7 @@ dependencies = [ "clap", "fluent", "glob", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "windows-sys 0.60.2", ] @@ -3210,7 +3210,7 @@ dependencies = [ "fluent", "nix", "rust-ini", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3220,7 +3220,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -3234,7 +3234,7 @@ dependencies = [ "num-bigint", "num-traits", "onig", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3276,7 +3276,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -3296,7 +3296,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3316,7 +3316,7 @@ dependencies = [ "clap", "fluent", "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3360,7 +3360,7 @@ dependencies = [ "file_diff", "filetime", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3371,7 +3371,7 @@ dependencies = [ "clap", "fluent", "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3400,7 +3400,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3426,7 +3426,7 @@ dependencies = [ "lscolors", "selinux", "terminal_size", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "uutils_term_grid", ] @@ -3468,7 +3468,7 @@ dependencies = [ "fluent", "rand 0.9.2", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3493,7 +3493,7 @@ dependencies = [ "fs_extra", "indicatif", "libc", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "windows-sys 0.60.2", ] @@ -3526,7 +3526,7 @@ dependencies = [ "clap", "fluent", "libc", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3546,7 +3546,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3597,7 +3597,7 @@ dependencies = [ "fluent", "itertools 0.14.0", "regex", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3626,7 +3626,7 @@ dependencies = [ "clap", "fluent", "regex", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3664,7 +3664,7 @@ dependencies = [ "clap", "fluent", "libc", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "windows-sys 0.60.2", ] @@ -3687,7 +3687,7 @@ dependencies = [ "fluent", "libc", "selinux", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3700,7 +3700,7 @@ dependencies = [ "fluent", "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3753,7 +3753,7 @@ dependencies = [ "rayon", "self_cell", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -3765,7 +3765,7 @@ dependencies = [ "clap", "fluent", "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3775,7 +3775,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3786,7 +3786,7 @@ dependencies = [ "clap", "fluent", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "uu_stdbuf_libstdbuf", "uucore", ] @@ -3838,7 +3838,7 @@ dependencies = [ "memchr", "memmap2", "regex", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3875,7 +3875,7 @@ dependencies = [ "clap", "fluent", "libc", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3899,7 +3899,7 @@ dependencies = [ "filetime", "fluent", "parse_datetime", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", "windows-sys 0.60.2", ] @@ -3938,7 +3938,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "uucore", ] @@ -3968,7 +3968,7 @@ version = "0.1.0" dependencies = [ "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -3998,7 +3998,7 @@ dependencies = [ "chrono", "clap", "fluent", - "thiserror 2.0.12", + "thiserror 2.0.14", "utmp-classic", "uucore", ] @@ -4031,7 +4031,7 @@ dependencies = [ "fluent", "libc", "nix", - "thiserror 2.0.12", + "thiserror 2.0.14", "unicode-width 0.2.1", "uucore", ] @@ -4106,7 +4106,7 @@ dependencies = [ "sha3", "sm3", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.14", "time", "unic-langid", "utmp-classic", From 8850001e0f5f7dc447b5204477d91a212b6ad9a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 22:34:28 +0000 Subject: [PATCH 05/83] chore(deps): update rust crate clap to v4.5.45 --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 924eccdc6..b3d1c5e03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.44" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", ] @@ -940,7 +940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1532,7 +1532,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2394,7 +2394,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2674,7 +2674,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4313,7 +4313,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From bb27d8aab7f57c960563db395e6f1b0f0f0263ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 03:08:19 +0000 Subject: [PATCH 06/83] chore(deps): update rust crate rayon to v1.11.0 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3d1c5e03..1c72d7fd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2253,9 +2253,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2263,9 +2263,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", From e455f770f97450f7eca54e1d2f4b0f25f975bcc6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 4 Aug 2025 18:23:52 +0200 Subject: [PATCH 07/83] l10n: Add LocalizedCommand trait for clap localization --- src/uucore/locales/en-US.ftl | 33 ++++ src/uucore/locales/fr-FR.ftl | 33 ++++ src/uucore/src/lib/lib.rs | 2 + src/uucore/src/lib/mods.rs | 1 + src/uucore/src/lib/mods/clap_localization.rs | 184 +++++++++++++++++++ src/uucore/src/lib/mods/locale.rs | 127 +++++++++++++ 6 files changed, 380 insertions(+) create mode 100644 src/uucore/locales/en-US.ftl create mode 100644 src/uucore/locales/fr-FR.ftl create mode 100644 src/uucore/src/lib/mods/clap_localization.rs diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl new file mode 100644 index 000000000..358eda0d5 --- /dev/null +++ b/src/uucore/locales/en-US.ftl @@ -0,0 +1,33 @@ +# 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-help-suggestion = For more information, try '{ $command } --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..cbeab358d --- /dev/null +++ b/src/uucore/locales/fr-FR.ftl @@ -0,0 +1,33 @@ +# 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-help-suggestion = Pour plus d'informations, essayez '{ $command } --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/lib.rs b/src/uucore/src/lib/lib.rs index ab28d657b..8c8dbb74e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -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")] 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..f5aed26f2 --- /dev/null +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -0,0 +1,184 @@ +// 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 +//! + +use crate::locale::translate; + +use clap::error::{ContextKind, ErrorKind}; +use clap::{ArgMatches, Command, Error}; +use std::ffi::OsString; + +/// Apply color to text using ANSI escape codes +pub fn colorize(text: &str, color_code: &str) -> String { + format!("\x1b[{color_code}m{text}\x1b[0m") +} + +/// Color constants for consistent styling +pub mod colors { + pub const RED: &str = "31"; + pub const YELLOW: &str = "33"; + pub const GREEN: &str = "32"; +} + +pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: i32) -> ! { + // Try to ensure localization is initialized for this utility + // If it's already initialized, that's fine - we'll use the existing one + let _ = crate::locale::setup_localization_with_common(util_name); + + match err.kind() { + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { + // For help and 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 => { + // Use clap's rendering system but capture the output to check if colors are used + let rendered = err.render(); + let rendered_str = rendered.to_string(); + + // Simple check - if the rendered output contains ANSI escape codes, colors are enabled + let colors_enabled = rendered_str.contains("\x1b["); + + if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { + let arg_str = invalid_arg.to_string(); + + // Get the uncolored words from common strings + let error_word = translate!("common-error"); + let tip_word = translate!("common-tip"); + + // Apply colors only if they're enabled in the original error + let (colored_arg, colored_error_word, colored_tip_word) = if colors_enabled { + ( + colorize(&arg_str, colors::YELLOW), + colorize(&error_word, colors::RED), + colorize(&tip_word, colors::GREEN), + ) + } else { + (arg_str.clone(), error_word.clone(), tip_word.clone()) + }; + + // Print main error message + let error_msg = translate!( + "clap-error-unexpected-argument", + "arg" => colored_arg.clone(), + "error_word" => colored_error_word + ); + eprintln!("{error_msg}"); + eprintln!(); + + // Show suggestion or generic tip + let suggestion = err.get(ContextKind::SuggestedArg); + if let Some(suggested_arg) = suggestion { + let colored_suggestion = if colors_enabled { + colorize(&suggested_arg.to_string(), colors::GREEN) + } else { + suggested_arg.to_string() + }; + let suggestion_msg = translate!( + "clap-error-similar-argument", + "tip_word" => colored_tip_word, + "suggestion" => colored_suggestion + ); + eprintln!(" {suggestion_msg}"); + } else { + let colored_tip_command = if colors_enabled { + colorize(&format!("-- {arg_str}"), colors::GREEN) + } else { + format!("-- {arg_str}") + }; + let tip_msg = translate!( + "clap-error-pass-as-value", + "arg" => colored_arg, + "tip_word" => colored_tip_word, + "tip_command" => colored_tip_command + ); + eprintln!(" {tip_msg}"); + } + + // Show usage and help + eprintln!(); + let usage_label = translate!("common-usage"); + let usage_pattern = translate!(&format!("{util_name}-usage")); + eprintln!("{usage_label}: {usage_pattern}"); + eprintln!(); + + let help_msg = translate!("clap-error-help-suggestion", "command" => util_name); + eprintln!("{help_msg}"); + + std::process::exit(exit_code); + } else { + // Generic fallback case + let rendered = err.render(); + let rendered_str = rendered.to_string(); + let colors_enabled = rendered_str.contains("\x1b["); + + let colored_error_word = if colors_enabled { + colorize(&translate!("common-error"), colors::RED) + } else { + translate!("common-error") + }; + eprintln!("{colored_error_word}: unexpected argument"); + std::process::exit(exit_code); + } + } + _ => { + // For other errors, print using clap's formatter but exit with code 1 + eprint!("{}", err.render()); + std::process::exit(1); + } + } +} + +/// 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; + + /// Try to get matches from args with localized error handling + fn try_get_matches_from_localized(self, itr: I) -> ArgMatches + where + Self: Sized, + I: IntoIterator, + T: Into + Clone; + + /// Try to get matches from mutable args with localized error handling + fn try_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 try_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 try_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..aec455cfe 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -147,6 +147,104 @@ fn init_localization( Ok(()) } +/// 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_with_common( + 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_with_common( + locale: &LanguageIdentifier, + locales_dir: &Path, + util_name: &str, +) -> Result<(), LocalizationError> { + let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) + .expect("Default locale should always be valid"); + + // Try to create a bundle that combines common and utility-specific strings + let english_bundle = create_bundle_with_common(&default_locale, locales_dir, util_name) + .or_else(|_| { + // Fallback to embedded utility-specific strings only + 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 with common strings + if let Ok(primary_bundle) = create_bundle_with_common(locale, locales_dir, util_name) { + // 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(()) +} + /// Create a bundle for a specific locale fn create_bundle( locale: &LanguageIdentifier, @@ -389,6 +487,35 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { } } +/// Enhanced version of setup_localization that also loads common/clap error strings +/// This function loads both utility-specific strings and common strings for clap error handling +pub fn setup_localization_with_common(p: &str) -> Result<(), LocalizationError> { + let locale = detect_system_locale().unwrap_or_else(|_| { + LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") + }); + + // Load common strings along with utility-specific strings + match get_locales_dir(p) { + Ok(locales_dir) => { + // Load both utility-specific and common strings + init_localization_with_common(&locale, &locales_dir, p) + } + Err(_) => { + // No locales directory found, use embedded English 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)?; + let localizer = Localizer::new(english_bundle); + + LOCALIZER.with(|lock| { + lock.set(localizer) + .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) + })?; + Ok(()) + } + } +} + #[cfg(not(debug_assertions))] fn resolve_locales_dir_from_exe_dir(exe_dir: &Path, p: &str) -> Option { // 1. /locales/ From b06b1e4ea8441af25827ddcde036310e05271a37 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 4 Aug 2025 18:27:45 +0200 Subject: [PATCH 08/83] l10n: Migrate all utilities to use LocalizedCommand --- src/bin/coreutils.rs | 22 ++++++++++++---------- src/uu/arch/src/arch.rs | 3 ++- src/uu/base32/src/base_common.rs | 7 ++++--- src/uu/basename/src/basename.rs | 3 ++- src/uu/cat/src/cat.rs | 3 ++- src/uu/chcon/src/chcon.rs | 3 ++- src/uu/chmod/src/chmod.rs | 3 ++- src/uu/cksum/src/cksum.rs | 3 ++- src/uu/comm/src/comm.rs | 3 ++- src/uu/cp/src/cp.rs | 3 ++- src/uu/csplit/src/csplit.rs | 3 ++- src/uu/cut/src/cut.rs | 3 ++- src/uu/date/src/date.rs | 3 ++- src/uu/dd/src/dd.rs | 3 ++- src/uu/df/src/df.rs | 3 ++- src/uu/dircolors/src/dircolors.rs | 3 ++- src/uu/dirname/src/dirname.rs | 3 ++- src/uu/du/src/du.rs | 3 ++- src/uu/factor/src/factor.rs | 3 ++- src/uu/false/src/false.rs | 2 ++ src/uu/fmt/src/fmt.rs | 3 ++- src/uu/fold/src/fold.rs | 3 ++- src/uu/groups/src/groups.rs | 3 ++- src/uu/hashsum/src/hashsum.rs | 3 ++- src/uu/head/src/head.rs | 4 +++- src/uu/hostid/src/hostid.rs | 3 ++- src/uu/hostname/src/hostname.rs | 3 ++- src/uu/id/src/id.rs | 3 ++- src/uu/install/Cargo.toml | 2 +- src/uu/install/src/install.rs | 3 ++- src/uu/join/src/join.rs | 3 ++- src/uu/kill/src/kill.rs | 3 ++- src/uu/link/src/link.rs | 3 ++- src/uu/ln/src/ln.rs | 5 ++++- src/uu/logname/src/logname.rs | 3 ++- src/uu/ls/src/ls.rs | 6 ++---- src/uu/mkdir/src/mkdir.rs | 3 ++- src/uu/mkfifo/src/mkfifo.rs | 3 ++- src/uu/mknod/src/mknod.rs | 3 ++- src/uu/mktemp/src/mktemp.rs | 4 ++++ src/uu/more/src/more.rs | 3 ++- src/uu/mv/src/mv.rs | 18 ++++++++++-------- src/uu/nl/src/nl.rs | 3 ++- src/uu/nproc/src/nproc.rs | 3 ++- src/uu/numfmt/src/numfmt.rs | 3 ++- src/uu/od/src/od.rs | 3 ++- src/uu/paste/src/paste.rs | 3 ++- src/uu/pathchk/src/pathchk.rs | 3 ++- src/uu/pinky/src/platform/openbsd.rs | 3 ++- src/uu/pinky/src/platform/unix.rs | 3 ++- src/uu/pr/src/pr.rs | 5 +++-- src/uu/printenv/src/printenv.rs | 3 ++- src/uu/printf/src/printf.rs | 3 ++- src/uu/ptx/src/ptx.rs | 3 ++- src/uu/pwd/src/pwd.rs | 3 ++- src/uu/readlink/src/readlink.rs | 3 ++- src/uu/rm/src/rm.rs | 3 ++- src/uu/rmdir/src/rmdir.rs | 3 ++- src/uu/shred/src/shred.rs | 3 ++- src/uu/shuf/src/shuf.rs | 3 ++- src/uu/sleep/src/sleep.rs | 3 ++- src/uu/sort/src/sort.rs | 4 ++++ src/uu/split/src/split.rs | 3 ++- src/uu/stat/src/stat.rs | 3 ++- src/uu/stty/src/stty.rs | 3 ++- src/uu/sum/src/sum.rs | 3 ++- src/uu/sync/src/sync.rs | 3 ++- src/uu/tac/src/tac.rs | 3 ++- src/uu/tee/src/tee.rs | 3 ++- src/uu/touch/src/touch.rs | 3 ++- src/uu/tr/src/tr.rs | 3 ++- src/uu/tsort/src/tsort.rs | 3 ++- src/uu/tty/src/tty.rs | 5 ++++- src/uu/uname/src/uname.rs | 3 ++- src/uu/unlink/src/unlink.rs | 3 ++- src/uu/uptime/src/uptime.rs | 3 ++- src/uu/users/src/users.rs | 3 ++- src/uu/wc/src/wc.rs | 3 ++- src/uu/who/src/platform/openbsd.rs | 3 ++- src/uu/who/src/platform/unix.rs | 3 ++- src/uu/whoami/src/whoami.rs | 3 ++- src/uu/yes/src/yes.rs | 3 ++- 82 files changed, 193 insertions(+), 101 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 64a79a3fd..c6d2283bd 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -81,16 +81,18 @@ fn find_prefixed_util<'a>( } fn setup_localization_or_exit(util_name: &str) { - locale::setup_localization(get_canonical_util_name(util_name)).unwrap_or_else(|err| { - match err { - uucore::locale::LocalizationError::ParseResource { - error: err_msg, - snippet, - } => eprintln!("Localization parse error at {snippet}: {err_msg}"), - other => eprintln!("Could not init the localization system: {other}"), - } - process::exit(99) - }); + locale::setup_localization_with_common(get_canonical_util_name(util_name)).unwrap_or_else( + |err| { + match err { + uucore::locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + process::exit(99) + }, + ); } #[allow(clippy::cognitive_complexity)] diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 29ad9d273..cd398a72b 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().try_get_matches_from_localized(args); let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, translate!("cannot-get-system")))?; diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index b7d1124ac..e60ea5366 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,7 +100,8 @@ 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.try_get_matches_from_localized(args); + Config::from(&matches) } pub fn base_app(about: &'static str, usage: &str) -> Command { diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 255ff6110..802eb4464 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().try_get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index c7f975952..22cd38f38 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().try_get_matches_from_localized(args); let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) { NumberingMode::NonEmpty diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index ca65cf4a7..de1dc2f64 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}; @@ -303,7 +304,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.try_get_matches_from_localized(args); let verbose = matches.get_flag(options::VERBOSE); diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index f2d2bd47a..cb540aa7a 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)?; + .try_get_matches_from_localized(args); let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 6dc28040e..959c23e40 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().try_get_matches_from_localized(args); let check = matches.get_flag(options::CHECK); diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index e55d181d6..81bcab418 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().try_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(); diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index e826ac3c4..3dee0fe01 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; @@ -778,7 +779,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().try_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..8c98d37c5 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().try_get_matches_from_localized(args); // get the file to split let file_name = matches.get_one::(options::FILE).unwrap(); diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 4bf323507..cf9bfdbe4 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().try_get_matches_from_localized(args); let complement = matches.get_flag(options::COMPLEMENT); let only_delimited = matches.get_flag(options::ONLY_DELIMITED); diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 6d5a418c2..1eadd6ce2 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().try_get_matches_from_localized(args); let format = if let Some(form) = matches.get_one::(OPT_FORMAT) { if !form.starts_with('+') { diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 4d77dcfe7..d231da8b1 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().try_get_matches_from_localized(args); let settings: Settings = Parser::new().parse( matches diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 6c8a1ee7a..2d6d34b23 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().try_get_matches_from_localized(args); #[cfg(windows)] { diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index b76d49f67..f6bd9ec40 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().try_get_matches_from_localized(args); let files = matches .get_many::(options::FILE) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 6d7a4a5a5..73618d7e0 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)?; + .try_get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index f39d257de..b43cee530 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().try_get_matches_from_localized(args); let summarize = matches.get_flag(options::SUMMARIZE); diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 776a98536..6dec840da 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().try_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); diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 92128f5f1..a0a3f944f 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 => { diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index f6e555055..80c1af0e3 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().try_get_matches_from_localized(&args); let files = extract_files(&matches)?; diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 318e3875b..c3e743d85 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().try_get_matches_from_localized(args); let bytes = matches.get_flag(options::BYTES); let spaces = matches.get_flag(options::SPACES); diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index 498c72802..9d6947223 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().try_get_matches_from_localized(args); let users: Vec = matches .get_many::(options::USERS) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index a6fc13e23..4204b979e 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.try_get_matches_from_localized(args); let input_length: Option<&usize> = if binary_name == "b2sum" { matches.get_one::(options::LENGTH) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index b7e4334c4..bfd8a076a 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; @@ -549,7 +550,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().try_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..c29b57489 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().try_get_matches_from_localized(args); hostid(); Ok(()) } diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 558c6aff1..74f2c48d9 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().try_get_matches_from_localized(args); #[cfg(windows)] let _handle = wsa::start().map_err_context(|| translate!("hostname-error-winsock"))?; diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 41e926450..688ac232a 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)?; + .try_get_matches_from_localized(args); let users: Vec = matches .get_many::(options::ARG_USERS) 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..338f90f6c 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().try_get_matches_from_localized(args); let paths: Vec = matches .get_many::(ARG_FILES) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 3dec9bef1..6ba16d101 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().try_get_matches_from_localized(args); let settings = parse_settings(&matches)?; diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 148489b2d..b1dc4dc92 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().try_get_matches_from_localized(args); let mode = if matches.get_flag(options::TABLE) { Mode::Table diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 20528a700..327ef09b0 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().try_get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(options::FILES) .unwrap_or_default() diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index df53b16b8..5d5ce9bdc 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) + .try_get_matches_from_localized(args); /* the list of files */ diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 77d8fe15c..d34712339 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().try_get_matches_from_localized(args); match get_userlogin() { Some(userlogin) => println!("{userlogin}"), diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f4c685863..5e9477c29 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1103,9 +1103,7 @@ 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) { + let matches = match uu_app().try_get_matches_from(args) { // clap successfully parsed the arguments: Ok(matches) => matches, // --help, --version, etc.: @@ -1118,7 +1116,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // All other argument parsing errors cause exit code 2: Err(e) => { - return Err(USimpleError::new(2, e.to_string())); + uucore::clap_localization::handle_clap_error_with_exit_code(e, "ls", 2); } }; diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index b2c349337..d1cf51647 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)?; + .try_get_matches_from_localized(args); let dirs = matches .get_many::(options::DIRS) diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 33d842d92..8032a5fad 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().try_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)))?; diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 5bd79ade9..53b7eb8d2 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().try_get_matches_from_localized(args); let file_type = matches.get_one::("type").unwrap(); diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 90ac7c875..b1dbacf71 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 diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 4bd2e80df..829e4f53b 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().try_get_matches_from_localized(args); let mut options = Options::from(&matches); if let Some(files) = matches.get_many::(options::FILES) { let length = files.len(); diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 948e486c9..0fd44643e 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().try_get_matches_from_localized(args); let files: Vec = matches .get_many::(ARG_FILES) @@ -161,11 +162,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect(); if files.len() == 1 && !matches.contains_id(OPT_TARGET_DIRECTORY) { - app.error( - ErrorKind::TooFewValues, - translate!("mv-error-insufficient-arguments", "arg_files" => ARG_FILES), - ) - .exit(); + return Err(UUsageError::new( + 1, + format!( + "The argument '<{ARG_FILES}>...' requires at least 2 values, but only 1 was provided" + ), + )); } let overwrite_mode = determine_overwrite_mode(&matches); diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 1e5eb3d72..414f5d735 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().try_get_matches_from_localized(args); let mut settings = Settings::default(); diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index f7be033bd..9d913d33e 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().try_get_matches_from_localized(args); let ignore = match matches.get_one::(OPT_IGNORE) { Some(numstr) => match numstr.trim().parse::() { diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index f8dddd8c6..852016495 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().try_get_matches_from_localized(args); let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 7db53b6b6..3562e38c3 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.try_get_matches_from_localized(&args); let od_options = OdOptions::new(&clap_matches, &args)?; diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index b6b9b59a8..6dc3d79e9 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().try_get_matches_from_localized(args); let serial = matches.get_flag(options::SERIAL); let delimiters = matches.get_one::(options::DELIMITER).unwrap(); diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 3b7a3c164..d5a318f0d 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().try_get_matches_from_localized(args); // set working mode let is_posix = matches.get_flag(options::POSIX); diff --git a/src/uu/pinky/src/platform/openbsd.rs b/src/uu/pinky/src/platform/openbsd.rs index fb7cd155b..c53839c47 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().try_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..e80f02d05 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)?; + .try_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..3e7bcd3d9 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; @@ -315,8 +316,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.try_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..6d875c537 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -5,6 +5,7 @@ use clap::{Arg, ArgAction, Command}; use std::env; +use uucore::LocalizedCommand; use uucore::translate; use uucore::{error::UResult, format_usage}; @@ -14,7 +15,7 @@ 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_localized(args); let variables: Vec = matches .get_many::(ARG_VARIABLES) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 2c536bcb6..08408b40b 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().try_get_matches_from_localized(args); let format = matches .get_one::(options::FORMAT) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 78ec37a47..f2a26ae82 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().try_get_matches_from_localized(args); let config = get_config(&matches)?; let input_files; diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 39dad5f9e..5fe5ae0ae 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().try_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 diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 8f61d9ab2..c9f06ff5b 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().try_get_matches_from_localized(args); let mut no_trailing_delimiter = matches.get_flag(OPT_NO_NEWLINE); let use_zero = matches.get_flag(OPT_ZERO); diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index f7971aa80..9595e25f6 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().try_get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(ARG_FILES) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 34346c0d6..bbbcae744 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().try_get_matches_from_localized(args); let opts = Opts { ignore: matches.get_flag(OPT_IGNORE_FAIL_NON_EMPTY), diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index a34797835..73f4c067c 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().try_get_matches_from_localized(args); if !matches.contains_id(options::FILE) { return Err(UUsageError::new( diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 254875eaa..d2e854b9b 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().try_get_matches_from_localized(args); let mode = if matches.get_flag(options::ECHO) { Mode::Echo( diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 1731c2af1..37d54ee99 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().try_get_matches_from_localized(args); let numbers = matches .get_many::(options::NUMBER) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 74b6253fb..29346315b 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1050,6 +1050,10 @@ 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). + 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(), 2); + } e.print().unwrap(); if e.use_stderr() { set_exit_code(2); diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index b8351c31e..a2c85464f 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().try_get_matches_from_localized(args); match Settings::from(&matches, obs_lines.as_deref()) { Ok(settings) => split(&settings), diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 6bc0aa0b3..f39e24842 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)?; + .try_get_matches_from_localized(args); let stater = Stater::new(&matches)?; let exit_status = stater.exec(); diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 5de8f9e36..63b0a6e62 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().try_get_matches_from_localized(args); let opts = Options::from(&matches)?; diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 8359ec002..061fc2509 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().try_get_matches_from_localized(args); let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 95af36c80..9e643aa7c 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().try_get_matches_from_localized(args); let files: Vec = matches .get_many::(ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 52d885bf4..4911a4b09 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().try_get_matches_from_localized(args); let before = matches.get_flag(options::BEFORE); let regex = matches.get_flag(options::REGEX); diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index d7d4b6704..14c015bf7 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().try_get_matches_from_localized(args); let append = matches.get_flag(options::APPEND); let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS); diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index fde4cdc1b..1f413e4e0 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().try_get_matches_from_localized(args); let mut filenames: Vec<&String> = matches .get_many::(ARG_FILES) diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 5e5316dbc..fbcde13d4 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().try_get_matches_from_localized(args); let delete_flag = matches.get_flag(options::DELETE); let complement_flag = matches.get_flag(options::COMPLEMENT); diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 646303bab..dab1ffc9c 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().try_get_matches_from_localized(args); let input = matches .get_one::(options::FILE) diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index d5c843fcd..b893fa8b4 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); diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index 092ec1a64..c30c00bb4 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().try_get_matches_from_localized(args); let options = Options { all: matches.get_flag(options::ALL), diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 47d6e04d0..c5935b40e 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().try_get_matches_from_localized(args); let path: &Path = matches.get_one::(OPT_PATH).unwrap().as_ref(); diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 41d0f6464..d8470e196 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().try_get_matches_from_localized(args); #[cfg(unix)] let file_path = matches.get_one::(options::PATH); diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 67761ca06..92c50d7cc 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)?; + .try_get_matches_from_localized(args); let maybe_file: Option<&Path> = matches.get_one::(ARG_FILE).map(AsRef::as_ref); diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 920f4602f..140001c9e 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().try_get_matches_from_localized(args); let settings = Settings::new(&matches); let inputs = Inputs::new(&matches)?; diff --git a/src/uu/who/src/platform/openbsd.rs b/src/uu/who/src/platform/openbsd.rs index 8e0bbd3b9..8d1e31dab 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().try_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..70b46a593 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)?; + .try_get_matches_from_localized(args); let files: Vec = matches .get_many::(options::FILE) diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 928e81edf..3b0b11ead 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().try_get_matches_from_localized(args); let username = whoami()?; println_verbatim(username).map_err_context(|| translate!("whoami-error-failed-to-print"))?; Ok(()) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 6b2bc6495..ec818a955 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().try_get_matches_from_localized(args); let mut buffer = Vec::with_capacity(BUF_SIZE); args_into_buffer(&mut buffer, matches.get_many::("STRING")).unwrap(); From 333ce0c119f03a844623154a84eb1662a1d6ca6e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 4 Aug 2025 10:51:29 +0200 Subject: [PATCH 09/83] l10n/github action: verify that clap localization works --- .github/workflows/l10n.yml | 170 +++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) 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 From 913844d080a15c8df96ee32dd7ec3c4c6b24b28b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 4 Aug 2025 22:22:31 +0200 Subject: [PATCH 10/83] add test (in sort) to verify that the clap error mgmt work (english and french) --- tests/by-util/test_sort.rs | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 657a3addd..15872affc 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1568,3 +1568,42 @@ 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)); + } + } +} +/* spell-checker: enable */ From c7ad0037f2d6559025ebc50ea03b7ced87e5c08a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 4 Aug 2025 22:55:18 +0200 Subject: [PATCH 11/83] cspell: also ignore flt files in uucore --- .vscode/cSpell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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, From 5086310687f400e8739b1d5b7127fcfe83d3cabd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 5 Aug 2025 10:44:12 +0200 Subject: [PATCH 12/83] l10n: also install uucore locales --- GNUmakefile | 8 ++++++++ 1 file changed, 8 insertions(+) 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"; \ From f1beeb904c5879b7f7ecfa09a4df9c5506e28a68 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 18:49:04 +0200 Subject: [PATCH 13/83] clap localization: address PR review comments - Make colorize() function private since only used internally - Remove redundant colors_enabled checks - Add apply_color helper closure to reduce code duplication - Remove try_ prefix from function names for consistency - Update all utilities to use renamed functions - Fix app variable reference in mv utility --- src/uu/arch/src/arch.rs | 2 +- src/uu/base32/src/base_common.rs | 2 +- src/uu/basename/src/basename.rs | 2 +- src/uu/cat/src/cat.rs | 2 +- src/uu/chcon/src/chcon.rs | 2 +- src/uu/chmod/src/chmod.rs | 2 +- src/uu/cksum/src/cksum.rs | 2 +- src/uu/comm/src/comm.rs | 2 +- src/uu/cp/src/cp.rs | 2 +- src/uu/csplit/src/csplit.rs | 2 +- src/uu/cut/src/cut.rs | 2 +- src/uu/date/src/date.rs | 2 +- src/uu/dd/src/dd.rs | 2 +- src/uu/df/src/df.rs | 2 +- src/uu/dircolors/src/dircolors.rs | 2 +- src/uu/dirname/src/dirname.rs | 2 +- src/uu/du/src/du.rs | 2 +- src/uu/factor/src/factor.rs | 2 +- src/uu/fmt/src/fmt.rs | 2 +- src/uu/fold/src/fold.rs | 2 +- src/uu/groups/src/groups.rs | 2 +- src/uu/hashsum/src/hashsum.rs | 2 +- src/uu/head/src/head.rs | 2 +- src/uu/hostid/src/hostid.rs | 2 +- src/uu/hostname/src/hostname.rs | 2 +- src/uu/id/src/id.rs | 2 +- src/uu/install/src/install.rs | 2 +- src/uu/join/src/join.rs | 2 +- src/uu/kill/src/kill.rs | 2 +- src/uu/link/src/link.rs | 2 +- src/uu/ln/src/ln.rs | 2 +- src/uu/logname/src/logname.rs | 2 +- src/uu/mkdir/src/mkdir.rs | 2 +- src/uu/mkfifo/src/mkfifo.rs | 2 +- src/uu/mknod/src/mknod.rs | 2 +- src/uu/more/src/more.rs | 2 +- src/uu/mv/src/mv.rs | 13 +- src/uu/nl/src/nl.rs | 2 +- src/uu/nproc/src/nproc.rs | 2 +- src/uu/numfmt/src/numfmt.rs | 2 +- src/uu/od/src/od.rs | 2 +- src/uu/paste/src/paste.rs | 2 +- src/uu/pathchk/src/pathchk.rs | 2 +- src/uu/pinky/src/platform/openbsd.rs | 2 +- src/uu/pinky/src/platform/unix.rs | 2 +- src/uu/pr/src/pr.rs | 2 +- src/uu/printenv/src/printenv.rs | 2 +- src/uu/printf/src/printf.rs | 2 +- src/uu/ptx/src/ptx.rs | 2 +- src/uu/pwd/src/pwd.rs | 2 +- src/uu/readlink/src/readlink.rs | 2 +- src/uu/rm/src/rm.rs | 2 +- src/uu/rmdir/src/rmdir.rs | 2 +- src/uu/shred/src/shred.rs | 2 +- src/uu/shuf/src/shuf.rs | 2 +- src/uu/sleep/src/sleep.rs | 2 +- src/uu/split/src/split.rs | 2 +- src/uu/stat/src/stat.rs | 2 +- src/uu/stty/src/stty.rs | 2 +- src/uu/sum/src/sum.rs | 2 +- src/uu/sync/src/sync.rs | 2 +- src/uu/tac/src/tac.rs | 2 +- src/uu/tee/src/tee.rs | 2 +- src/uu/touch/src/touch.rs | 2 +- src/uu/tr/src/tr.rs | 2 +- src/uu/tsort/src/tsort.rs | 2 +- src/uu/uname/src/uname.rs | 2 +- src/uu/unlink/src/unlink.rs | 2 +- src/uu/uptime/src/uptime.rs | 2 +- src/uu/users/src/users.rs | 2 +- src/uu/wc/src/wc.rs | 2 +- src/uu/who/src/platform/openbsd.rs | 2 +- src/uu/who/src/platform/unix.rs | 2 +- src/uu/whoami/src/whoami.rs | 2 +- src/uu/yes/src/yes.rs | 2 +- src/uucore/src/lib/mods/clap_localization.rs | 211 +++++++++++++------ tests/by-util/test_du.rs | 10 + tests/by-util/test_mv.rs | 24 +++ tests/test_util_name.rs | 1 + 79 files changed, 267 insertions(+), 140 deletions(-) diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index cd398a72b..3b7b4dc10 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -12,7 +12,7 @@ use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from_localized(args); + uu_app().get_matches_from_localized(args); let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, translate!("cannot-get-system")))?; diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index e60ea5366..619eb9b4a 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -100,7 +100,7 @@ pub fn parse_base_cmd_args( usage: &str, ) -> UResult { let command = base_app(about, usage); - let matches = command.try_get_matches_from_localized(args); + let matches = command.get_matches_from_localized(args); Config::from(&matches) } diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 802eb4464..557d981d7 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -30,7 +30,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // // Argument parsing // - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 22cd38f38..f69364310 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -231,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) { NumberingMode::NonEmpty diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index de1dc2f64..263d73475 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -304,7 +304,7 @@ struct Options { } fn parse_command_line(config: Command, args: impl uucore::Args) -> Result { - let matches = config.try_get_matches_from_localized(args); + let matches = config.get_matches_from_localized(args); let verbose = matches.get_flag(options::VERBOSE); diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index cb540aa7a..01fa0422c 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -113,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_localized(args); + .get_matches_from_localized(args); let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 959c23e40..dd36cfefc 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -237,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let check = matches.get_flag(options::CHECK); diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 81bcab418..8ba393b5b 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -281,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_localized(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(); diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3dee0fe01..01c08324e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -779,7 +779,7 @@ pub fn uu_app() -> Command { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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 8c98d37c5..4a13ad40f 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -605,7 +605,7 @@ where #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); // get the file to split let file_name = matches.get_one::(options::FILE).unwrap(); diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index cf9bfdbe4..1bc8cb0d6 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -483,7 +483,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }) .collect(); - let matches = uu_app().try_get_matches_from_localized(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); diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 1eadd6ce2..6a33aa74f 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -112,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_localized(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('+') { diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index d231da8b1..0de57fe70 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1416,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let settings: Settings = Parser::new().parse( matches diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 2d6d34b23..7db47355d 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -407,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); #[cfg(windows)] { diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index f6bd9ec40..0a58ce37a 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -121,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let files = matches .get_many::(options::FILE) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 73618d7e0..5f301edd7 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -22,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_localized(args); + .get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index b43cee530..12d94edf1 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -581,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let summarize = matches.get_flag(options::SUMMARIZE); diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 6dec840da..1e7a176b6 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -80,7 +80,7 @@ fn write_result( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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); diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 80c1af0e3..ed7ab7ff7 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -335,7 +335,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let matches = uu_app().try_get_matches_from_localized(&args); + let matches = uu_app().get_matches_from_localized(&args); let files = extract_files(&matches)?; diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index c3e743d85..34bc6b656 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -32,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let bytes = matches.get_flag(options::BYTES); let spaces = matches.get_flag(options::SPACES); diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index 9d6947223..156555798 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -48,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let users: Vec = matches .get_many::(options::USERS) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 4204b979e..199a09bc7 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -182,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_localized(args); + let matches = command.get_matches_from_localized(args); let input_length: Option<&usize> = if binary_name == "b2sum" { matches.get_one::(options::LENGTH) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index bfd8a076a..c3f1c2c0c 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -551,7 +551,7 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args_vec: Vec<_> = arg_iterate(args)?.collect(); - let matches = uu_app().try_get_matches_from_localized(args_vec); + 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 c29b57489..efbe75475 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -14,7 +14,7 @@ use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from_localized(args); + uu_app().get_matches_from_localized(args); hostid(); Ok(()) } diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 74f2c48d9..734cfdbed 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -61,7 +61,7 @@ mod wsa { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); #[cfg(windows)] let _handle = wsa::start().map_err_context(|| translate!("hostname-error-winsock"))?; diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 688ac232a..86fd86a17 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -122,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_localized(args); + .get_matches_from_localized(args); let users: Vec = matches .get_many::(options::ARG_USERS) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 338f90f6c..bb3663779 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -166,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let paths: Vec = matches .get_many::(ARG_FILES) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 6ba16d101..a55c9afd5 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -822,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let settings = parse_settings(&matches)?; diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index b1dc4dc92..aef9b45e3 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -41,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let mode = if matches.get_flag(options::TABLE) { Mode::Table diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 327ef09b0..8f18bf86b 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -20,7 +20,7 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(options::FILES) .unwrap_or_default() diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 5d5ce9bdc..d9a7afbd7 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -97,7 +97,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(after_help) - .try_get_matches_from_localized(args); + .get_matches_from_localized(args); /* the list of files */ diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index d34712339..bb34f4b74 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -24,7 +24,7 @@ fn get_userlogin() -> Option { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _ = uu_app().try_get_matches_from_localized(args); + let _ = uu_app().get_matches_from_localized(args); match get_userlogin() { Some(userlogin) => println!("{userlogin}"), diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index d1cf51647..c82309087 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -82,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_localized(args); + .get_matches_from_localized(args); let dirs = matches .get_many::(options::DIRS) diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 8032a5fad..ae3466da6 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -24,7 +24,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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)))?; diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 53b7eb8d2..b67e48e0c 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -112,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let file_type = matches.get_one::("type").unwrap(); diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 829e4f53b..473a7f859 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -152,7 +152,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { print!("\r"); println!("{panic_info}"); })); - let matches = uu_app().try_get_matches_from_localized(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(); diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 0fd44643e..b3ae7114e 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -153,7 +153,7 @@ static OPT_SELINUX: &str = "selinux"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let files: Vec = matches .get_many::(ARG_FILES) @@ -162,12 +162,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect(); if files.len() == 1 && !matches.contains_id(OPT_TARGET_DIRECTORY) { - return Err(UUsageError::new( - 1, - format!( - "The argument '<{ARG_FILES}>...' requires at least 2 values, but only 1 was provided" - ), - )); + let err = uu_app().error( + ErrorKind::TooFewValues, + translate!("mv-error-insufficient-arguments", "arg_files" => ARG_FILES), + ); + uucore::clap_localization::handle_clap_error_with_exit_code(err, uucore::util_name(), 1); } let overwrite_mode = determine_overwrite_mode(&matches); diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 414f5d735..f759b5926 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -177,7 +177,7 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let mut settings = Settings::default(); diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index 9d913d33e..7137ebda7 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -27,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let ignore = match matches.get_one::(OPT_IGNORE) { Some(numstr) => match numstr.trim().parse::() { diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 852016495..008f51558 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -255,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 3562e38c3..936e32c88 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -221,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_localized(&args); + let clap_matches = clap_opts.get_matches_from_localized(&args); let od_options = OdOptions::new(&clap_matches, &args)?; diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 6dc3d79e9..807e4debf 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -25,7 +25,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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(); diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index d5a318f0d..9016a4878 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -35,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); // set working mode let is_posix = matches.get_flag(options::POSIX); diff --git a/src/uu/pinky/src/platform/openbsd.rs b/src/uu/pinky/src/platform/openbsd.rs index c53839c47..d8dcbc928 100644 --- a/src/uu/pinky/src/platform/openbsd.rs +++ b/src/uu/pinky/src/platform/openbsd.rs @@ -10,7 +10,7 @@ use uucore::error::UResult; use uucore::translate; pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from_localized(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 e80f02d05..3581cedba 100644 --- a/src/uu/pinky/src/platform/unix.rs +++ b/src/uu/pinky/src/platform/unix.rs @@ -35,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_localized(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 3e7bcd3d9..fef4ba5cc 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -317,7 +317,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let opt_args = recreate_arguments(&args); let command = uu_app(); - let matches = command.try_get_matches_from_mut_localized(opt_args); + 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 6d875c537..a7809b830 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -15,7 +15,7 @@ static ARG_VARIABLES: &str = "variables"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let variables: Vec = matches .get_many::(ARG_VARIABLES) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 08408b40b..f5a7bc67c 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -22,7 +22,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let format = matches .get_one::(options::FORMAT) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index f2a26ae82..728c17d4c 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -729,7 +729,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let config = get_config(&matches)?; let input_files; diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 5fe5ae0ae..130b11096 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -110,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_localized(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 diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index c9f06ff5b..b9aca641c 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -30,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_localized(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); diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 9595e25f6..7030d0627 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -144,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let files: Vec<_> = matches .get_many::(ARG_FILES) diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index bbbcae744..25bc666b6 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -26,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let opts = Opts { ignore: matches.get_flag(OPT_IGNORE_FAIL_NON_EMPTY), diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 73f4c067c..adc0db2bd 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -239,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); if !matches.contains_id(options::FILE) { return Err(UUsageError::new( diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index d2e854b9b..3e80e745e 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -52,7 +52,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let mode = if matches.get_flag(options::ECHO) { Mode::Echo( diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 37d54ee99..87141e571 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -21,7 +21,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let numbers = matches .get_many::(options::NUMBER) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index a2c85464f..2bcb5a3a0 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -52,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); match Settings::from(&matches, obs_lines.as_deref()) { Ok(settings) => split(&settings), diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index f39e24842..0142f3dd5 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1221,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_localized(args); + .get_matches_from_localized(args); let stater = Stater::new(&matches)?; let exit_status = stater.exec(); diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 63b0a6e62..0391b32a5 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -243,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let opts = Options::from(&matches)?; diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 061fc2509..c45ffc944 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -99,7 +99,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 9e643aa7c..4efaebd5c 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -174,7 +174,7 @@ mod platform { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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()) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 4911a4b09..f24320183 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -33,7 +33,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let before = matches.get_flag(options::BEFORE); let regex = matches.get_flag(options::REGEX); diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 14c015bf7..29dfef9d5 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -52,7 +52,7 @@ enum OutputErrorMode { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(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); diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 1f413e4e0..d61802b3c 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -187,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let mut filenames: Vec<&String> = matches .get_many::(ARG_FILES) diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index fbcde13d4..c254252fb 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -41,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_localized(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); diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index dab1ffc9c..a83478d20 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -44,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let input = matches .get_one::(options::FILE) diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index c30c00bb4..84dd4ca7c 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -121,7 +121,7 @@ pub struct Options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from_localized(args); + let matches = uu_app().get_matches_from_localized(args); let options = Options { all: matches.get_flag(options::ALL), diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index c5935b40e..4fc5492c6 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -19,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let path: &Path = matches.get_one::(OPT_PATH).unwrap().as_ref(); diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index d8470e196..22a715306 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -48,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); #[cfg(unix)] let file_path = matches.get_one::(options::PATH); diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 92c50d7cc..9439a3efa 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -38,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_localized(args); + .get_matches_from_localized(args); let maybe_file: Option<&Path> = matches.get_one::(ARG_FILE).map(AsRef::as_ref); diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 140001c9e..8b3070989 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -377,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_localized(args); + let matches = uu_app().get_matches_from_localized(args); let settings = Settings::new(&matches); let inputs = Inputs::new(&matches)?; diff --git a/src/uu/who/src/platform/openbsd.rs b/src/uu/who/src/platform/openbsd.rs index 8d1e31dab..4a2954d27 100644 --- a/src/uu/who/src/platform/openbsd.rs +++ b/src/uu/who/src/platform/openbsd.rs @@ -12,7 +12,7 @@ use uucore::error::UResult; use uucore::translate; pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from_localized(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 70b46a593..7f7f5dc91 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -29,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_localized(args); + .get_matches_from_localized(args); let files: Vec = matches .get_many::(options::FILE) diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 3b0b11ead..40398accf 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -14,7 +14,7 @@ mod platform; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from_localized(args); + uu_app().get_matches_from_localized(args); let username = whoami()?; println_verbatim(username).map_err_context(|| translate!("whoami-error-failed-to-print"))?; Ok(()) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index ec818a955..5affbb6e3 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -22,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_localized(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(); diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index f5aed26f2..e14dbb0b7 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -13,16 +13,79 @@ use clap::error::{ContextKind, ErrorKind}; use clap::{ArgMatches, Command, Error}; use std::ffi::OsString; -/// Apply color to text using ANSI escape codes -pub fn colorize(text: &str, color_code: &str) -> String { - format!("\x1b[{color_code}m{text}\x1b[0m") +/// 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 { + // Most validation errors should 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 constants for consistent styling -pub mod colors { - pub const RED: &str = "31"; - pub const YELLOW: &str = "33"; - pub const GREEN: &str = "32"; +/// 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()) +} + +/// Display usage information and help suggestion for errors that require it +/// This consolidates the shared logic between clap errors and UUsageError +pub fn display_usage_and_help(util_name: &str) { + eprintln!(); + // Try to get usage information from localization + let usage_key = format!("{}-usage", util_name); + 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!(); + let help_msg = translate!("clap-error-help-suggestion", "command" => crate::execution_phrase()); + eprintln!("{help_msg}"); } pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: i32) -> ! { @@ -30,6 +93,19 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // If it's already initialized, that's fine - we'll use the existing one let _ = crate::locale::setup_localization_with_common(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 | ErrorKind::DisplayVersion => { // For help and version, use clap's built-in formatting and exit with 0 @@ -38,13 +114,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: std::process::exit(0); } ErrorKind::UnknownArgument => { - // Use clap's rendering system but capture the output to check if colors are used - let rendered = err.render(); - let rendered_str = rendered.to_string(); - - // Simple check - if the rendered output contains ANSI escape codes, colors are enabled - let colors_enabled = rendered_str.contains("\x1b["); - + // UnknownArgument gets special handling for suggestions, but should still show simple help if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { let arg_str = invalid_arg.to_string(); @@ -52,16 +122,9 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let error_word = translate!("common-error"); let tip_word = translate!("common-tip"); - // Apply colors only if they're enabled in the original error - let (colored_arg, colored_error_word, colored_tip_word) = if colors_enabled { - ( - colorize(&arg_str, colors::YELLOW), - colorize(&error_word, colors::RED), - colorize(&tip_word, colors::GREEN), - ) - } else { - (arg_str.clone(), error_word.clone(), tip_word.clone()) - }; + let colored_arg = maybe_colorize(&arg_str, Color::Yellow); + let colored_error_word = maybe_colorize(&error_word, Color::Red); + let colored_tip_word = maybe_colorize(&tip_word, Color::Green); // Print main error message let error_msg = translate!( @@ -75,11 +138,8 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // Show suggestion or generic tip let suggestion = err.get(ContextKind::SuggestedArg); if let Some(suggested_arg) = suggestion { - let colored_suggestion = if colors_enabled { - colorize(&suggested_arg.to_string(), colors::GREEN) - } else { - suggested_arg.to_string() - }; + let colored_suggestion = + maybe_colorize(&suggested_arg.to_string(), Color::Green); let suggestion_msg = translate!( "clap-error-similar-argument", "tip_word" => colored_tip_word, @@ -87,11 +147,8 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: ); eprintln!(" {suggestion_msg}"); } else { - let colored_tip_command = if colors_enabled { - colorize(&format!("-- {arg_str}"), colors::GREEN) - } else { - format!("-- {arg_str}") - }; + let colored_tip_command = + maybe_colorize(&format!("-- {arg_str}"), Color::Green); let tip_msg = translate!( "clap-error-pass-as-value", "arg" => colored_arg, @@ -101,36 +158,72 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: eprintln!(" {tip_msg}"); } - // Show usage and help + // Show usage information for unknown arguments but use simple --help format eprintln!(); + // Try to get usage information from localization + let usage_key = format!("{}-usage", util_name); + let usage_text = translate!(&usage_key); + let formatted_usage = crate::format_usage(&usage_text); let usage_label = translate!("common-usage"); - let usage_pattern = translate!(&format!("{util_name}-usage")); - eprintln!("{usage_label}: {usage_pattern}"); + eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); - - let help_msg = translate!("clap-error-help-suggestion", "command" => util_name); - eprintln!("{help_msg}"); + // Use simple --help format for GNU test compatibility + eprintln!("For more information, try '--help'."); std::process::exit(exit_code); } else { // Generic fallback case - let rendered = err.render(); - let rendered_str = rendered.to_string(); - let colors_enabled = rendered_str.contains("\x1b["); - - let colored_error_word = if colors_enabled { - colorize(&translate!("common-error"), colors::RED) - } else { - translate!("common-error") - }; + let colored_error_word = maybe_colorize(&translate!("common-error"), 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) => { + // For 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!("For more information, try '--help'."); + } else { + // Fallback to original rendering if we can't parse + eprint!("{}", err.render()); + } + std::process::exit(exit_code); + } _ => { - // For other errors, print using clap's formatter but exit with code 1 - eprint!("{}", err.render()); - std::process::exit(1); + // 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!("For more information, try '--help'."); + + std::process::exit(exit_code); } } } @@ -143,15 +236,15 @@ pub trait LocalizedCommand { where Self: Sized; - /// Try to get matches from args with localized error handling - fn try_get_matches_from_localized(self, itr: I) -> ArgMatches + /// 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; - /// Try to get matches from mutable args with localized error handling - fn try_get_matches_from_mut_localized(self, itr: I) -> ArgMatches + /// Get matches from mutable args with localized error handling + fn get_matches_from_mut_localized(self, itr: I) -> ArgMatches where Self: Sized, I: IntoIterator, @@ -164,7 +257,7 @@ impl LocalizedCommand for Command { .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) } - fn try_get_matches_from_localized(self, itr: I) -> ArgMatches + fn get_matches_from_localized(self, itr: I) -> ArgMatches where I: IntoIterator, T: Into + Clone, @@ -173,7 +266,7 @@ impl LocalizedCommand for Command { .unwrap_or_else(|err| handle_clap_error_with_exit_code(err, crate::util_name(), 1)) } - fn try_get_matches_from_mut_localized(mut self, itr: I) -> ArgMatches + fn get_matches_from_mut_localized(mut self, itr: I) -> ArgMatches where I: IntoIterator, T: Into + Clone, diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 6c4191a20..ba64152e7 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -856,6 +856,16 @@ 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("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_mv.rs b/tests/by-util/test_mv.rs index 8da94e864..2f2f5d1de 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -2547,3 +2547,27 @@ 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/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!( From c7342a939be384ae11c70ce007280e2904224a36 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 23:43:47 +0200 Subject: [PATCH 14/83] clap: override the usage section --- src/uucore/src/lib/mods/clap_localization.rs | 87 +++++++++++++------- tests/by-util/test_comm.rs | 15 ++++ 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index e14dbb0b7..5c47fa14e 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -114,57 +114,82 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: std::process::exit(0); } ErrorKind::UnknownArgument => { + // Force localization initialization - ignore any previous failures + crate::locale::setup_localization_with_common(util_name).ok(); + // UnknownArgument gets special handling for suggestions, but should still show simple help if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { let arg_str = invalid_arg.to_string(); - // Get the uncolored words from common strings - let error_word = translate!("common-error"); - let tip_word = translate!("common-tip"); + // Get the uncolored words from common strings with fallbacks + let error_word = { + let translated = translate!("common-error"); + if translated == "common-error" { "error".to_string() } else { translated } + }; + let tip_word = { + let translated = translate!("common-tip"); + if translated == "common-tip" { "tip".to_string() } else { translated } + }; let colored_arg = maybe_colorize(&arg_str, Color::Yellow); let colored_error_word = maybe_colorize(&error_word, Color::Red); let colored_tip_word = maybe_colorize(&tip_word, Color::Green); - // Print main error message - let error_msg = translate!( - "clap-error-unexpected-argument", - "arg" => colored_arg.clone(), - "error_word" => colored_error_word - ); + // Print main error message with fallback + let error_msg = { + let translated = translate!( + "clap-error-unexpected-argument", + "arg" => colored_arg.clone(), + "error_word" => colored_error_word.clone() + ); + if translated.starts_with("clap-error-unexpected-argument") { + format!("{}: unexpected argument '{}' found", colored_error_word, colored_arg) + } else { + translated + } + }; eprintln!("{error_msg}"); eprintln!(); - // Show suggestion or generic tip + // Show suggestion if available let suggestion = err.get(ContextKind::SuggestedArg); if let Some(suggested_arg) = suggestion { let colored_suggestion = maybe_colorize(&suggested_arg.to_string(), Color::Green); - let suggestion_msg = translate!( - "clap-error-similar-argument", - "tip_word" => colored_tip_word, - "suggestion" => colored_suggestion - ); - eprintln!(" {suggestion_msg}"); + let suggestion_msg = { + let translated = translate!( + "clap-error-similar-argument", + "tip_word" => colored_tip_word.clone(), + "suggestion" => colored_suggestion.clone() + ); + if translated.starts_with("clap-error-similar-argument") { + format!(" {}: a similar argument exists: '{}'", colored_tip_word, colored_suggestion) + } else { + format!(" {}", translated) + } + }; + eprintln!("{suggestion_msg}"); + eprintln!(); } else { - let colored_tip_command = - maybe_colorize(&format!("-- {arg_str}"), Color::Green); - let tip_msg = translate!( - "clap-error-pass-as-value", - "arg" => colored_arg, - "tip_word" => colored_tip_word, - "tip_command" => colored_tip_command - ); - eprintln!(" {tip_msg}"); + // 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 but use simple --help format - eprintln!(); - // Try to get usage information from localization + // Try to get usage information from localization with fallback let usage_key = format!("{}-usage", util_name); let usage_text = translate!(&usage_key); let formatted_usage = crate::format_usage(&usage_text); - let usage_label = translate!("common-usage"); + let usage_label = { + let translated = translate!("common-usage"); + if translated == "common-usage" { "Usage".to_string() } else { translated } + }; eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); // Use simple --help format for GNU test compatibility @@ -173,7 +198,11 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: std::process::exit(exit_code); } else { // Generic fallback case - let colored_error_word = maybe_colorize(&translate!("common-error"), Color::Red); + let error_word = { + let translated = translate!("common-error"); + if translated == "common-error" { "error".to_string() } else { translated } + }; + let colored_error_word = maybe_colorize(&error_word, Color::Red); eprintln!("{colored_error_word}: unexpected argument"); std::process::exit(exit_code); } 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'."); +} From 2063aa10090f0fc5db733484dbd180856efff084 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 23:45:01 +0200 Subject: [PATCH 15/83] clap: improve the clap support + add tests --- src/uucore/src/lib/mods/clap_localization.rs | 38 +++++++++++++++---- src/uucore/src/lib/mods/locale.rs | 40 ++++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 5c47fa14e..96e0681e2 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -116,7 +116,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: ErrorKind::UnknownArgument => { // Force localization initialization - ignore any previous failures crate::locale::setup_localization_with_common(util_name).ok(); - + // UnknownArgument gets special handling for suggestions, but should still show simple help if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { let arg_str = invalid_arg.to_string(); @@ -124,11 +124,19 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // Get the uncolored words from common strings with fallbacks let error_word = { let translated = translate!("common-error"); - if translated == "common-error" { "error".to_string() } else { translated } + if translated == "common-error" { + "error".to_string() + } else { + translated + } }; let tip_word = { let translated = translate!("common-tip"); - if translated == "common-tip" { "tip".to_string() } else { translated } + if translated == "common-tip" { + "tip".to_string() + } else { + translated + } }; let colored_arg = maybe_colorize(&arg_str, Color::Yellow); @@ -143,7 +151,10 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: "error_word" => colored_error_word.clone() ); if translated.starts_with("clap-error-unexpected-argument") { - format!("{}: unexpected argument '{}' found", colored_error_word, colored_arg) + format!( + "{}: unexpected argument '{}' found", + colored_error_word, colored_arg + ) } else { translated } @@ -151,7 +162,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: eprintln!("{error_msg}"); eprintln!(); - // Show suggestion if available + // Show suggestion if available let suggestion = err.get(ContextKind::SuggestedArg); if let Some(suggested_arg) = suggestion { let colored_suggestion = @@ -163,7 +174,10 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: "suggestion" => colored_suggestion.clone() ); if translated.starts_with("clap-error-similar-argument") { - format!(" {}: a similar argument exists: '{}'", colored_tip_word, colored_suggestion) + format!( + " {}: a similar argument exists: '{}'", + colored_tip_word, colored_suggestion + ) } else { format!(" {}", translated) } @@ -188,7 +202,11 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let formatted_usage = crate::format_usage(&usage_text); let usage_label = { let translated = translate!("common-usage"); - if translated == "common-usage" { "Usage".to_string() } else { translated } + if translated == "common-usage" { + "Usage".to_string() + } else { + translated + } }; eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); @@ -200,7 +218,11 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // Generic fallback case let error_word = { let translated = translate!("common-error"); - if translated == "common-error" { "error".to_string() } else { translated } + if translated == "common-error" { + "error".to_string() + } else { + translated + } }; let colored_error_word = maybe_colorize(&error_word, Color::Red); eprintln!("{colored_error_word}: unexpected argument"); diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index aec455cfe..8e1a09a23 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -1402,6 +1402,46 @@ invalid-syntax = This is { $missing panic!("Expected LocalizationError::ParseResource with snippet"); } } + + #[test] + 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 + + // 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 + + let tip_msg = get_message("common-tip"); + assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized + + // Now initialize with setup_localization_with_common + let result = setup_localization_with_common("comm"); + if result.is_err() { + // If setup fails (e.g., no embedded locales for comm), try with a known utility + let _ = setup_localization_with_common("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(); + } } #[cfg(all(test, not(debug_assertions)))] From 74be4c20609f22e0f9cb8a21b7d18bf792b5382e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 10 Aug 2025 09:31:22 +0200 Subject: [PATCH 16/83] Port printenv translation: fix tests/misc/invalid-opt --- src/uu/printenv/src/printenv.rs | 6 ++++-- src/uucore/src/lib/mods/clap_localization.rs | 8 ++++++++ tests/by-util/test_printenv.rs | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index a7809b830..ea93904a1 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -5,7 +5,6 @@ use clap::{Arg, ArgAction, Command}; use std::env; -use uucore::LocalizedCommand; use uucore::translate; use uucore::{error::UResult, format_usage}; @@ -15,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_localized(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) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 96e0681e2..673f15b22 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -117,6 +117,14 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // Force localization initialization - ignore any previous failures crate::locale::setup_localization_with_common(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, + }; + // UnknownArgument gets special handling for suggestions, but should still show simple help if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) { let arg_str = invalid_arg.to_string(); 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'"); +} From c86560a84b676a722dd067d3001120f8fd983d20 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 10 Aug 2025 09:38:53 +0200 Subject: [PATCH 17/83] Port factor for translation --- src/uucore/src/lib/mods/clap_localization.rs | 25 ++++++++------------ tests/by-util/test_factor.rs | 13 ++++++++++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 673f15b22..8ef624dd1 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -125,11 +125,15 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: _ => 1, }; - // UnknownArgument gets special handling for suggestions, but should still show simple help + // 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 the uncolored words from common strings with fallbacks + // Get localized error word with fallback let error_word = { let translated = translate!("common-error"); if translated == "common-error" { @@ -138,18 +142,9 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: translated } }; - let tip_word = { - let translated = translate!("common-tip"); - if translated == "common-tip" { - "tip".to_string() - } else { - translated - } - }; let colored_arg = maybe_colorize(&arg_str, Color::Yellow); let colored_error_word = maybe_colorize(&error_word, Color::Red); - let colored_tip_word = maybe_colorize(&tip_word, Color::Green); // Print main error message with fallback let error_msg = { @@ -173,6 +168,8 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // 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 = { @@ -203,9 +200,8 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: } } - // Show usage information for unknown arguments but use simple --help format - // Try to get usage information from localization with fallback - let usage_key = format!("{}-usage", util_name); + // 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 = { @@ -218,7 +214,6 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: }; eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); - // Use simple --help format for GNU test compatibility eprintln!("For more information, try '--help'."); std::process::exit(exit_code); 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(); From ef49b446d29a43d28e0295368c8973e6c79c9644 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 10 Aug 2025 11:19:20 +0200 Subject: [PATCH 18/83] clap: improve translation support --- src/bin/coreutils.rs | 22 +- src/uucore/src/lib/mods/clap_localization.rs | 74 +- src/uucore/src/lib/mods/locale.rs | 880 +++++++++---------- 3 files changed, 442 insertions(+), 534 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index c6d2283bd..64a79a3fd 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -81,18 +81,16 @@ fn find_prefixed_util<'a>( } fn setup_localization_or_exit(util_name: &str) { - locale::setup_localization_with_common(get_canonical_util_name(util_name)).unwrap_or_else( - |err| { - match err { - uucore::locale::LocalizationError::ParseResource { - error: err_msg, - snippet, - } => eprintln!("Localization parse error at {snippet}: {err_msg}"), - other => eprintln!("Could not init the localization system: {other}"), - } - process::exit(99) - }, - ); + locale::setup_localization(get_canonical_util_name(util_name)).unwrap_or_else(|err| { + match err { + uucore::locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + process::exit(99) + }); } #[allow(clippy::cognitive_complexity)] diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 8ef624dd1..11784cf53 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -89,9 +89,8 @@ pub fn display_usage_and_help(util_name: &str) { } pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: i32) -> ! { - // Try to ensure localization is initialized for this utility - // If it's already initialized, that's fine - we'll use the existing one - let _ = crate::locale::setup_localization_with_common(util_name); + // 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(); @@ -115,7 +114,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: } ErrorKind::UnknownArgument => { // Force localization initialization - ignore any previous failures - crate::locale::setup_localization_with_common(util_name).ok(); + crate::locale::setup_localization(util_name).ok(); // Choose exit code based on utility name let exit_code = match util_name { @@ -134,34 +133,17 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let arg_str = invalid_arg.to_string(); // Get localized error word with fallback - let error_word = { - let translated = translate!("common-error"); - if translated == "common-error" { - "error".to_string() - } else { - translated - } - }; + 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 = { - let translated = translate!( - "clap-error-unexpected-argument", - "arg" => colored_arg.clone(), - "error_word" => colored_error_word.clone() - ); - if translated.starts_with("clap-error-unexpected-argument") { - format!( - "{}: unexpected argument '{}' found", - colored_error_word, colored_arg - ) - } else { - translated - } - }; + let error_msg = translate!( + "clap-error-unexpected-argument", + "arg" => colored_arg.clone(), + "error_word" => colored_error_word.clone() + ); eprintln!("{error_msg}"); eprintln!(); @@ -172,21 +154,11 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let colored_tip_word = maybe_colorize(&tip_word, Color::Green); let colored_suggestion = maybe_colorize(&suggested_arg.to_string(), Color::Green); - let suggestion_msg = { - let translated = translate!( - "clap-error-similar-argument", - "tip_word" => colored_tip_word.clone(), - "suggestion" => colored_suggestion.clone() - ); - if translated.starts_with("clap-error-similar-argument") { - format!( - " {}: a similar argument exists: '{}'", - colored_tip_word, colored_suggestion - ) - } else { - format!(" {}", translated) - } - }; + let suggestion_msg = translate!( + "clap-error-similar-argument", + "tip_word" => colored_tip_word.clone(), + "suggestion" => colored_suggestion.clone() + ); eprintln!("{suggestion_msg}"); eprintln!(); } else { @@ -204,14 +176,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let usage_key = format!("{util_name}-usage"); let usage_text = translate!(&usage_key); let formatted_usage = crate::format_usage(&usage_text); - let usage_label = { - let translated = translate!("common-usage"); - if translated == "common-usage" { - "Usage".to_string() - } else { - translated - } - }; + let usage_label = translate!("common-usage"); eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); eprintln!("For more information, try '--help'."); @@ -219,14 +184,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: std::process::exit(exit_code); } else { // Generic fallback case - let error_word = { - let translated = translate!("common-error"); - if translated == "common-error" { - "error".to_string() - } else { - translated - } - }; + 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); diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 8e1a09a23..c8e735403 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -107,46 +107,6 @@ thread_local! { static LOCALIZER: OnceLock = const { OnceLock::new() }; } -/// Initialize localization with a specific locale and config -fn init_localization( - locale: &LanguageIdentifier, - locales_dir: &Path, - util_name: &str, -) -> Result<(), LocalizationError> { - 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) - })?; - - 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) { - // 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 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 @@ -167,7 +127,7 @@ fn find_uucore_locales_dir(utility_locales_dir: &Path) -> Option { } /// Create a bundle that combines common and utility-specific strings -fn create_bundle_with_common( +fn create_bundle( locale: &LanguageIdentifier, locales_dir: &Path, util_name: &str, @@ -209,7 +169,7 @@ fn create_bundle_with_common( } /// Initialize localization with common strings in addition to utility-specific strings -fn init_localization_with_common( +fn init_localization( locale: &LanguageIdentifier, locales_dir: &Path, util_name: &str, @@ -218,18 +178,17 @@ fn init_localization_with_common( .expect("Default locale should always be valid"); // Try to create a bundle that combines common and utility-specific strings - let english_bundle = create_bundle_with_common(&default_locale, locales_dir, util_name) - .or_else(|_| { - // Fallback to embedded utility-specific strings only - create_english_bundle_from_embedded(&default_locale, util_name) - })?; + 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 with common strings - if let Ok(primary_bundle) = create_bundle_with_common(locale, locales_dir, util_name) { + 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 { @@ -245,53 +204,29 @@ fn init_localization_with_common( 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, @@ -304,41 +239,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 { @@ -426,6 +351,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 @@ -466,42 +392,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. - match get_locales_dir(p) { - Ok(locales_dir) => init_localization(&locale, &locales_dir, p), - Err(_) => { - // No locales directory found, use embedded English 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)?; - let localizer = Localizer::new(english_bundle); - - LOCALIZER.with(|lock| { - lock.set(localizer) - .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) - })?; - Ok(()) - } - } -} - -/// Enhanced version of setup_localization that also loads common/clap error strings -/// This function loads both utility-specific strings and common strings for clap error handling -pub fn setup_localization_with_common(p: &str) -> Result<(), LocalizationError> { - let locale = detect_system_locale().unwrap_or_else(|_| { - LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") - }); - // Load common strings along with utility-specific strings match get_locales_dir(p) { Ok(locales_dir) => { // Load both utility-specific and common strings - init_localization_with_common(&locale, &locales_dir, p) + 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)?; @@ -664,6 +562,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"); @@ -729,31 +683,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(); @@ -765,13 +700,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"); } } @@ -780,24 +715,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(), ) @@ -812,7 +752,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(), ) @@ -829,12 +769,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(), ) @@ -854,7 +794,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(), ) @@ -872,10 +812,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 @@ -892,7 +829,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 @@ -913,7 +850,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 @@ -931,11 +868,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 { @@ -955,7 +892,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!"); @@ -964,16 +901,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; @@ -981,7 +908,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()); @@ -1000,7 +927,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(); @@ -1018,6 +945,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 @@ -1066,20 +1256,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 { @@ -1099,20 +1288,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 { @@ -1153,218 +1340,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 { @@ -1380,29 +1355,6 @@ invalid-syntax = This is { $missing assert!(bundle_string.contains("Bundle error: Bundle creation failed")); } - #[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_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_clap_localization_fallbacks() { std::thread::spawn(|| { @@ -1416,11 +1368,11 @@ invalid-syntax = This is { $missing let tip_msg = get_message("common-tip"); assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized - // Now initialize with setup_localization_with_common - let result = setup_localization_with_common("comm"); + // 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_with_common("test"); + let _ = setup_localization("test"); } // Test that common strings are available after initialization From 1e8bd80948f8c9927134527ffddf325c853e8270 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 10 Aug 2025 13:37:09 +0200 Subject: [PATCH 19/83] allow translation of "For more information, try '--help'." --- src/uucore/locales/en-US.ftl | 1 + src/uucore/locales/fr-FR.ftl | 1 + src/uucore/src/lib/mods/clap_localization.rs | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 358eda0d5..676f9ad3b 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -13,6 +13,7 @@ clap-error-unexpected-argument = { $error_word }: unexpected argument '{ $arg }' 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-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 diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index cbeab358d..8bd69e0f6 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -13,6 +13,7 @@ clap-error-unexpected-argument = { $error_word } : argument inattendu '{ $arg }' 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-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 diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 11784cf53..152c17ddb 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -179,7 +179,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let usage_label = translate!("common-usage"); eprintln!("{}: {}", usage_label, formatted_usage); eprintln!(); - eprintln!("For more information, try '--help'."); + eprintln!("{}", translate!("common-help-suggestion")); std::process::exit(exit_code); } else { @@ -199,7 +199,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: eprintln!("{}", main_error_line); eprintln!(); // Use the execution phrase for the help suggestion to match test expectations - eprintln!("For more information, try '--help'."); + eprintln!("{}", translate!("common-help-suggestion")); } else { // Fallback to original rendering if we can't parse eprint!("{}", err.render()); @@ -233,7 +233,7 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // For other errors, just show help suggestion eprintln!(); - eprintln!("For more information, try '--help'."); + eprintln!("{}", translate!("common-help-suggestion")); std::process::exit(exit_code); } From 517993f2260b7d421a48f4165c061df4102da001 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 08:35:27 +0200 Subject: [PATCH 20/83] clap: translate Usage too --- src/uu/arch/src/arch.rs | 1 + src/uu/base32/src/base_common.rs | 1 + src/uu/basename/src/basename.rs | 1 + src/uu/cat/src/cat.rs | 1 + src/uu/chcon/src/chcon.rs | 1 + src/uu/chgrp/src/chgrp.rs | 1 + src/uu/chmod/src/chmod.rs | 1 + src/uu/chown/src/chown.rs | 1 + src/uu/chroot/src/chroot.rs | 1 + src/uu/cksum/src/cksum.rs | 1 + src/uu/comm/src/comm.rs | 1 + src/uu/cp/src/cp.rs | 1 + src/uu/csplit/src/csplit.rs | 1 + src/uu/cut/src/cut.rs | 1 + src/uu/date/src/date.rs | 1 + src/uu/dd/src/dd.rs | 1 + src/uu/df/src/df.rs | 1 + src/uu/dircolors/src/dircolors.rs | 1 + src/uu/dirname/src/dirname.rs | 1 + src/uu/du/src/du.rs | 1 + src/uu/echo/src/echo.rs | 1 + src/uu/env/src/env.rs | 1 + src/uu/expand/src/expand.rs | 1 + src/uu/expr/src/expr.rs | 1 + src/uu/factor/src/factor.rs | 1 + src/uu/false/src/false.rs | 1 + src/uu/fmt/src/fmt.rs | 1 + src/uu/fold/src/fold.rs | 1 + src/uu/groups/src/groups.rs | 1 + src/uu/hashsum/src/hashsum.rs | 1 + src/uu/head/src/head.rs | 1 + src/uu/hostid/src/hostid.rs | 1 + src/uu/hostname/src/hostname.rs | 1 + src/uu/id/src/id.rs | 1 + src/uu/install/src/install.rs | 1 + src/uu/join/src/join.rs | 1 + src/uu/kill/src/kill.rs | 1 + src/uu/link/src/link.rs | 1 + src/uu/ln/src/ln.rs | 1 + src/uu/logname/src/logname.rs | 1 + src/uu/ls/src/ls.rs | 14 +- src/uu/mkdir/src/mkdir.rs | 1 + src/uu/mkfifo/src/mkfifo.rs | 1 + src/uu/mknod/src/mknod.rs | 1 + src/uu/mktemp/src/mktemp.rs | 1 + src/uu/more/src/more.rs | 1 + src/uu/mv/src/mv.rs | 1 + src/uu/nice/src/nice.rs | 1 + src/uu/nl/src/nl.rs | 1 + src/uu/nohup/src/nohup.rs | 1 + src/uu/nproc/src/nproc.rs | 1 + src/uu/numfmt/src/numfmt.rs | 1 + src/uu/od/src/od.rs | 1 + src/uu/paste/src/paste.rs | 1 + src/uu/pathchk/src/pathchk.rs | 1 + src/uu/pinky/src/pinky.rs | 1 + src/uu/pr/src/pr.rs | 1 + src/uu/printenv/src/printenv.rs | 1 + src/uu/printf/src/printf.rs | 1 + src/uu/ptx/src/ptx.rs | 1 + src/uu/pwd/src/pwd.rs | 1 + src/uu/readlink/src/readlink.rs | 1 + src/uu/realpath/src/realpath.rs | 1 + src/uu/rm/src/rm.rs | 1 + src/uu/rmdir/src/rmdir.rs | 1 + src/uu/runcon/src/runcon.rs | 1 + src/uu/seq/src/seq.rs | 1 + src/uu/shred/src/shred.rs | 1 + src/uu/shuf/src/shuf.rs | 1 + src/uu/sleep/src/sleep.rs | 1 + src/uu/sort/src/sort.rs | 13 +- src/uu/split/src/split.rs | 1 + src/uu/stat/src/stat.rs | 1 + src/uu/stdbuf/src/stdbuf.rs | 1 + src/uu/stty/src/stty.rs | 1 + src/uu/sum/src/sum.rs | 1 + src/uu/sync/src/sync.rs | 1 + src/uu/tac/src/tac.rs | 1 + src/uu/tail/src/args.rs | 1 + src/uu/tee/src/tee.rs | 1 + src/uu/test/src/test.rs | 4 +- src/uu/timeout/src/timeout.rs | 1 + src/uu/touch/src/touch.rs | 1 + src/uu/tr/src/tr.rs | 1 + src/uu/true/src/true.rs | 1 + src/uu/truncate/src/truncate.rs | 1 + src/uu/tsort/src/tsort.rs | 1 + src/uu/tty/src/tty.rs | 1 + src/uu/uname/src/uname.rs | 1 + src/uu/unexpand/src/unexpand.rs | 1 + src/uu/uniq/src/uniq.rs | 1 + src/uu/unlink/src/unlink.rs | 1 + src/uu/uptime/src/uptime.rs | 1 + src/uu/users/src/users.rs | 1 + src/uu/wc/src/wc.rs | 1 + src/uu/who/src/who.rs | 1 + src/uu/whoami/src/whoami.rs | 1 + src/uu/yes/src/yes.rs | 1 + src/uucore/src/lib/lib.rs | 35 +++++ src/uucore/src/lib/mods/clap_localization.rs | 48 ++++--- tests/by-util/test_sort.rs | 129 +++++++++++++++++++ 101 files changed, 298 insertions(+), 40 deletions(-) diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 3b7b4dc10..8fe80b602 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -24,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 619eb9b4a..db1e97016 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -107,6 +107,7 @@ pub fn parse_base_cmd_args( 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 557d981d7..61ac69289 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -82,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 f69364310..f03c7c3b9 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -287,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 263d73475..25c2d099e 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -157,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) 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 01fa0422c..92ea6024e 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -178,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 dd36cfefc..01e3da55f 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -344,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 8ba393b5b..b77c549d7 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -316,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 01c08324e..41cded5f0 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -521,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{}", diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 4a13ad40f..fc18b97da 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -630,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 1bc8cb0d6..57f0e61d1 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -595,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 6a33aa74f..1685844d9 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -265,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 0de57fe70..0ddeefeb4 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1443,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 7db47355d..3a7a10f92 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -463,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 0a58ce37a..87a459f29 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -239,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 5f301edd7..aac8e57f3 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -64,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 12d94edf1..64a7662bb 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -805,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 1e7a176b6..8516b1eb6 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -122,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 a0a3f944f..108349031 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -47,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 ed7ab7ff7..46f9d547f 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -353,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 34bc6b656..22fc6df23 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -62,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 156555798..17624ffde 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -82,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 199a09bc7..0fc23e35f 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -312,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 c3f1c2c0c..818062ba9 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -72,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) diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index efbe75475..8a4f5efd8 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -22,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 734cfdbed..acefbf732 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.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!("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 86fd86a17..0852d9927 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -350,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/src/install.rs b/src/uu/install/src/install.rs index bb3663779..66e80b90d 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -184,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 a55c9afd5..dd1323506 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -855,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 aef9b45e3..e749e982b 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -100,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 8f18bf86b..773d52d20 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -37,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 d9a7afbd7..8b21bd093 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -144,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 bb34f4b74..1cf1872a8 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -37,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 5e9477c29..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, @@ -1104,18 +1104,9 @@ impl Config { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = match uu_app().try_get_matches_from(args) { - // clap successfully parsed the arguments: 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) => { + // Use localization handler for all clap errors uucore::clap_localization::handle_clap_error_with_exit_code(e, "ls", 2); } }; @@ -1134,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 c82309087..fb82d963c 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -112,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 ae3466da6..5cae85cc8 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -84,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 b67e48e0c..d922c3b82 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -171,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 b1dbacf71..23d22d361 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -397,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 473a7f859..8aa6b7729 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -213,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 b3ae7114e..b89290149 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -226,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 f759b5926..890de1e86 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -230,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 7137ebda7..dd53e81b9 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -93,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 008f51558..e5cec7692 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -280,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 936e32c88..e63a29a00 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -252,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 807e4debf..82a03f93b 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.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!("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 9016a4878..c46ed39ae 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -82,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/pr/src/pr.rs b/src/uu/pr/src/pr.rs index fef4ba5cc..75fea4be1 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -159,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"))) diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index ea93904a1..3ae77a77e 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -57,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 f5a7bc67c..e48d253f5 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -85,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 728c17d4c..b8352e06f 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -771,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 130b11096..fccf8c7b3 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -142,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 b9aca641c..f634c970a 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -108,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 7030d0627..9e3fa9b39 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -227,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 25bc666b6..d2c2ac105 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -171,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 adc0db2bd..c158678a4 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -316,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 3e80e745e..45c1e4ece 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -146,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 87141e571..4cbaa6e6f 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -40,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 29346315b..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; @@ -1051,14 +1051,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // 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). 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(), 2); - } - e.print().unwrap(); - if e.use_stderr() { - set_exit_code(2); - } - return Ok(()); + // Always use the localization handler for proper error and help handling + handle_clap_error_with_exit_code(e, uucore::util_name(), 2); } }; @@ -1349,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 2bcb5a3a0..70c475782 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -228,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 0142f3dd5..4d5dde949 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1235,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 0391b32a5..69400c855 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -1007,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 c45ffc944..2366a59d3 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -138,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 4efaebd5c..d58463591 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -227,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 f24320183..799e65571 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -57,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 29dfef9d5..5c6a120e7 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -95,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 d61802b3c..451b5b560 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -255,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 c254252fb..709cf227b 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -171,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 a83478d20..33822a474 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.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())) .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 b893fa8b4..1ac6ed362 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -58,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 84dd4ca7c..d23d8b7fc 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -142,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 4fc5492c6..14602cf38 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -30,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 22a715306..6e028fbd6 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -72,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 9439a3efa..ca63e527e 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -90,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 8b3070989..fa111e4e4 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -388,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/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 40398accf..f65dfd078 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -28,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 5affbb6e3..028f6ba7e 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -41,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/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 8c8dbb74e..6b1419e62 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -228,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/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 152c17ddb..824b606e5 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -17,7 +17,7 @@ use std::ffi::OsString; /// Based on clap's own design patterns and error categorization fn should_show_simple_help_for_clap_error(kind: ErrorKind) -> bool { match kind { - // Most validation errors should show simple help + // Show simple help ErrorKind::InvalidValue | ErrorKind::InvalidSubcommand | ErrorKind::ValueValidation @@ -73,21 +73,6 @@ fn colorize(text: &str, color: Color) -> String { format!("\x1b[{}m{text}\x1b[0m", color.code()) } -/// Display usage information and help suggestion for errors that require it -/// This consolidates the shared logic between clap errors and UUsageError -pub fn display_usage_and_help(util_name: &str) { - eprintln!(); - // Try to get usage information from localization - let usage_key = format!("{}-usage", util_name); - 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!(); - let help_msg = translate!("clap-error-help-suggestion", "command" => crate::execution_phrase()); - eprintln!("{help_msg}"); -} - 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); @@ -106,8 +91,25 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: }; match err.kind() { - ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { - // For help and version, use clap's built-in formatting and exit with 0 + 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); @@ -204,7 +206,15 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: // Fallback to original rendering if we can't parse eprint!("{}", err.render()); } - std::process::exit(exit_code); + + // 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 diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 15872affc..73046a7e5 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1606,4 +1606,133 @@ fn test_argument_suggestion() { } } } + +#[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("LC_ALL", locale) + .arg("--unknown-option") + .env("LANG", locale) + .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("LC_ALL", "en_US.UTF-8") + .arg("--help") + .env("LANG", "en_US.UTF-8") + .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("LC_ALL", "fr_FR.UTF-8") + .arg("--help") + .env("LANG", "fr_FR.UTF-8") + .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("error:")); + assert!(stderr_en.contains("-k")); + + // Test in French + let result_fr = new_ucmd!() + .env("LANG", "fr_FR.UTF-8") + .env("LC_ALL", "fr_FR.UTF-8") + .arg("-k") + .fails(); + + let stderr_fr = result_fr.stderr_str(); + // The main error message should contain French "erreur" + assert!(stderr_fr.contains("error:") || stderr_fr.contains("erreur")); +} + +#[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("LC_ALL", locale) + .arg("-k") + .env("LANG", locale) + .arg("invalid") + .fails(); + + let stderr = result.stderr_str(); + assert!(stderr.contains(expected_message)); + } +} + +#[test] +fn test_clap_localization_tip_for_value_with_dash() { + // Test tip for passing values that look like options + let result_en = new_ucmd!() + 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 + ]; + assert!(stderr_en.contains("tip:") || stderr_en.contains("conseil:")); + 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)); + } + let result_fr = new_ucmd!() + } + } + // The tip should be preserved from clap + assert!(stderr_fr.contains("tip:") || stderr_fr.contains("conseil:")); /* spell-checker: enable */ From 89989770d85b2c3fe8b86e861e0c80125bd5392d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 22:03:59 +0200 Subject: [PATCH 21/83] clap: also support translations for invalid values --- src/uucore/locales/en-US.ftl | 3 + src/uucore/locales/fr-FR.ftl | 3 + src/uucore/src/lib/mods/clap_localization.rs | 72 +++++++++++++++++++- tests/by-util/test_ls.rs | 36 ++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 676f9ad3b..da3dbaf3c 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -12,6 +12,9 @@ common-version = version 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'. diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index 8bd69e0f6..b6c5a7bcf 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -12,6 +12,9 @@ common-version = version 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'. diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 824b606e5..8f3bbb5a4 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -6,11 +6,17 @@ //! 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 @@ -194,7 +200,71 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, 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) => { - // For simple validation errors, use the same simple format as other errors + // 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(); + + // Get localized error word + let error_word = translate!("common-error"); + let colored_error_word = maybe_colorize(&error_word, Color::Red); + + // Apply color to value and option if colors are enabled + let colored_value = maybe_colorize(&value, Color::Yellow); + let colored_option = maybe_colorize(&option, Color::Green); + + // Print localized error message + let error_msg = translate!( + "clap-error-invalid-value", + "error_word" => colored_error_word, + "value" => colored_value, + "option" => colored_option + ); + eprintln!("{error_msg}"); + + // For ValueValidation errors, include the validation error details + if matches!(kind, ErrorKind::ValueValidation) { + if let Some(source) = err.source() { + eprintln!(" {}", source); + } + } + + // 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 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: From 1d8dafcea1a91b1207efa3197f6774938ea21cdc Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 23:26:13 +0200 Subject: [PATCH 22/83] fix some last clap errors mgmt --- src/uucore/src/lib/lib.rs | 2 +- src/uucore/src/lib/mods/clap_localization.rs | 57 +++++++++++++------- tests/by-util/test_base32.rs | 5 +- tests/by-util/test_base64.rs | 3 +- tests/by-util/test_du.rs | 4 +- tests/by-util/test_sort.rs | 13 +---- tests/by-util/test_split.rs | 5 +- 7 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 6b1419e62..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")] diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 8f3bbb5a4..c2b665830 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -214,30 +214,49 @@ pub fn handle_clap_error_with_exit_code(err: Error, util_name: &str, exit_code: let option = arg.to_string(); let value = value.to_string(); - // Get localized error word - let error_word = translate!("common-error"); - let colored_error_word = maybe_colorize(&error_word, Color::Red); + // 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); - // Apply color to value and option if colors are enabled - 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 + ); - // Print localized error message - let error_msg = translate!( - "clap-error-invalid-value", - "error_word" => colored_error_word, - "value" => colored_value, - "option" => colored_option - ); - eprintln!("{error_msg}"); - - // For ValueValidation errors, include the validation error details - if matches!(kind, ErrorKind::ValueValidation) { - if let Some(source) = err.source() { - eprintln!(" {}", source); + // 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) { 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_du.rs b/tests/by-util/test_du.rs index ba64152e7..86c358724 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -862,7 +862,9 @@ fn test_du_threshold_error_handling() { new_ucmd!() .arg("--threshold") .fails() - .stderr_contains("a value is required for '--threshold ' but none was supplied") + .stderr_contains( + "error: a value is required for '--threshold ' but none was supplied", + ) .stderr_contains("For more information, try '--help'."); } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 73046a7e5..70e294d6c 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1674,19 +1674,8 @@ fn test_clap_localization_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("error:")); + assert!(stderr_en.contains(" a value is required for '--key ' but none was supplied")); assert!(stderr_en.contains("-k")); - - // Test in French - let result_fr = new_ucmd!() - .env("LANG", "fr_FR.UTF-8") - .env("LC_ALL", "fr_FR.UTF-8") - .arg("-k") - .fails(); - - let stderr_fr = result_fr.stderr_str(); - // The main error message should contain French "erreur" - assert!(stderr_fr.contains("error:") || stderr_fr.contains("erreur")); } #[test] 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] From fe74a5e8e6ca493e43af80a37c3322f4f4ca379e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 13 Aug 2025 14:18:42 +0200 Subject: [PATCH 23/83] l10n: address review comments --- src/uucore/src/lib/features/perms.rs | 7 ++++--- src/uucore/src/lib/features/proc_info.rs | 2 ++ src/uucore/src/lib/mods/locale.rs | 3 +++ tests/by-util/test_dd.rs | 4 ++-- tests/by-util/test_mv.rs | 1 - tests/by-util/test_sort.rs | 18 +++++++----------- 6 files changed, 18 insertions(+), 17 deletions(-) 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/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index c8e735403..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; 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_mv.rs b/tests/by-util/test_mv.rs index 2f2f5d1de..23d7315fb 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -2550,7 +2550,6 @@ fn test_mv_selinux_context() { #[test] fn test_mv_error_usage_display_missing_arg() { - new_ucmd!() .arg("--target-directory=.") .fails() diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 70e294d6c..5c1bb1570 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1630,9 +1630,9 @@ fn test_clap_localization_unknown_argument() { for (locale, expected_strings) in test_cases { let result = new_ucmd!() + .env("LANG", locale) .env("LC_ALL", locale) .arg("--unknown-option") - .env("LANG", locale) .fails(); result.code_is(2); // sort uses exit code 2 for invalid options @@ -1647,9 +1647,9 @@ fn test_clap_localization_unknown_argument() { 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") - .env("LANG", "en_US.UTF-8") .succeeds(); let stdout_en = result_en.stdout_str(); @@ -1658,9 +1658,9 @@ fn test_clap_localization_help_message() { // 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") - .env("LANG", "fr_FR.UTF-8") .succeeds(); let stdout_fr = result_fr.stdout_str(); @@ -1687,9 +1687,9 @@ fn test_clap_localization_invalid_value() { for (locale, expected_message) in test_cases { let result = new_ucmd!() + .env("LANG", locale) .env("LC_ALL", locale) .arg("-k") - .env("LANG", locale) .arg("invalid") .fails(); @@ -1700,13 +1700,11 @@ fn test_clap_localization_invalid_value() { #[test] fn test_clap_localization_tip_for_value_with_dash() { - // Test tip for passing values that look like options - let result_en = new_ucmd!() 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 ]; - assert!(stderr_en.contains("tip:") || stderr_en.contains("conseil:")); + for (locale, expected_strings) in test_cases { let result = new_ucmd!() .env("LANG", locale) @@ -1719,9 +1717,7 @@ fn test_clap_localization_tip_for_value_with_dash() { for expected in expected_strings { assert!(stderr.contains(expected)); } - let result_fr = new_ucmd!() - } } - // The tip should be preserved from clap - assert!(stderr_fr.contains("tip:") || stderr_fr.contains("conseil:")); +} + /* spell-checker: enable */ From 8fcd4ea579f4b90affd9e31272bccf5daeb80812 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:57:33 +0000 Subject: [PATCH 24/83] chore(deps): update actions/checkout action to v5 --- .github/workflows/l10n.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index ccec22d54..d6909eb5c 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -136,7 +136,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable From c00c92ccc68c95774f561d6264ad40573251c3f0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 00:06:41 +0200 Subject: [PATCH 25/83] fuzz: add fuzzer to test non-UTF-8 path handling across all utilities --- fuzz/Cargo.toml | 6 + fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 180 +++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 fuzz/fuzz_targets/fuzz_non_utf8_paths.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 363c10064..142821224 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -138,3 +138,9 @@ name = "fuzz_cksum" path = "fuzz_targets/fuzz_cksum.rs" test = false doc = false + +[[bin]] +name = "fuzz_non_utf8_paths" +path = "fuzz_targets/fuzz_non_utf8_paths.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs new file mode 100644 index 000000000..3fffa4a30 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -0,0 +1,180 @@ +// 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. + +#![no_main] +use libfuzzer_sys::fuzz_target; +use rand::prelude::IndexedRandom; +use std::env::temp_dir; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::PathBuf; + +use uufuzz::{run_gnu_cmd, CommandResult}; +// Programs that typically take file/path arguments and should be tested +static PATH_PROGRAMS: &[&str] = &[ + "basename", "cat", "chmod", "cp", "dirname", "du", "head", "ln", "ls", "mkdir", "mv", + "readlink", "realpath", "rm", "rmdir", "tail", "touch", "unlink", +]; + +fn generate_non_utf8_bytes() -> Vec { + let mut rng = rand::rng(); + let mut bytes = Vec::new(); + + // Start with some valid UTF-8 to make it look like a reasonable path + bytes.extend_from_slice(b"test_"); + + // Add some invalid UTF-8 sequences + match rng.random_range(0..4) { + 0 => bytes.extend_from_slice(&[0xFF, 0xFE]), // Invalid UTF-8 + 1 => bytes.extend_from_slice(&[0xC0, 0x80]), // Overlong encoding + 2 => bytes.extend_from_slice(&[0xED, 0xA0, 0x80]), // UTF-16 surrogate + _ => bytes.extend_from_slice(&[0xF4, 0x90, 0x80, 0x80]), // Beyond Unicode range + } + + bytes +} + +fn generate_non_utf8_osstring() -> OsString { + OsString::from_vec(generate_non_utf8_bytes()) +} + +fn setup_test_files() -> Result<(PathBuf, Vec), std::io::Error> { + let mut rng = rand::rng(); + let temp_root = temp_dir().join(format!("utf8_test_{}", rng.random::())); + fs::create_dir_all(&temp_root)?; + + let mut test_files = Vec::new(); + + // Create some files with non-UTF-8 names + for i in 0..3 { + let mut path_bytes = temp_root.as_os_str().as_bytes().to_vec(); + path_bytes.push(b'/'); + + if i == 0 { + // One normal UTF-8 file for comparison + path_bytes.extend_from_slice(b"normal_file.txt"); + } else { + // Files with invalid UTF-8 names + path_bytes.extend_from_slice(&generate_non_utf8_bytes()); + } + + let file_path = PathBuf::from(OsStr::from_bytes(&path_bytes)); + + // Try to create the file - this may fail on some filesystems + if let Ok(mut file) = fs::File::create(&file_path) { + use std::io::Write; + let _ = write!(file, "test content for file {}\n", i); + test_files.push(file_path); + } + } + + Ok((temp_root, test_files)) +} + +fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResult { + let path_os = path.as_os_str(); + let args = vec![OsString::from(program), path_os.to_owned()]; + + // Try to run the GNU version to compare behavior + match run_gnu_cmd(program, &args[1..], false, None) { + Ok(result) => result, + Err(error_result) => { + // GNU command failed, return the error + error_result + } + } +} + +fn cleanup_test_files(temp_root: &PathBuf) { + let _ = fs::remove_dir_all(temp_root); +} + +fn check_for_utf8_error_and_panic(result: &CommandResult, program: &str, path: &PathBuf) { + let stderr_lower = result.stderr.to_lowercase(); + let is_utf8_error = stderr_lower.contains("invalid utf-8") + || stderr_lower.contains("not valid unicode") + || stderr_lower.contains("invalid utf8") + || stderr_lower.contains("utf-8 error"); + + if is_utf8_error { + println!( + "UTF-8 conversion error detected in {}: {}", + program, result.stderr + ); + println!("Path: {:?}", path); + println!("Exit code: {}", result.exit_code); + panic!( + "FUZZER FAILURE: {} failed with UTF-8 error on non-UTF-8 path: {:?}", + program, path + ); + } +} + +fuzz_target!(|_data: &[u8]| { + let mut rng = rand::rng(); + + // Set up test environment + let (temp_root, test_files) = match setup_test_files() { + Ok(files) => files, + Err(_) => return, // Skip if we can't set up test files + }; + + // Pick a random program that works with paths + let program = PATH_PROGRAMS.choose(&mut rng).unwrap(); + + // Test with files that have non-UTF-8 names + for test_file in &test_files { + let result = test_program_with_non_utf8_path(program, test_file); + + // Check if the program handled the non-UTF-8 path gracefully + // This will panic on the first UTF-8 error found + check_for_utf8_error_and_panic(&result, program, test_file); + + // Special test for chmod since that's what the bug report specifically mentions + if *program == "chmod" && test_file.to_string_lossy().contains('\u{FFFD}') { + // This path contains replacement characters, indicating invalid UTF-8 + println!("Testing chmod with non-UTF-8 path: {:?}", test_file); + + // Try chmod with basic permissions + let chmod_args = vec![ + OsString::from("chmod"), + OsString::from("644"), + test_file.as_os_str().to_owned(), + ]; + + let chmod_result = run_gnu_cmd("chmod", &chmod_args[1..], false, None); + match chmod_result { + Ok(result) => { + check_for_utf8_error_and_panic(&result, "chmod", test_file); + } + Err(error) => { + check_for_utf8_error_and_panic(&error, "chmod", test_file); + } + } + } + } + + // Test creating directories with non-UTF-8 names + if *program == "mkdir" { + let non_utf8_dir_name = generate_non_utf8_osstring(); + let non_utf8_dir = temp_root.join(non_utf8_dir_name); + + let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; + + let mkdir_result = run_gnu_cmd("mkdir", &mkdir_args[1..], false, None); + match mkdir_result { + Ok(result) => { + check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); + } + Err(error) => { + check_for_utf8_error_and_panic(&error, "mkdir", &non_utf8_dir); + } + } + } + + // Clean up + cleanup_test_files(&temp_root); +}); From 1cfb30ecb3c2b8df2cefd022d2c3e83d4ff474a0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 00:12:31 +0200 Subject: [PATCH 26/83] chmod: fix handling of non-UTF-8 filenames --- src/uu/chmod/src/chmod.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 92ea6024e..d8152d8bd 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -119,11 +119,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let quiet = matches.get_flag(options::QUIET); let verbose = matches.get_flag(options::VERBOSE); let preserve_root = matches.get_flag(options::PRESERVE_ROOT); - let fmode = match matches.get_one::(options::REFERENCE) { + let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode() & 0o7777), Err(_) => { - return Err(ChmodError::CannotStat(fref.to_string()).into()); + return Err(ChmodError::CannotStat(fref.to_string_lossy().to_string()).into()); } }, None => None, @@ -135,16 +135,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { modes.unwrap().to_string() // modes is required }; - // FIXME: enable non-utf8 paths - let mut files: Vec = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) + let mut files: Vec = matches + .get_many::(options::FILE) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let cmode = if fmode.is_some() { // "--reference" and MODE are mutually exclusive // if "--reference" was used MODE needs to be interpreted as another FILE // it wasn't possible to implement this behavior directly with clap - files.push(cmode); + files.push(OsString::from(cmode)); None } else { Some(cmode) @@ -236,6 +235,7 @@ pub fn uu_app() -> Command { Arg::new(options::REFERENCE) .long("reference") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .help(translate!("chmod-help-reference")), ) .arg( @@ -248,7 +248,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .required_unless_present(options::MODE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) // Add common arguments with chgrp, chown & chmod .args(uucore::perms::common_args()) @@ -267,11 +268,10 @@ struct Chmoder { } impl Chmoder { - fn chmod(&self, files: &[String]) -> UResult<()> { + fn chmod(&self, files: &[OsString]) -> UResult<()> { let mut r = Ok(()); for filename in files { - let filename = &filename[..]; let file = Path::new(filename); if !file.exists() { if file.is_symlink() { @@ -285,18 +285,22 @@ impl Chmoder { } if !self.quiet { - show!(ChmodError::DanglingSymlink(filename.to_string())); + show!(ChmodError::DanglingSymlink( + filename.to_string_lossy().to_string() + )); set_exit_code(1); } if self.verbose { println!( "{}", - translate!("chmod-verbose-failed-dangling", "file" => filename.quote()) + translate!("chmod-verbose-failed-dangling", "file" => filename.to_string_lossy().quote()) ); } } else if !self.quiet { - show!(ChmodError::NoSuchFile(filename.to_string())); + show!(ChmodError::NoSuchFile( + filename.to_string_lossy().to_string() + )); } // GNU exits with exit code 1 even if -q or --quiet are passed // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. @@ -308,8 +312,8 @@ impl Chmoder { // should not change the permissions in this case continue; } - if self.recursive && self.preserve_root && filename == "/" { - return Err(ChmodError::PreserveRoot(filename.to_string()).into()); + if self.recursive && self.preserve_root && file == Path::new("/") { + return Err(ChmodError::PreserveRoot("/".to_string()).into()); } if self.recursive { r = self.walk_dir_with_context(file, true); From c366551b020b7ead53d307de205f5375b0f8d701 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 00:18:12 +0200 Subject: [PATCH 27/83] tests: add test for chmod with non-UTF-8 filenames --- tests/by-util/test_chmod.rs | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index ea5d3f898..a93815dba 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -1183,3 +1183,88 @@ fn test_chmod_recursive_symlink_combinations() { 0o100_600 ); } + +#[test] +#[cfg(target_os = "linux")] +fn test_chmod_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a file with non-UTF-8 name + // Using bytes that form an invalid UTF-8 sequence + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the file using OpenOptions with the non-UTF-8 name + OpenOptions::new() + .mode(0o644) + .create(true) + .write(true) + .truncate(true) + .open(at.plus(non_utf8_name)) + .unwrap(); + + // Verify initial permissions + let initial_perms = metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode(); + assert_eq!(initial_perms & 0o777, 0o644); + + // Test chmod with the non-UTF-8 filename + scene + .ucmd() + .arg("755") + .arg(non_utf8_name) + .succeeds() + .no_stderr(); + + // Verify permissions were changed + let new_perms = metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode(); + assert_eq!(new_perms & 0o777, 0o755); + + // Test with multiple non-UTF-8 files + let non_utf8_bytes2 = b"file_\xC0\x80.dat"; + let non_utf8_name2 = OsStr::from_bytes(non_utf8_bytes2); + + OpenOptions::new() + .mode(0o666) + .create(true) + .write(true) + .truncate(true) + .open(at.plus(non_utf8_name2)) + .unwrap(); + + // Change permissions on both files at once + scene + .ucmd() + .arg("644") + .arg(non_utf8_name) + .arg(non_utf8_name2) + .succeeds() + .no_stderr(); + + // Verify both files have the new permissions + assert_eq!( + metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o644 + ); + assert_eq!( + metadata(at.plus(non_utf8_name2)) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o644 + ); +} From b301131a673515747ff790d698435b1d72fdead9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:27 +0200 Subject: [PATCH 28/83] dirname: fix handling of non-UTF-8 filenames --- src/uu/dirname/src/dirname.rs | 10 ++++++---- tests/by-util/test_dirname.rs | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index aac8e57f3..2782c7fb3 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::path::Path; use uucore::LocalizedCommand; use uucore::display::print_verbatim; @@ -26,8 +27,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - let dirnames: Vec = matches - .get_many::(options::DIR) + let dirnames: Vec = matches + .get_many::(options::DIR) .unwrap_or_default() .cloned() .collect(); @@ -47,7 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } None => { - if p.is_absolute() || path == "/" { + if p.is_absolute() || path.as_os_str() == "/" { print!("/"); } else { print!("."); @@ -79,6 +80,7 @@ pub fn uu_app() -> Command { Arg::new(options::DIR) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/tests/by-util/test_dirname.rs b/tests/by-util/test_dirname.rs index e73ff2b09..933e882d7 100644 --- a/tests/by-util/test_dirname.rs +++ b/tests/by-util/test_dirname.rs @@ -64,3 +64,23 @@ fn test_pwd() { fn test_empty() { new_ucmd!().arg("").succeeds().stdout_is(".\n"); } + +#[test] +#[cfg(unix)] +fn test_dirname_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE/file.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Test that dirname handles non-UTF-8 paths without crashing + let result = new_ucmd!().arg(non_utf8_name).succeeds(); + + // Just verify it didn't crash and produced some output + // The exact output format may vary due to lossy conversion + let output = result.stdout_str_lossy(); + assert!(!output.is_empty()); + assert!(output.contains("test_")); +} From becbc0e19d14a4ed8274ee37a6cdb6dad3bdb829 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:32 +0200 Subject: [PATCH 29/83] head: fix handling of non-UTF-8 filenames --- src/uu/head/src/head.rs | 50 ++++++++++++++++++++++++++++---------- tests/by-util/test_head.rs | 34 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 818062ba9..7c543b157 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -49,6 +49,7 @@ enum HeadError { ParseError(String), #[error("{}", translate!("head-error-bad-encoding"))] + #[allow(dead_code)] BadEncoding, #[error("{}", translate!("head-error-num-too-large"))] @@ -129,6 +130,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILES_NAME) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } @@ -186,7 +188,9 @@ fn arg_iterate<'a>( None => Ok(Box::new(vec![first, second].into_iter().chain(args))), } } else { - Err(HeadError::BadEncoding) + // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option + // like "-5". Treat it as a regular file argument. + Ok(Box::new(vec![first, second].into_iter().chain(args))) } } else { Ok(Box::new(vec![first].into_iter())) @@ -200,7 +204,7 @@ struct HeadOptions { pub line_ending: LineEnding, pub presume_input_pipe: bool, pub mode: Mode, - pub files: Vec, + pub files: Vec, } impl HeadOptions { @@ -215,9 +219,9 @@ impl HeadOptions { options.mode = Mode::from(matches)?; - options.files = match matches.get_many::(options::FILES_NAME) { + options.files = match matches.get_many::(options::FILES_NAME) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; Ok(options) @@ -463,8 +467,8 @@ fn head_file(input: &mut File, options: &HeadOptions) -> io::Result { fn uu_head(options: &HeadOptions) -> UResult<()> { let mut first = true; for file in &options.files { - let res = match file.as_str() { - "-" => { + let res = match file.to_str() { + Some("-") => { if (options.files.len() > 1 && !options.quiet) || options.verbose { if !first { println!(); @@ -508,12 +512,12 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { Ok(()) } - name => { - let mut file = match File::open(name) { + Some(name) => { + let mut file_handle = match File::open(file) { Ok(f) => f, Err(err) => { show!(err.map_err_context( - || translate!("head-error-cannot-open", "name" => name.quote()) + || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) )); continue; } @@ -524,15 +528,35 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { } println!("==> {name} <=="); } - head_file(&mut file, options)?; + head_file(&mut file_handle, options)?; + Ok(()) + } + None => { + // Handle files with non-UTF-8 names + let mut file_handle = match File::open(file) { + Ok(f) => f, + Err(err) => { + show!(err.map_err_context( + || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) + )); + continue; + } + }; + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + println!(); + } + println!("==> {} <==", file.to_string_lossy()); + } + head_file(&mut file_handle, options)?; Ok(()) } }; if let Err(e) = res { - let name = if file.as_str() == "-" { - "standard input" + let name = if file == "-" { + "standard input".to_string() } else { - file + file.to_string_lossy().into_owned() }; return Err(HeadError::Io { name: name.to_string(), diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 30f8378b9..2cdabdf3d 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -858,3 +858,37 @@ fn test_write_to_dev_full() { } } } + +#[test] +#[cfg(target_os = "linux")] +fn test_head_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file with some content + std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap(); + + // Test that head handles non-UTF-8 file names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // The result should contain the file content + let output = result.stdout_str_lossy(); + assert!(output.contains("line1")); + assert!(output.contains("line2")); + assert!(output.contains("line3")); + + // Test with line count argument + scene.ucmd() + .args(&["-n", "2"]) + .arg(non_utf8_name) + .succeeds() + .stdout_contains("line1") + .stdout_contains("line2"); +} From ce7c57175423147b1872656e900ea3147298d029 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:36 +0200 Subject: [PATCH 30/83] realpath: fix handling of non-UTF-8 filenames --- src/uu/realpath/src/realpath.rs | 15 ++++++++------- tests/by-util/test_realpath.rs | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 1b4ea6028..f855f5cea 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -5,8 +5,9 @@ // spell-checker:ignore (ToDO) retcode -use clap::{Arg, ArgAction, ArgMatches, Command, builder::NonEmptyStringValueParser}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use std::{ + ffi::OsString, io::{Write, stdout}, path::{Path, PathBuf}, }; @@ -40,7 +41,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { /* the list of files */ let paths: Vec = matches - .get_many::(ARG_FILES) + .get_many::(ARG_FILES) .unwrap() .map(PathBuf::from) .collect(); @@ -145,21 +146,21 @@ pub fn uu_app() -> Command { Arg::new(OPT_RELATIVE_TO) .long(OPT_RELATIVE_TO) .value_name("DIR") - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(clap::value_parser!(OsString)) .help(translate!("realpath-help-relative-to")), ) .arg( Arg::new(OPT_RELATIVE_BASE) .long(OPT_RELATIVE_BASE) .value_name("DIR") - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(clap::value_parser!(OsString)) .help(translate!("realpath-help-relative-base")), ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) .required(true) - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::AnyPath), ) } @@ -174,10 +175,10 @@ fn prepare_relative_options( resolve_mode: ResolveMode, ) -> UResult<(Option, Option)> { let relative_to = matches - .get_one::(OPT_RELATIVE_TO) + .get_one::(OPT_RELATIVE_TO) .map(PathBuf::from); let relative_base = matches - .get_one::(OPT_RELATIVE_BASE) + .get_one::(OPT_RELATIVE_BASE) .map(PathBuf::from); let relative_to = canonicalize_relative_option(relative_to, can_mode, resolve_mode)?; let relative_base = canonicalize_relative_option(relative_base, can_mode, resolve_mode)?; diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index ee156f5d0..bc1bf9276 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -464,3 +464,28 @@ fn test_realpath_trailing_slash() { fn test_realpath_empty() { new_ucmd!().fails_with_code(1); } + +#[test] +#[cfg(target_os = "linux")] +fn test_realpath_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file + at.touch(non_utf8_name); + + // Test that realpath handles non-UTF-8 paths without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // The result should contain the non-UTF-8 bytes + let output = result.stdout_str_lossy(); + assert!(output.contains("test_")); + assert!(output.contains(".txt")); +} From b8702bd5df391150504c48e32dbe1782817b1f77 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:40 +0200 Subject: [PATCH 31/83] ln: fix handling of non-UTF-8 filenames --- src/uu/ln/src/ln.rs | 14 +++++++------ tests/by-util/test_ln.rs | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 8b21bd093..d047ab12e 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -34,7 +34,7 @@ pub struct Settings { symbolic: bool, relative: bool, logical: bool, - target_dir: Option, + target_dir: Option, no_target_dir: bool, no_dereference: bool, verbose: bool, @@ -102,7 +102,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { /* the list of files */ let paths: Vec = matches - .get_many::(ARG_FILES) + .get_many::(ARG_FILES) .unwrap() .map(PathBuf::from) .collect(); @@ -131,8 +131,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { logical, relative: matches.get_flag(options::RELATIVE), target_dir: matches - .get_one::(options::TARGET_DIRECTORY) - .map(String::from), + .get_one::(options::TARGET_DIRECTORY) + .map(PathBuf::from), no_target_dir: matches.get_flag(options::NO_TARGET_DIRECTORY), no_dereference: matches.get_flag(options::NO_DEREFERENCE), verbose: matches.get_flag(options::VERBOSE), @@ -210,6 +210,7 @@ pub fn uu_app() -> Command { .help(translate!("ln-help-target-directory")) .value_name("DIRECTORY") .value_hint(clap::ValueHint::DirPath) + .value_parser(clap::value_parser!(OsString)) .conflicts_with(options::NO_TARGET_DIRECTORY), ) .arg( @@ -238,6 +239,7 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)) .required(true) .num_args(1..), ) @@ -245,9 +247,9 @@ pub fn uu_app() -> Command { fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { // Handle cases where we create links in a directory first. - if let Some(ref name) = settings.target_dir { + if let Some(ref target_path) = settings.target_dir { // 4th form: a directory is specified by -t. - return link_files_in_dir(files, &PathBuf::from(name), settings); + return link_files_in_dir(files, target_path, settings); } if !settings.no_target_dir { if files.len() == 1 { diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 998595035..bdcbe20ec 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -843,3 +843,46 @@ fn test_ln_seen_file() { ); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_ln_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + let non_utf8_link_bytes = b"link_\xFF\xFE.txt"; + let non_utf8_link_name = OsStr::from_bytes(non_utf8_link_bytes); + + // Create the actual file + at.touch(non_utf8_name); + + // Test creating a hard link with non-UTF-8 file names + scene.ucmd() + .arg(non_utf8_name) + .arg(non_utf8_link_name) + .succeeds(); + + // Both files should exist + assert!(at.file_exists(non_utf8_name)); + assert!(at.file_exists(non_utf8_link_name)); + + // Test creating a symbolic link with non-UTF-8 file names + let symlink_bytes = b"symlink_\xFF\xFE.txt"; + let symlink_name = OsStr::from_bytes(symlink_bytes); + + scene.ucmd() + .args(&["-s"]) + .arg(non_utf8_name) + .arg(symlink_name) + .succeeds(); + + // Check if symlink was created successfully + let symlink_path = at.plus(symlink_name); + assert!(symlink_path.is_symlink()); +} From a0f1e34d101045c1f5c9df5ab861ed626f2b0b52 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:45 +0200 Subject: [PATCH 32/83] rm: fix handling of non-UTF-8 filenames --- src/uu/rm/src/rm.rs | 20 ++++++++++---------- tests/by-util/test_rm.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 9e3fa9b39..c00a94639 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -31,17 +31,17 @@ enum RmError { #[error("{}", translate!("rm-error-missing-operand", "util_name" => uucore::execution_phrase()))] MissingOperand, #[error("{}", translate!("rm-error-cannot-remove-no-such-file", "file" => _0.quote()))] - CannotRemoveNoSuchFile(String), + CannotRemoveNoSuchFile(OsString), #[error("{}", translate!("rm-error-cannot-remove-permission-denied", "file" => _0.quote()))] - CannotRemovePermissionDenied(String), + CannotRemovePermissionDenied(OsString), #[error("{}", translate!("rm-error-cannot-remove-is-directory", "file" => _0.quote()))] - CannotRemoveIsDirectory(String), + CannotRemoveIsDirectory(OsString), #[error("{}", translate!("rm-error-dangerous-recursive-operation"))] DangerousRecursiveOperation, #[error("{}", translate!("rm-error-use-no-preserve-root"))] UseNoPreserveRoot, - #[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0))] - RefusingToRemoveDirectory(String), + #[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0.to_string_lossy()))] + RefusingToRemoveDirectory(OsString), } impl UError for RmError {} @@ -366,7 +366,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool { } else { show_error!( "{}", - RmError::CannotRemoveNoSuchFile(filename.to_string_lossy().to_string()) + RmError::CannotRemoveNoSuchFile(filename.to_os_string()) ); true } @@ -542,7 +542,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { if path_is_current_or_parent_directory(path) { show_error!( "{}", - RmError::RefusingToRemoveDirectory(path.display().to_string()) + RmError::RefusingToRemoveDirectory(path.as_os_str().to_os_string()) ); return true; } @@ -559,7 +559,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } else { show_error!( "{}", - RmError::CannotRemoveIsDirectory(path.to_string_lossy().to_string()) + RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string()) ); had_err = true; } @@ -580,7 +580,7 @@ fn remove_dir(path: &Path, options: &Options) -> bool { if !options.dir && !options.recursive { show_error!( "{}", - RmError::CannotRemoveIsDirectory(path.to_string_lossy().to_string()) + RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string()) ); return true; } @@ -621,7 +621,7 @@ fn remove_file(path: &Path, options: &Options) -> bool { // GNU compatibility (rm/fail-eacces.sh) show_error!( "{}", - RmError::CannotRemovePermissionDenied(path.to_string_lossy().to_string()) + RmError::CannotRemovePermissionDenied(path.as_os_str().to_os_string()) ); } else { show_error!("cannot remove {}: {e}", path.quote()); diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index ec7de9136..18dbec8fa 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1037,3 +1037,43 @@ fn test_inaccessible_dir_recursive() { assert!(!at.dir_exists("a/unreadable")); assert!(!at.dir_exists("a")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_rm_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file + at.touch(non_utf8_name); + assert!(at.file_exists(non_utf8_name)); + + // Test that rm handles non-UTF-8 file names without crashing + scene.ucmd() + .arg(non_utf8_name) + .succeeds(); + + // The file should be removed + assert!(!at.file_exists(non_utf8_name)); + + // Test with directory + let non_utf8_dir_bytes = b"test_dir_\xFF\xFE"; + let non_utf8_dir_name = OsStr::from_bytes(non_utf8_dir_bytes); + + at.mkdir(non_utf8_dir_name); + assert!(at.dir_exists(non_utf8_dir_name)); + + scene.ucmd() + .args(&["-r"]) + .arg(non_utf8_dir_name) + .succeeds(); + + assert!(!at.dir_exists(non_utf8_dir_name)); +} From 57fff7e032b0d048f9fb274d3fc9761e2ca92754 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:32:50 +0200 Subject: [PATCH 33/83] unlink: fix handling of non-UTF-8 filenames --- tests/by-util/test_unlink.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index 905f579a6..002a5929c 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -75,3 +75,31 @@ fn test_unlink_symlink() { assert!(at.file_exists("foo")); assert!(!at.file_exists("bar")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_unlink_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + use uutests::util::TestScenario; + use uutests::util_name; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file + at.touch(non_utf8_name); + assert!(at.file_exists(non_utf8_name)); + + // Test that unlink handles non-UTF-8 file names without crashing + scene.ucmd() + .arg(non_utf8_name) + .succeeds(); + + // The file should be removed + assert!(!at.file_exists(non_utf8_name)); +} From a3ebaaa730b5d6a8845533e01cdda58ef2081190 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 10:51:28 +0200 Subject: [PATCH 34/83] cat: fix handling of non-UTF-8 filenames --- src/uu/cat/src/cat.rs | 14 ++++++++------ tests/by-util/test_cat.rs | 26 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index f03c7c3b9..d19c39f56 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -10,6 +10,7 @@ mod platform; use crate::platform::is_unsafe_overwrite; use clap::{Arg, ArgAction, Command}; use memchr::memchr2; +use std::ffi::OsString; use std::fs::{File, metadata}; use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write}; /// Unix domain socket support @@ -267,9 +268,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .any(|v| matches.get_flag(v)); let squeeze_blank = matches.get_flag(options::SQUEEZE_BLANK); - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let options = OutputOptions { @@ -294,6 +295,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) .arg( @@ -379,7 +381,7 @@ fn cat_handle( } } -fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { +fn cat_path(path: &OsString, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { match get_input_type(path)? { InputType::StdIn => { let stdin = io::stdin(); @@ -417,7 +419,7 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat } } -fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { +fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> { let mut state = OutputState { line_number: LineNumber::new(), at_line_start: true, @@ -452,8 +454,8 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { /// # Arguments /// /// * `path` - Path on a file system to classify metadata -fn get_input_type(path: &str) -> CatResult { - if path == "-" { +fn get_input_type(path: &OsString) -> CatResult { + if path.to_str() == Some("-") { return Ok(InputType::StdIn); } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index c3e25c6d0..647775a3a 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -14,9 +14,9 @@ use std::process::Stdio; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +use uutests::util_name; #[cfg(not(windows))] use uutests::util::vec_of_size; -use uutests::util_name; #[test] fn test_output_simple() { @@ -747,6 +747,30 @@ fn test_write_fast_read_error() { ucmd.arg("foo").fails().stderr_contains("Permission denied"); } +#[test] +#[cfg(target_os = "linux")] +fn test_cat_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file with some content + std::fs::write(at.plus(non_utf8_name), "Hello, non-UTF-8 world!\n").unwrap(); + + // Test that cat handles non-UTF-8 file names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // The result should contain the file content + let output = result.stdout_str_lossy(); + assert_eq!(output, "Hello, non-UTF-8 world!\n"); +} + #[test] #[cfg(target_os = "linux")] fn test_appending_same_input_output() { From c0da27addcad59ba27bb220a835fdf22624a762f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 11:07:26 +0200 Subject: [PATCH 35/83] touch: fix handling of non-UTF-8 filenames --- src/uu/touch/src/touch.rs | 28 +++++++++++++++++----------- tests/by-util/test_touch.rs | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 451b5b560..7354c20c9 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -157,7 +157,7 @@ fn is_first_filename_timestamp( reference: Option<&OsString>, date: Option<&str>, timestamp: Option<&str>, - files: &[&String], + files: &[&OsString], ) -> bool { if timestamp.is_none() && reference.is_none() @@ -166,8 +166,11 @@ fn is_first_filename_timestamp( // env check is last as the slowest op && matches!(std::env::var("_POSIX2_VERSION").as_deref(), Ok("199209")) { - let s = files[0]; - all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) + if let Some(s) = files[0].to_str() { + all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) + } else { + false + } } else { false } @@ -189,8 +192,8 @@ fn shr2(s: &str) -> String { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let mut filenames: Vec<&String> = matches - .get_many::(ARG_FILES) + let mut filenames: Vec<&OsString> = matches + .get_many::(ARG_FILES) .ok_or_else(|| { USimpleError::new( 1, @@ -211,12 +214,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|t| t.to_owned()); if is_first_filename_timestamp(reference, date.as_deref(), timestamp.as_deref(), &filenames) { - timestamp = if filenames[0].len() == 10 { - Some(shr2(filenames[0])) - } else { - Some(filenames[0].to_string()) - }; - filenames = filenames[1..].to_vec(); + if let Some(first_file) = filenames[0].to_str() { + timestamp = if first_file.len() == 10 { + Some(shr2(first_file)) + } else { + Some(first_file.to_string()) + }; + filenames = filenames[1..].to_vec(); + } } let source = if let Some(reference) = reference { @@ -338,6 +343,7 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .num_args(1..) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::AnyPath), ) .group( diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 6c2d3ff1b..88c91ebcb 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1013,3 +1013,26 @@ fn test_touch_f_option() { assert!(at.file_exists(file)); at.remove(file); } + +#[test] +#[cfg(target_os = "linux")] +fn test_touch_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Test that touch handles non-UTF-8 file names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // Verify no output and file was created + result.no_output(); + + // Check that the file was created (using the raw path) + assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); +} From 5c77fe1ab5538f0667dd108f6705ecf7665e76b8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 11:20:49 +0200 Subject: [PATCH 36/83] readlink: fix handling of non-UTF-8 filenames --- src/uu/readlink/src/readlink.rs | 16 +++++++++------- tests/by-util/test_readlink.rs | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index f634c970a..c5e28ca8b 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) errno use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs; use std::io::{Write, stdout}; use std::path::{Path, PathBuf}; @@ -54,9 +55,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { MissingHandling::Normal }; - let files: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec = matches + .get_many::(ARG_FILES) + .map(|v| v.map(PathBuf::from).collect()) .unwrap_or_default(); if files.is_empty() { @@ -78,7 +79,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; for f in &files { - let p = PathBuf::from(f); + let p = f; let path_result = if res_mode == ResolveMode::None { fs::read_link(&p) } else { @@ -93,7 +94,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return if verbose { Err(USimpleError::new( 1, - err.map_err_context(move || f.maybe_quote().to_string()) + err.map_err_context(move || f.to_string_lossy().to_string()) .to_string(), )) } else { @@ -171,13 +172,14 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::AnyPath), ) } fn show(path: &Path, line_ending: Option) -> std::io::Result<()> { - let path = path.to_str().unwrap(); - print!("{path}"); + use uucore::display::print_verbatim; + print_verbatim(path)?; if let Some(line_ending) = line_ending { print!("{line_ending}"); } diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index f837f2b4a..06b92ec2a 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -373,3 +373,28 @@ fn test_delimiters() { .stderr_contains("ignoring --no-newline with multiple arguments") .stdout_is("/a\n/a\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_readlink_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a target file and a symlink with non-UTF-8 bytes in the name + at.touch("target_file"); + let non_utf8_bytes = b"symlink_\xFF\xFE"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create symlink using std::os::unix::fs::symlink + std::os::unix::fs::symlink(at.plus_as_string("target_file"), at.plus(non_utf8_name)).unwrap(); + + // Test that readlink handles non-UTF-8 symlink names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // The result should contain the target path + let output = result.stdout_str_lossy(); + assert!(output.contains("target_file")); +} From 581e752598c37de66b24b931534f00fec7e6706e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 11:26:09 +0200 Subject: [PATCH 37/83] ln: fix handling of non-UTF-8 filenames --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 29 ++++++++++++++++++------ src/uu/ln/src/ln.rs | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index 3fffa4a30..62298d921 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -76,13 +76,26 @@ fn setup_test_files() -> Result<(PathBuf, Vec), std::io::Error> { fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResult { let path_os = path.as_os_str(); - let args = vec![OsString::from(program), path_os.to_owned()]; - // Try to run the GNU version to compare behavior - match run_gnu_cmd(program, &args[1..], false, None) { + // Use the locally built uutils binary instead of system PATH + let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; + + // Build appropriate arguments for each program + let local_args = match program { + "chmod" => vec![OsString::from(program), OsString::from("644"), path_os.to_owned()], + "cp" | "mv" | "ln" => { + // These need a destination - create a temp destination + let dest_path = path.with_extension("dest"); + vec![OsString::from(program), path_os.to_owned(), dest_path.as_os_str().to_owned()] + }, + _ => vec![OsString::from(program), path_os.to_owned()], + }; + + // Try to run the local uutils version + match run_gnu_cmd(local_binary, &local_args, false, None) { Ok(result) => result, Err(error_result) => { - // GNU command failed, return the error + // Local command failed, return the error error_result } } @@ -138,14 +151,15 @@ fuzz_target!(|_data: &[u8]| { // This path contains replacement characters, indicating invalid UTF-8 println!("Testing chmod with non-UTF-8 path: {:?}", test_file); - // Try chmod with basic permissions + // Try chmod with basic permissions using local binary + let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; let chmod_args = vec![ OsString::from("chmod"), OsString::from("644"), test_file.as_os_str().to_owned(), ]; - let chmod_result = run_gnu_cmd("chmod", &chmod_args[1..], false, None); + let chmod_result = run_gnu_cmd(local_binary, &chmod_args, false, None); match chmod_result { Ok(result) => { check_for_utf8_error_and_panic(&result, "chmod", test_file); @@ -162,9 +176,10 @@ fuzz_target!(|_data: &[u8]| { let non_utf8_dir_name = generate_non_utf8_osstring(); let non_utf8_dir = temp_root.join(non_utf8_dir_name); + let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; - let mkdir_result = run_gnu_cmd("mkdir", &mkdir_args[1..], false, None); + let mkdir_result = run_gnu_cmd(local_binary, &mkdir_args, false, None); match mkdir_result { Ok(result) => { check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index d047ab12e..a45fc17c6 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -61,7 +61,7 @@ enum LnError { #[error("{}", translate!("ln-error-missing-destination", "operand" => _0.quote()))] MissingDestination(PathBuf), - #[error("{}", translate!("ln-error-extra-operand", "operand" => format!("{_0:?}").trim_matches('"'), "program" => _1.clone()))] + #[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))] ExtraOperand(OsString, String), } From f15dac138ead8c3ae33cbf86ca87beec485e29b9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 13 Aug 2025 20:29:13 +0200 Subject: [PATCH 38/83] chmod/readlink/du: improve non-UTF-8 filename handling and code style --- src/uu/du/src/du.rs | 23 ++++++++++++++++++----- src/uu/readlink/src/readlink.rs | 4 ++-- tests/by-util/test_cat.rs | 10 +++++----- tests/by-util/test_head.rs | 12 +++++++----- tests/by-util/test_ln.rs | 20 +++++++++++--------- tests/by-util/test_readlink.rs | 8 ++++---- tests/by-util/test_realpath.rs | 8 ++++---- tests/by-util/test_rm.rs | 25 ++++++++++--------------- tests/by-util/test_touch.rs | 8 ++++---- tests/by-util/test_unlink.rs | 12 +++++------- 10 files changed, 70 insertions(+), 60 deletions(-) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 64a7662bb..64c99998a 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -7,9 +7,14 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; +#[cfg(unix)] +use std::ffi::OsStr; +use std::ffi::OsString; use std::fs::Metadata; use std::fs::{self, DirEntry, File}; use std::io::{BufRead, BufReader, stdout}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; #[cfg(windows)] @@ -568,6 +573,9 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { ); set_exit_code(1); } else { + #[cfg(unix)] + let p = PathBuf::from(OsStr::from_bytes(&path)); + #[cfg(windows)] let p = PathBuf::from(String::from_utf8_lossy(&path).to_string()); if !paths.contains(&p) { paths.push(p); @@ -594,21 +602,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { summarize, )?; - let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { - if file_from == "-" && matches.get_one::(options::FILE).is_some() { + let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { + if file_from.to_string_lossy() == "-" + && matches.get_one::(options::FILE).is_some() + { return Err(std::io::Error::other( translate!("du-error-extra-operand-with-files0-from", "file" => matches - .get_one::(options::FILE) + .get_one::(options::FILE) .unwrap() + .to_string_lossy() .quote() ), ) .into()); } - read_files_from(file_from)? - } else if let Some(files) = matches.get_many::(options::FILE) { + read_files_from(&file_from.to_string_lossy())? + } else if let Some(files) = matches.get_many::(options::FILE) { let files = files.map(PathBuf::from); if count_links { files.collect() @@ -984,6 +995,7 @@ pub fn uu_app() -> Command { .long("files0-from") .value_name("FILE") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .help(translate!("du-help-files0-from")) .action(ArgAction::Append), ) @@ -1010,6 +1022,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)) .action(ArgAction::Append), ) } diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index c5e28ca8b..4d5e32f78 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -81,9 +81,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for f in &files { let p = f; let path_result = if res_mode == ResolveMode::None { - fs::read_link(&p) + fs::read_link(p) } else { - canonicalize(&p, can_mode, res_mode) + canonicalize(p, can_mode, res_mode) }; match path_result { diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 647775a3a..c809231c7 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -14,9 +14,9 @@ use std::process::Stdio; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; -use uutests::util_name; #[cfg(not(windows))] use uutests::util::vec_of_size; +use uutests::util_name; #[test] fn test_output_simple() { @@ -755,17 +755,17 @@ fn test_cat_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create the actual file with some content std::fs::write(at.plus(non_utf8_name), "Hello, non-UTF-8 world!\n").unwrap(); - + // Test that cat handles non-UTF-8 file names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - + // The result should contain the file content let output = result.stdout_str_lossy(); assert_eq!(output, "Hello, non-UTF-8 world!\n"); diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 2cdabdf3d..88c9e5358 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -867,17 +867,17 @@ fn test_head_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create the actual file with some content std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap(); - + // Test that head handles non-UTF-8 file names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - + // The result should contain the file content let output = result.stdout_str_lossy(); assert!(output.contains("line1")); @@ -885,10 +885,12 @@ fn test_head_non_utf8_paths() { assert!(output.contains("line3")); // Test with line count argument - scene.ucmd() + scene + .ucmd() .args(&["-n", "2"]) .arg(non_utf8_name) .succeeds() .stdout_contains("line1") .stdout_contains("line2"); } + // Test that head handles non-UTF-8 file names without crashing diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index bdcbe20ec..71f9b5716 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -852,36 +852,38 @@ fn test_ln_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); let non_utf8_link_bytes = b"link_\xFF\xFE.txt"; let non_utf8_link_name = OsStr::from_bytes(non_utf8_link_bytes); - + // Create the actual file at.touch(non_utf8_name); - + // Test creating a hard link with non-UTF-8 file names - scene.ucmd() + scene + .ucmd() .arg(non_utf8_name) .arg(non_utf8_link_name) .succeeds(); - + // Both files should exist assert!(at.file_exists(non_utf8_name)); assert!(at.file_exists(non_utf8_link_name)); - + // Test creating a symbolic link with non-UTF-8 file names let symlink_bytes = b"symlink_\xFF\xFE.txt"; let symlink_name = OsStr::from_bytes(symlink_bytes); - - scene.ucmd() + + scene + .ucmd() .args(&["-s"]) .arg(non_utf8_name) .arg(symlink_name) .succeeds(); - + // Check if symlink was created successfully let symlink_path = at.plus(symlink_name); assert!(symlink_path.is_symlink()); diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 06b92ec2a..c3791a5fb 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -382,18 +382,18 @@ fn test_readlink_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a target file and a symlink with non-UTF-8 bytes in the name at.touch("target_file"); let non_utf8_bytes = b"symlink_\xFF\xFE"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create symlink using std::os::unix::fs::symlink std::os::unix::fs::symlink(at.plus_as_string("target_file"), at.plus(non_utf8_name)).unwrap(); - + // Test that readlink handles non-UTF-8 symlink names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - + // The result should contain the target path let output = result.stdout_str_lossy(); assert!(output.contains("target_file")); diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index bc1bf9276..a081b163f 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -473,17 +473,17 @@ fn test_realpath_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create the actual file at.touch(non_utf8_name); - + // Test that realpath handles non-UTF-8 paths without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - + // The result should contain the non-UTF-8 bytes let output = result.stdout_str_lossy(); assert!(output.contains("test_")); diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 18dbec8fa..e14268a20 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1046,34 +1046,29 @@ fn test_rm_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create the actual file at.touch(non_utf8_name); assert!(at.file_exists(non_utf8_name)); - + // Test that rm handles non-UTF-8 file names without crashing - scene.ucmd() - .arg(non_utf8_name) - .succeeds(); - + scene.ucmd().arg(non_utf8_name).succeeds(); + // The file should be removed assert!(!at.file_exists(non_utf8_name)); - + // Test with directory let non_utf8_dir_bytes = b"test_dir_\xFF\xFE"; let non_utf8_dir_name = OsStr::from_bytes(non_utf8_dir_bytes); - + at.mkdir(non_utf8_dir_name); assert!(at.dir_exists(non_utf8_dir_name)); - - scene.ucmd() - .args(&["-r"]) - .arg(non_utf8_dir_name) - .succeeds(); - + + scene.ucmd().args(&["-r"]).arg(non_utf8_dir_name).succeeds(); + assert!(!at.dir_exists(non_utf8_dir_name)); } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 88c91ebcb..aa088204a 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1022,17 +1022,17 @@ fn test_touch_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Test that touch handles non-UTF-8 file names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - + // Verify no output and file was created result.no_output(); - + // Check that the file was created (using the raw path) assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); } diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index 002a5929c..b508fef38 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -86,20 +86,18 @@ fn test_unlink_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - + // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - + // Create the actual file at.touch(non_utf8_name); assert!(at.file_exists(non_utf8_name)); - + // Test that unlink handles non-UTF-8 file names without crashing - scene.ucmd() - .arg(non_utf8_name) - .succeeds(); - + scene.ucmd().arg(non_utf8_name).succeeds(); + // The file should be removed assert!(!at.file_exists(non_utf8_name)); } From 5f575d5e5fc57c344e2b1e4985119ca9d2856ae9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 12:10:35 +0200 Subject: [PATCH 39/83] fuzz: improve fuzzer performance and program distribution --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 94 ++++++++++++------------ 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index 62298d921..cfdc60e8b 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -6,6 +6,8 @@ #![no_main] use libfuzzer_sys::fuzz_target; use rand::prelude::IndexedRandom; +use rand::Rng; +use std::collections::HashSet; use std::env::temp_dir; use std::ffi::{OsStr, OsString}; use std::fs; @@ -79,15 +81,23 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu // Use the locally built uutils binary instead of system PATH let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; - + // Build appropriate arguments for each program let local_args = match program { - "chmod" => vec![OsString::from(program), OsString::from("644"), path_os.to_owned()], + "chmod" => vec![ + OsString::from(program), + OsString::from("644"), + path_os.to_owned(), + ], "cp" | "mv" | "ln" => { // These need a destination - create a temp destination let dest_path = path.with_extension("dest"); - vec![OsString::from(program), path_os.to_owned(), dest_path.as_os_str().to_owned()] - }, + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + } _ => vec![OsString::from(program), path_os.to_owned()], }; @@ -135,61 +145,53 @@ fuzz_target!(|_data: &[u8]| { Err(_) => return, // Skip if we can't set up test files }; - // Pick a random program that works with paths - let program = PATH_PROGRAMS.choose(&mut rng).unwrap(); + // Pick multiple random programs to test in each iteration + let num_programs_to_test = rng.random_range(1..=3); // Test 1-3 programs per iteration + let mut tested_programs = HashSet::new(); - // Test with files that have non-UTF-8 names - for test_file in &test_files { - let result = test_program_with_non_utf8_path(program, test_file); + for _ in 0..num_programs_to_test { + // Pick a random program that we haven't tested yet in this iteration + let available_programs: Vec<_> = PATH_PROGRAMS + .iter() + .filter(|p| !tested_programs.contains(*p)) + .collect(); - // Check if the program handled the non-UTF-8 path gracefully - // This will panic on the first UTF-8 error found - check_for_utf8_error_and_panic(&result, program, test_file); + if available_programs.is_empty() { + break; + } - // Special test for chmod since that's what the bug report specifically mentions - if *program == "chmod" && test_file.to_string_lossy().contains('\u{FFFD}') { - // This path contains replacement characters, indicating invalid UTF-8 - println!("Testing chmod with non-UTF-8 path: {:?}", test_file); + let program = available_programs.choose(&mut rng).unwrap(); + tested_programs.insert(*program); - // Try chmod with basic permissions using local binary - let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; - let chmod_args = vec![ - OsString::from("chmod"), - OsString::from("644"), - test_file.as_os_str().to_owned(), - ]; + // Test with one random file that has non-UTF-8 names (not all files to speed up) + if let Some(test_file) = test_files.choose(&mut rng) { + let result = test_program_with_non_utf8_path(program, test_file); - let chmod_result = run_gnu_cmd(local_binary, &chmod_args, false, None); - match chmod_result { + // Check if the program handled the non-UTF-8 path gracefully + check_for_utf8_error_and_panic(&result, program, test_file); + } + + // Special cases for programs that need additional testing + if **program == "mkdir" { + let non_utf8_dir_name = generate_non_utf8_osstring(); + let non_utf8_dir = temp_root.join(non_utf8_dir_name); + + let local_binary = + "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; + let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; + + let mkdir_result = run_gnu_cmd(local_binary, &mkdir_args, false, None); + match mkdir_result { Ok(result) => { - check_for_utf8_error_and_panic(&result, "chmod", test_file); + check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); } Err(error) => { - check_for_utf8_error_and_panic(&error, "chmod", test_file); + check_for_utf8_error_and_panic(&error, "mkdir", &non_utf8_dir); } } } } - // Test creating directories with non-UTF-8 names - if *program == "mkdir" { - let non_utf8_dir_name = generate_non_utf8_osstring(); - let non_utf8_dir = temp_root.join(non_utf8_dir_name); - - let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; - let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; - - let mkdir_result = run_gnu_cmd(local_binary, &mkdir_args, false, None); - match mkdir_result { - Ok(result) => { - check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); - } - Err(error) => { - check_for_utf8_error_and_panic(&error, "mkdir", &non_utf8_dir); - } - } - } - // Clean up cleanup_test_files(&temp_root); }); From b67d04557ee25f8c6603bd11ae89b332ce8dbbf0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 12:13:28 +0200 Subject: [PATCH 40/83] fuzz: add concise logging to show which programs are being tested --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index cfdc60e8b..b4a0f9b2b 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -149,6 +149,8 @@ fuzz_target!(|_data: &[u8]| { let num_programs_to_test = rng.random_range(1..=3); // Test 1-3 programs per iteration let mut tested_programs = HashSet::new(); + let mut programs_tested = Vec::::new(); + for _ in 0..num_programs_to_test { // Pick a random program that we haven't tested yet in this iteration let available_programs: Vec<_> = PATH_PROGRAMS @@ -162,6 +164,7 @@ fuzz_target!(|_data: &[u8]| { let program = available_programs.choose(&mut rng).unwrap(); tested_programs.insert(*program); + programs_tested.push(program.to_string()); // Test with one random file that has non-UTF-8 names (not all files to speed up) if let Some(test_file) = test_files.choose(&mut rng) { @@ -192,6 +195,8 @@ fuzz_target!(|_data: &[u8]| { } } + println!("Tested programs: {}", programs_tested.join(", ")); + // Clean up cleanup_test_files(&temp_root); }); From 9f0447cbe9aa2886ceab3bea63c95051aeac4293 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 12:39:54 +0200 Subject: [PATCH 41/83] Add non-UTF-8 filename support to tee and improve fuzzer coverage --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 71 ++++++++++++++++++++++-- src/uu/tee/src/tee.rs | 16 +++--- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index b4a0f9b2b..78a6c2d0a 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -17,8 +17,22 @@ use std::path::PathBuf; use uufuzz::{run_gnu_cmd, CommandResult}; // Programs that typically take file/path arguments and should be tested static PATH_PROGRAMS: &[&str] = &[ - "basename", "cat", "chmod", "cp", "dirname", "du", "head", "ln", "ls", "mkdir", "mv", - "readlink", "realpath", "rm", "rmdir", "tail", "touch", "unlink", + // Core file operations + "cat", "cp", "mv", "rm", "ln", "link", "unlink", "touch", "truncate", + // Directory operations + "ls", "mkdir", "rmdir", "du", "stat", "mktemp", + // Path operations + "basename", "dirname", "readlink", "realpath", "pathchk", + // File content operations + "head", "tail", "tee", "more", "od", "wc", "cksum", "sum", + // File processing + "sort", "uniq", "split", "csplit", "cut", "tr", "shred", + // File permissions/ownership + "chmod", "chown", "chgrp", "install", + // Text processing with files + "comm", "join", "paste", "pr", "fmt", "fold", "expand", "unexpand", + // Directory listing variants + "dir", "vdir", ]; fn generate_non_utf8_bytes() -> Vec { @@ -84,20 +98,65 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu // Build appropriate arguments for each program let local_args = match program { + // Programs that need mode/permissions "chmod" => vec![ OsString::from(program), OsString::from("644"), path_os.to_owned(), ], - "cp" | "mv" | "ln" => { - // These need a destination - create a temp destination + "chown" => vec![ + OsString::from(program), + OsString::from("root:root"), + path_os.to_owned(), + ], + "chgrp" => vec![ + OsString::from(program), + OsString::from("root"), + path_os.to_owned(), + ], + // Programs that need source and destination + "cp" | "mv" | "ln" | "link" => { let dest_path = path.with_extension("dest"); vec![ OsString::from(program), path_os.to_owned(), dest_path.as_os_str().to_owned(), ] - } + }, + "install" => { + let dest_path = path.with_extension("dest"); + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + }, + // Programs that need size/truncate operations + "truncate" => vec![ + OsString::from(program), + OsString::from("--size=0"), + path_os.to_owned(), + ], + "split" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("split_prefix_"), + ], + "csplit" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("1"), + ], + // Programs that work with multiple files (use just one for testing) + "comm" | "join" => { + // These need two files, use the same file twice for simplicity + vec![ + OsString::from(program), + path_os.to_owned(), + path_os.to_owned(), + ] + }, + // Programs that typically take file input _ => vec![OsString::from(program), path_os.to_owned()], }; @@ -175,7 +234,7 @@ fuzz_target!(|_data: &[u8]| { } // Special cases for programs that need additional testing - if **program == "mkdir" { + if **program == "mkdir" || **program == "mktemp" { let non_utf8_dir_name = generate_non_utf8_osstring(); let non_utf8_dir = temp_root.join(non_utf8_dir_name); diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 5c6a120e7..faf848ebd 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -6,6 +6,7 @@ // cSpell:ignore POLLERR POLLRDBAND pfds revents use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +use std::ffi::OsString; use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write, stdin, stdout}; use std::path::PathBuf; @@ -34,7 +35,7 @@ struct Options { append: bool, ignore_interrupts: bool, ignore_pipe_errors: bool, - files: Vec, + files: Vec, output_error: Option, } @@ -77,8 +78,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let files = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) + .get_many::(options::FILE) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let options = Options { @@ -127,7 +128,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::IGNORE_PIPE_ERRORS) @@ -252,7 +254,7 @@ fn copy(mut input: impl Read, mut output: impl Write) -> Result { /// If that error should lead to program termination, this function returns Some(Err()), /// otherwise it returns None. fn open( - name: &str, + name: &OsString, append: bool, output_error: Option<&OutputErrorMode>, ) -> Option> { @@ -266,10 +268,10 @@ fn open( match mode.write(true).create(true).open(path.as_path()) { Ok(file) => Some(Ok(NamedWriter { inner: Box::new(file), - name: name.to_owned(), + name: name.to_string_lossy().to_string(), })), Err(f) => { - show_error!("{}: {f}", name.maybe_quote()); + show_error!("{}: {f}", name.to_string_lossy().maybe_quote()); match output_error { Some(OutputErrorMode::Exit | OutputErrorMode::ExitNoPipe) => Some(Err(f)), _ => None, From 6c996865c9386edb294fdde571bc19a22afd10fa Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 13:37:23 +0200 Subject: [PATCH 42/83] Fix comm to handle non-UTF-8 filenames --- src/uu/comm/src/comm.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index b77c549d7..e383d4d6f 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -6,8 +6,10 @@ // spell-checker:ignore (ToDO) delim mkdelim pairable use std::cmp::Ordering; +use std::ffi::OsString; use std::fs::{File, metadata}; use std::io::{self, BufRead, BufReader, Read, Stdin, stdin}; +use std::path::Path; use uucore::LocalizedCommand; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -115,7 +117,7 @@ impl OrderChecker { } // Check if two files are identical by comparing their contents -pub fn are_files_identical(path1: &str, path2: &str) -> io::Result { +pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result { // First compare file sizes let metadata1 = metadata(path1)?; let metadata2 = metadata(path2)?; @@ -174,11 +176,11 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) let should_check_order = !no_check_order && (check_order || if let (Some(file1), Some(file2)) = ( - opts.get_one::(options::FILE_1), - opts.get_one::(options::FILE_2), + opts.get_one::(options::FILE_1), + opts.get_one::(options::FILE_2), ) { - !(paths_refer_to_same_file(file1, file2, true) - || are_files_identical(file1, file2).unwrap_or(false)) + !(paths_refer_to_same_file(file1.as_os_str(), file2.as_os_str(), true) + || are_files_identical(Path::new(file1), Path::new(file2)).unwrap_or(false)) } else { true }); @@ -264,7 +266,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) } } -fn open_file(name: &str, line_ending: LineEnding) -> io::Result { +fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result { if name == "-" { Ok(LineReader::new(Input::Stdin(stdin()), line_ending)) } else { @@ -283,10 +285,12 @@ fn open_file(name: &str, line_ending: LineEnding) -> io::Result { pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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(); - let mut f1 = open_file(filename1, line_ending).map_err_context(|| filename1.to_string())?; - let mut f2 = open_file(filename2, line_ending).map_err_context(|| filename2.to_string())?; + let filename1 = matches.get_one::(options::FILE_1).unwrap(); + let filename2 = matches.get_one::(options::FILE_2).unwrap(); + let mut f1 = open_file(filename1, line_ending) + .map_err_context(|| filename1.to_string_lossy().to_string())?; + let mut f2 = open_file(filename2, line_ending) + .map_err_context(|| filename2.to_string_lossy().to_string())?; // Due to default_value(), there must be at least one value here, thus unwrap() must not panic. let all_delimiters = matches @@ -360,12 +364,14 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE_1) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::FILE_2) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::TOTAL) From 1056ebe0d5f2eedbfc5f741df5d8b58abdfb2fd6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 14:06:04 +0200 Subject: [PATCH 43/83] Fix join to handle non-UTF-8 filenames --- src/uu/join/src/join.rs | 22 +++++++++++++++------- tests/by-util/test_join.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index dd1323506..4dc1dd146 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -413,7 +413,7 @@ impl Line { struct State<'a> { key: usize, - file_name: &'a str, + file_name: &'a OsString, file_num: FileNum, print_unpaired: bool, lines: Split>, @@ -427,7 +427,7 @@ struct State<'a> { impl<'a> State<'a> { fn new( file_num: FileNum, - name: &'a str, + name: &'a OsString, stdin: &'a Stdin, key: usize, line_ending: LineEnding, @@ -436,7 +436,8 @@ impl<'a> State<'a> { let file_buf = if name == "-" { Box::new(stdin.lock()) as Box } else { - let file = File::open(name).map_err_context(|| format!("{}", name.maybe_quote()))?; + let file = File::open(name) + .map_err_context(|| format!("{}", name.to_string_lossy().maybe_quote()))?; Box::new(BufReader::new(file)) as Box }; @@ -639,7 +640,7 @@ impl<'a> State<'a> { && (input.check_order == CheckOrder::Enabled || (self.has_unpaired && !self.has_failed)) { - let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string)); + let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.to_string_lossy().maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string)); // This is fatal if the check is enabled. if input.check_order == CheckOrder::Enabled { return Err(JoinError::UnorderedInput(err_msg)); @@ -826,8 +827,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = parse_settings(&matches)?; - let file1 = matches.get_one::("file1").unwrap(); - let file2 = matches.get_one::("file2").unwrap(); + let file1 = matches.get_one::("file1").unwrap(); + let file2 = matches.get_one::("file2").unwrap(); if file1 == "-" && file2 == "-" { return Err(USimpleError::new( @@ -951,6 +952,7 @@ pub fn uu_app() -> Command { .required(true) .value_name("FILE1") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .hide(true), ) .arg( @@ -958,11 +960,17 @@ pub fn uu_app() -> Command { .required(true) .value_name("FILE2") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .hide(true), ) } -fn exec(file1: &str, file2: &str, settings: Settings, sep: Sep) -> UResult<()> { +fn exec( + file1: &OsString, + file2: &OsString, + settings: Settings, + sep: Sep, +) -> UResult<()> { let stdin = stdin(); let mut state1 = State::new( diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index e9924eea9..65b927717 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -533,3 +533,41 @@ fn test_full() { .fails() .stderr_contains("No space left on device"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_join_non_utf8_paths() { + use std::fs; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create files with non-UTF-8 names using shell commands + // since the test framework doesn't support OsStr for file names + let test_dir = at.subdir.as_path(); + + // Create temporary files with valid names first + at.write("temp1.txt", "a 1\n"); + at.write("temp2.txt", "a 2\n"); + + // Rename them to non-UTF-8 names using std::fs + let file1_bytes = b"test_\xFF\xFE_1.txt"; + let file2_bytes = b"test_\xFF\xFE_2.txt"; + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file1_name = std::ffi::OsStr::from_bytes(file1_bytes); + let file2_name = std::ffi::OsStr::from_bytes(file2_bytes); + + fs::rename(test_dir.join("temp1.txt"), test_dir.join(file1_name)).unwrap(); + fs::rename(test_dir.join("temp2.txt"), test_dir.join(file2_name)).unwrap(); + + // Test that join can handle non-UTF-8 filenames + ts.ucmd() + .arg(file1_name) + .arg(file2_name) + .succeeds() + .stdout_only("a 1 2\n"); + } +} From 0e4ed4d462fd4431eaf045670ceec94b3ff59bc5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 14:45:01 +0200 Subject: [PATCH 44/83] Fix shred to handle non-UTF-8 filenames --- src/uu/shred/src/shred.rs | 12 +++++++----- tests/by-util/test_shred.rs | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index c158678a4..b7b882c26 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; #[cfg(unix)] use libc::S_IWUSR; use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom}; +use std::ffi::OsString; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Seek, Write}; #[cfg(unix)] @@ -297,7 +298,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let zero = matches.get_flag(options::ZERO); let verbose = matches.get_flag(options::VERBOSE); - for path_str in matches.get_many::(options::FILE).unwrap() { + for path_str in matches.get_many::(options::FILE).unwrap() { show_if_err!(wipe_file( path_str, iterations, @@ -396,7 +397,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -428,7 +430,7 @@ fn pass_name(pass_type: &PassType) -> String { #[allow(clippy::too_many_arguments)] #[allow(clippy::cognitive_complexity)] fn wipe_file( - path_str: &str, + path_str: &OsString, n_passes: usize, remove_method: RemoveMethod, size: Option, @@ -605,7 +607,7 @@ fn do_pass( /// Repeatedly renames the file with strings of decreasing length (most likely all 0s) /// Return the path of the file after its last renaming or None in case of an error fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Option { - let file_name_len = orig_path.file_name().unwrap().to_str().unwrap().len(); + let file_name_len = orig_path.file_name().unwrap().len(); let mut last_path = PathBuf::from(orig_path); @@ -657,7 +659,7 @@ fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Op fn do_remove( path: &Path, - orig_filename: &str, + orig_filename: &OsString, verbose: bool, remove_method: RemoveMethod, ) -> Result<(), io::Error> { diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index a31ef4bf4..ad8f79200 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -316,3 +316,26 @@ fn test_shred_rename_exhaustion() { assert!(!at.file_exists("test")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_shred_non_utf8_paths() { + use std::fs; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create test file with non-UTF-8 name + at.write("temp.txt", "test content"); + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + + fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); + + // Test that shred can handle non-UTF-8 filenames + ts.ucmd().arg(file_name).succeeds(); + } +} From 0706675830e599b5dcafdb64b77a450a80927ae8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 14:52:14 +0200 Subject: [PATCH 45/83] Fix mktemp to handle non-UTF-8 templates --- src/uu/mktemp/src/mktemp.rs | 44 +++++++++++++++++++++--------------- tests/by-util/test_mktemp.rs | 14 ++++++++++++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 23d22d361..b9cb58289 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -13,7 +13,7 @@ use uucore::format_usage; use uucore::translate; use std::env; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::io::ErrorKind; use std::iter; use std::path::{MAIN_SEPARATOR, Path, PathBuf}; @@ -105,7 +105,7 @@ pub struct Options { pub treat_as_template: bool, /// The template to use for the name of the temporary file. - pub template: String, + pub template: OsString, } impl Options { @@ -123,12 +123,12 @@ impl Options { .ok() .map_or_else(env::temp_dir, PathBuf::from), }); - let (tmpdir, template) = match matches.get_one::(ARG_TEMPLATE) { + let (tmpdir, template) = match matches.get_one::(ARG_TEMPLATE) { // If no template argument is given, `--tmpdir` is implied. None => { let tmpdir = Some(tmpdir.unwrap_or_else(env::temp_dir)); let template = DEFAULT_TEMPLATE; - (tmpdir, template.to_string()) + (tmpdir, OsString::from(template)) } Some(template) => { let tmpdir = if env::var(TMPDIR_ENV_VAR).is_ok() && matches.get_flag(OPT_T) { @@ -142,7 +142,7 @@ impl Options { } else { None }; - (tmpdir, template.to_string()) + (tmpdir, template.clone()) } }; Self { @@ -200,23 +200,31 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> { impl Params { fn from(options: Options) -> Result { + // Convert OsString template to string for processing + let template_str = match options.template.to_str() { + Some(s) => s, + None => { + // For non-UTF-8 templates, return an error + return Err(MkTempError::InvalidTemplate(options.template.to_string_lossy().into_owned())); + } + }; + // The template argument must end in 'X' if a suffix option is given. - if options.suffix.is_some() && !options.template.ends_with('X') { - return Err(MkTempError::MustEndInX(options.template)); + if options.suffix.is_some() && !template_str.ends_with('X') { + return Err(MkTempError::MustEndInX(template_str.to_string())); } // Get the start and end indices of the randomized part of the template. // // For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7. - let Some((i, j)) = find_last_contiguous_block_of_xs(&options.template) else { + let Some((i, j)) = find_last_contiguous_block_of_xs(template_str) else { let s = match options.suffix { // If a suffix is specified, the error message includes the template without the suffix. - Some(_) => options - .template + Some(_) => template_str .chars() - .take(options.template.len()) + .take(template_str.len()) .collect::(), - None => options.template, + None => template_str.to_string(), }; return Err(MkTempError::TooFewXs(s)); }; @@ -227,16 +235,16 @@ impl Params { // then `prefix` is "a/b/c/d". let tmpdir = options.tmpdir; let prefix_from_option = tmpdir.clone().unwrap_or_default(); - let prefix_from_template = &options.template[..i]; + let prefix_from_template = &template_str[..i]; let prefix = Path::new(&prefix_from_option) .join(prefix_from_template) .display() .to_string(); if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { - return Err(MkTempError::PrefixContainsDirSeparator(options.template)); + return Err(MkTempError::PrefixContainsDirSeparator(template_str.to_string())); } if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { - return Err(MkTempError::InvalidTemplate(options.template)); + return Err(MkTempError::InvalidTemplate(template_str.to_string())); } // Split the parent directory from the file part of the prefix. @@ -263,7 +271,7 @@ impl Params { // For example, if the suffix command-line argument is ".txt" and // the template is "XXXabc", then `suffix` is "abc.txt". let suffix_from_option = options.suffix.unwrap_or_default(); - let suffix_from_template = &options.template[j..]; + let suffix_from_template = &template_str[j..]; let suffix = format!("{suffix_from_template}{suffix_from_option}"); if suffix.contains(MAIN_SEPARATOR) { return Err(MkTempError::SuffixContainsDirSeparator(suffix)); @@ -360,7 +368,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // If POSIXLY_CORRECT was set, template MUST be the last argument. if matches.contains_id(ARG_TEMPLATE) { // Template argument was provided, check if was the last one. - if args.last().unwrap() != OsStr::new(&options.template) { + if args.last().unwrap() != &options.template { return Err(Box::new(MkTempError::TooManyTemplates)); } } @@ -457,7 +465,7 @@ pub fn uu_app() -> Command { .help(translate!("mktemp-help-t")) .action(ArgAction::SetTrue), ) - .arg(Arg::new(ARG_TEMPLATE).num_args(..=1)) + .arg(Arg::new(ARG_TEMPLATE).num_args(..=1).value_parser(clap::value_parser!(OsString))) } fn dry_exec(tmpdir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult { diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index 60714ad7f..872924eb3 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -997,3 +997,17 @@ fn test_missing_short_tmpdir_flag() { .no_stdout() .stderr_contains("a value is required for '-p ' but none was supplied"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_template() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let ts = TestScenario::new(util_name!()); + + // Test that mktemp gracefully handles non-UTF-8 templates with an error instead of panicking + let template = OsStr::from_bytes(b"test_\xFF\xFE_XXXXXX"); + + ts.ucmd().arg(template).fails().stderr_contains("invalid"); +} From b54d999cc6e6362f05a3c3a53cc1a64261b30f06 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 14:57:19 +0200 Subject: [PATCH 46/83] Fix more to handle non-UTF-8 filenames --- src/uu/more/src/more.rs | 17 ++++++++++------- tests/by-util/test_more.rs | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 8aa6b7729..262dc9940 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use std::{ + ffi::OsString, fs::File, io::{BufRead, BufReader, Stdin, Stdout, Write, stdin, stdout}, panic::set_hook, @@ -154,12 +155,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { })); let matches = uu_app().get_matches_from_localized(args); let mut options = Options::from(&matches); - if let Some(files) = matches.get_many::(options::FILES) { + if let Some(files) = matches.get_many::(options::FILES) { let length = files.len(); - let mut files_iter = files.map(|s| s.as_str()).peekable(); - while let (Some(file), next_file) = (files_iter.next(), files_iter.peek()) { - let file = Path::new(file); + let mut files_iter = files.peekable(); + while let (Some(file_os), next_file) = (files_iter.next(), files_iter.peek()) { + let file = Path::new(file_os); if file.is_dir() { show!(UUsageError::new( 0, @@ -188,11 +189,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } Ok(opened_file) => opened_file, }; + let next_file_str = next_file.map(|f| f.to_string_lossy().into_owned()); more( InputType::File(BufReader::new(opened_file)), length > 1, - file.to_str(), - next_file.copied(), + Some(&file.to_string_lossy()), + next_file_str.as_deref(), &mut options, )?; } @@ -311,7 +313,8 @@ pub fn uu_app() -> Command { .required(false) .action(ArgAction::Append) .help(translate!("more-help-files")) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 4cd984d7c..e925c71b9 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -126,3 +126,27 @@ fn test_invalid_file_perms() { .stderr_contains("permission denied"); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_more_non_utf8_paths() { + use std::fs; + + if std::io::stdout().is_terminal() { + let (at, mut ucmd) = at_and_ucmd!(); + + // Create test file with normal name first + at.write("temp.txt", "test content for non-UTF-8 file"); + + // Rename to non-UTF-8 name + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); + + // Test that more can handle non-UTF-8 filenames without crashing + ucmd.arg(file_name).succeeds(); + } + } +} From 693bdd77483c4d839493dafdcb9da6d0ef35124c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:02:25 +0200 Subject: [PATCH 47/83] Fix truncate to handle non-UTF-8 filenames --- src/uu/truncate/src/truncate.rs | 33 ++++++++++++++++++--------------- tests/by-util/test_truncate.rs | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 0ad207b7a..a2750b72a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (ToDO) RFILE refsize rfilename fsize tsize use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::{OpenOptions, metadata}; use std::io::ErrorKind; #[cfg(unix)] @@ -94,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } })?; - let files: Vec = matches - .get_many::(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec = matches + .get_many::(options::ARG_FILES) + .map(|v| v.cloned().collect()) .unwrap_or_default(); if files.is_empty() { @@ -158,7 +159,8 @@ pub fn uu_app() -> Command { .value_name("FILE") .action(ArgAction::Append) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -174,18 +176,18 @@ pub fn uu_app() -> Command { /// /// If the file could not be opened, or there was a problem setting the /// size of the file. -fn file_truncate(filename: &str, create: bool, size: u64) -> UResult<()> { +fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> { + let path = Path::new(filename); + #[cfg(unix)] - if let Ok(metadata) = metadata(filename) { + if let Ok(metadata) = metadata(path) { if metadata.file_type().is_fifo() { return Err(USimpleError::new( 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.quote()), + translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), )); } } - - let path = Path::new(filename); match OpenOptions::new().write(true).create(create).open(path) { Ok(file) => file.set_len(size), Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()), @@ -216,7 +218,7 @@ fn file_truncate(filename: &str, create: bool, size: u64) -> UResult<()> { fn truncate_reference_and_size( rfilename: &str, size_string: &str, - filenames: &[String], + filenames: &[OsString], create: bool, ) -> UResult<()> { let mode = match parse_mode_and_size(size_string) { @@ -275,7 +277,7 @@ fn truncate_reference_and_size( /// If at least one file is a named pipe (also known as a fifo). fn truncate_reference_file_only( rfilename: &str, - filenames: &[String], + filenames: &[OsString], create: bool, ) -> UResult<()> { let metadata = metadata(rfilename).map_err(|e| match e.kind() { @@ -312,7 +314,7 @@ fn truncate_reference_file_only( /// the size of at least one file. /// /// If at least one file is a named pipe (also known as a fifo). -fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> UResult<()> { +fn truncate_size_only(size_string: &str, filenames: &[OsString], create: bool) -> UResult<()> { let mode = parse_mode_and_size(size_string).map_err(|e| { USimpleError::new(1, translate!("truncate-error-invalid-number", "error" => e)) })?; @@ -325,13 +327,14 @@ fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> } for filename in filenames { - let fsize = match metadata(filename) { + let path = Path::new(filename); + let fsize = match metadata(path) { Ok(m) => { #[cfg(unix)] if m.file_type().is_fifo() { return Err(USimpleError::new( 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.quote()), + translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), )); } m.len() @@ -351,7 +354,7 @@ fn truncate( _: bool, reference: Option, size: Option, - filenames: &[String], + filenames: &[OsString], ) -> UResult<()> { let create = !no_create; diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 789bd1d66..ef1ea872c 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -420,3 +420,27 @@ fn test_fifo_error_reference_and_size() { .no_stdout() .stderr_contains("cannot open 'fifo' for writing: No such device or address"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_truncate_non_utf8_paths() { + use std::fs; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create test file with normal name first + at.write("temp.txt", "test content"); + + // Rename to non-UTF-8 name + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + + fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); + + // Test that truncate can handle non-UTF-8 filenames + ts.ucmd().arg("-s").arg("10").arg(file_name).succeeds(); + } +} From 6e55a2a3bbd4b643909abfc493821279882f02aa Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:06:54 +0200 Subject: [PATCH 48/83] Fix cut to handle non-UTF-8 filenames --- src/uu/cut/src/cut.rs | 17 +++++++++-------- tests/by-util/test_cut.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 57f0e61d1..aea44c988 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -343,11 +343,11 @@ fn cut_fields( } } -fn cut_files(mut filenames: Vec, mode: &Mode) { +fn cut_files(mut filenames: Vec, mode: &Mode) { let mut stdin_read = false; if filenames.is_empty() { - filenames.push("-".to_owned()); + filenames.push(OsString::from("-")); } let mut out: Box = if stdout().is_terminal() { @@ -370,12 +370,12 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { stdin_read = true; } else { - let path = Path::new(&filename[..]); + let path = Path::new(filename); if path.is_dir() { show_error!( "{}: {}", - filename.maybe_quote(), + filename.to_string_lossy().maybe_quote(), translate!("cut-error-is-directory") ); set_exit_code(1); @@ -384,7 +384,7 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { show_if_err!( File::open(path) - .map_err_context(|| filename.maybe_quote().to_string()) + .map_err_context(|| filename.to_string_lossy().to_string()) .and_then(|file| { match &mode { Mode::Bytes(ranges, opts) | Mode::Characters(ranges, opts) => { @@ -577,8 +577,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }, }; - let files: Vec = matches - .get_many::(options::FILE) + let files: Vec = matches + .get_many::(options::FILE) .unwrap_or_default() .cloned() .collect(); @@ -681,6 +681,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index a2406679c..616e20a58 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -385,3 +385,33 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("cut: write error: No space left on device\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_cut_non_utf8_paths() { + use std::fs; + use uutests::util::TestScenario; + use uutests::util_name; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create test file with normal name first + at.write("temp.txt", "a\tb\tc\n1\t2\t3\n"); + + // Rename to non-UTF-8 name + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + + fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); + + // Test that cut can handle non-UTF-8 filenames + ts.ucmd() + .arg("-f1,3") + .arg(file_name) + .succeeds() + .stdout_only("a\tc\n1\t3\n"); + } +} From fddaa1187a4f46be967082945a5bbcb0ade208b4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:10:35 +0200 Subject: [PATCH 49/83] Fix sum to handle non-UTF-8 filenames --- src/uu/sum/src/sum.rs | 53 +++++++++++++++++++++------------------ tests/by-util/test_sum.rs | 13 ++++++++++ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 2366a59d3..5832df778 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) sysv use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{ErrorKind, Read, Write, stdin, stdout}; use std::path::Path; @@ -67,27 +68,26 @@ fn sysv_sum(mut reader: impl Read) -> std::io::Result<(usize, u16)> { Ok((blocks_read, ret as u16)) } -fn open(name: &str) -> UResult> { - match name { - "-" => Ok(Box::new(stdin()) as Box), - _ => { - let path = &Path::new(name); - if path.is_dir() { - return Err(USimpleError::new( - 2, - translate!("sum-error-is-directory", "name" => name.maybe_quote()), - )); - } - // Silent the warning as we want to the error message - if path.metadata().is_err() { - return Err(USimpleError::new( - 2, - translate!("sum-error-no-such-file-or-directory", "name" => name.maybe_quote()), - )); - } - let f = File::open(path).map_err_context(String::new)?; - Ok(Box::new(f) as Box) +fn open(name: &OsString) -> UResult> { + if name == "-" { + Ok(Box::new(stdin()) as Box) + } else { + let path = Path::new(name); + if path.is_dir() { + return Err(USimpleError::new( + 2, + translate!("sum-error-is-directory", "name" => name.to_string_lossy().maybe_quote()), + )); } + // Silent the warning as we want to the error message + if path.metadata().is_err() { + return Err(USimpleError::new( + 2, + translate!("sum-error-no-such-file-or-directory", "name" => name.to_string_lossy().maybe_quote()), + )); + } + let f = File::open(path).map_err_context(String::new)?; + Ok(Box::new(f) as Box) } } @@ -101,9 +101,9 @@ mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let sysv = matches.get_flag(options::SYSTEM_V_COMPATIBLE); @@ -127,7 +127,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = stdout().lock(); if print_names { - writeln!(stdout, "{sum:0width$} {blocks:width$} {file}")?; + writeln!( + stdout, + "{sum:0width$} {blocks:width$} {}", + file.to_string_lossy() + )?; } else { writeln!(stdout, "{sum:0width$} {blocks:width$}")?; } @@ -146,7 +150,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .action(ArgAction::Append) .hide(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::BSD_COMPATIBLE) diff --git a/tests/by-util/test_sum.rs b/tests/by-util/test_sum.rs index 89c454aff..d4e4e8c55 100644 --- a/tests/by-util/test_sum.rs +++ b/tests/by-util/test_sum.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -80,3 +82,14 @@ fn test_invalid_metadata() { .fails() .stderr_is("sum: b: No such file or directory\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_sum_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content").unwrap(); + + ucmd.arg(&filename).succeeds(); +} From 39b772146451625b5a55020f5145a1501610ab6f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:14:11 +0200 Subject: [PATCH 50/83] Fix install to handle non-UTF-8 filenames --- src/uu/install/src/install.rs | 28 +++++++++++++++++----------- tests/by-util/test_install.rs | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 66e80b90d..937b3d23c 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -10,6 +10,7 @@ mod mode; use clap::{Arg, ArgAction, ArgMatches, Command}; use file_diff::diff; use filetime::{FileTime, set_file_times}; +use std::ffi::OsString; use std::fmt::Debug; use std::fs::File; use std::fs::{self, metadata}; @@ -168,9 +169,9 @@ static ARG_FILES: &str = "files"; pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let paths: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let paths: Vec = matches + .get_many::(ARG_FILES) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let behavior = behavior(&matches)?; @@ -303,7 +304,8 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .num_args(1..) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -435,7 +437,7 @@ fn behavior(matches: &ArgMatches) -> UResult { /// /// Returns a Result type with the Err variant containing the error message. /// -fn directory(paths: &[String], b: &Behavior) -> UResult<()> { +fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> { if paths.is_empty() { Err(InstallError::DirNeedsArg.into()) } else { @@ -518,7 +520,7 @@ fn is_potential_directory_path(path: &Path) -> bool { /// Returns a Result type with the Err variant containing the error message. /// #[allow(clippy::cognitive_complexity)] -fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { +fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { // first check that paths contains at least one element if paths.is_empty() { return Err(UUsageError::new( @@ -528,7 +530,7 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { } if b.no_target_dir && paths.len() > 2 { return Err(InstallError::ExtraOperand( - paths[2].clone(), + paths[2].to_string_lossy().into_owned(), format_usage(&translate!("install-usage")), ) .into()); @@ -544,7 +546,7 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { if paths.is_empty() { return Err(UUsageError::new( 1, - translate!("install-error-missing-destination-operand", "path" => last_path.to_str().unwrap()), + translate!("install-error-missing-destination-operand", "path" => last_path.to_string_lossy()), )); } @@ -566,8 +568,12 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { if let Some(to_create) = to_create { // if the path ends in /, remove it + let to_create_owned; let to_create = if to_create.to_string_lossy().ends_with('/') { - Path::new(to_create.to_str().unwrap().trim_end_matches('/')) + let path_str = to_create.to_string_lossy(); + let trimmed = path_str.trim_end_matches('/'); + to_create_owned = PathBuf::from(trimmed); + to_create_owned.as_path() } else { to_create }; @@ -835,7 +841,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { /// fn strip_file(to: &Path, b: &Behavior) -> UResult<()> { // Check if the filename starts with a hyphen and adjust the path - let to_str = to.as_os_str().to_str().unwrap_or_default(); + let to_str = to.to_string_lossy(); let to = if to_str.starts_with('-') { let mut new_path = PathBuf::from("."); new_path.push(to); @@ -1085,7 +1091,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { } // Check if the contents of the source and destination files differ. - if !diff(from.to_str().unwrap(), to.to_str().unwrap()) { + if !diff(&from.to_string_lossy(), &to.to_string_lossy()) { return true; } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index ea3448bbb..ded775579 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -7,6 +7,8 @@ #[cfg(not(target_os = "openbsd"))] use filetime::FileTime; use std::fs; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; #[cfg(not(windows))] use std::process::Command; @@ -2366,3 +2368,16 @@ fn test_install_compare_with_mode_bits() { ); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_install_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + let source_filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + let dest_dir = "target_dir"; + + std::fs::write(at.plus(&source_filename), b"test content").unwrap(); + at.mkdir(dest_dir); + + ucmd.arg(&source_filename).arg(dest_dir).succeeds(); +} From 20793600f7e44aee45bcf726c559f3ca83de43b4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:15:54 +0200 Subject: [PATCH 51/83] Fix csplit to handle non-UTF-8 filenames --- src/uu/csplit/src/csplit.rs | 6 ++++-- tests/by-util/test_csplit.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index fc18b97da..a3e10e2b0 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -6,6 +6,7 @@ #![allow(rustdoc::private_intra_doc_links)] use std::cmp::Ordering; +use std::ffi::OsString; use std::io::{self, BufReader, ErrorKind}; use std::{ fs::{File, remove_file}, @@ -608,7 +609,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); // get the file to split - let file_name = matches.get_one::(options::FILE).unwrap(); + let file_name = matches.get_one::(options::FILE).unwrap(); // get the patterns to split on let patterns: Vec = matches @@ -689,7 +690,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::PATTERN) diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index ea1d9ebf9..b13d6c35d 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1501,3 +1501,15 @@ fn test_stdin_no_trailing_newline() { .succeeds() .stdout_only("2\n5\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_csplit_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\nline4\nline5\n").unwrap(); + + ucmd.arg(&filename).arg("3").succeeds(); +} From 89f434d2bc957f6ef96ad2fb0f35a28dd451d797 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:19:17 +0200 Subject: [PATCH 52/83] Fix fmt to handle non-UTF-8 filenames --- src/uu/fmt/src/fmt.rs | 57 ++++++++++++++++++++++----------------- tests/by-util/test_fmt.rs | 16 ++++++++++- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 46f9d547f..4919fb435 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -6,8 +6,10 @@ // spell-checker:ignore (ToDO) PSKIP linebreak ostream parasplit tabwidth xanti xprefix use clap::{Arg, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; +use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; @@ -205,27 +207,27 @@ impl FmtOptions { /// /// A `UResult<()>` indicating success or failure. fn process_file( - file_name: &str, + file_name: &OsString, fmt_opts: &FmtOptions, ostream: &mut BufWriter, ) -> UResult<()> { - let mut fp = BufReader::new(match file_name { - "-" => Box::new(stdin()) as Box, - _ => { - let f = File::open(file_name).map_err_context( - || translate!("fmt-error-cannot-open-for-reading", "file" => file_name.quote()), - )?; - if f.metadata() - .map_err_context( - || translate!("fmt-error-cannot-get-metadata", "file" => file_name.quote()), - )? - .is_dir() - { - return Err(FmtError::ReadError.into()); - } - - Box::new(f) as Box + let mut fp = BufReader::new(if file_name == "-" { + Box::new(stdin()) as Box + } else { + let path = Path::new(file_name); + let f = File::open(path).map_err_context( + || translate!("fmt-error-cannot-open-for-reading", "file" => path.quote()), + )?; + if f.metadata() + .map_err_context( + || translate!("fmt-error-cannot-get-metadata", "file" => path.quote()), + )? + .is_dir() + { + return Err(FmtError::ReadError.into()); } + + Box::new(f) as Box }); let p_stream = ParagraphStream::new(fmt_opts, &mut fp); @@ -258,23 +260,24 @@ fn process_file( /// # Returns /// A `UResult<()>` with the file names, or an error if one of the file names could not be parsed /// (e.g., it is given as a negative number not in the first argument and not after a -- -fn extract_files(matches: &ArgMatches) -> UResult> { +fn extract_files(matches: &ArgMatches) -> UResult> { let in_first_pos = matches .index_of(options::FILES_OR_WIDTH) .is_some_and(|x| x == 1); let is_neg = |s: &str| s.parse::().is_ok_and(|w| w < 0); - let files: UResult> = matches - .get_many::(options::FILES_OR_WIDTH) + let files: UResult> = matches + .get_many::(options::FILES_OR_WIDTH) .into_iter() .flatten() .enumerate() .filter_map(|(i, x)| { - if is_neg(x) { + let x_str = x.to_string_lossy(); + if is_neg(&x_str) { if in_first_pos && i == 0 { None } else { - let first_num = x + let first_num = x_str .chars() .nth(1) .expect("a negative number should be at least two characters long"); @@ -287,7 +290,7 @@ fn extract_files(matches: &ArgMatches) -> UResult> { .collect(); if files.as_ref().is_ok_and(|f| f.is_empty()) { - Ok(vec!["-".into()]) + Ok(vec![OsString::from("-")]) } else { files } @@ -304,8 +307,11 @@ fn extract_width(matches: &ArgMatches) -> UResult> { } if let Some(1) = matches.index_of(options::FILES_OR_WIDTH) { - let width_arg = matches.get_one::(options::FILES_OR_WIDTH).unwrap(); - if let Some(num) = width_arg.strip_prefix('-') { + let width_arg = matches + .get_one::(options::FILES_OR_WIDTH) + .unwrap(); + let width_str = width_arg.to_string_lossy(); + if let Some(num) = width_str.strip_prefix('-') { Ok(num.parse::().ok()) } else { // will be treated as a file name @@ -456,6 +462,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .value_name("FILES") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .allow_negative_numbers(true), ) } diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 54d827f83..abf2e132c 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -4,7 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore plass samp - +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::new_ucmd; #[test] @@ -374,3 +375,16 @@ fn test_fmt_knuth_plass_line_breaking() { .succeeds() .stdout_is(expected); } + +#[test] +#[cfg(target_os = "linux")] +fn test_fmt_non_utf8_paths() { + use uutests::at_and_ucmd; + + let (at, mut ucmd) = at_and_ucmd!(); + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + + std::fs::write(at.plus(&filename), b"hello world this is a test").unwrap(); + + ucmd.arg(&filename).succeeds(); +} From b91eb4266f55307a7810637000d3258a88e29196 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:21:42 +0200 Subject: [PATCH 53/83] Fix split to handle non-UTF-8 filenames --- src/uu/split/src/split.rs | 21 +++++++++++---------- tests/by-util/test_split.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 70c475782..f16cb14d4 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -375,7 +375,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_INPUT) .default_value("-") - .value_hint(ValueHint::FilePath), + .value_hint(ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg(Arg::new(ARG_PREFIX).default_value("x")) } @@ -387,7 +388,7 @@ pub fn uu_app() -> Command { struct Settings { prefix: String, suffix: Suffix, - input: String, + input: OsString, /// When supplied, a shell command to output to instead of xaa, xab … filter: Option, strategy: Strategy, @@ -491,7 +492,7 @@ impl Settings { let result = Self { prefix: matches.get_one::(ARG_PREFIX).unwrap().clone(), suffix, - input: matches.get_one::(ARG_INPUT).unwrap().clone(), + input: matches.get_one::(ARG_INPUT).unwrap().clone(), filter: matches.get_one::(OPT_FILTER).cloned(), strategy, verbose: matches.value_source(OPT_VERBOSE) == Some(ValueSource::CommandLine), @@ -529,7 +530,7 @@ impl Settings { filename: &str, is_new: bool, ) -> io::Result>> { - if platform::paths_refer_to_same_file(&self.input, filename) { + if platform::paths_refer_to_same_file(&self.input.to_string_lossy(), filename) { return Err(io::Error::other( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); @@ -598,7 +599,7 @@ fn custom_write_all( /// /// Note: The `buf` might end up with either partial or entire input content. fn get_input_size( - input: &String, + input: &OsString, reader: &mut R, buf: &mut Vec, io_blksize: Option, @@ -633,12 +634,12 @@ where // STDIN stream that did not fit all content into a buffer // Most likely continuous/infinite input stream Err(io::Error::other( - translate!("split-error-cannot-determine-input-size", "input" => input), + translate!("split-error-cannot-determine-input-size", "input" => input.to_string_lossy()), )) } else { // Could be that file size is larger than set read limit // Get the file size from filesystem metadata - let metadata = metadata(input)?; + let metadata = metadata(Path::new(input))?; let metadata_size = metadata.len(); if num_bytes <= metadata_size { Ok(metadata_size) @@ -658,7 +659,7 @@ where // TODO It might be possible to do more here // to address all possible file types and edge cases Err(io::Error::other( - translate!("split-error-cannot-determine-file-size", "input" => input), + translate!("split-error-cannot-determine-file-size", "input" => input.to_string_lossy()), )) } } @@ -1167,7 +1168,7 @@ where Err(error) => { return Err(USimpleError::new( 1, - translate!("split-error-cannot-read-from-input", "input" => settings.input.clone(), "error" => error), + translate!("split-error-cannot-read-from-input", "input" => settings.input.to_string_lossy(), "error" => error), )); } } @@ -1529,7 +1530,7 @@ fn split(settings: &Settings) -> UResult<()> { Box::new(stdin()) as Box } else { let r = File::open(Path::new(&settings.input)).map_err_context( - || translate!("split-error-cannot-open-for-reading", "file" => settings.input.quote()), + || translate!("split-error-cannot-open-for-reading", "file" => settings.input.to_string_lossy().quote()), )?; Box::new(r) as Box }; diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 34b24d84d..41523b355 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -10,6 +10,8 @@ use regex::Regex; use rlimit::Resource; #[cfg(not(windows))] use std::env; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use std::path::Path; use std::{ fs::{File, read_dir}, @@ -2006,3 +2008,17 @@ fn test_long_lines() { assert_eq!(at.read("xac").len(), 131_072); assert!(!at.plus("xad").exists()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\nline4\nline5\n").unwrap(); + + ucmd.arg(&filename).succeeds(); + + // Check that at least one split file was created + assert!(at.plus("xaa").exists()); +} From 581edf09d38958e4ccde1bfe7453359b91f175a9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:26:19 +0200 Subject: [PATCH 54/83] Fix chgrp to handle non-UTF-8 filenames --- src/uu/chgrp/src/chgrp.rs | 9 ++++++--- src/uucore/src/lib/features/perms.rs | 12 +++++++----- tests/by-util/test_chgrp.rs | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index aca69d103..f45cd33dd 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -7,6 +7,7 @@ use uucore::display::Quotable; pub use uucore::entries; +use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; @@ -37,15 +38,16 @@ fn parse_gid_from_str(group: &str) -> Result { fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option, String)> { let mut raw_group = String::new(); - let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { - fs::metadata(file) + let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { + let path = std::path::Path::new(file); + fs::metadata(path) .map(|meta| { let gid = meta.gid(); raw_group = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()); Some(gid) }) .map_err_context( - || translate!("chgrp-error-failed-to-get-attributes", "file" => file.quote()), + || translate!("chgrp-error-failed-to-get-attributes", "file" => path.quote()), )? } else { let group = matches @@ -153,6 +155,7 @@ pub fn uu_app() -> Command { .long(options::REFERENCE) .value_name("RFILE") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(std::ffi::OsString)) .help(translate!("chgrp-help-reference")), ) .arg( diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index bddce9cf2..c03d0032a 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -16,6 +16,7 @@ use clap::{Arg, ArgMatches, Command}; use libc::{gid_t, uid_t}; use options::traverse; +use std::ffi::OsString; use walkdir::WalkDir; use std::ffi::CString; @@ -193,7 +194,7 @@ pub struct ChownExecutor { pub traverse_symlinks: TraverseSymlinks, pub verbosity: Verbosity, pub filter: IfFrom, - pub files: Vec, + pub files: Vec, pub recursive: bool, pub preserve_root: bool, pub dereference: bool, @@ -597,13 +598,14 @@ pub fn chown_base( .value_hint(clap::ValueHint::FilePath) .action(clap::ArgAction::Append) .required(true) - .num_args(1..), + .num_args(1..) + .value_parser(clap::value_parser!(std::ffi::OsString)), ); let matches = command.try_get_matches_from(args)?; - let files: Vec = matches - .get_many::(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec = matches + .get_many::(options::ARG_FILES) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let preserve_root = matches.get_flag(options::preserve_root::PRESERVE); diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index e0085b368..38081f5ac 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -4,6 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (words) nosuchgroup groupname +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uucore::process::getegid; use uutests::{at_and_ucmd, new_ucmd}; #[cfg(not(target_vendor = "apple"))] @@ -599,3 +601,17 @@ fn test_numeric_group_formats() { let final_gid = at.plus("test_file").metadata().unwrap().gid(); assert_eq!(final_gid, first_group.as_raw()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_chgrp_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content").unwrap(); + + // Get current user's primary group + let current_gid = getegid(); + + ucmd.arg(current_gid.to_string()).arg(&filename).succeeds(); +} From d45113f574af930c99aa5e272ef1b2e24ba460c9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:28:11 +0200 Subject: [PATCH 55/83] Fix paste to handle non-UTF-8 filenames --- src/uu/paste/src/paste.rs | 24 +++++++++++++----------- tests/by-util/test_paste.rs | 21 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 82a03f93b..540938ccd 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -5,9 +5,11 @@ use clap::{Arg, ArgAction, Command}; use std::cell::{OnceCell, RefCell}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufRead, BufReader, Stdin, Write, stdin, stdout}; use std::iter::Cycle; +use std::path::Path; use std::rc::Rc; use std::slice::Iter; use uucore::LocalizedCommand; @@ -30,7 +32,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let serial = matches.get_flag(options::SERIAL); let delimiters = matches.get_one::(options::DELIMITER).unwrap(); let files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .unwrap() .cloned() .collect(); @@ -67,7 +69,8 @@ pub fn uu_app() -> Command { .value_name("FILE") .action(ArgAction::Append) .default_value("-") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::ZERO_TERMINATED) @@ -80,7 +83,7 @@ pub fn uu_app() -> Command { #[allow(clippy::cognitive_complexity)] fn paste( - filenames: Vec, + filenames: Vec, serial: bool, delimiters: &str, line_ending: LineEnding, @@ -92,17 +95,16 @@ fn paste( let mut input_source_vec = Vec::with_capacity(filenames.len()); for filename in filenames { - let input_source = match filename.as_str() { - "-" => InputSource::StandardInput( + let input_source = if filename == "-" { + InputSource::StandardInput( stdin_once_cell .get_or_init(|| Rc::new(RefCell::new(stdin()))) .clone(), - ), - st => { - let file = File::open(st)?; - - InputSource::File(BufReader::new(file)) - } + ) + } else { + let path = Path::new(&filename); + let file = File::open(path)?; + InputSource::File(BufReader::new(file)) }; input_source_vec.push(input_source); diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 8da77e3ff..a87f21598 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -4,7 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore bsdutils toybox - +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -252,6 +253,7 @@ FIRST!SECOND@THIRD#FOURTH!ABCDEFG } #[test] +#[cfg(unix)] fn test_non_utf8_input() { // 0xC0 is not valid UTF-8 const INPUT: &[u8] = b"Non-UTF-8 test: \xC0\x00\xC0.\n"; @@ -375,3 +377,20 @@ fn test_data() { .stdout_is(example.out); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_paste_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename1 = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + let filename2 = std::ffi::OsString::from_vec(vec![0xF0, 0x90]); + + std::fs::write(at.plus(&filename1), b"line1\nline2\n").unwrap(); + std::fs::write(at.plus(&filename2), b"col1\ncol2\n").unwrap(); + + ucmd.arg(&filename1) + .arg(&filename2) + .succeeds() + .stdout_is("line1\tcol1\nline2\tcol2\n"); +} From 8b383360365c3afc54f2c90d89fdb7d87d285d73 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:29:32 +0200 Subject: [PATCH 56/83] Fix pathchk to handle non-UTF-8 filenames --- src/uu/pathchk/src/pathchk.rs | 9 ++++++--- tests/by-util/test_pathchk.rs | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index c46ed39ae..e73ac2d7c 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) lstat use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs; use std::io::{ErrorKind, Write}; use uucore::LocalizedCommand; @@ -53,7 +54,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; // take necessary actions - let paths = matches.get_many::(options::PATH); + let paths = matches.get_many::(options::PATH); if paths.is_none() { return Err(UUsageError::new( 1, @@ -65,8 +66,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // FIXME: TCS, seems inefficient and overly verbose (?) let mut res = true; for p in paths.unwrap() { + let path_str = p.to_string_lossy(); let mut path = Vec::new(); - for path_segment in p.split('/') { + for path_segment in path_str.split('/') { path.push(path_segment.to_string()); } res &= check_path(&mode, &path); @@ -108,7 +110,8 @@ pub fn uu_app() -> Command { Arg::new(options::PATH) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 064f0aa27..d0197d9bf 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::new_ucmd; #[test] @@ -164,3 +166,17 @@ fn test_posix_all() { // fail on empty path new_ucmd!().args(&["-p", "-P", ""]).fails().no_stdout(); } + +#[test] +#[cfg(target_os = "linux")] +fn test_pathchk_non_utf8_paths() { + use uutests::at_and_ucmd; + + let (at, mut ucmd) = at_and_ucmd!(); + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + + // Create the file so pathchk can check it exists + std::fs::write(at.plus(&filename), b"test").unwrap(); + + ucmd.arg(&filename).succeeds(); +} From f02436be0fb3138524a913f453d2a1f7ae767b37 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:51:58 +0200 Subject: [PATCH 57/83] Fix expand to handle non-UTF-8 filenames --- src/uu/expand/src/expand.rs | 18 ++++++++++-------- tests/by-util/test_expand.rs | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index cd528316e..5b0b8f4e7 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -170,7 +170,7 @@ fn tabstops_parse(s: &str) -> Result<(RemainingMode, Vec), ParseError> { } struct Options { - files: Vec, + files: Vec, tabstops: Vec, tspaces: String, iflag: bool, @@ -204,9 +204,9 @@ impl Options { .unwrap(); // length of tabstops is guaranteed >= 1 let tspaces = " ".repeat(nspaces); - let files: Vec = match matches.get_many::(options::FILES) { - Some(s) => s.map(|v| v.to_string()).collect(), - None => vec!["-".to_owned()], + let files: Vec = match matches.get_many::(options::FILES) { + Some(s) => s.cloned().collect(), + None => vec![OsString::from("-")], }; Ok(Self { @@ -283,16 +283,18 @@ pub fn uu_app() -> Command { Arg::new(options::FILES) .action(ArgAction::Append) .hide(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } -fn open(path: &str) -> UResult>> { +fn open(path: &OsString) -> UResult>> { let file_buf; if path == "-" { Ok(BufReader::new(Box::new(stdin()) as Box)) } else { - file_buf = File::open(path).map_err_context(|| path.to_string())?; + let path_ref = Path::new(path); + file_buf = File::open(path_ref).map_err_context(|| path.to_string_lossy().to_string())?; Ok(BufReader::new(Box::new(file_buf) as Box)) } } @@ -446,7 +448,7 @@ fn expand(options: &Options) -> UResult<()> { if Path::new(file).is_dir() { show_error!( "{}", - translate!("expand-error-is-directory", "file" => file) + translate!("expand-error-is-directory", "file" => file.to_string_lossy()) ); set_exit_code(1); continue; diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 799feb4a3..741aad366 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -426,3 +426,19 @@ fn test_nonexisting_file() { .stderr_contains("expand: nonexistent: No such file or directory") .stdout_contains_line("// !note: file contains significant whitespace"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_expand_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + use uutests::at_and_ucmd; + + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"hello\tworld\ntest\tline\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("hello world\ntest line\n"); +} From b5f8931b176088c7d304b0481346b4a4c02f63a8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 16:28:16 +0200 Subject: [PATCH 58/83] Fix stdbuf to handle non-UTF-8 filenames --- src/uu/chgrp/src/chgrp.rs | 3 +-- src/uu/mktemp/src/mktemp.rs | 21 +++++++++++++-------- src/uu/stdbuf/src/stdbuf.rs | 8 +++++--- tests/by-util/test_stdbuf.rs | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index f45cd33dd..07859d07d 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -6,8 +6,7 @@ // spell-checker:ignore (ToDO) COMFOLLOW Chowner RFILE RFILE's derefer dgid nonblank nonprint nonprinting use uucore::display::Quotable; -pub use uucore::entries; -use uucore::display::Quotable; +use uucore::entries; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index b9cb58289..98ffdadbf 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -201,12 +201,11 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> { impl Params { fn from(options: Options) -> Result { // Convert OsString template to string for processing - let template_str = match options.template.to_str() { - Some(s) => s, - None => { - // For non-UTF-8 templates, return an error - return Err(MkTempError::InvalidTemplate(options.template.to_string_lossy().into_owned())); - } + let Some(template_str) = options.template.to_str() else { + // For non-UTF-8 templates, return an error + return Err(MkTempError::InvalidTemplate( + options.template.to_string_lossy().into_owned(), + )); }; // The template argument must end in 'X' if a suffix option is given. @@ -241,7 +240,9 @@ impl Params { .display() .to_string(); if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { - return Err(MkTempError::PrefixContainsDirSeparator(template_str.to_string())); + return Err(MkTempError::PrefixContainsDirSeparator( + template_str.to_string(), + )); } if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { return Err(MkTempError::InvalidTemplate(template_str.to_string())); @@ -465,7 +466,11 @@ pub fn uu_app() -> Command { .help(translate!("mktemp-help-t")) .action(ArgAction::SetTrue), ) - .arg(Arg::new(ARG_TEMPLATE).num_args(..=1).value_parser(clap::value_parser!(OsString))) + .arg( + Arg::new(ARG_TEMPLATE) + .num_args(..=1) + .value_parser(clap::value_parser!(OsString)), + ) } fn dry_exec(tmpdir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult { diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index c2c6beab8..e8721d981 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) tempdir dyld dylib optgrps libstdbuf use clap::{Arg, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::os::unix::process::ExitStatusExt; use std::path::PathBuf; use std::process; @@ -183,9 +184,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = ProgramOptions::try_from(&matches).map_err(|e| UUsageError::new(125, e.to_string()))?; - let mut command_values = matches.get_many::(options::COMMAND).unwrap(); + let mut command_values = matches.get_many::(options::COMMAND).unwrap(); let mut command = process::Command::new(command_values.next().unwrap()); - let command_params: Vec<&str> = command_values.map(|s| s.as_ref()).collect(); + let command_params: Vec<&OsString> = command_values.collect(); let tmp_dir = tempdir().unwrap(); let (preload_env, libstdbuf) = get_preload_env(&tmp_dir)?; @@ -269,6 +270,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .hide(true) .required(true) - .value_hint(clap::ValueHint::CommandName), + .value_hint(clap::ValueHint::CommandName) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 810e5df5d..71e368bf8 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore dyld dylib setvbuf +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] use uutests::util::TestScenario; @@ -216,3 +218,20 @@ fn test_libstdbuf_preload() { "uutils echo should not show architecture mismatch" ); } + +#[cfg(target_os = "linux")] +#[test] +fn test_stdbuf_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content for stdbuf\n").unwrap(); + + ucmd.arg("-o0") + .arg("cat") + .arg(&filename) + .succeeds() + .stdout_is("test content for stdbuf\n"); +} From 373aafc5ffa1e1b7ccf1b77ad7359cb663d41fa6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 16:32:41 +0200 Subject: [PATCH 59/83] Fix ptx to handle non-UTF-8 filenames --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 213 +++++++++++++++++++++-- src/uu/ptx/src/ptx.rs | 45 +++-- 2 files changed, 223 insertions(+), 35 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index 78a6c2d0a..b02ad1dab 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -17,23 +17,87 @@ use std::path::PathBuf; use uufuzz::{run_gnu_cmd, CommandResult}; // Programs that typically take file/path arguments and should be tested static PATH_PROGRAMS: &[&str] = &[ - // Core file operations "cat", "cp", "mv", "rm", "ln", "link", "unlink", "touch", "truncate", - // Directory operations - "ls", "mkdir", "rmdir", "du", "stat", "mktemp", + "cat", + "cp", + "mv", + "rm", + "ln", + "link", + "unlink", + "touch", // Path operations - "basename", "dirname", "readlink", "realpath", "pathchk", - // File content operations - "head", "tail", "tee", "more", "od", "wc", "cksum", "sum", + "ls", "mkdir", "rmdir", "du", "stat", "mktemp", "df", // Path operations + "ls", + "mkdir", + "rmdir", + "du", + "stat", + "df", + "df", // Path operations + "basename", + "dirname", + "readlink", + "realpath", + "pathchk", // File processing - "sort", "uniq", "split", "csplit", "cut", "tr", "shred", - // File permissions/ownership - "chmod", "chown", "chgrp", "install", + "head", "tail", "tee", "more", "od", "wc", "cksum", "sum", "nl", "tac", // File processing + "head", + "tail", + "tee", + "more", + "od", + "wc", + "cksum", + "sum", + "tac", + "tac", // File processing + "sort", + "uniq", + "split", + "csplit", + "cut", + "tr", + "shred", + "shuf", + "ptx", // Text processing with files - "comm", "join", "paste", "pr", "fmt", "fold", "expand", "unexpand", - // Directory listing variants + "chmod", "chown", "chgrp", "install", "chcon", "runcon", // Text processing with files + "chmod", + "chown", + "chgrp", + "install", + "runcon", + "runcon", // Text processing with files + "comm", + "join", + "paste", + "pr", + "fmt", + "fold", + "expand", "dir", "vdir", + "dir", + "mkfifo", "mknod", + "mkfifo", + "mknod", + // File I/O utilities + // File I/O utilities + "dd", + "sync", + "stdbuf", + "dircolors", + "base32", "base64", "basenc", + "base32", + "base64", + "stty", "tty", + "stty", + "env", "nohup", "nice", "timeout", + "env", + "nohup", + "nice", ]; + "timeout", fn generate_non_utf8_bytes() -> Vec { let mut rng = rand::rng(); @@ -94,7 +158,8 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu let path_os = path.as_os_str(); // Use the locally built uutils binary instead of system PATH - let local_binary = "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); // Build appropriate arguments for each program let local_args = match program { @@ -114,6 +179,22 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("root"), path_os.to_owned(), ], + "chcon" => vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + path_os.to_owned(), + ], + "runcon" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, // Programs that need source and destination "cp" | "mv" | "ln" | "link" => { let dest_path = path.with_extension("dest"); @@ -122,7 +203,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu path_os.to_owned(), dest_path.as_os_str().to_owned(), ] - }, + } "install" => { let dest_path = path.with_extension("dest"); vec![ @@ -130,7 +211,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu path_os.to_owned(), dest_path.as_os_str().to_owned(), ] - }, + } // Programs that need size/truncate operations "truncate" => vec![ OsString::from(program), @@ -147,6 +228,104 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu path_os.to_owned(), OsString::from("1"), ], + // File creation programs + "mkfifo" | "mknod" => { + let new_path = path.with_extension("new"); + if program == "mknod" { + vec![ + OsString::from(program), + new_path.as_os_str().to_owned(), + OsString::from("c"), + OsString::from("1"), + OsString::from("3"), + ] + vec![ + } + }, + } + "dd" => vec![ + OsString::from(program), + OsString::from(format!("if={}", path_os.to_string_lossy())), + OsString::from("of=/dev/null"), + OsString::from("bs=1"), + OsString::from("count=1"), + ], + // Hashsum needs algorithm + "hashsum" => vec![ + OsString::from(program), + OsString::from("--md5"), + path_os.to_owned(), + ], + "base32" | "base64" | "basenc" => vec![ + "df" => vec![ + "chroot" => { + "df" => vec![OsString::from(program), path_os.to_owned()], + // chroot needs a directory and command + vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("true"), + }, + } + "stty" => vec![ + "sync" => vec![OsString::from(program), path_os.to_owned()], + OsString::from(program), + OsString::from("-F"), + path_os.to_owned(), + "tty" => vec![ + "tty" => vec![OsString::from(program)], // tty doesn't take file args, but test anyway + "env" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, + "nohup" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, + "nice" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, + "timeout" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("1"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, + "stdbuf" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("-o0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + }, // Programs that work with multiple files (use just one for testing) "comm" | "join" => { // These need two files, use the same file twice for simplicity @@ -155,7 +334,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu path_os.to_owned(), path_os.to_owned(), ] - }, + } // Programs that typically take file input _ => vec![OsString::from(program), path_os.to_owned()], }; @@ -238,8 +417,8 @@ fuzz_target!(|_data: &[u8]| { let non_utf8_dir_name = generate_non_utf8_osstring(); let non_utf8_dir = temp_root.join(non_utf8_dir_name); - let local_binary = - "/home/sylvestre/dev/debian/coreutils.disable-loca/target/debug/coreutils"; + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; let mkdir_result = run_gnu_cmd(local_binary, &mkdir_args, false, None); diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index b8352e06f..60ded7c83 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -7,10 +7,12 @@ use std::cmp; use std::collections::{BTreeSet, HashMap, HashSet}; +use std::ffi::OsString; use std::fmt::Write as FmtWrite; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::num::ParseIntError; +use std::path::Path; use clap::{Arg, ArgAction, Command}; use regex::Regex; @@ -66,13 +68,12 @@ fn read_word_filter_file( option: &str, ) -> std::io::Result> { let filename = matches - .get_one::(option) - .expect("parsing options failed!") - .to_string(); + .get_one::(option) + .expect("parsing options failed!"); let reader: BufReader> = BufReader::new(if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }); let mut words: HashSet = HashSet::new(); @@ -88,12 +89,12 @@ fn read_char_filter_file( option: &str, ) -> std::io::Result> { let filename = matches - .get_one::(option) + .get_one::(option) .expect("parsing options failed!"); let mut reader: Box = if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }; let mut buffer = String::new(); @@ -275,14 +276,14 @@ struct FileContent { type FileMap = HashMap; -fn read_input(input_files: &[String]) -> std::io::Result { +fn read_input(input_files: &[OsString]) -> std::io::Result { let mut file_map: FileMap = HashMap::new(); let mut offset: usize = 0; for filename in input_files { let reader: BufReader> = BufReader::new(if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }); let lines: Vec = reader.lines().collect::>>()?; @@ -292,7 +293,7 @@ fn read_input(input_files: &[String]) -> std::io::Result { let chars_lines: Vec> = lines.iter().map(|x| x.chars().collect()).collect(); let size = lines.len(); file_map.insert( - filename.to_owned(), + filename.to_string_lossy().into_owned(), FileContent { lines, chars_lines, @@ -736,7 +737,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let output_file; let mut files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .into_iter() .flatten() .cloned(); @@ -745,18 +746,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { input_files = { let mut files = files.collect::>(); if files.is_empty() { - files.push("-".to_string()); + files.push(OsString::from("-")); } files }; output_file = "-".to_string(); } else { - input_files = vec![files.next().unwrap_or("-".to_string())]; - output_file = files.next().unwrap_or("-".to_string()); + input_files = vec![files.next().unwrap_or(OsString::from("-"))]; + output_file = files + .next() + .unwrap_or(OsString::from("-")) + .to_string_lossy() + .into_owned(); if let Some(file) = files.next() { return Err(UUsageError::new( 1, - translate!("ptx-error-extra-operand", "operand" => file.quote()), + translate!("ptx-error-extra-operand", "operand" => file.to_string_lossy().quote()), )); } } @@ -778,7 +783,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::AUTO_REFERENCE) @@ -856,7 +862,8 @@ pub fn uu_app() -> Command { .long(options::BREAK_FILE) .help(translate!("ptx-help-break-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::IGNORE_CASE) @@ -878,7 +885,8 @@ pub fn uu_app() -> Command { .long(options::IGNORE_FILE) .help(translate!("ptx-help-ignore-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::ONLY_FILE) @@ -886,7 +894,8 @@ pub fn uu_app() -> Command { .long(options::ONLY_FILE) .help(translate!("ptx-help-only-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::REFERENCES) From b46188cdb20599e024a821d2a532b18de01a358b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:06:35 +0200 Subject: [PATCH 60/83] Fix nl to handle non-UTF-8 filenames --- src/uu/nl/src/nl.rs | 11 +++++++---- tests/by-util/test_nl.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 890de1e86..f63be4022 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufRead, BufReader, Read, stdin}; use std::path::Path; @@ -195,9 +196,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let mut stats = Stats::new(settings.starting_line_number); @@ -216,7 +217,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ); set_exit_code(1); } else { - let reader = File::open(path).map_err_context(|| file.to_string())?; + let reader = + File::open(path).map_err_context(|| file.to_string_lossy().to_string())?; let mut buffer = BufReader::new(reader); nl(&mut buffer, &mut stats, &settings)?; } @@ -245,7 +247,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::BODY_NUMBERING) diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index 7e9fb7c14..34a869dbb 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.rs @@ -9,6 +9,22 @@ use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; +#[test] +#[cfg(target_os = "linux")] +fn test_nl_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line 1\nline 2\nline 3\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_contains("1\t") + .stdout_contains("2\t") + .stdout_contains("3\t"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From a69e6c9bd3af7ba1880cffd0b4f034c17d88af31 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:11:43 +0200 Subject: [PATCH 61/83] Fix dircolors to handle non-UTF-8 filenames --- src/uu/dircolors/src/dircolors.rs | 18 ++++++++++++------ tests/by-util/test_dircolors.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 87a459f29..c00f5d210 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -7,6 +7,7 @@ use std::borrow::Borrow; use std::env; +use std::ffi::OsString; use std::fmt::Write as _; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -124,7 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); let files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .map_or(vec![], |file_values| file_values.collect()); // clap provides .conflicts_with / .conflicts_with_all, but we want to @@ -149,7 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if !files.is_empty() { return Err(UUsageError::new( 1, - translate!("dircolors-error-extra-operand-print-database", "operand" => files[0].quote()), + translate!("dircolors-error-extra-operand-print-database", "operand" => files[0].to_string_lossy().quote()), )); } @@ -198,14 +199,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else if files.len() > 1 { return Err(UUsageError::new( 1, - translate!("dircolors-error-extra-operand", "operand" => files[1].quote()), + translate!("dircolors-error-extra-operand", "operand" => files[1].to_string_lossy().quote()), )); - } else if files[0].eq("-") { + } else if files[0] == "-" { let fin = BufReader::new(std::io::stdin()); // For example, for echo "owt 40;33"|dircolors -b - - result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]); + result = parse( + fin.lines().map_while(Result::ok), + &out_format, + &files[0].to_string_lossy(), + ); } else { - let path = Path::new(files[0]); + let path = Path::new(&files[0]); if path.is_dir() { return Err(USimpleError::new( 2, @@ -280,6 +285,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .action(ArgAction::Append), ) } diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index dde00a494..dd98e5d43 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -3,10 +3,28 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore overridable colorterm +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use dircolors::{OutputFmt, StrUtils, guess_syntax}; +#[test] +#[cfg(target_os = "linux")] +fn test_dircolors_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"NORMAL 00\n*.txt 32\n").unwrap(); + + ucmd.env("SHELL", "bash") + .arg(&filename) + .succeeds() + .stdout_contains("LS_COLORS=") + .stdout_contains("*.txt=32"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From 596784e70eb6932a1873d5154a09038cbc09189a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:12:54 +0200 Subject: [PATCH 62/83] Fix base64/base32/basenc to handle non-UTF-8 filenames --- src/uu/base32/src/base_common.rs | 6 ++++-- tests/by-util/test_base64.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index db1e97016..5c5dd983d 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -6,6 +6,7 @@ // spell-checker:ignore hexupper lsbf msbf unpadded nopad aGVsbG8sIHdvcmxkIQ use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; @@ -44,14 +45,14 @@ pub mod options { impl Config { pub fn from(options: &clap::ArgMatches) -> UResult { - let to_read = match options.get_many::(options::FILE) { + let to_read = match options.get_many::(options::FILE) { Some(mut values) => { let name = values.next().unwrap(); if let Some(extra_op) = values.next() { return Err(UUsageError::new( BASE_CMD_PARSE_ERROR, - translate!("base-common-extra-operand", "operand" => extra_op.quote()), + translate!("base-common-extra-operand", "operand" => extra_op.to_string_lossy().quote()), )); } @@ -143,6 +144,7 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { Arg::new(options::FILE) .index(1) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index ad0d1c2b1..17b46ab29 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -2,9 +2,25 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +#[test] +#[cfg(target_os = "linux")] +fn test_base64_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"hello world").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("aGVsbG8gd29ybGQ=\n"); +} + #[test] fn test_encode() { let input = "hello, world!"; From ba7d902e50d77c31fc77f9295a9cc4de2342cd88 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:14:01 +0200 Subject: [PATCH 63/83] Fix tsort to handle non-UTF-8 filenames --- src/uu/tsort/src/tsort.rs | 12 +++++++----- tests/by-util/test_tsort.rs | 12 ++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 33822a474..41a3cb79d 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -5,6 +5,7 @@ //spell-checker:ignore TAOCP indegree use clap::{Arg, Command}; use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::OsString; use std::path::Path; use thiserror::Error; use uucore::display::Quotable; @@ -47,26 +48,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); let input = matches - .get_one::(options::FILE) + .get_one::(options::FILE) .expect("Value is required by clap"); let data = if input == "-" { let stdin = std::io::stdin(); std::io::read_to_string(stdin)? } else { - let path = Path::new(&input); + let path = Path::new(input); if path.is_dir() { - return Err(TsortError::IsDir(input.to_string()).into()); + return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); } std::fs::read_to_string(path)? }; // Create the directed graph from pairs of tokens in the input data. - let mut g = Graph::new(input.clone()); + let mut g = Graph::new(input.to_string_lossy().to_string()); for ab in data.split_whitespace().collect::>().chunks(2) { match ab { [a, b] => g.add_edge(a, b), - _ => return Err(TsortError::NumTokensOdd(input.to_string()).into()), + _ => return Err(TsortError::NumTokensOdd(input.to_string_lossy().to_string()).into()), } } @@ -85,6 +86,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .default_value("-") .hide(true) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index b680aa928..eb1a8630d 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -7,6 +7,18 @@ use uutests::at_and_ucmd; use uutests::new_ucmd; +#[test] +#[cfg(target_os = "linux")] +fn test_tsort_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"a b\nb c\n").unwrap(); + + ucmd.arg(&filename).succeeds().stdout_is("a\nb\nc\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From 741370d771723a5bc03aaf64bde68d2fc560a24a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:20:51 +0200 Subject: [PATCH 64/83] Fix tac to handle non-UTF-8 filenames --- src/uu/tac/src/tac.rs | 21 ++++++++++++--------- tests/by-util/test_tac.rs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 799e65571..c41cc8198 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -9,12 +9,12 @@ mod error; use clap::{Arg, ArgAction, Command}; use memchr::memmem; use memmap2::Mmap; +use std::ffi::OsString; use std::io::{BufWriter, Read, Write, stdin, stdout}; use std::{ fs::{File, read}, path::Path, }; -use uucore::display::Quotable; use uucore::error::UError; use uucore::error::UResult; use uucore::{format_usage, show}; @@ -46,9 +46,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { raw_separator }; - let files: Vec<&str> = match matches.get_many::(options::FILE) { - Some(v) => v.map(|s| s.as_str()).collect(), - None => vec!["-"], + let files: Vec = match matches.get_many::(options::FILE) { + Some(v) => v.cloned().collect(), + None => vec![OsString::from("-")], }; tac(&files, before, regex, separator) @@ -86,6 +86,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } @@ -221,7 +222,7 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> } #[allow(clippy::cognitive_complexity)] -fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResult<()> { +fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UResult<()> { // Compile the regular expression pattern if it is provided. let maybe_pattern = if regex { match regex::bytes::Regex::new(separator) { @@ -232,7 +233,7 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul None }; - for &filename in filenames { + for filename in filenames { let mmap; let buf; @@ -253,13 +254,15 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul } else { let path = Path::new(filename); if path.is_dir() { - let e: Box = TacError::InvalidArgument(String::from(filename)).into(); + let e: Box = + TacError::InvalidArgument(filename.to_string_lossy().to_string()).into(); show!(e); continue; } if path.metadata().is_err() { - let e: Box = TacError::FileNotFound(String::from(filename)).into(); + let e: Box = + TacError::FileNotFound(filename.to_string_lossy().to_string()).into(); show!(e); continue; } @@ -274,7 +277,7 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul &buf } Err(e) => { - let s = format!("{}", filename.quote()); + let s = filename.to_string_lossy(); let e: Box = TacError::ReadError(s.to_string(), e).into(); show!(e); continue; diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 42e7b76d6..0f5aad488 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -3,10 +3,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore axxbxx bxxaxx axxx axxxx xxaxx xxax xxxxa axyz zyax zyxa +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; +#[test] +#[cfg(target_os = "linux")] +fn test_tac_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("line3\nline2\nline1\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From 82043f2ce3f4231eec07fa254f0834b64f1b2f8a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 21:51:16 +0200 Subject: [PATCH 65/83] run fuzz_non_utf8_paths in the ci --- .github/workflows/fuzzing.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index a25ca34d0..cf8d943c3 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -62,6 +62,7 @@ jobs: - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } - { name: fuzz_seq_parse_number, should_pass: true } + - { name: fuzz_non_utf8_paths, should_pass: true } steps: - uses: actions/checkout@v5 From 98224ed0cff9a923c5be1bb80951f530fe9d5059 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 22:38:11 +0200 Subject: [PATCH 66/83] Finalize the fuzzer --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 66 ++++++++++++------------ 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index b02ad1dab..ac7480f32 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -3,10 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore osstring + #![no_main] use libfuzzer_sys::fuzz_target; -use rand::prelude::IndexedRandom; use rand::Rng; +use rand::prelude::IndexedRandom; use std::collections::HashSet; use std::env::temp_dir; use std::ffi::{OsStr, OsString}; @@ -14,10 +16,10 @@ use std::fs; use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::path::PathBuf; -use uufuzz::{run_gnu_cmd, CommandResult}; +use uufuzz::{CommandResult, run_gnu_cmd}; // Programs that typically take file/path arguments and should be tested static PATH_PROGRAMS: &[&str] = &[ - "cat", "cp", "mv", "rm", "ln", "link", "unlink", "touch", "truncate", + // Core file operations "cat", "cp", "mv", @@ -26,22 +28,22 @@ static PATH_PROGRAMS: &[&str] = &[ "link", "unlink", "touch", + "truncate", // Path operations - "ls", "mkdir", "rmdir", "du", "stat", "mktemp", "df", // Path operations "ls", "mkdir", "rmdir", "du", "stat", + "mktemp", "df", - "df", // Path operations "basename", "dirname", "readlink", "realpath", "pathchk", + "chroot", // File processing - "head", "tail", "tee", "more", "od", "wc", "cksum", "sum", "nl", "tac", // File processing "head", "tail", "tee", @@ -50,8 +52,8 @@ static PATH_PROGRAMS: &[&str] = &[ "wc", "cksum", "sum", + "nl", "tac", - "tac", // File processing "sort", "uniq", "split", @@ -61,14 +63,14 @@ static PATH_PROGRAMS: &[&str] = &[ "shred", "shuf", "ptx", + "tsort", // Text processing with files - "chmod", "chown", "chgrp", "install", "chcon", "runcon", // Text processing with files "chmod", "chown", "chgrp", "install", + "chcon", "runcon", - "runcon", // Text processing with files "comm", "join", "paste", @@ -76,28 +78,28 @@ static PATH_PROGRAMS: &[&str] = &[ "fmt", "fold", "expand", - "dir", "vdir", + "unexpand", "dir", - "mkfifo", "mknod", + "vdir", "mkfifo", "mknod", - // File I/O utilities + "hashsum", // File I/O utilities "dd", "sync", "stdbuf", "dircolors", - "base32", "base64", "basenc", + // Encoding/decoding utilities "base32", "base64", - "stty", "tty", + "basenc", "stty", - "env", "nohup", "nice", "timeout", + "tty", "env", "nohup", "nice", -]; "timeout", +]; fn generate_non_utf8_bytes() -> Vec { let mut rng = rand::rng(); @@ -194,7 +196,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } // Programs that need source and destination "cp" | "mv" | "ln" | "link" => { let dest_path = path.with_extension("dest"); @@ -239,9 +241,9 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("1"), OsString::from("3"), ] - vec![ + } else { + vec![OsString::from(program), new_path.as_os_str().to_owned()] } - }, } "dd" => vec![ OsString::from(program), @@ -256,23 +258,23 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("--md5"), path_os.to_owned(), ], - "base32" | "base64" | "basenc" => vec![ - "df" => vec![ - "chroot" => { + // Encoding/decoding programs + "base32" | "base64" | "basenc" => vec![OsString::from(program), path_os.to_owned()], "df" => vec![OsString::from(program), path_os.to_owned()], + "chroot" => { // chroot needs a directory and command vec![ OsString::from(program), path_os.to_owned(), OsString::from("true"), - }, + ] } - "stty" => vec![ "sync" => vec![OsString::from(program), path_os.to_owned()], + "stty" => vec![ OsString::from(program), OsString::from("-F"), path_os.to_owned(), - "tty" => vec![ + ], "tty" => vec![OsString::from(program)], // tty doesn't take file args, but test anyway "env" => { let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") @@ -283,7 +285,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } "nohup" => { let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") .unwrap_or_else(|_| "target/release/coreutils".to_string()); @@ -293,7 +295,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } "nice" => { let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") .unwrap_or_else(|_| "target/release/coreutils".to_string()); @@ -303,7 +305,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } "timeout" => { let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") .unwrap_or_else(|_| "target/release/coreutils".to_string()); @@ -314,7 +316,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } "stdbuf" => { let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") .unwrap_or_else(|_| "target/release/coreutils".to_string()); @@ -325,7 +327,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu OsString::from("cat"), path_os.to_owned(), ] - }, + } // Programs that work with multiple files (use just one for testing) "comm" | "join" => { // These need two files, use the same file twice for simplicity @@ -340,7 +342,7 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu }; // Try to run the local uutils version - match run_gnu_cmd(local_binary, &local_args, false, None) { + match run_gnu_cmd(&local_binary, &local_args, false, None) { Ok(result) => result, Err(error_result) => { // Local command failed, return the error @@ -421,7 +423,7 @@ fuzz_target!(|_data: &[u8]| { .unwrap_or_else(|_| "target/release/coreutils".to_string()); let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; - let mkdir_result = run_gnu_cmd(local_binary, &mkdir_args, false, None); + let mkdir_result = run_gnu_cmd(&local_binary, &mkdir_args, false, None); match mkdir_result { Ok(result) => { check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); From faf3f7e526222208f06cb63e44cc6470e191c94e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 09:59:51 +0200 Subject: [PATCH 67/83] head: remove old test that we know support it --- src/uu/head/src/head.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 7c543b157..db88f3f64 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -693,15 +693,6 @@ mod tests { assert_eq!(arg_outputs("head"), Ok("head".to_owned())); } - #[test] - #[cfg(target_os = "linux")] - fn test_arg_iterate_bad_encoding() { - use std::os::unix::ffi::OsStringExt; - let invalid = OsString::from_vec(vec![b'\x80', b'\x81']); - // this arises from a conversion from OsString to &str - assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_err()); - } - #[test] fn read_early_exit() { let mut empty = io::BufReader::new(Cursor::new(Vec::new())); From 1ab3a8df4f88d40065317f20c2cdb5632a0b8dfd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 18:02:32 +0200 Subject: [PATCH 68/83] address review comments --- src/uu/cat/src/cat.rs | 2 +- src/uu/du/src/du.rs | 22 +++++++--------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index d19c39f56..89c9f2111 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -455,7 +455,7 @@ fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> { /// /// * `path` - Path on a file system to classify metadata fn get_input_type(path: &OsString) -> CatResult { - if path.to_str() == Some("-") { + if path == "-" { return Ok(InputType::StdIn); } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 64c99998a..d4e20054d 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -7,14 +7,11 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; -#[cfg(unix)] use std::ffi::OsStr; use std::ffi::OsString; use std::fs::Metadata; use std::fs::{self, DirEntry, File}; use std::io::{BufRead, BufReader, stdout}; -#[cfg(unix)] -use std::os::unix::ffi::OsStrExt; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; #[cfg(windows)] @@ -535,7 +532,7 @@ impl StatPrinter { } /// Read file paths from the specified file, separated by null characters -fn read_files_from(file_name: &str) -> Result, std::io::Error> { +fn read_files_from(file_name: &OsStr) -> Result, std::io::Error> { let reader: Box = if file_name == "-" { // Read from standard input Box::new(BufReader::new(std::io::stdin())) @@ -544,7 +541,7 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { let path = PathBuf::from(file_name); if path.is_dir() { return Err(std::io::Error::other( - translate!("du-error-read-error-is-directory", "file" => file_name), + translate!("du-error-read-error-is-directory", "file" => file_name.to_string_lossy()), )); } @@ -553,7 +550,7 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { Ok(file) => Box::new(BufReader::new(file)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => { return Err(std::io::Error::other( - translate!("du-error-cannot-open-for-reading", "file" => file_name), + translate!("du-error-cannot-open-for-reading", "file" => file_name.to_string_lossy()), )); } Err(e) => return Err(e), @@ -569,14 +566,11 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { let line_number = i + 1; show_error!( "{}", - translate!("du-error-invalid-zero-length-file-name", "file" => file_name, "line" => line_number) + translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number) ); set_exit_code(1); } else { - #[cfg(unix)] - let p = PathBuf::from(OsStr::from_bytes(&path)); - #[cfg(windows)] - let p = PathBuf::from(String::from_utf8_lossy(&path).to_string()); + let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap()); if !paths.contains(&p) { paths.push(p); } @@ -603,9 +597,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )?; let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { - if file_from.to_string_lossy() == "-" - && matches.get_one::(options::FILE).is_some() - { + if file_from == "-" && matches.get_one::(options::FILE).is_some() { return Err(std::io::Error::other( translate!("du-error-extra-operand-with-files0-from", "file" => matches @@ -618,7 +610,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .into()); } - read_files_from(&file_from.to_string_lossy())? + read_files_from(file_from)? } else if let Some(files) = matches.get_many::(options::FILE) { let files = files.map(PathBuf::from); if count_links { From a7f3cb0209cf379b51152c287a5fa0adac12e1cf Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 9 Aug 2025 19:02:49 +0200 Subject: [PATCH 69/83] realpath: fix regression with empty string validation Fixes issue introduced in b965c944837df66b233f57fca7275fbed4e4d311 where switching from NonEmptyStringValueParser to OsString parser removed validation that arguments cannot be empty strings. - Add custom NonEmptyOsStringParser that validates OsString is not empty - Use the parser for FILES, --relative-to, and --relative-base arguments - Add test case to verify empty strings are rejected with exit code 1 - Fixes GNU realpath test failure --- src/uu/realpath/src/realpath.rs | 46 +++++++++++++++++++++++++++++---- tests/by-util/test_realpath.rs | 21 +++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index f855f5cea..6709e7e3a 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -5,9 +5,12 @@ // spell-checker:ignore (ToDO) retcode -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::{ + Arg, ArgAction, ArgMatches, Command, + builder::{TypedValueParser, ValueParserFactory}, +}; use std::{ - ffi::OsString, + ffi::{OsStr, OsString}, io::{Write, stdout}, path::{Path, PathBuf}, }; @@ -34,6 +37,39 @@ const OPT_RELATIVE_BASE: &str = "relative-base"; const ARG_FILES: &str = "files"; +/// Custom parser that validates `OsString` is not empty +#[derive(Clone, Debug)] +struct NonEmptyOsStringParser; + +impl TypedValueParser for NonEmptyOsStringParser { + type Value = OsString; + + fn parse_ref( + &self, + _cmd: &Command, + _arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + if value.is_empty() { + let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation); + err.insert( + clap::error::ContextKind::Custom, + clap::error::ContextValue::String("invalid operand: empty string".to_string()), + ); + return Err(err); + } + Ok(value.to_os_string()) + } +} + +impl ValueParserFactory for NonEmptyOsStringParser { + type Parser = Self; + + fn value_parser() -> Self::Parser { + Self + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args).with_exit_code(1)?; @@ -146,21 +182,21 @@ pub fn uu_app() -> Command { Arg::new(OPT_RELATIVE_TO) .long(OPT_RELATIVE_TO) .value_name("DIR") - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-to")), ) .arg( Arg::new(OPT_RELATIVE_BASE) .long(OPT_RELATIVE_BASE) .value_name("DIR") - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-base")), ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) .required(true) - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .value_hint(clap::ValueHint::AnyPath), ) } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index a081b163f..24ca48870 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -489,3 +489,24 @@ fn test_realpath_non_utf8_paths() { assert!(output.contains("test_")); assert!(output.contains(".txt")); } + +#[test] +fn test_realpath_empty_string() { + // Test that empty string arguments are rejected with exit code 1 + new_ucmd!().arg("").fails().code_is(1); + + // Test that empty --relative-base is rejected + new_ucmd!() + .arg("--relative-base=") + .arg("--relative-to=.") + .arg(".") + .fails() + .code_is(1); + + // Test that empty --relative-to is rejected + new_ucmd!() + .arg("--relative-to=") + .arg(".") + .fails() + .code_is(1); +} From 5ae195b99a26b8e6e95a20f0dfd046040772784a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 22:26:10 +0200 Subject: [PATCH 70/83] Update tests/by-util/test_pathchk.rs Co-authored-by: Daniel Hofstetter --- tests/by-util/test_pathchk.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index d0197d9bf..85f5c09ea 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -170,13 +170,6 @@ fn test_posix_all() { #[test] #[cfg(target_os = "linux")] fn test_pathchk_non_utf8_paths() { - use uutests::at_and_ucmd; - - let (at, mut ucmd) = at_and_ucmd!(); let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); - - // Create the file so pathchk can check it exists - std::fs::write(at.plus(&filename), b"test").unwrap(); - - ucmd.arg(&filename).succeeds(); + new_ucmd!().arg(&filename).succeeds(); } From 344798e144e0da1ba07ee9d7c940d5980a51c021 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 22:26:20 +0200 Subject: [PATCH 71/83] Update src/uu/readlink/src/readlink.rs Co-authored-by: Nicolas Boichat --- src/uu/readlink/src/readlink.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 4d5e32f78..03d6d73ff 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -178,8 +178,7 @@ pub fn uu_app() -> Command { } fn show(path: &Path, line_ending: Option) -> std::io::Result<()> { - use uucore::display::print_verbatim; - print_verbatim(path)?; + uucore::display::print_verbatim(path)?; if let Some(line_ending) = line_ending { print!("{line_ending}"); } From f65c36d6094971578ef8546cd7cc75dedb04c1e4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 23:16:40 +0200 Subject: [PATCH 72/83] address review comments --- src/uu/head/src/head.rs | 9 +++++++++ src/uu/ptx/src/ptx.rs | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index db88f3f64..f18b4c979 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -693,6 +693,15 @@ mod tests { assert_eq!(arg_outputs("head"), Ok("head".to_owned())); } + #[test] + #[cfg(target_os = "linux")] + fn test_arg_iterate_bad_encoding() { + use std::os::unix::ffi::OsStringExt; + let invalid = OsString::from_vec(vec![b'\x80', b'\x81']); + // this arises from a conversion from OsString to &str + assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_ok()); + } + #[test] fn read_early_exit() { let mut empty = io::BufReader::new(Cursor::new(Vec::new())); diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 60ded7c83..7fc44665c 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -192,7 +192,7 @@ struct WordRef { local_line_nr: usize, position: usize, position_end: usize, - filename: String, + filename: OsString, } #[derive(Debug, Error)] @@ -274,7 +274,7 @@ struct FileContent { offset: usize, } -type FileMap = HashMap; +type FileMap = HashMap; fn read_input(input_files: &[OsString]) -> std::io::Result { let mut file_map: FileMap = HashMap::new(); @@ -293,7 +293,7 @@ fn read_input(input_files: &[OsString]) -> std::io::Result { let chars_lines: Vec> = lines.iter().map(|x| x.chars().collect()).collect(); let size = lines.len(); file_map.insert( - filename.to_string_lossy().into_owned(), + filename.clone(), FileContent { lines, chars_lines, @@ -661,7 +661,7 @@ fn write_traditional_output( for word_ref in words { let file_map_value: &FileContent = file_map - .get(&(word_ref.filename)) + .get(&word_ref.filename) .expect("Missing file in file map"); let FileContent { ref lines, From f450505e6e6a187dafa245a258ddeae6cdf472cb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 23:32:55 +0200 Subject: [PATCH 73/83] ptx: write_traditional_output also support non-utf8 --- src/uu/ptx/src/ptx.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 7fc44665c..a5696b96b 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -7,7 +7,7 @@ use std::cmp; use std::collections::{BTreeSet, HashMap, HashSet}; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::fmt::Write as FmtWrite; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; @@ -647,15 +647,16 @@ fn write_traditional_output( config: &Config, file_map: &FileMap, words: &BTreeSet, - output_filename: &str, + output_filename: &OsStr, ) -> UResult<()> { - let mut writer: BufWriter> = BufWriter::new(if output_filename == "-" { - Box::new(stdout()) - } else { - let file = File::create(output_filename) - .map_err_context(|| output_filename.maybe_quote().to_string())?; - Box::new(file) - }); + let mut writer: BufWriter> = + BufWriter::new(if output_filename == OsStr::new("-") { + Box::new(stdout()) + } else { + let file = File::create(output_filename) + .map_err_context(|| output_filename.to_string_lossy().quote().to_string())?; + Box::new(file) + }); let context_reg = Regex::new(&config.context_regex).unwrap(); @@ -734,7 +735,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = get_config(&matches)?; let input_files; - let output_file; + let output_file: OsString; let mut files = matches .get_many::(options::FILE) @@ -750,14 +751,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } files }; - output_file = "-".to_string(); + output_file = OsString::from("-"); } else { input_files = vec![files.next().unwrap_or(OsString::from("-"))]; - output_file = files - .next() - .unwrap_or(OsString::from("-")) - .to_string_lossy() - .into_owned(); + output_file = files.next().unwrap_or(OsString::from("-")); if let Some(file) = files.next() { return Err(UUsageError::new( 1, From a15f9646e84f16d2c31e337c7e68a3fc32976397 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 23:34:36 +0200 Subject: [PATCH 74/83] realpath/split: fix support non-utf-8 --- src/uu/readlink/src/readlink.rs | 5 ++--- src/uu/realpath/locales/en-US.ftl | 3 +++ src/uu/realpath/locales/fr-FR.ftl | 3 +++ src/uu/realpath/src/realpath.rs | 2 +- src/uu/split/src/platform/unix.rs | 5 +++-- src/uu/split/src/platform/windows.rs | 3 ++- src/uu/split/src/split.rs | 2 +- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 03d6d73ff..383acb409 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -78,8 +78,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Some(LineEnding::from_zero_flag(use_zero)) }; - for f in &files { - let p = f; + for p in &files { let path_result = if res_mode == ResolveMode::None { fs::read_link(p) } else { @@ -94,7 +93,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return if verbose { Err(USimpleError::new( 1, - err.map_err_context(move || f.to_string_lossy().to_string()) + err.map_err_context(move || p.to_string_lossy().to_string()) .to_string(), )) } else { diff --git a/src/uu/realpath/locales/en-US.ftl b/src/uu/realpath/locales/en-US.ftl index d0fdf586b..9da567cdc 100644 --- a/src/uu/realpath/locales/en-US.ftl +++ b/src/uu/realpath/locales/en-US.ftl @@ -11,3 +11,6 @@ realpath-help-canonicalize-existing = canonicalize by following every symlink in realpath-help-canonicalize-missing = canonicalize by following every symlink in every component of the given name recursively, without requirements on components existence realpath-help-relative-to = print the resolved path relative to DIR realpath-help-relative-base = print absolute paths unless paths below DIR + +# Error messages +realpath-invalid-empty-operand = invalid operand: empty string diff --git a/src/uu/realpath/locales/fr-FR.ftl b/src/uu/realpath/locales/fr-FR.ftl index f1f94d14b..16b522ee9 100644 --- a/src/uu/realpath/locales/fr-FR.ftl +++ b/src/uu/realpath/locales/fr-FR.ftl @@ -11,3 +11,6 @@ realpath-help-canonicalize-existing = canonicaliser en suivant récursivement ch realpath-help-canonicalize-missing = canonicaliser en suivant récursivement chaque lien symbolique dans chaque composant du nom donné, sans exigences sur l'existence des composants realpath-help-relative-to = afficher le chemin résolu relativement à RÉP realpath-help-relative-base = afficher les chemins absolus sauf pour les chemins sous RÉP + +# Messages d'erreur +realpath-invalid-empty-operand = opérande invalide : chaîne vide diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 6709e7e3a..2f1efacc6 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -54,7 +54,7 @@ impl TypedValueParser for NonEmptyOsStringParser { let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation); err.insert( clap::error::ContextKind::Custom, - clap::error::ContextValue::String("invalid operand: empty string".to_string()), + clap::error::ContextValue::String(translate!("realpath-invalid-empty-operand")), ); return Err(err); } diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index 97cb782ba..d1257954d 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::env; +use std::ffi::OsStr; use std::io::Write; use std::io::{BufWriter, Error, Result}; use std::path::Path; @@ -163,12 +164,12 @@ pub fn instantiate_current_writer( } } -pub fn paths_refer_to_same_file(p1: &str, p2: &str) -> bool { +pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool { // We have to take symlinks and relative paths into account. let p1 = if p1 == "-" { FileInformation::from_file(&std::io::stdin()) } else { - FileInformation::from_path(Path::new(&p1), true) + FileInformation::from_path(Path::new(p1), true) }; fs::infos_refer_to_same_file(p1, FileInformation::from_path(Path::new(p2), true)) } diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index f6659ebce..e443a9cfb 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::ffi::OsStr; use std::io::Write; use std::io::{BufWriter, Error, Result}; use std::path::Path; @@ -39,7 +40,7 @@ pub fn instantiate_current_writer( Ok(BufWriter::new(Box::new(file) as Box)) } -pub fn paths_refer_to_same_file(p1: &str, p2: &str) -> bool { +pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool { // Windows doesn't support many of the unix ways of paths being equals fs::paths_refer_to_same_file(Path::new(p1), Path::new(p2), true) } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index f16cb14d4..90b55b6b2 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -530,7 +530,7 @@ impl Settings { filename: &str, is_new: bool, ) -> io::Result>> { - if platform::paths_refer_to_same_file(&self.input.to_string_lossy(), filename) { + if platform::paths_refer_to_same_file(&self.input, filename.as_ref()) { return Err(io::Error::other( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); From 39fbdeecb2e81474d693d605e59ec3c96d186ad2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 11 Aug 2025 23:47:23 +0200 Subject: [PATCH 75/83] tac/touch: fix support non-utf-8 --- src/uu/tac/src/error.rs | 9 +++++---- src/uu/tac/src/tac.rs | 11 ++++------- src/uu/touch/src/touch.rs | 32 ++++++++++++++------------------ tests/by-util/test_truncate.rs | 21 +++++---------------- tests/by-util/test_unlink.rs | 10 ++-------- 5 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/uu/tac/src/error.rs b/src/uu/tac/src/error.rs index a69a34a0d..133a46266 100644 --- a/src/uu/tac/src/error.rs +++ b/src/uu/tac/src/error.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. //! Errors returned by tac during processing of a file. +use std::ffi::OsString; use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; @@ -16,16 +17,16 @@ pub enum TacError { InvalidRegex(regex::Error), /// An argument to tac is invalid. #[error("{}", translate!("tac-error-invalid-argument", "argument" => .0.maybe_quote()))] - InvalidArgument(String), + InvalidArgument(OsString), /// The specified file is not found on the filesystem. #[error("{}", translate!("tac-error-file-not-found", "filename" => .0.quote()))] - FileNotFound(String), + FileNotFound(OsString), /// An error reading the contents of a file or stdin. /// /// The parameters are the name of the file and the underlying /// [`std::io::Error`] that caused this error. - #[error("{}", translate!("tac-error-read-error", "filename" => .0.clone(), "error" => .1))] - ReadError(String, std::io::Error), + #[error("{}", translate!("tac-error-read-error", "filename" => .0.quote(), "error" => .1))] + ReadError(OsString, std::io::Error), /// An error writing the (reversed) contents of a file or stdin. /// /// The parameter is the underlying [`std::io::Error`] that caused diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index c41cc8198..521e2c9db 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -244,7 +244,7 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR } else { let mut buf1 = Vec::new(); if let Err(e) = stdin().read_to_end(&mut buf1) { - let e: Box = TacError::ReadError("stdin".to_string(), e).into(); + let e: Box = TacError::ReadError(OsString::from("stdin"), e).into(); show!(e); continue; } @@ -254,15 +254,13 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR } else { let path = Path::new(filename); if path.is_dir() { - let e: Box = - TacError::InvalidArgument(filename.to_string_lossy().to_string()).into(); + let e: Box = TacError::InvalidArgument(filename.clone()).into(); show!(e); continue; } if path.metadata().is_err() { - let e: Box = - TacError::FileNotFound(filename.to_string_lossy().to_string()).into(); + let e: Box = TacError::FileNotFound(filename.clone()).into(); show!(e); continue; } @@ -277,8 +275,7 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR &buf } Err(e) => { - let s = filename.to_string_lossy(); - let e: Box = TacError::ReadError(s.to_string(), e).into(); + let e: Box = TacError::ReadError(filename.clone(), e).into(); show!(e); continue; } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 7354c20c9..6994a5f70 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -159,21 +159,18 @@ fn is_first_filename_timestamp( timestamp: Option<&str>, files: &[&OsString], ) -> bool { - if timestamp.is_none() + timestamp.is_none() && reference.is_none() && date.is_none() && files.len() >= 2 // env check is last as the slowest op && matches!(std::env::var("_POSIX2_VERSION").as_deref(), Ok("199209")) - { - if let Some(s) = files[0].to_str() { - all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) - } else { - false - } - } else { - false - } + && files[0].to_str().is_some_and(is_timestamp) +} + +// Check if string is a valid POSIX timestamp (8 digits or 10 digits with valid year range) +fn is_timestamp(s: &str) -> bool { + all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) } /// Cycle the last two characters to the beginning of the string. @@ -214,14 +211,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|t| t.to_owned()); if is_first_filename_timestamp(reference, date.as_deref(), timestamp.as_deref(), &filenames) { - if let Some(first_file) = filenames[0].to_str() { - timestamp = if first_file.len() == 10 { - Some(shr2(first_file)) - } else { - Some(first_file.to_string()) - }; - filenames = filenames[1..].to_vec(); - } + let first_file = filenames[0].to_str().unwrap(); + timestamp = if first_file.len() == 10 { + Some(shr2(first_file)) + } else { + Some(first_file.to_string()) + }; + filenames = filenames[1..].to_vec(); } let source = if let Some(reference) = reference { diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index ef1ea872c..36ce92dc9 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -424,23 +424,12 @@ fn test_fifo_error_reference_and_size() { #[test] #[cfg(target_os = "linux")] fn test_truncate_non_utf8_paths() { - use std::fs; - + use std::os::unix::ffi::OsStrExt; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + at.write(&file_name.to_string_lossy(), "test content"); - // Create test file with normal name first - at.write("temp.txt", "test content"); - - // Rename to non-UTF-8 name - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - - fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); - - // Test that truncate can handle non-UTF-8 filenames - ts.ucmd().arg("-s").arg("10").arg(file_name).succeeds(); - } + // Test that truncate can handle non-UTF-8 filenames + ts.ucmd().arg("-s").arg("10").arg(file_name).succeeds(); } diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index b508fef38..7a2b50ac3 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -81,23 +81,17 @@ fn test_unlink_symlink() { fn test_unlink_non_utf8_paths() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; - use uutests::util::TestScenario; - use uutests::util_name; - - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; + let (at, mut ucmd) = at_and_ucmd!(); // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - // Create the actual file at.touch(non_utf8_name); assert!(at.file_exists(non_utf8_name)); // Test that unlink handles non-UTF-8 file names without crashing - scene.ucmd().arg(non_utf8_name).succeeds(); + ucmd.arg(non_utf8_name).succeeds(); - // The file should be removed assert!(!at.file_exists(non_utf8_name)); } From 5a6986d55f4fa1a7e940e4ed06f486f04e63e99b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 12 Aug 2025 00:15:18 +0200 Subject: [PATCH 76/83] split: prefix & suffix also support non-utf8 --- src/uu/split/src/filenames.rs | 73 +++++++++++++++-------------- src/uu/split/src/split.rs | 11 +++-- tests/by-util/test_split.rs | 87 ++++++++++++++++++++++++----------- 3 files changed, 108 insertions(+), 63 deletions(-) diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 5d8796dd9..5c09bf6f2 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -39,6 +39,7 @@ use crate::{ OPT_NUMERIC_SUFFIXES_SHORT, OPT_SUFFIX_LENGTH, }; use clap::ArgMatches; +use std::ffi::{OsStr, OsString}; use std::path::is_separator; use thiserror::Error; use uucore::display::Quotable; @@ -76,7 +77,7 @@ pub struct Suffix { length: usize, start: usize, auto_widening: bool, - additional: String, + additional: OsString, } /// An error when parsing suffix parameters from command-line arguments. @@ -219,11 +220,13 @@ impl Suffix { } let additional = matches - .get_one::(OPT_ADDITIONAL_SUFFIX) + .get_one::(OPT_ADDITIONAL_SUFFIX) .unwrap() - .to_string(); - if additional.chars().any(is_separator) { - return Err(SuffixError::ContainsSeparator(additional)); + .clone(); + if additional.to_string_lossy().chars().any(is_separator) { + return Err(SuffixError::ContainsSeparator( + additional.to_string_lossy().to_string(), + )); } let result = Self { @@ -300,14 +303,14 @@ impl Suffix { /// assert_eq!(it.next().unwrap(), "chunk_02.txt"); /// ``` pub struct FilenameIterator<'a> { - prefix: &'a str, - additional_suffix: &'a str, + prefix: &'a OsStr, + additional_suffix: &'a OsStr, number: Number, first_iteration: bool, } impl<'a> FilenameIterator<'a> { - pub fn new(prefix: &'a str, suffix: &'a Suffix) -> UResult { + pub fn new(prefix: &'a OsStr, suffix: &'a Suffix) -> UResult { let radix = suffix.stype.radix(); let number = if suffix.auto_widening { Number::DynamicWidth(DynamicWidthNumber::new(radix, suffix.start)) @@ -321,7 +324,7 @@ impl<'a> FilenameIterator<'a> { })?, ) }; - let additional_suffix = suffix.additional.as_str(); + let additional_suffix = &suffix.additional; Ok(FilenameIterator { prefix, @@ -345,7 +348,9 @@ impl Iterator for FilenameIterator<'_> { // struct parameters unchanged. Some(format!( "{}{}{}", - self.prefix, self.number, self.additional_suffix + self.prefix.to_string_lossy(), + self.number, + self.additional_suffix.to_string_lossy() )) } } @@ -364,14 +369,14 @@ mod tests { length: 2, start: 0, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(26 * 26 - 1).unwrap(), "chunk_zz.txt"); assert_eq!(it.next(), None); } @@ -383,14 +388,14 @@ mod tests { length: 2, start: 0, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(10 * 10 - 1).unwrap(), "chunk_99.txt"); assert_eq!(it.next(), None); } @@ -402,14 +407,14 @@ mod tests { length: 2, start: 0, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(26 * 25 - 1).unwrap(), "chunk_yz.txt"); assert_eq!(it.next().unwrap(), "chunk_zaaa.txt"); assert_eq!(it.next().unwrap(), "chunk_zaab.txt"); @@ -422,14 +427,14 @@ mod tests { length: 2, start: 0, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(10 * 9 - 1).unwrap(), "chunk_89.txt"); assert_eq!(it.next().unwrap(), "chunk_9000.txt"); assert_eq!(it.next().unwrap(), "chunk_9001.txt"); @@ -442,9 +447,9 @@ mod tests { length: 2, start: 5, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_05.txt"); assert_eq!(it.next().unwrap(), "chunk_06.txt"); assert_eq!(it.next().unwrap(), "chunk_07.txt"); @@ -457,9 +462,9 @@ mod tests { length: 2, start: 9, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_09.txt"); assert_eq!(it.next().unwrap(), "chunk_0a.txt"); assert_eq!(it.next().unwrap(), "chunk_0b.txt"); @@ -472,9 +477,9 @@ mod tests { length: 3, start: 999, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_999.txt"); assert!(it.next().is_none()); @@ -483,9 +488,9 @@ mod tests { length: 3, start: 1000, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let it = FilenameIterator::new("chunk_", &suffix); + let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); let suffix = Suffix { @@ -493,9 +498,9 @@ mod tests { length: 3, start: 0xfff, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_fff.txt"); assert!(it.next().is_none()); @@ -504,9 +509,9 @@ mod tests { length: 3, start: 0x1000, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let it = FilenameIterator::new("chunk_", &suffix); + let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); } } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 90b55b6b2..7d6cdbd94 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -274,6 +274,7 @@ pub fn uu_app() -> Command { .allow_hyphen_values(true) .value_name("SUFFIX") .default_value("") + .value_parser(clap::value_parser!(OsString)) .help(translate!("split-help-additional-suffix")), ) .arg( @@ -378,7 +379,11 @@ pub fn uu_app() -> Command { .value_hint(ValueHint::FilePath) .value_parser(clap::value_parser!(OsString)), ) - .arg(Arg::new(ARG_PREFIX).default_value("x")) + .arg( + Arg::new(ARG_PREFIX) + .default_value("x") + .value_parser(clap::value_parser!(OsString)), + ) } /// Parameters that control how a file gets split. @@ -386,7 +391,7 @@ pub fn uu_app() -> Command { /// You can convert an [`ArgMatches`] instance into a [`Settings`] /// instance by calling [`Settings::from`]. struct Settings { - prefix: String, + prefix: OsString, suffix: Suffix, input: OsString, /// When supplied, a shell command to output to instead of xaa, xab … @@ -490,7 +495,7 @@ impl Settings { }; let result = Self { - prefix: matches.get_one::(ARG_PREFIX).unwrap().clone(), + prefix: matches.get_one::(ARG_PREFIX).unwrap().clone(), suffix, input: matches.get_one::(ARG_INPUT).unwrap().clone(), filter: matches.get_one::(OPT_FILTER).cloned(), diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 41523b355..9bef61230 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1726,32 +1726,7 @@ fn test_split_non_utf8_argument_unix() { let opt_value = [0x66, 0x6f, 0x80, 0x6f]; let opt_value = OsStr::from_bytes(&opt_value[..]); let name = OsStr::from_bytes(name.as_bytes()); - ucmd.args(&[opt, opt_value, name]) - .fails() - .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); -} - -/// Test if there are invalid (non UTF-8) in the arguments - windows -/// clap is expected to fail/panic -#[test] -#[cfg(windows)] -fn test_split_non_utf8_argument_windows() { - use std::ffi::OsString; - use std::os::windows::ffi::OsStringExt; - - let (at, mut ucmd) = at_and_ucmd!(); - let name = "test_split_non_utf8_argument"; - let opt = OsString::from("--additional-suffix"); - RandomFile::new(&at, name).add_lines(2000); - // Here the values 0x0066 and 0x006f correspond to 'f' and 'o' - // respectively. The value 0xD800 is a lone surrogate half, invalid - // in a UTF-16 sequence. - let opt_value = [0x0066, 0x006f, 0xD800, 0x006f]; - let opt_value = OsString::from_wide(&opt_value[..]); - let name = OsString::from(name); - ucmd.args(&[opt, opt_value, name]) - .fails() - .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); + ucmd.args(&[opt, opt_value, name]).succeeds(); } // Test '--separator' / '-t' option following GNU tests example @@ -2022,3 +1997,63 @@ fn test_split_non_utf8_paths() { // Check that at least one split file was created assert!(at.plus("xaa").exists()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_prefix() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("input.txt", "line1\nline2\nline3\nline4\n"); + + let prefix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); + ucmd.arg("input.txt").arg(prefix).succeeds(); + + // Check that split files were created (functionality works) + // The actual filename may be converted due to lossy conversion, but the command should succeed + let entries: Vec<_> = std::fs::read_dir(at.as_string()).unwrap().collect(); + let split_files = entries + .iter() + .filter_map(|e| e.as_ref().ok()) + .filter(|entry| { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + name_str.starts_with("�") || name_str.len() > 2 // split files should exist + }) + .count(); + assert!( + split_files > 0, + "Expected at least one split file to be created" + ); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_additional_suffix() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("input.txt", "line1\nline2\nline3\nline4\n"); + + let suffix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); + ucmd.args(&["input.txt", "--additional-suffix"]) + .arg(suffix) + .succeeds(); + + // Check that split files were created (functionality works) + // The actual filename may be converted due to lossy conversion, but the command should succeed + let entries: Vec<_> = std::fs::read_dir(at.as_string()).unwrap().collect(); + let split_files = entries + .iter() + .filter_map(|e| e.as_ref().ok()) + .filter(|entry| { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + name_str.ends_with("�") || name_str.starts_with('x') // split files should exist + }) + .count(); + assert!( + split_files > 0, + "Expected at least one split file to be created" + ); +} From 71b4de24e6a56772404a8e7df276ccc7f1baac02 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 12 Aug 2025 00:18:18 +0200 Subject: [PATCH 77/83] test: improve support non-utf-8 --- tests/by-util/test_more.rs | 22 ++++++++-------------- tests/by-util/test_readlink.rs | 11 ++++------- tests/by-util/test_realpath.rs | 5 ----- tests/by-util/test_shred.rs | 19 +++++-------------- tests/by-util/test_touch.rs | 9 +-------- 5 files changed, 18 insertions(+), 48 deletions(-) diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index e925c71b9..a46648a8b 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -130,23 +130,17 @@ fn test_invalid_file_perms() { #[test] #[cfg(target_os = "linux")] fn test_more_non_utf8_paths() { - use std::fs; - + use std::os::unix::ffi::OsStrExt; if std::io::stdout().is_terminal() { let (at, mut ucmd) = at_and_ucmd!(); - + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); // Create test file with normal name first - at.write("temp.txt", "test content for non-UTF-8 file"); + at.write( + &file_name.to_string_lossy(), + "test content for non-UTF-8 file", + ); - // Rename to non-UTF-8 name - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); - - // Test that more can handle non-UTF-8 filenames without crashing - ucmd.arg(file_name).succeeds(); - } + // Test that more can handle non-UTF-8 filenames without crashing + ucmd.arg(file_name).succeeds(); } } diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index c3791a5fb..ebc85f543 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -382,19 +382,16 @@ fn test_readlink_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - - // Create a target file and a symlink with non-UTF-8 bytes in the name - at.touch("target_file"); + let file_name = "target_file"; + at.touch(file_name); let non_utf8_bytes = b"symlink_\xFF\xFE"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - // Create symlink using std::os::unix::fs::symlink - std::os::unix::fs::symlink(at.plus_as_string("target_file"), at.plus(non_utf8_name)).unwrap(); + std::os::unix::fs::symlink(at.plus_as_string(file_name), at.plus(non_utf8_name)).unwrap(); // Test that readlink handles non-UTF-8 symlink names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - // The result should contain the target path let output = result.stdout_str_lossy(); - assert!(output.contains("target_file")); + assert!(output.contains(file_name)); } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 24ca48870..249614bf4 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -478,13 +478,9 @@ fn test_realpath_non_utf8_paths() { let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - // Create the actual file at.touch(non_utf8_name); - - // Test that realpath handles non-UTF-8 paths without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - // The result should contain the non-UTF-8 bytes let output = result.stdout_str_lossy(); assert!(output.contains("test_")); assert!(output.contains(".txt")); @@ -503,7 +499,6 @@ fn test_realpath_empty_string() { .fails() .code_is(1); - // Test that empty --relative-to is rejected new_ucmd!() .arg("--relative-to=") .arg(".") diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index ad8f79200..aa95a769a 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -320,22 +320,13 @@ fn test_shred_rename_exhaustion() { #[test] #[cfg(target_os = "linux")] fn test_shred_non_utf8_paths() { - use std::fs; - + use std::os::unix::ffi::OsStrExt; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - // Create test file with non-UTF-8 name - at.write("temp.txt", "test content"); + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + std::fs::write(at.plus(file_name), "test content").unwrap(); - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - - fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); - - // Test that shred can handle non-UTF-8 filenames - ts.ucmd().arg(file_name).succeeds(); - } + // Test that shred can handle non-UTF-8 filenames + ts.ucmd().arg(file_name).succeeds(); } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index aa088204a..f5c66bc24 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1023,16 +1023,9 @@ fn test_touch_non_utf8_paths() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - // Create a test file with non-UTF-8 bytes in the name let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - // Test that touch handles non-UTF-8 file names without crashing - let result = scene.ucmd().arg(non_utf8_name).succeeds(); - - // Verify no output and file was created - result.no_output(); - - // Check that the file was created (using the raw path) + scene.ucmd().arg(non_utf8_name).succeeds().no_output(); assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); } From 66a1fc8bbf1278de9a12be181a216dec55a00f4d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 12 Aug 2025 00:40:02 +0200 Subject: [PATCH 78/83] mktemp: options support non-ut8 too --- src/uu/mktemp/src/mktemp.rs | 39 ++++++++++++++-------------- tests/by-util/test_mktemp.rs | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 98ffdadbf..ceb8605bf 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -235,10 +235,7 @@ impl Params { let tmpdir = options.tmpdir; let prefix_from_option = tmpdir.clone().unwrap_or_default(); let prefix_from_template = &template_str[..i]; - let prefix = Path::new(&prefix_from_option) - .join(prefix_from_template) - .display() - .to_string(); + let prefix_path = Path::new(&prefix_from_option).join(prefix_from_template); if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { return Err(MkTempError::PrefixContainsDirSeparator( template_str.to_string(), @@ -250,21 +247,23 @@ impl Params { // Split the parent directory from the file part of the prefix. // - // For example, if `prefix` is "a/b/c/d", then `directory` is - // "a/b/c" is `prefix` gets reassigned to "d". - let (directory, prefix) = if prefix.ends_with(MAIN_SEPARATOR) { - (prefix, String::new()) - } else { - let path = Path::new(&prefix); - let directory = match path.parent() { - None => String::new(), - Some(d) => d.display().to_string(), - }; - let prefix = match path.file_name() { - None => String::new(), - Some(f) => f.to_str().unwrap().to_string(), - }; - (directory, prefix) + // For example, if `prefix_path` is "a/b/c/d", then `directory` is + // "a/b/c" and `prefix` gets reassigned to "d". + let (directory, prefix) = { + let prefix_str = prefix_path.to_string_lossy(); + if prefix_str.ends_with(MAIN_SEPARATOR) { + (prefix_path, String::new()) + } else { + let directory = match prefix_path.parent() { + None => PathBuf::new(), + Some(d) => d.to_path_buf(), + }; + let prefix = match prefix_path.file_name() { + None => String::new(), + Some(f) => f.to_str().unwrap().to_string(), + }; + (directory, prefix) + } }; // Combine the suffix from the template with the suffix given as an option. @@ -285,7 +284,7 @@ impl Params { let num_rand_chars = j - i; Ok(Self { - directory: directory.into(), + directory, prefix, num_rand_chars, suffix, diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index 872924eb3..405c7bfee 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -1011,3 +1011,52 @@ fn test_non_utf8_template() { ts.ucmd().arg(template).fails().stderr_contains("invalid"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_path() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test that mktemp can handle non-UTF8 directory paths with -p option + ucmd.arg("-p").arg(at.plus(dir_name)).succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_long_option() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test that mktemp can handle non-UTF8 directory paths with --tmpdir option + // Note: Due to test framework limitations with non-UTF8 arguments and --tmpdir= syntax, + // we'll test a more limited scenario that still validates non-UTF8 path handling + ucmd.arg("-p") + .arg(at.plus(dir_name)) + .arg("tmpXXXXXX") + .succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_directory_creation() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test directory creation (-d flag) with non-UTF8 directory paths + // We can't easily verify the exact output path because of UTF8 conversion issues, + // but we can verify the command succeeds + ucmd.arg("-d").arg("-p").arg(at.plus(dir_name)).succeeds(); +} From 83e5be8a5285394f2d6528f2250cd7cbef5b64cd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 12 Aug 2025 00:45:58 +0200 Subject: [PATCH 79/83] install: options support non-ut8 too --- src/uu/install/src/install.rs | 18 +++++++++++------- tests/by-util/test_install.rs | 10 ++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 937b3d23c..903f3928d 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -569,13 +569,17 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { if let Some(to_create) = to_create { // if the path ends in /, remove it let to_create_owned; - let to_create = if to_create.to_string_lossy().ends_with('/') { - let path_str = to_create.to_string_lossy(); - let trimmed = path_str.trim_end_matches('/'); - to_create_owned = PathBuf::from(trimmed); - to_create_owned.as_path() - } else { - to_create + let to_create = match uucore::os_str_as_bytes(to_create.as_os_str()) { + Ok(path_bytes) if path_bytes.ends_with(b"/") => { + let mut trimmed_bytes = path_bytes; + while trimmed_bytes.ends_with(b"/") { + trimmed_bytes = &trimmed_bytes[..trimmed_bytes.len() - 1]; + } + let trimmed_os_str = std::ffi::OsStr::from_bytes(trimmed_bytes); + to_create_owned = PathBuf::from(trimmed_os_str); + to_create_owned.as_path() + } + _ => to_create, }; if !to_create.exists() { diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index ded775579..4d1eed3e2 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2380,4 +2380,14 @@ fn test_install_non_utf8_paths() { at.mkdir(dest_dir); ucmd.arg(&source_filename).arg(dest_dir).succeeds(); + + // Test with trailing slash and directory creation (-D flag) + let (at, mut ucmd) = at_and_ucmd!(); + let source_file = "source.txt"; + let mut target_path = std::ffi::OsString::from_vec(vec![0xFF, 0xFE, b'd', b'i', b'r']); + target_path.push("/target.txt"); + + at.touch(source_file); + + ucmd.arg("-D").arg(source_file).arg(&target_path).succeeds(); } From 5930ba0eb5d8be708d597986f05b7f946b230bab Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 12 Aug 2025 00:51:26 +0200 Subject: [PATCH 80/83] head: refactor the code a bit --- src/uu/head/src/head.rs | 149 ++++++++++++++++--------------------- tests/by-util/test_head.rs | 1 - 2 files changed, 66 insertions(+), 84 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index f18b4c979..e4837a630 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -180,12 +180,17 @@ fn arg_iterate<'a>( let first = args.next().unwrap(); if let Some(second) = args.next() { if let Some(s) = second.to_str() { - match parse::parse_obsolete(s) { - Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), - Some(Err(parse::ParseError)) => Err(HeadError::ParseError( - translate!("head-error-bad-argument-format", "arg" => s.quote()), - )), - None => Ok(Box::new(vec![first, second].into_iter().chain(args))), + if let Some(v) = parse::parse_obsolete(s) { + match v { + Ok(iter) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), + Err(parse::ParseError) => Err(HeadError::ParseError( + translate!("head-error-bad-argument-format", "arg" => s.quote()), + )), + } + } else { + // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option + // like "-5". Treat it as a regular file argument. + Ok(Box::new(vec![first, second].into_iter().chain(args))) } } else { // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option @@ -467,90 +472,68 @@ fn head_file(input: &mut File, options: &HeadOptions) -> io::Result { fn uu_head(options: &HeadOptions) -> UResult<()> { let mut first = true; for file in &options.files { - let res = match file.to_str() { - Some("-") => { - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - println!(); - } - println!("{}", translate!("head-header-stdin")); + let res = if file == "-" { + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + println!(); } - let stdin = io::stdin(); - - #[cfg(unix)] - { - let stdin_raw_fd = stdin.as_raw_fd(); - let mut stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) }; - let current_pos = stdin_file.stream_position(); - if let Ok(current_pos) = current_pos { - // We have a seekable file. Ensure we set the input stream to the - // last byte read so that any tools that parse the remainder of - // the stdin stream read from the correct place. - - let bytes_read = head_file(&mut stdin_file, options)?; - stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?; - } else { - let _bytes_read = head_file(&mut stdin_file, options)?; - } - } - - #[cfg(not(unix))] - { - let mut stdin = stdin.lock(); - - match options.mode { - Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), - Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n), - Mode::FirstLines(n) => { - read_n_lines(&mut stdin, n, options.line_ending.into()) - } - Mode::AllButLastLines(n) => { - read_but_last_n_lines(&mut stdin, n, options.line_ending.into()) - } - }?; - } - - Ok(()) + println!("{}", translate!("head-header-stdin")); } - Some(name) => { - let mut file_handle = match File::open(file) { - Ok(f) => f, - Err(err) => { - show!(err.map_err_context( - || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) - )); - continue; - } - }; - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - println!(); - } - println!("==> {name} <=="); + let stdin = io::stdin(); + + #[cfg(unix)] + { + let stdin_raw_fd = stdin.as_raw_fd(); + let mut stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) }; + let current_pos = stdin_file.stream_position(); + if let Ok(current_pos) = current_pos { + // We have a seekable file. Ensure we set the input stream to the + // last byte read so that any tools that parse the remainder of + // the stdin stream read from the correct place. + + let bytes_read = head_file(&mut stdin_file, options)?; + stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?; + } else { + let _bytes_read = head_file(&mut stdin_file, options)?; } - head_file(&mut file_handle, options)?; - Ok(()) } - None => { - // Handle files with non-UTF-8 names - let mut file_handle = match File::open(file) { - Ok(f) => f, - Err(err) => { - show!(err.map_err_context( - || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) - )); - continue; + + #[cfg(not(unix))] + { + let mut stdin = stdin.lock(); + + match options.mode { + Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), + Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n), + Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.line_ending.into()), + Mode::AllButLastLines(n) => { + read_but_last_n_lines(&mut stdin, n, options.line_ending.into()) } - }; - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - println!(); - } - println!("==> {} <==", file.to_string_lossy()); + }?; + } + + Ok(()) + } else { + let mut file_handle = match File::open(file) { + Ok(f) => f, + Err(err) => { + show!(err.map_err_context( + || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) + )); + continue; + } + }; + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + println!(); + } + match file.to_str() { + Some(name) => println!("==> {name} <=="), + None => println!("==> {} <==", file.to_string_lossy()), } - head_file(&mut file_handle, options)?; - Ok(()) } + head_file(&mut file_handle, options)?; + Ok(()) }; if let Err(e) = res { let name = if file == "-" { diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 88c9e5358..884506351 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -872,7 +872,6 @@ fn test_head_non_utf8_paths() { let non_utf8_bytes = b"test_\xFF\xFE.txt"; let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); - // Create the actual file with some content std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap(); // Test that head handles non-UTF-8 file names without crashing From 1c5b95d1bb478adad7735315cac7f001eec1b763 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 13 Aug 2025 22:12:53 +0200 Subject: [PATCH 81/83] non-utf8: address review commits --- src/uu/ln/src/ln.rs | 18 +++++++++--------- src/uu/readlink/src/readlink.rs | 1 - tests/by-util/test_cut.rs | 33 ++++++++++++++------------------- tests/by-util/test_head.rs | 11 ----------- tests/by-util/test_join.rs | 23 +++++++++-------------- tests/by-util/test_stdbuf.rs | 1 + 6 files changed, 33 insertions(+), 54 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index a45fc17c6..0557dd4f3 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -30,7 +30,7 @@ use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; pub struct Settings { overwrite: OverwriteMode, backup: BackupMode, - suffix: String, + suffix: OsString, symbolic: bool, relative: bool, logical: bool, @@ -126,7 +126,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = Settings { overwrite: overwrite_mode, backup: backup_mode, - suffix: backup_suffix, + suffix: OsString::from(backup_suffix), symbolic, logical, relative: matches.get_flag(options::RELATIVE), @@ -447,16 +447,16 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { Ok(()) } -fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf { - let mut p = path.as_os_str().to_str().unwrap().to_owned(); - p.push_str(suffix); - PathBuf::from(p) +fn simple_backup_path(path: &Path, suffix: &OsString) -> PathBuf { + let mut file_name = path.file_name().unwrap_or_default().to_os_string(); + file_name.push(suffix); + path.with_file_name(file_name) } fn numbered_backup_path(path: &Path) -> PathBuf { let mut i: u64 = 1; loop { - let new_path = simple_backup_path(path, &format!(".~{i}~")); + let new_path = simple_backup_path(path, &OsString::from(format!(".~{i}~"))); if !new_path.exists() { return new_path; } @@ -464,8 +464,8 @@ fn numbered_backup_path(path: &Path) -> PathBuf { } } -fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf { - let test_path = simple_backup_path(path, ".~1~"); +fn existing_backup_path(path: &Path, suffix: &OsString) -> PathBuf { + let test_path = simple_backup_path(path, &OsString::from(".~1~")); if test_path.exists() { return numbered_backup_path(path); } diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 383acb409..dd2c785b4 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -11,7 +11,6 @@ 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}; use uucore::line_ending::LineEnding; diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 616e20a58..b94a15834 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -389,29 +389,24 @@ fn test_failed_write_is_reported() { #[test] #[cfg(target_os = "linux")] fn test_cut_non_utf8_paths() { - use std::fs; + use std::fs::File; + use std::io::Write; + use std::os::unix::ffi::OsStrExt; use uutests::util::TestScenario; use uutests::util_name; let ts = TestScenario::new(util_name!()); - let at = &ts.fixtures; + let test_dir = ts.fixtures.subdir.as_path(); - // Create test file with normal name first - at.write("temp.txt", "a\tb\tc\n1\t2\t3\n"); + // Create file directly with non-UTF-8 name + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + let mut file = File::create(test_dir.join(file_name)).unwrap(); + file.write_all(b"a\tb\tc\n1\t2\t3\n").unwrap(); - // Rename to non-UTF-8 name - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - - fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); - - // Test that cut can handle non-UTF-8 filenames - ts.ucmd() - .arg("-f1,3") - .arg(file_name) - .succeeds() - .stdout_only("a\tc\n1\t3\n"); - } + // Test that cut can handle non-UTF-8 filenames + ts.ucmd() + .arg("-f1,3") + .arg(file_name) + .succeeds() + .stdout_only("a\tc\n1\t3\n"); } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 884506351..c2d886266 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -874,22 +874,11 @@ fn test_head_non_utf8_paths() { std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap(); - // Test that head handles non-UTF-8 file names without crashing let result = scene.ucmd().arg(non_utf8_name).succeeds(); - // The result should contain the file content let output = result.stdout_str_lossy(); assert!(output.contains("line1")); assert!(output.contains("line2")); assert!(output.contains("line3")); - - // Test with line count argument - scene - .ucmd() - .args(&["-n", "2"]) - .arg(non_utf8_name) - .succeeds() - .stdout_contains("line1") - .stdout_contains("line2"); } // Test that head handles non-UTF-8 file names without crashing diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index 65b927717..8a239b965 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -537,20 +537,13 @@ fn test_full() { #[test] #[cfg(target_os = "linux")] fn test_join_non_utf8_paths() { - use std::fs; + use std::fs::File; + use std::io::Write; let ts = TestScenario::new(util_name!()); - let at = &ts.fixtures; + let test_dir = ts.fixtures.subdir.as_path(); - // Create files with non-UTF-8 names using shell commands - // since the test framework doesn't support OsStr for file names - let test_dir = at.subdir.as_path(); - - // Create temporary files with valid names first - at.write("temp1.txt", "a 1\n"); - at.write("temp2.txt", "a 2\n"); - - // Rename them to non-UTF-8 names using std::fs + // Create files directly with non-UTF-8 names let file1_bytes = b"test_\xFF\xFE_1.txt"; let file2_bytes = b"test_\xFF\xFE_2.txt"; @@ -560,10 +553,12 @@ fn test_join_non_utf8_paths() { let file1_name = std::ffi::OsStr::from_bytes(file1_bytes); let file2_name = std::ffi::OsStr::from_bytes(file2_bytes); - fs::rename(test_dir.join("temp1.txt"), test_dir.join(file1_name)).unwrap(); - fs::rename(test_dir.join("temp2.txt"), test_dir.join(file2_name)).unwrap(); + let mut file1 = File::create(test_dir.join(file1_name)).unwrap(); + file1.write_all(b"a 1\n").unwrap(); + + let mut file2 = File::create(test_dir.join(file2_name)).unwrap(); + file2.write_all(b"a 2\n").unwrap(); - // Test that join can handle non-UTF-8 filenames ts.ucmd() .arg(file1_name) .arg(file2_name) diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 71e368bf8..8c3fef587 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -220,6 +220,7 @@ fn test_libstdbuf_preload() { } #[cfg(target_os = "linux")] +#[cfg(not(target_env = "musl"))] #[test] fn test_stdbuf_non_utf8_paths() { use std::os::unix::ffi::OsStringExt; From 0a7ea787337ab6c1cb4b217dfdae1b17543e6d75 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 14 Aug 2025 10:50:52 +0200 Subject: [PATCH 82/83] split: bring back the test --- tests/by-util/test_head.rs | 2 +- tests/by-util/test_split.rs | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index c2d886266..2acc783ea 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -881,4 +881,4 @@ fn test_head_non_utf8_paths() { assert!(output.contains("line2")); assert!(output.contains("line3")); } - // Test that head handles non-UTF-8 file names without crashing +// Test that head handles non-UTF-8 file names without crashing diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 9bef61230..627683172 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1711,7 +1711,7 @@ fn test_split_invalid_input() { /// Test if there are invalid (non UTF-8) in the arguments - unix /// clap is expected to fail/panic #[test] -#[cfg(unix)] +#[cfg(target_os = "linux")] fn test_split_non_utf8_argument_unix() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -1729,6 +1729,29 @@ fn test_split_non_utf8_argument_unix() { ucmd.args(&[opt, opt_value, name]).succeeds(); } +/// Test if there are invalid (non UTF-8) in the arguments - windows +/// clap is expected to fail/panic +#[test] +#[cfg(windows)] +fn test_split_non_utf8_argument_windows() { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + + let (at, mut ucmd) = at_and_ucmd!(); + let name = "test_split_non_utf8_argument"; + let opt = OsString::from("--additional-suffix"); + RandomFile::new(&at, name).add_lines(2000); + // Here the values 0x0066 and 0x006f correspond to 'f' and 'o' + // respectively. The value 0xD800 is a lone surrogate half, invalid + // in a UTF-16 sequence. + let opt_value = [0x0066, 0x006f, 0xD800, 0x006f]; + let opt_value = OsString::from_wide(&opt_value[..]); + let name = OsString::from(name); + ucmd.args(&[opt, opt_value, name]) + .fails() + .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); +} + // Test '--separator' / '-t' option following GNU tests example // test separators: '\n' , '\0' , ';' // test with '--lines=2' , '--line-bytes=4' , '--number=l/3' , '--number=r/3' , '--number=l/1/3' , '--number=r/1/3' From 0532ea4727d1da95adc6ab97e5e684da07139d17 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 14 Aug 2025 14:18:15 +0200 Subject: [PATCH 83/83] split: adjust the test for windows --- tests/by-util/test_split.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 627683172..f710e1442 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1747,9 +1747,7 @@ fn test_split_non_utf8_argument_windows() { let opt_value = [0x0066, 0x006f, 0xD800, 0x006f]; let opt_value = OsString::from_wide(&opt_value[..]); let name = OsString::from(name); - ucmd.args(&[opt, opt_value, name]) - .fails() - .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); + ucmd.args(&[opt, opt_value, name]).succeeds(); } // Test '--separator' / '-t' option following GNU tests example