diff --git a/src/uu/false/locales/en-US.ftl b/src/uu/false/locales/en-US.ftl index b4bbe787bd9..f959c3f3a2d 100644 --- a/src/uu/false/locales/en-US.ftl +++ b/src/uu/false/locales/en-US.ftl @@ -4,5 +4,8 @@ false-about = Returns false, an unsuccessful exit status. will try to write the help or version text. Any IO error during this operation is diagnosed, yet the program will also return 1. +false-usage = false [ignored command line arguments] + or: false OPTION + false-help-text = Print help information false-version-text = Print version information diff --git a/src/uu/false/locales/fr-FR.ftl b/src/uu/false/locales/fr-FR.ftl index c97c8483cd3..87049376f24 100644 --- a/src/uu/false/locales/fr-FR.ftl +++ b/src/uu/false/locales/fr-FR.ftl @@ -3,5 +3,9 @@ false-about = Renvoie false, un code de sortie indiquant un échec. Retourne immédiatement avec le code de sortie 1. Lorsqu'il est invoqué avec l'une des options reconnues, il tente d'afficher l'aide ou la version. Toute erreur d'entrée/sortie pendant cette opération est signalée, mais le programme retourne également 1. + +false-usage = false [arguments de ligne de commande ignorés] + ou: false OPTION + false-help-text = Afficher l'aide false-version-text = Afficher les informations de version diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index d99d8ece4aa..d60fbf7821e 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; use std::io::Write; -use uucore::{crate_version, translate}; +use uucore::{crate_version, format_usage, translate}; // uucore::main does not support no-result pub fn uumain(mut args: impl uucore::Args) -> i32 { @@ -33,7 +33,8 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { pub fn uu_app() -> Command { Command::new("false") .version(crate_version!()) - .help_template(uucore::localized_help_template("false")) + .help_template(uucore::localized_help_template_usage_first("false")) + .override_usage(format_usage(&translate!("false-usage"))) .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/true/locales/en-US.ftl b/src/uu/true/locales/en-US.ftl index c5806a10d32..89ddc588f40 100644 --- a/src/uu/true/locales/en-US.ftl +++ b/src/uu/true/locales/en-US.ftl @@ -4,5 +4,8 @@ true-about = Returns true, a successful exit status. options. In those cases it will try to write the help or version text. Any IO error during this operation causes the program to return 1 instead. +true-usage = true [ignored command line arguments] + or: true OPTION + true-help-text = Print help information true-version-text = Print version information diff --git a/src/uu/true/locales/fr-FR.ftl b/src/uu/true/locales/fr-FR.ftl index 491d11d35f5..61fb9b4582e 100644 --- a/src/uu/true/locales/fr-FR.ftl +++ b/src/uu/true/locales/fr-FR.ftl @@ -4,5 +4,9 @@ true-about = Renvoie true, un code de sortie indiquant le succès. Retourne immédiatement avec le code de sortie 0, sauf si l'une des options reconnues est utilisée. Dans ce cas, il essaiera d'afficher l'aide ou la version. Une erreur d'entrée/sortie pendant cette opération entraîne un retour avec le code de sortie 1. + +true-usage = true [arguments de ligne de commande ignorés] + ou: true OPTION + true-help-text = Afficher l'aide true-version-text = Afficher les informations de version diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 6e259d17f21..901875a47f2 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; use std::io::Write; -use uucore::{crate_version, translate}; +use uucore::{crate_version, format_usage, translate}; // uucore::main does not support no-result pub fn uumain(mut args: impl uucore::Args) -> i32 { @@ -38,7 +38,8 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { pub fn uu_app() -> Command { Command::new("true") .version(crate_version!()) - .help_template(uucore::localized_help_template("true")) + .help_template(uucore::localized_help_template_usage_first("true")) + .override_usage(format_usage(&translate!("true-usage"))) .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/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4dfffed2995..8ac7dce36a7 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -282,19 +282,26 @@ pub fn format_usage(s: &str) -> String { /// .help_template(localized_help_template("myutil")); /// ``` pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { - use std::io::IsTerminal; + localized_help_template_with_colors(util_name, detect_colors_enabled()) +} - // Determine if colors should be enabled - same logic as configure_localized_command - let colors_enabled = if std::env::var("NO_COLOR").is_ok() { - false - } else if std::env::var("CLICOLOR_FORCE").is_ok() || std::env::var("FORCE_COLOR").is_ok() { - true - } else { - IsTerminal::is_terminal(&std::io::stdout()) - && std::env::var("TERM").unwrap_or_default() != "dumb" - }; +/// Like [`localized_help_template`], but places the localized "Usage:" line +/// *before* the `--about` text instead of after it. +/// +/// GNU utilities print the usage synopsis first (e.g. `Usage: false +/// [ignored command line arguments]`) followed by the description. Most +/// uutils utilities keep the description first since their usage line is +/// less informative on its own, but for utilities like `true`/`false` where +/// the usage line itself communicates useful information, matching GNU's +/// ordering is clearer. +pub fn localized_help_template_usage_first(util_name: &str) -> clap::builder::StyledStr { + localized_help_template_with_order(util_name, detect_colors_enabled(), true) +} - localized_help_template_with_colors(util_name, colors_enabled) +/// Determine whether colored output should be used, based on the same +/// environment variables and TTY checks as `configure_localized_command`. +fn detect_colors_enabled() -> bool { + clap_localization::should_use_color_for_stream(&std::io::stdout()) } /// Create a localized help template with explicit color control @@ -302,6 +309,18 @@ pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { pub fn localized_help_template_with_colors( util_name: &str, colors_enabled: bool, +) -> clap::builder::StyledStr { + localized_help_template_with_order(util_name, colors_enabled, false) +} + +/// Shared implementation behind [`localized_help_template_with_colors`] and +/// [`localized_help_template_usage_first`]. When `usage_first` is `true`, +/// the "Usage:" line is placed before `{about-with-newline}` rather than +/// after it. +fn localized_help_template_with_order( + util_name: &str, + colors_enabled: bool, + usage_first: bool, ) -> clap::builder::StyledStr { use std::fmt::Write; @@ -311,21 +330,30 @@ pub fn localized_help_template_with_colors( // Get the localized "Usage" label let usage_label = crate::locale::translate!("common-usage"); + // Build the styled usage header (bold + underline like clap's default) + let mut usage_line = String::new(); + if colors_enabled { + write!( + usage_line, + "\x1b[1m\x1b[4m{usage_label}:\x1b[0m {{usage}}\n\n" + ) + .unwrap(); + } else { + write!(usage_line, "{usage_label}: {{usage}}\n\n").unwrap(); + } + // Create a styled template let mut template = clap::builder::StyledStr::new(); - // Add the basic template parts - writeln!(template, "{{before-help}}{{about-with-newline}}").unwrap(); - - // Add styled usage header (bold + underline like clap's default) - if colors_enabled { - write!( + if usage_first { + writeln!( template, - "\x1b[1m\x1b[4m{usage_label}:\x1b[0m {{usage}}\n\n" + "{{before-help}}{usage_line}{{about-with-newline}}" ) .unwrap(); } else { - write!(template, "{usage_label}: {{usage}}\n\n").unwrap(); + writeln!(template, "{{before-help}}{{about-with-newline}}").unwrap(); + write!(template, "{usage_line}").unwrap(); } // Add the rest diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index fc4f838c216..8b0f00c5d46 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -53,7 +53,7 @@ fn get_color_choice() -> clap::ColorChoice { } /// Generic helper to check if colors should be enabled for a given stream -fn should_use_color_for_stream(stream: &S) -> bool { +pub(crate) fn should_use_color_for_stream(stream: &S) -> bool { match get_color_choice() { clap::ColorChoice::Always => true, clap::ColorChoice::Never => false, diff --git a/tests/by-util/test_false.rs b/tests/by-util/test_false.rs index cc0874256b1..37ea73c1eff 100644 --- a/tests/by-util/test_false.rs +++ b/tests/by-util/test_false.rs @@ -27,6 +27,18 @@ fn test_help() { .stdout_contains("false"); } +#[test] +fn test_help_starts_with_usage() { + // GNU's `false --help` starts with a "Usage:" line; ensure ours does too. + // NO_COLOR avoids ANSI styling so the raw stdout comparison is stable. + let result = new_ucmd!().env("NO_COLOR", "1").args(&["--help"]).fails(); + assert!( + result + .stdout_str() + .starts_with("Usage: false [ignored command line arguments]") + ); +} + #[test] fn test_short_options() { for option in ["-h", "-V"] { diff --git a/tests/by-util/test_true.rs b/tests/by-util/test_true.rs index 23c947fb29c..cc6584f2d88 100644 --- a/tests/by-util/test_true.rs +++ b/tests/by-util/test_true.rs @@ -30,6 +30,21 @@ fn test_help() { .stdout_contains("true"); } +#[test] +fn test_help_starts_with_usage() { + // GNU's `true --help` starts with a "Usage:" line; ensure ours does too. + // NO_COLOR avoids ANSI styling so the raw stdout comparison is stable. + let result = new_ucmd!() + .env("NO_COLOR", "1") + .args(&["--help"]) + .succeeds(); + assert!( + result + .stdout_str() + .starts_with("Usage: true [ignored command line arguments]") + ); +} + #[test] fn test_short_options() { for option in ["-h", "-V"] {