use crate::args::Args;
use clap::builder::PossibleValue;
use clap::{Arg, ArgAction, Command, CommandFactory};
use derive_more::{Display, Error};
use itertools::Itertools;
use pipe_trait::Pipe;
use std::borrow::Cow;
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum RenderUsageMdError {
#[display("--{_0} has a visible_alias that duplicates its own flag name")]
RedundantVisibleLongAlias(#[error(not(source))] String),
#[display("-{_0} has a visible_short_alias that duplicates its own flag name")]
RedundantVisibleShortAlias(#[error(not(source))] char),
}
pub fn render_usage_md() -> Result<String, RenderUsageMdError> {
let mut command: Command = Args::command();
reject_redundant_aliases(&command)?;
let mut out = String::new();
let usage = command.render_usage().to_string();
if let Some(usage) = usage.strip_prefix("Usage:") {
out.push_str("# Usage\n\n```sh\n");
out.push_str(usage.trim());
out.push_str("\n```\n\n");
}
let mut arguments_heading_written = false;
for arg in command.get_arguments() {
if !arg.is_positional() || arg.is_hide_set() || arg.is_hide_long_help_set() {
continue;
}
if !arguments_heading_written {
arguments_heading_written = true;
out.push_str("## Arguments\n\n");
}
render_argument(&mut out, arg);
}
if arguments_heading_written {
out.push('\n');
}
let mut options_heading_written = false;
for arg in command.get_arguments() {
if arg.is_positional() || arg.is_hide_set() || arg.is_hide_long_help_set() {
continue;
}
if !options_heading_written {
options_heading_written = true;
out.push_str("## Options\n\n");
}
render_option(&mut out, arg);
}
if let Some(after_help) = command.get_after_long_help() {
let text = after_help.to_string();
let mut lines_iter = text.lines();
let mut has_examples = false;
for line in lines_iter.by_ref() {
if line.trim() == "Examples:" {
has_examples = true;
break;
}
}
if has_examples {
out.push_str("## Examples\n\n");
render_examples_section(&mut out, lines_iter);
}
}
Ok(out)
}
fn render_argument(out: &mut String, arg: &Arg) {
let name = arg
.get_value_names()
.and_then(|names| names.first())
.map(|n| n.as_str())
.unwrap_or_else(|| arg.get_id().as_str());
let is_multiple = arg
.get_num_args()
.map(|r| r.max_values() > 1)
.unwrap_or(false);
let display_name = if arg.is_required_set() {
if is_multiple {
format!("<{name}>...")
} else {
format!("<{name}>")
}
} else if is_multiple {
format!("[{name}]...")
} else {
format!("[{name}]")
};
let desc = get_help_text(arg);
let desc = ensure_ends_with_punctuation(&desc);
out.push_str(&format!("* `{display_name}`: {desc}\n"));
}
fn render_option(out: &mut String, arg: &Arg) {
let Some(primary_long) = arg.get_long() else {
return;
};
write_option_anchors(out, arg, primary_long);
out.push_str(&format!("### `--{primary_long}`\n\n"));
let aliases = collect_option_display_aliases(arg);
let default_values = collect_option_default_values(arg);
let possible_values = collect_option_possible_values(arg);
let has_metadata =
!aliases.is_empty() || !default_values.is_empty() || !possible_values.is_empty();
if !aliases.is_empty() {
let aliases_str = aliases.iter().map(|alias| format!("`{alias}`")).join(", ");
out.push_str(&format!("* _Aliases:_ {aliases_str}.\n"));
}
if !default_values.is_empty() {
let default_values_str = default_values.join(", ");
out.push_str(&format!("* _Default:_ `{default_values_str}`.\n"));
}
if !possible_values.is_empty() {
out.push_str("* _Choices:_\n");
for possible_value in &possible_values {
let name = possible_value.get_name();
if let Some(help) = possible_value.get_help() {
out.push_str(&format!(" - `{name}`: {help}\n"));
} else {
out.push_str(&format!(" - `{name}`\n"));
}
}
}
if has_metadata {
out.push('\n');
}
write_option_description(out, arg);
}
fn write_option_anchors(out: &mut String, arg: &Arg, primary_long: &str) {
let append_anchor = |out: &mut String, id: &str| {
out.push_str(&format!(r#"<a id="{id}" name="{id}"></a>"#));
};
let append_anchor_for_short = |out: &mut String, short: char| {
append_anchor(out, &format!("option-{short}"));
};
if let Some(short) = arg.get_short() {
append_anchor_for_short(out, short);
}
append_anchor(out, primary_long);
for alias in arg.get_visible_aliases().unwrap_or_default() {
append_anchor(out, alias);
}
for short in arg.get_visible_short_aliases().unwrap_or_default() {
append_anchor_for_short(out, short);
}
out.push('\n');
}
fn collect_option_display_aliases(arg: &Arg) -> Vec<String> {
let long_aliases = arg
.get_visible_aliases()
.into_iter()
.flatten()
.map(|alias| format!("--{alias}"));
let short_aliases = arg
.get_visible_short_aliases()
.into_iter()
.flatten()
.map(|alias| format!("-{alias}"));
arg.get_short()
.map(|short| format!("-{short}"))
.into_iter()
.chain(long_aliases)
.chain(short_aliases)
.collect()
}
fn collect_option_default_values(arg: &Arg) -> Vec<Cow<'_, str>> {
if arg.is_hide_default_value_set() {
return Vec::new();
}
if !arg.is_positional() && matches!(arg.get_action(), ArgAction::SetTrue) {
return Vec::new();
}
arg.get_default_values()
.iter()
.map(|value| value.to_string_lossy())
.collect()
}
fn collect_option_possible_values(arg: &Arg) -> Vec<PossibleValue> {
if arg.is_hide_possible_values_set() {
return Vec::new();
}
arg.get_possible_values()
.into_iter()
.filter(|possible_value| !possible_value.is_hide_set())
.collect()
}
fn write_option_description(out: &mut String, arg: &Arg) {
let description = get_help_text(arg);
if !description.is_empty() {
let description = ensure_ends_with_punctuation(&description);
out.push_str(&format!("{description}\n\n"));
} else {
out.push('\n');
}
}
fn get_help_text(arg: &Arg) -> Cow<'static, str> {
if !arg.is_positional() && arg.get_id() == "help" {
return Cow::Borrowed("Print help");
}
match (arg.get_help(), arg.get_long_help()) {
(None, None) => Cow::Borrowed(""),
(Some(help), None) | (_, Some(help)) => Cow::Owned(help.to_string()),
}
}
fn render_examples_section<'a>(out: &mut String, lines: impl Iterator<Item = &'a str>) {
for line in lines {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(command) = line.strip_prefix('$') {
let command = command.trim();
out.push_str(&format!("```sh\n{command}\n```\n\n"));
continue;
}
out.push_str(&format!("### {line}\n\n"));
}
}
fn ensure_ends_with_punctuation(line: &str) -> Cow<'_, str> {
if line.is_empty() || line.ends_with('.') || line.ends_with('!') || line.ends_with('?') {
Cow::Borrowed(line)
} else {
Cow::Owned(format!("{line}."))
}
}
fn reject_redundant_aliases(command: &Command) -> Result<(), RenderUsageMdError> {
for arg in command.get_arguments() {
if let Some(primary_long) = arg.get_long() {
for alias in arg.get_visible_aliases().unwrap_or_default() {
if alias == primary_long {
return primary_long
.to_owned()
.pipe(RenderUsageMdError::RedundantVisibleLongAlias)
.pipe(Err);
}
}
}
if let Some(primary_short) = arg.get_short() {
for alias in arg.get_visible_short_aliases().unwrap_or_default() {
if alias == primary_short {
return primary_short
.pipe(RenderUsageMdError::RedundantVisibleShortAlias)
.pipe(Err);
}
}
}
}
Ok(())
}