clap icon indicating copy to clipboard operation
clap copied to clipboard

Arg::next_line_help adds newlines to ALL options

Open playbahn opened this issue 3 months ago • 6 comments

Please complete the following tasks

Rust Version

rustc 1.90.0 (1159e78c4 2025-09-14)

Clap Version

4.5.48

Minimal reproducible code

fn main() {
    let _ = command!()
        .arg(
            arg!(--two <VALUE> "arg one")
                .required(true)
                .next_line_help(true),
        )
        .arg(arg!(--one <VALUE> "arg two").required(true))
        .get_matches();
}

Steps to reproduce the bug with the above code

cargo run -- -h

Actual Behaviour

Arg::next_line_help should render help on the next line for only the Arg it was applied to. Instead, help for every Arg is rendered on the next line.

Usage: clap-builder --two <VALUE> --one <VALUE>

Options:
      --two <VALUE>
          arg one
      --one <VALUE>
          arg two
  -h, --help
          Print help
  -V, --version
          Print version

Expected Behaviour

Arg::next_line_help should render help on the next line for only the Arg it was applied to. Help for every Arg without next_line_help should be rendered on the current line.

Additional Context

Looks the problem is here: https://github.com/clap-rs/clap/blob/0bb3ad7e12e729be9f152391558689ac4fdd31ec/clap_builder/src/output/help_template.rs#L501-L511 This snippet shows next_line_help is only evaluated once before printing all the args with self.write_arg(arg, next_line_help, longest), and so the same next line printing behavior persists.

This was initially discussed in #6139.

Debug Output

