use clap::Command;
pub fn tree(command: &Command) -> String {
let mut lines = Vec::new();
walk(command, "", &mut lines);
lines.sort();
lines.join("\n")
}
fn walk(command: &Command, path: &str, lines: &mut Vec<String>) {
let mut children = command.get_subcommands().peekable();
if children.peek().is_none() {
if !path.is_empty() {
lines.push(leaf(command, path));
}
return;
}
for child in command.get_subcommands() {
let child_path = if path.is_empty() {
child.get_name().to_string()
} else {
format!("{path} {}", child.get_name())
};
walk(child, &child_path, lines);
}
}
fn leaf(command: &Command, path: &str) -> String {
let mut parts: Vec<String> = command
.get_arguments()
.filter(|arg| arg.get_id() != "help" && arg.get_id() != "version")
.map(argument)
.collect();
parts.sort();
format!("{path}\t{}", parts.join(" "))
}
fn argument(arg: &clap::Arg) -> String {
let mut spelling = String::new();
if let Some(long) = arg.get_long() {
spelling.push_str(&format!("--{long}"));
}
if let Some(short) = arg.get_short() {
spelling.push_str(&format!("/-{short}"));
}
if arg.is_positional() {
let id = arg.get_id().as_str();
let name = id.split('\u{1}').next().unwrap_or(id);
spelling.push_str(&format!("<{name}>"));
}
if arg.is_required_set() {
spelling.push('!');
}
if matches!(arg.get_action(), clap::ArgAction::Append) {
spelling.push_str("...");
}
spelling
}
pub fn diff(expected: &str, actual: &str) -> Option<String> {
let expected: Vec<&str> = expected.trim().lines().collect();
let actual: Vec<&str> = actual.trim().lines().collect();
if expected == actual {
return None;
}
let mut report = String::new();
for line in &expected {
if !actual.contains(line) {
report.push_str(&format!("- {line}\n"));
}
}
for line in &actual {
if !expected.contains(line) {
report.push_str(&format!("+ {line}\n"));
}
}
Some(report)
}
#[cfg(test)]
mod tests {
use super::*;
fn command() -> clap::Command {
clap::Command::new("root").subcommand(
clap::Command::new("widgets").subcommand(
clap::Command::new("list")
.arg(clap::Arg::new("limit").long("limit").short('l'))
.arg(clap::Arg::new("name").long("name").required(true)),
),
)
}
#[test]
fn a_leaf_renders_its_spelling_shorts_and_requirement() {
assert_eq!(tree(&command()), "widgets list\t--limit/-l --name!");
}
#[test]
fn a_respelled_flag_shows_up_as_the_line_that_moved() {
let before = tree(&command());
let after = tree(
&clap::Command::new("root").subcommand(
clap::Command::new("widgets").subcommand(
clap::Command::new("list")
.arg(clap::Arg::new("limit").long("max").short('l'))
.arg(clap::Arg::new("name").long("name").required(true)),
),
),
);
let report = diff(&before, &after).expect("a re-spelled flag is a difference");
assert!(report.contains("- widgets list\t--limit/-l"), "{report}");
assert!(report.contains("+ widgets list\t--max/-l"), "{report}");
}
#[test]
fn an_unchanged_tree_reports_nothing() {
assert!(diff(&tree(&command()), &tree(&command())).is_none());
}
}