From 9fde1c277d5ab529bc2b657d8c310cdb15b77324 Mon Sep 17 00:00:00 2001 From: jamesarch Date: Wed, 20 May 2026 19:52:09 +0800 Subject: [PATCH] true/false: put `Usage:` on the first line of --help (GH #10279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GNU `true --help` and `false --help` both open with the `Usage:` line, as documented at the launchpad bug linked in the issue: $ LANG=C /usr/bin/false --help | head -1 Usage: /usr/bin/false [ignored command line arguments] uutils put the about text first, so the first line of --help was "Returns false, an unsuccessful exit status." instead. Tools that grep the first line for the synopsis broke. Fix: add `uucore::localized_help_template_usage_first` (a GNU-style ordering variant of the existing `localized_help_template`) and use it from `true` and `false`. The new helper keeps the localized "Usage:" label and the bold+underline styling, but emits it before the about block instead of after. Other utilities continue to use the existing template — this PR only affects `true` and `false` so we don't risk breaking snapshot tests elsewhere. Tests: `test_help_starts_with_usage` added for both utilities, asserting the first line of `--help` starts with `Usage:`. Closes #10279. Co-Authored-By: Claude Opus 4.7 --- src/uu/false/src/false.rs | 3 ++- src/uu/true/src/true.rs | 3 ++- src/uucore/src/lib/lib.rs | 51 +++++++++++++++++++++++++++++++++++++ tests/by-util/test_false.rs | 13 ++++++++++ tests/by-util/test_true.rs | 13 ++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index d99d8ece4aa..c5397f31c11 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -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")) + // GH #10279: GNU `false --help` puts `Usage:` on the first line. + .help_template(uucore::localized_help_template_usage_first("false")) .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/src/true.rs b/src/uu/true/src/true.rs index 6e259d17f21..a65d444ea77 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -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")) + // GH #10279: GNU `true --help` puts `Usage:` on the first line. + .help_template(uucore::localized_help_template_usage_first("true")) .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 e136c4d8e3b..5ef234af643 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -331,6 +331,57 @@ pub fn localized_help_template_with_colors( template } +/// Creates a localized help template that places the `Usage:` line first, +/// matching GNU coreutils' help output ordering. +/// +/// The default [`localized_help_template`] puts the `about` text before the +/// usage line. GNU coreutils — including `true(1)` and `false(1)` — puts +/// `Usage:` on the very first line of `--help`. Use this variant for +/// utilities that need that GNU-style layout (see GH #10279). +pub fn localized_help_template_usage_first(util_name: &str) -> clap::builder::StyledStr { + use std::io::IsTerminal; + + 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" + }; + + localized_help_template_usage_first_with_colors(util_name, colors_enabled) +} + +/// Like [`localized_help_template_usage_first`] but with explicit color control. +pub fn localized_help_template_usage_first_with_colors( + util_name: &str, + colors_enabled: bool, +) -> clap::builder::StyledStr { + use std::fmt::Write; + + let _ = locale::setup_localization(util_name); + let usage_label = crate::locale::translate!("common-usage"); + + let mut template = clap::builder::StyledStr::new(); + + // Usage first. + if colors_enabled { + writeln!(template, "\x1b[1m\x1b[4m{usage_label}:\x1b[0m {{usage}}").unwrap(); + } else { + writeln!(template, "{usage_label}: {{usage}}").unwrap(); + } + + // Then the about text, then options. + write!( + template, + "{{before-help}}{{about-with-newline}}\n{{all-args}}{{after-help}}" + ) + .unwrap(); + + 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/tests/by-util/test_false.rs b/tests/by-util/test_false.rs index cc0874256b1..dd02d93f6e5 100644 --- a/tests/by-util/test_false.rs +++ b/tests/by-util/test_false.rs @@ -27,6 +27,19 @@ fn test_help() { .stdout_contains("false"); } +/// GH #10279: GNU `false --help` opens with the `Usage:` line. The previous +/// uutils help template put the about text first, breaking config tools +/// that grep the first line. +#[test] +fn test_help_starts_with_usage() { + let result = new_ucmd!().args(&["--help"]).fails(); + let first_line = result.stdout_str().lines().next().unwrap_or(""); + assert!( + first_line.starts_with("Usage:"), + "expected --help first line to start with 'Usage:', got {first_line:?}" + ); +} + #[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..b1efb99e63c 100644 --- a/tests/by-util/test_true.rs +++ b/tests/by-util/test_true.rs @@ -30,6 +30,19 @@ fn test_help() { .stdout_contains("true"); } +/// GH #10279: GNU `true --help` opens with the `Usage:` line. The previous +/// uutils help template put the about text first, breaking config tools +/// that grep the first line. +#[test] +fn test_help_starts_with_usage() { + let result = new_ucmd!().args(&["--help"]).succeeds(); + let first_line = result.stdout_str().lines().next().unwrap_or(""); + assert!( + first_line.starts_with("Usage:"), + "expected --help first line to start with 'Usage:', got {first_line:?}" + ); +} + #[test] fn test_short_options() { for option in ["-h", "-V"] {