Debug Output
[clap_builder::builder::command]Command::_do_parse
[clap_builder::builder::command]Command::_build: name="clap-builder"
[clap_builder::builder::command]Command::_propagate:clap-builder
[clap_builder::builder::command]Command::_check_help_and_version:clap-builder expand_help_tree=false
[clap_builder::builder::command]Command::long_help_exists
[clap_builder::builder::command]Command::_check_help_and_version: Building default --help
[clap_builder::builder::command]Command::_check_help_and_version: Building default --version
[clap_builder::builder::command]Command::_propagate_global_args:clap-builder
[clap_builder::builder::debug_asserts]Command::_debug_asserts
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:two
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:one
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:help
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:version
[clap_builder::builder::debug_asserts]Command::_verify_positionals
[clap_builder::parser::parser]Parser::get_matches_with
[clap_builder::parser::parser]Parser::parse
[clap_builder::parser::parser]Parser::get_matches_with: Begin parsing '"-h"'
[clap_builder::parser::parser]Parser::possible_subcommand: arg=Ok("-h")
[clap_builder::parser::parser]Parser::get_matches_with: sc=None
[clap_builder::parser::parser]Parser::parse_short_arg: short_arg=ShortFlags { inner: "h", utf8_prefix: CharIndices { front_offset: 0, iter: Chars(['h']) }, invalid_suffix: None }
[clap_builder::parser::parser]Parser::parse_short_arg:iter:h
[clap_builder::parser::parser]Parser::parse_short_arg:iter:h: Found valid opt or flag
[clap_builder::parser::parser]Parser::react action=Help, identifier=Some(Short), source=CommandLine
[clap_builder::parser::parser]Help: use_long=false
[clap_builder::builder::command]Command::write_help_err: clap-builder, use_long=false
[  clap_builder::output::help]write_help
[clap_builder::output::help_template]HelpTemplate::new cmd=clap-builder, use_long=false
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=two
[clap_builder::output::help_template]HelpTemplate::write_templated_help
[clap_builder::output::help_template]HelpTemplate::write_before_help
[ clap_builder::output::usage]Usage::create_usage_no_title
[ clap_builder::output::usage]Usage::create_usage_no_title
[ clap_builder::output::usage]Usage::write_help_usage
[ clap_builder::output::usage]Usage::write_arg_usage; incl_reqs=true
[ clap_builder::output::usage]Usage::needs_options_tag
[ clap_builder::output::usage]Usage::needs_options_tag:iter: f=two
[ clap_builder::output::usage]Usage::needs_options_tag:iter Option is required
[ clap_builder::output::usage]Usage::needs_options_tag:iter: f=one
[ clap_builder::output::usage]Usage::needs_options_tag:iter Option is required
[ clap_builder::output::usage]Usage::needs_options_tag:iter: f=help
[ clap_builder::output::usage]Usage::needs_options_tag:iter Option is built-in
[ clap_builder::output::usage]Usage::needs_options_tag:iter: f=version
[ clap_builder::output::usage]Usage::needs_options_tag:iter Option is built-in
[ clap_builder::output::usage]Usage::needs_options_tag: [OPTIONS] not required
[ clap_builder::output::usage]Usage::write_args: incls=[]
[ clap_builder::output::usage]Usage::get_args: unrolled_reqs=["two", "one"]
[ clap_builder::output::usage]Usage::write_subcommand_usage
[ clap_builder::output::usage]Usage::create_usage_no_title: usage=clap-builder --two <VALUE> --one <VALUE>
[clap_builder::output::help_template]HelpTemplate::write_all_args
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=two
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=one
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=help
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=version
[clap_builder::output::help_template]HelpTemplate::write_args Options
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=two
[clap_builder::output::help_template]HelpTemplate::write_args: arg="two" longest=17
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=one
[clap_builder::output::help_template]HelpTemplate::write_args: arg="one" longest=17
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=help
[clap_builder::output::help_template]HelpTemplate::write_args: arg="help" longest=17
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=version
[clap_builder::output::help_template]HelpTemplate::write_args: arg="version" longest=17
[clap_builder::output::help_template]should_show_arg: use_long=false, arg=two
[clap_builder::output::help_template]HelpTemplate::spec_vals: a=--two <VALUE>
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found short aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: a=--two <VALUE>
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found short aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found aliases...[]
[clap_builder::output::help_template]HelpTemplate::short
[clap_builder::output::help_template]HelpTemplate::long
[clap_builder::output::help_template]HelpTemplate::align_to_about: arg=two, next_line_help=true, longest=17
[clap_builder::output::help_template]HelpTemplate::align_to_about: printing long help so skip alignment
[clap_builder::output::help_template]HelpTemplate::help
[clap_builder::output::help_template]HelpTemplate::help: Next Line...true
[clap_builder::output::help_template]HelpTemplate::help: help_width=10, spaces=7, avail=90
[clap_builder::output::help_template]HelpTemplate::spec_vals: a=--one <VALUE>
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found short aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found aliases...[]
[clap_builder::output::help_template]HelpTemplate::short
[clap_builder::output::help_template]HelpTemplate::long
[clap_builder::output::help_template]HelpTemplate::align_to_about: arg=one, next_line_help=true, longest=17
[clap_builder::output::help_template]HelpTemplate::align_to_about: printing long help so skip alignment
[clap_builder::output::help_template]HelpTemplate::help
[clap_builder::output::help_template]HelpTemplate::help: Next Line...true
[clap_builder::output::help_template]HelpTemplate::help: help_width=10, spaces=7, avail=90
[clap_builder::output::help_template]HelpTemplate::spec_vals: a=--help
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found short aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found aliases...[]
[clap_builder::output::help_template]HelpTemplate::short
[clap_builder::output::help_template]HelpTemplate::long
[clap_builder::output::help_template]HelpTemplate::align_to_about: arg=help, next_line_help=true, longest=17
[clap_builder::output::help_template]HelpTemplate::align_to_about: printing long help so skip alignment
[clap_builder::output::help_template]HelpTemplate::help
[clap_builder::output::help_template]HelpTemplate::help: Next Line...true
[clap_builder::output::help_template]HelpTemplate::help: help_width=10, spaces=10, avail=90
[clap_builder::output::help_template]HelpTemplate::spec_vals: a=--version
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found short aliases...[]
[clap_builder::output::help_template]HelpTemplate::spec_vals: Found aliases...[]
[clap_builder::output::help_template]HelpTemplate::short
[clap_builder::output::help_template]HelpTemplate::long
[clap_builder::output::help_template]HelpTemplate::align_to_about: arg=version, next_line_help=true, longest=17
[clap_builder::output::help_template]HelpTemplate::align_to_about: printing long help so skip alignment
[clap_builder::output::help_template]HelpTemplate::help
[clap_builder::output::help_template]HelpTemplate::help: Next Line...true
[clap_builder::output::help_template]HelpTemplate::help: help_width=10, spaces=13, avail=90
[clap_builder::output::help_template]HelpTemplate::write_after_help
[clap_builder::builder::command]Command::color: Color setting...
[clap_builder::builder::command]Auto
[clap_builder::builder::command]Command::color: Color setting...
[clap_builder::builder::command]Auto
Usage: clap-builder --two <VALUE> --one <VALUE>

