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-create instead of ## myapp create)
- Usage lines (e.g.,
Usage: myapp myapp create instead of Usage: 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: When build() is called, clap sets display_name and bin_name on 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 (in src/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" (from display_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 ..." (uses bin_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_name on all subcommands after build():
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
Description
When using
clap-markdownwith a CLI that has subcommands, callingCommand::build()before generating markdown results in duplicate command paths in:## myapp myapp-createinstead of## myapp create)Usage: myapp myapp createinstead ofUsage: myapp create)Steps to Reproduce
Expected Behavior
Actual Behavior
Root Cause Analysis
After investigating, the issue stems from how
clapandclap-markdowninteract: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):This returns
"myapp-create"(fromdisplay_name) instead of just"create".Path building in
build_command_markdown(): The code builds paths by accumulating:Result:
["myapp", "myapp-create"]→ displayed as"myapp myapp-create"Usage line generation:
render_usage()returns"Usage: myapp create ..."(usesbin_name)"myapp create ...""myapp " + "myapp create ..."="myapp myapp create ..."Why
build()is Often NecessaryCalling
build()is required when:#[arg(requires = "some_global_arg")])Without
build(), clap-markdown may panic with errors like:Suggested Solutions
Option 1: Use
get_name()instead ofget_canonical_name()for building command pathsOption 2: Check if
display_name/bin_namealready contains the parent path and avoid double-addingOption 3: Don't prepend
parent_command_pathto the usage string sincerender_usage()already includes the full path afterbuild()Option 4: Clear
display_nameandbin_nameinternally before processingCurrent Workaround
We currently work around this by:
Clearing
display_nameon all subcommands afterbuild():Post-processing the generated markdown to remove duplicate patterns like
"myapp myapp "→"myapp "Environment