use clap::Command;
pub fn args_description(cmd: &Command) -> String {
let mut description = String::from("Positional command line arguments");
let raw = {
let cmd = &mut cmd.clone();
cmd.render_usage().to_string()
};
if let Some(pattern) = extract_positional_pattern(&raw) {
description.push_str("\nUsage pattern: ");
description.push_str(&pattern);
}
description
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next(); for inner in chars.by_ref() {
if inner.is_ascii_alphabetic() || matches!(inner, '~' | '@') {
break;
}
}
} else {
out.push(c);
}
}
out
}
fn extract_positional_pattern(raw: &str) -> Option<String> {
let clean = strip_ansi(raw);
let after_label = clean
.trim_start()
.strip_prefix("Usage: ")
.or_else(|| clean.trim_start().strip_prefix("Usage:"))
.unwrap_or(&clean)
.trim();
let no_options = after_label
.replace(" [OPTIONS]", "")
.replace(" [flags]", "")
.replace("[OPTIONS]", "")
.replace("[flags]", "")
.replace(" [COMMAND]", "")
.replace("[COMMAND]", "")
.replace(" <COMMAND>", "")
.replace("<COMMAND>", "")
.replace(" [SUBCOMMAND]", "")
.replace("[SUBCOMMAND]", "")
.replace(" <SUBCOMMAND>", "")
.replace("<SUBCOMMAND>", "");
let no_options = no_options.trim();
no_options.find(['<', '[']).and_then(|first_arg_idx| {
let positionals = no_options[first_arg_idx..].trim();
if positionals.is_empty() {
None
} else {
Some(positionals.to_owned())
}
})
}
#[cfg(test)]
mod tests {
use clap::{Arg, ArgAction, Command};
use super::*;
fn description_for(cmd: &Command) -> String {
args_description(cmd)
}
fn cmd_with_arg(arg: Arg) -> Command {
Command::new("my-cli").arg(arg)
}
#[test]
fn leaf_with_no_positionals_emits_only_first_line() {
let cmd = cmd_with_arg(
Arg::new("verbose")
.long("verbose")
.action(ArgAction::SetTrue),
);
let desc = description_for(&cmd);
assert_eq!(desc, "Positional command line arguments");
assert!(!desc.contains('\n'));
}
#[test]
fn leaf_with_one_positional_emits_pattern() {
let cmd = cmd_with_arg(Arg::new("name").required(true));
let desc = description_for(&cmd);
assert!(
desc.contains("Positional command line arguments"),
"must start with canonical phrase"
);
assert!(
desc.contains("Usage pattern:"),
"must contain 'Usage pattern:' line"
);
assert!(
desc.contains("<name>"),
"must contain the <name> positional"
);
}
#[test]
fn leaf_with_multiple_positionals_emits_pattern() {
let cmd = Command::new("my-cli")
.arg(Arg::new("name").required(true))
.arg(Arg::new("path").required(true));
let desc = description_for(&cmd);
assert!(
desc.contains("Positional command line arguments"),
"must start with canonical phrase"
);
assert!(
desc.contains("Usage pattern:"),
"must contain 'Usage pattern:' line"
);
assert!(
desc.contains("<name>") && desc.contains("<path>"),
"must contain both positionals"
);
}
#[test]
fn description_starts_with_canonical_phrase() {
let cmd1 = cmd_with_arg(
Arg::new("verbose")
.long("verbose")
.action(ArgAction::SetTrue),
);
assert!(description_for(&cmd1).starts_with("Positional command line arguments"));
let cmd2 = cmd_with_arg(Arg::new("file").required(true));
assert!(description_for(&cmd2).starts_with("Positional command line arguments"));
}
#[test]
fn subcommand_path_is_stripped() {
let mut parent = Command::new("my-cli")
.subcommand(Command::new("sub").arg(Arg::new("file").required(true)));
parent.build();
let sub = parent
.find_subcommand("sub")
.expect("sub subcommand not found");
let desc = description_for(sub);
assert!(desc.contains("<file>"), "usage pattern must contain <file>");
let usage_line = desc
.lines()
.find(|l| l.contains("Usage pattern:"))
.expect("must have Usage pattern: line");
assert!(
!usage_line.contains("my-cli"),
"usage pattern must not contain root command"
);
assert!(
!usage_line.contains(" sub"),
"usage pattern must not contain intermediate subcommand"
);
}
#[test]
fn command_with_only_optional_positionals_emits_pattern() {
let cmd = cmd_with_arg(Arg::new("optional").required(false));
let desc = description_for(&cmd);
assert!(
desc.contains("Positional command line arguments"),
"must start with canonical phrase"
);
assert!(
desc.contains("Usage pattern:"),
"must contain 'Usage pattern:' line"
);
assert!(
desc.contains("[optional]"),
"must contain the [optional] positional"
);
}
#[test]
fn extract_positional_pattern_strips_options_placeholder() {
let pattern = extract_positional_pattern("Usage: my-cli [OPTIONS] <file> [output]");
assert_eq!(
pattern.as_deref(),
Some("<file> [output]"),
"must strip command path and [OPTIONS]"
);
}
#[test]
fn extract_positional_pattern_handles_flags_placeholder() {
let pattern = extract_positional_pattern("Usage: my-cli [flags] <name>");
assert_eq!(
pattern.as_deref(),
Some("<name>"),
"must handle [flags] placeholder"
);
}
#[test]
fn extract_positional_pattern_returns_none_for_no_positionals() {
let pattern = extract_positional_pattern("Usage: my-cli [OPTIONS]");
assert_eq!(pattern, None, "must return None when no positionals exist");
}
#[test]
fn extract_positional_pattern_returns_none_for_empty_usage() {
let pattern = extract_positional_pattern("");
assert_eq!(pattern, None, "must handle empty input gracefully");
}
#[test]
fn parent_with_positionals_and_subcommand_strips_command_placeholder() {
let mut parent = Command::new("my-cli")
.arg(Arg::new("name").required(true))
.subcommand(Command::new("sub"));
parent.build();
let leaf = parent.clone(); let desc = description_for(&leaf);
assert!(
desc.contains("Usage pattern: <name>"),
"expected `Usage pattern: <name>`, got: {desc}"
);
assert!(
!desc.contains("COMMAND"),
"subcommand placeholder must be stripped, got: {desc}"
);
}
#[test]
fn extract_positional_pattern_strips_ansi_escapes() {
let with_ansi = "\u{1b}[1mUsage:\u{1b}[0m my-cli [OPTIONS] \u{1b}[33m<NAME>\u{1b}[0m";
let pattern = extract_positional_pattern(with_ansi);
let p = pattern.expect("should extract pattern");
assert!(
!p.contains('\u{1b}'),
"ANSI escape leaked into pattern: {p:?}"
);
assert!(p.contains("<NAME>"), "actual pattern content lost: {p:?}");
}
}