Options:
      --two <VALUE>
          arg one
      --one <VALUE>
          arg two
  -h, --help
          Print help
  -V, --version
          Print version

playbahn avatar Oct 17 '25 16:10 playbahn

This works:

diff --git a/clap_builder/src/output/help_template.rs b/clap_builder/src/output/help_template.rs
index c954f5c..634751a 100644
--- a/clap_builder/src/output/help_template.rs
+++ b/clap_builder/src/output/help_template.rs
@@ -498,9 +498,12 @@ impl HelpTemplate<'_, '_> {
             ord_v.insert(key, arg);
         }
 
-        let next_line_help = self.will_args_wrap(args, longest);
+        // let next_line_help = self.will_args_wrap(args, longest);
 
         for (i, (_, arg)) in ord_v.iter().enumerate() {
+            let spec_vals = &self.spec_vals(arg);
+            let next_line_help = self.arg_next_line_help(arg, spec_vals, longest);
+
             if i != 0 {
                 self.writer.push_str("\n");
                 if next_line_help && self.use_long {
@@ -724,7 +727,7 @@ impl HelpTemplate<'_, '_> {
     }
 
     /// Will use next line help on writing args.
-    fn will_args_wrap(&self, args: &[&Arg], longest: usize) -> bool {
+    fn _will_args_wrap(&self, args: &[&Arg], longest: usize) -> bool {
         args.iter()
             .filter(|arg| should_show_arg(self.use_long, arg))
             .any(|arg| {
diff --git a/tests/builder/help.rs b/tests/builder/help.rs
index ebbeafb..d09a89b 100644
--- a/tests/builder/help.rs
+++ b/tests/builder/help.rs
@@ -1056,10 +1056,8 @@ Options:
           marchés d'exportation. Le café est souvent
           une contribution majeure aux exportations
           des régions productrices.
-  -h, --help
-          Print help
-  -V, --version
-          Print version
+  -h, --help         Print help
+  -V, --version      Print version
 
 "#]];
     utils::assert_output(cmd, "ctest --help", expected, false);

playbahn avatar Oct 18 '25 23:10 playbahn

From https://github.com/clap-rs/clap/discussions/6139#discussioncomment-14567129

Looks like this has been broken since #2174.

epage avatar Oct 20 '25 20:10 epage

From https://github.com/clap-rs/clap/discussions/6139#discussioncomment-14567129

Looks like this has been broken since #2174.

Yes, but the diff above fixes this

playbahn avatar Oct 20 '25 21:10 playbahn

One question I have is if the overflow case (force_next_line) should force all to be on the next line or only some. Might be worth testing clap v2 to see what it did. By inspection of #2174, it looks like clap v2 forced all to be on the next line.

epage avatar Oct 20 '25 21:10 epage

One question I have is if the overflow case ...

My bias is that "force_next_line" should apply to one single Arg. A help string may have width that is just a bit over (self.term_w - taken). Forcing all to be on the next line just cause of one help string wastes real estate (my terminal is 170x45, on a low dpi display).

playbahn avatar Oct 22 '25 18:10 playbahn

I would be most inclined to favor

  • minimizing unrelated behavior changes to the actual problem
  • going back to the v2 behavior

Since it looks like these are one and the same, so I don't have to deal with deciding which to prioritize. Maybe at another time, we can re-evaluate the auto-next-line behavior. We do have other issues that will change the auto-next-line behavior.

epage avatar Oct 22 '25 19:10 epage