-
Notifications
You must be signed in to change notification settings - Fork 26
Description
Description
When using clap-markdown with a CLI that has subcommands, calling Command::build() before generating markdown results in duplicate command paths in:
- Section headers (e.g.,
## myapp myapp-createinstead of## myapp create) - Usage lines (e.g.,
Usage: myapp myapp createinstead ofUsage: myapp create) - Table of Contents entries
Steps to Reproduce
use clap::{CommandFactory, Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Create something
Create,
/// List items
List,
}
fn main() {
let mut cmd = Cli::command();
// build() is often needed to propagate global arguments
cmd.build();
let markdown = clap_markdown::help_markdown_command(&cmd);
println!("{}", markdown);
}Expected Behavior
## `myapp create`
Create something
**Usage:** `myapp create`Actual Behavior
## `myapp myapp-create`
Create something
**Usage:** `myapp myapp create`Root Cause Analysis
After investigating, the issue stems from how clap and clap-markdown interact:
-
clap's
build()behavior: Whenbuild()is called, clap setsdisplay_nameandbin_nameon subcommands to include the parent command path:display_name="myapp-create"(hyphen-separated)bin_name="myapp create"(space-separated)
-
clap-markdown's
get_canonical_name()function (insrc/lib.rs):fn get_canonical_name(command: &clap::Command) -> String { command .get_display_name() .or_else(|| command.get_bin_name()) .map(|name| name.to_owned()) .unwrap_or_else(|| command.get_name().to_owned()) }
This returns
"myapp-create"(fromdisplay_name) instead of just"create". -
Path building in
build_command_markdown(): The code builds paths by accumulating:let command_path = { let mut command_path = parent_command_path.clone(); command_path.push(title_name); // title_name = "myapp-create" command_path };
Result:
["myapp", "myapp-create"]→ displayed as"myapp myapp-create" -
Usage line generation:
writeln!( buffer, "**Usage:** `{}{}`\n", if parent_command_path.is_empty() { String::new() } else { let mut s = parent_command_path.join(" "); s.push_str(" "); s }, command.clone().render_usage().to_string().replace("Usage: ", "") )?;
render_usage()returns"Usage: myapp create ..."(usesbin_name)- After stripping prefix:
"myapp create ..." - Prepending parent path:
"myapp " + "myapp create ..."="myapp myapp create ..."
Why build() is Often Necessary
Calling build() is required when:
- Using global arguments that need to be propagated to subcommands
- Subcommands reference arguments defined at the parent level (e.g.,
#[arg(requires = "some_global_arg")])
Without build(), clap-markdown may panic with errors like:
Command create: Argument or group 'some_global_arg' specified in 'requires*' for 'some_flag' does not exist
Suggested Solutions
Option 1: Use get_name() instead of get_canonical_name() for building command paths
fn get_canonical_name(command: &clap::Command) -> String {
// Always use the simple command name for path building
command.get_name().to_owned()
}Option 2: Check if display_name/bin_name already contains the parent path and avoid double-adding
Option 3: Don't prepend parent_command_path to the usage string since render_usage() already includes the full path after build()
Option 4: Clear display_name and bin_name internally before processing
Current Workaround
We currently work around this by:
-
Clearing
display_nameon all subcommands afterbuild():fn clear_display_names(cmd: &mut clap::Command) { for sub in cmd.get_subcommands_mut() { let mut owned = std::mem::take(sub); owned = owned.display_name(clap::builder::Resettable::Reset); clear_display_names(&mut owned); *sub = owned; } }
-
Post-processing the generated markdown to remove duplicate patterns like
"myapp myapp "→"myapp "
Environment
- clap version: 4.5.x
- clap-markdown version: 0.1.4
- Rust version: 1.85