Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,9 @@ Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and
- [x] **The granular hides** — `hide_default_value`, `hide_env`, `hide_env_values`,
`hide_possible_values`, `hide_short_help`, and `hide_long_help` round-trip
through KDL and the clap bridge and are honored by Rust and Go help output.
- [ ] **`help_template`, `next_line_help`, and `flatten_help`.**
- [x] **`next_line_help`.** Command-wide block layout survives typed metadata,
KDL, the clap bridge, and both Rust and generated Go help renderers.
- [ ] **`help_template` and `flatten_help`.**
- [x] **`subcommand_help_heading` / `subcommand_value_name`.** Custom subcommand
section labels and synopsis placeholders survive KDL, typed Rust, generated
Go, and the clap bridge and are rendered by both help implementations.
Expand Down
117 changes: 106 additions & 11 deletions argv/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,23 @@ pub fn short_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) ->
|a| a.help_heading,
|out, a| {
let usage = arg_usage(a);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = a.help.filter(|h| !h.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
if a.hide_default_value { &[] } else { a.default },
);
return;
}
match a.help.filter(|h| !h.trim().is_empty()) {
Some(help) => {
let _ = write!(out, " {usage:<arg_col$} {help}");
Expand Down Expand Up @@ -667,6 +684,23 @@ pub fn short_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) ->
.max()
.unwrap_or(0);
let short_entry = |out: &mut String, f: &FlagMeta<'_>, usage: String| {
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = f.help.filter(|h| !h.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_default_value { &[] } else { f.default },
);
return;
}
match f.help.filter(|h| !h.trim().is_empty()) {
Some(help) => {
let _ = write!(out, " {usage:<flag_col$} {help}");
Comment thread
cursor[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -758,14 +792,28 @@ fn commands_section(out: &mut String, path: &[&str], meta: &CommandMeta<'_>) {
let _ = write!(out, " [aliases: {}]", visible_aliases.join(", "));
}
if let Some(about) = sub.about {
let _ = write!(out, " {about}");
if meta.next_line_help {
out.push('\n');
write_indented(out, about.trim_end(), 4);
continue;
Comment thread
cursor[bot] marked this conversation as resolved.
}
// The row writes its own newline below. Trim trailing whitespace in both
// layouts, as usage-lib does before choosing a layout.
let _ = write!(out, " {}", about.trim_end());
}
out.push('\n');
}
let _ = writeln!(
out,
" help Print this message or the help of the given subcommand(s)"
);
if meta.next_line_help {
let _ = writeln!(
out,
" help\n Print this message or the help of the given subcommand(s)"
);
} else {
let _ = writeln!(
out,
" help Print this message or the help of the given subcommand(s)"
);
}
}

/// One section per heading, unheaded first, in the order the headings first appear.
Expand Down Expand Up @@ -1017,7 +1065,14 @@ pub fn long_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) ->
|a| a.help_heading,
|out, a| {
let text = a.long_help.or(a.help);
entry(out, &arg_usage(a), text, arg_col, width);
entry(
out,
&arg_usage(a),
text,
arg_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if a.hide_possible_values {
Expand Down Expand Up @@ -1046,7 +1101,14 @@ pub fn long_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) ->
|f| f.help_heading,
|out, f| {
let text = f.long_help.or(f.help);
entry(out, &column_usage(f), text, flag_col, width);
entry(
out,
&column_usage(f),
text,
flag_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if f.hide_possible_values {
Expand All @@ -1070,7 +1132,7 @@ pub fn long_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) ->
|_| None,
|out, (f, usage)| {
let text = f.long_help.or(f.help);
entry(out, usage, text, flag_col, width);
entry(out, usage, text, flag_col, width, meta.next_line_help);
long_annotations(
out,
if f.hide_possible_values {
Expand Down Expand Up @@ -1146,7 +1208,14 @@ fn write_indented(out: &mut String, text: &str, indent: usize) {
}

/// One entry: its usage, and its help either beside it or beneath it.
fn entry(out: &mut String, usage: &str, help: Option<&str>, col: usize, width: usize) {
fn entry(
out: &mut String,
usage: &str,
help: Option<&str>,
col: usize,
width: usize,
next_line: bool,
) {
let Some(help) = help.filter(|h| !h.trim().is_empty()) else {
let _ = writeln!(out, " {usage}");
return;
Expand All @@ -1156,7 +1225,7 @@ fn entry(out: &mut String, usage: &str, help: Option<&str>, col: usize, width: u
// there is room left for it to say anything.
let indent = 2 + col + 2;
let room = width.saturating_sub(indent);
if help.contains('\n') || room < 10 {
if next_line || help.contains('\n') || room < 10 {
let _ = writeln!(out, " {usage}");
write_indented(out, help, 4);
return;
Expand Down Expand Up @@ -1688,7 +1757,33 @@ pub fn render_at_styled(

#[cfg(test)]
mod style_tests {
use super::{styled_help, Style};
use super::{commands_section, styled_help, Style};
use crate::spec::CommandMeta;
use crate::Command;

#[test]
fn short_command_rows_trim_trailing_help_whitespace() {
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
about: Some("run it\n"),
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();

commands_section(&mut page, &[], &root_meta);

assert!(page.contains(" run run it\n help"));
assert!(!page.contains(" run run it\n\n help"));
}

#[test]
fn coloured_help_styles_structure_without_changing_plain_text() {
Expand Down
9 changes: 9 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,8 @@ pub struct CommandMeta<'a> {
pub subcommand_help_heading: Option<&'a str>,
/// Placeholder used for a subcommand in the usage synopsis.
pub subcommand_value_name: Option<&'a str>,
/// Put each argument, flag, and subcommand description on the following line.
pub next_line_help: bool,
/// Fixed help width. Zero disables wrapping.
pub term_width: Option<usize>,
/// Maximum detected terminal width when `term_width` is unset. Zero disables the cap.
Expand Down Expand Up @@ -767,6 +769,7 @@ impl CommandMeta<'_> {
subcommand_required: false,
subcommand_help_heading: None,
subcommand_value_name: None,
next_line_help: false,
term_width: None,
max_term_width: None,
args_override_self: true,
Expand Down Expand Up @@ -1215,6 +1218,9 @@ impl Spec<'_> {
if let Some(name) = self.root.subcommand_value_name {
prop(out, "subcommand_value_name", name)?;
}
if self.root.next_line_help {
writeln!(out, "next_line_help #true")?;
}
if let Some(width) = self.root.term_width {
writeln!(out, "term_width {width}")?;
}
Expand Down Expand Up @@ -1464,6 +1470,9 @@ fn write_command<'a>(
if let Some(name) = meta.subcommand_value_name {
write!(out, " subcommand_value_name={}", quoted(name))?;
}
if meta.next_line_help {
out.push_str(" next_line_help=#true");
}
if let Some(width) = meta.term_width {
write!(out, " term_width={width}")?;
}
Expand Down
5 changes: 0 additions & 5 deletions clap_usage/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,6 @@ fn visit(cmd: &Command, ancestors: &[String], losses: &mut BTreeSet<FidelityLoss
FidelityFeature::FlattenHelp,
"flatten_help",
);
command_loss(
cmd.is_next_line_help_set(),
FidelityFeature::NextLineHelp,
"next_line_help",
);
command_loss(
cmd.is_disable_help_flag_set(),
FidelityFeature::DisableHelpFlag,
Expand Down
11 changes: 11 additions & 0 deletions clap_usage/tests/fidelity_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ fn subcommand_presentation_is_lossless() {
assert_eq!(spec.cmd.subcommand_value_name.as_deref(), Some("ACTION"));
}

#[test]
fn next_line_help_is_lossless() {
let mut command = Command::new("ex")
.next_line_help(true)
.arg(Arg::new("config").long("config").help("Config file"))
.subcommand(Command::new("run").about("Run it"));
let (spec, report) = spec_with_report(&mut command, "ex");
assert!(report.is_lossless(), "{report:#?}");
assert!(spec.cmd.next_line_help);
}

#[test]
fn ranged_distinct_value_names_are_reported_as_lossy() {
let mut command = Command::new("ex").arg(
Expand Down
1 change: 1 addition & 0 deletions conformance/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub fn build(
subcommand_required: cmd.subcommand_required,
subcommand_help_heading: opt(&cmd.subcommand_help_heading),
subcommand_value_name: opt(&cmd.subcommand_value_name),
next_line_help: cmd.next_line_help,
term_width: cmd.term_width,
max_term_width: cmd.max_term_width,
args_override_self: cmd.args_override_self,
Expand Down
4 changes: 4 additions & 0 deletions derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub fn emit(cli: &Cli) -> TokenStream {
let allow_missing_positional = cli.allow_missing_positional;
let subcommand_help_heading = option_str(cli.subcommand_help_heading.as_deref());
let subcommand_value_name = option_str(cli.subcommand_value_name.as_deref());
let next_line_help = cli.next_line_help;
let term_width = option_usize(cli.term_width);
let max_term_width = option_usize(cli.max_term_width);
let usage = option_str(cli.usage.as_deref());
Expand Down Expand Up @@ -470,6 +471,7 @@ pub fn emit(cli: &Cli) -> TokenStream {
subcommand_required: #subcommand_required,
subcommand_help_heading: #subcommand_help_heading,
subcommand_value_name: #subcommand_value_name,
next_line_help: #next_line_help,
term_width: #term_width,
max_term_width: #max_term_width,
args_override_self: #args_override_self,
Expand Down Expand Up @@ -3568,6 +3570,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream {
});
let subcommand_help_heading = option_str(cli.subcommand_help_heading.as_deref());
let subcommand_value_name = option_str(cli.subcommand_value_name.as_deref());
let next_line_help = cli.next_line_help;
let term_width = option_usize(cli.term_width);
let max_term_width = option_usize(cli.max_term_width);
let unknown_flags = unknown_flags_tokens(cli);
Expand Down Expand Up @@ -3716,6 +3719,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream {
subcommand_required: #subcommand_required,
subcommand_help_heading: #subcommand_help_heading,
subcommand_value_name: #subcommand_value_name,
next_line_help: #next_line_help,
term_width: #term_width,
max_term_width: #max_term_width,
args_override_self: #args_override_self,
Expand Down
5 changes: 4 additions & 1 deletion derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub struct Cli {
pub allow_missing_positional: bool,
pub subcommand_help_heading: Option<String>,
pub subcommand_value_name: Option<String>,
pub next_line_help: bool,
pub term_width: Option<usize>,
pub max_term_width: Option<usize>,
/// Declared descriptions, for the case a doc comment cannot express: a long form that does
Expand Down Expand Up @@ -585,6 +586,7 @@ impl Cli {
allow_missing_positional: false,
subcommand_help_heading: None,
subcommand_value_name: None,
next_line_help: false,
term_width: None,
max_term_width: None,
about_attr: None,
Expand Down Expand Up @@ -746,6 +748,7 @@ impl Cli {
"subcommand_value_name" => {
cli.subcommand_value_name = Some(string_value(&meta)?)
}
"next_line_help" => cli.next_line_help = flag_value(&meta)?,
"term_width" => cli.term_width = Some(int_value(&meta)?),
"max_term_width" => cli.max_term_width = Some(int_value(&meta)?),
"restart_token" => cli.restart_token = Some(string_value(&meta)?),
Expand All @@ -760,7 +763,7 @@ impl Cli {
"unknown option `{other}` on a struct; usage::Cli takes \
`name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `author`, `license`, `repository`, `usage`, `verbatim_doc_comment`, `unknown_flags`, \
`default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, `dont_delimit_trailing_values`, `args_override_self`, `subcommand_negates_reqs`, `args_conflicts_with_subcommands`, `subcommand_precedence_over_arg`, `allow_missing_positional`, \
`next_help_heading`, `subcommand_help_heading`, `term_width`, `max_term_width`, \
`next_help_heading`, `subcommand_help_heading`, `next_line_help`, `term_width`, `max_term_width`, \
`subcommand_value_name`, `restart_token`, `mount` and \
`group` here, and the description comes from the doc \
comment"
Expand Down
3 changes: 2 additions & 1 deletion docs/rust/clap-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ the Rust declaration, not only from generated KDL, wherever the bridge column sa
| `subcommand_help_heading`, `subcommand_value_name` | yes | yes | yes | yes | yes | yes | Customize the subcommand section label and the synopsis placeholder. |
| `verbatim_doc_comment` | yes | n/a | yes | yes | yes | n/a | Commands, fields, and variants preserve line breaks and indentation when requested. |
| `rename_all`, `rename_all_env` | yes | n/a | yes | yes | yes | n/a | Full clap casing vocabulary; bare `env` uses the environment casing policy. |
| `help_template`, `next_line_help`, `flatten_help` | no | n/a | no | no | no | no | No equivalent yet. |
| `next_line_help` | yes | yes | yes | yes | yes | yes | Put command, argument, and flag descriptions below their usage instead of beside it. |
| `help_template`, `flatten_help` | no | n/a | no | no | no | no | No equivalent yet. |
| `term_width`, `max_term_width` | yes | yes | yes | yes | yes | no | Fixed width overrides a detected-width cap; clap exposes no bridge getters for these settings. |
| help styles and color | n/a | n/a | n/a | yes | lossy | no | Help and diagnostics use automatic ANSI styles; clap's custom style palette is not portable. |
| built-in help/version action and flag control | no | no | no | no | no | no | Custom actions and disabling or relocating built-ins are not represented. |
Expand Down
2 changes: 2 additions & 0 deletions docs/rust/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ convenience entry point for CLIs that want immediate print-and-exit behavior.
- `before_help`, `after_help`, `before_long_help`, `after_long_help` add text around the page —
`after_long_help` is the conventional home for an Examples section.
- `help_heading` on a field groups it under a heading.
- `next_line_help` on a command puts every argument, flag, and subcommand description beneath
its usage instead of in an aligned column beside it.
- `hide` removes an entry from help, docs, and completions while still parsing.

The rendered output matches what usage-lib renders from the same spec — the two renderers are
Expand Down
6 changes: 5 additions & 1 deletion docs/rust/migrating-from-clap.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,11 @@ Command-level presentation settings keep their clap spellings:

```rust
#[derive(usage::Cli)]
#[command(subcommand_help_heading = "Actions", subcommand_value_name = "ACTION")]
#[command(
subcommand_help_heading = "Actions",
subcommand_value_name = "ACTION",
next_line_help
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
Expand Down
1 change: 1 addition & 0 deletions docs/spec/reference/cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cmd "config" help="Manage the CLI config" {
cmd "config" hide=#true // hide command from docs and completions
cmd "config" subcommand_required=#true // subcommand is not optional
cmd "config" subcommand_help_heading="Actions" subcommand_value_name="ACTION"
cmd "config" next_line_help=#true // put descriptions below each entry

// these are shown under -h
cmd "config" before_help="shown before the command"
Expand Down
1 change: 1 addition & 0 deletions go/argv/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type Help struct {
AfterLongHelp string
SubcommandHelpHeading string
SubcommandValueName string
NextLineHelp bool
// Examples are worked invocations, printed last.
Examples []Example
}
Expand Down
Loading