//! Generate a Markdown-compatible reference for the uv command-line interface. use std::cmp::max; use std::path::PathBuf; use anstream::println; use anyhow::{bail, Result}; use clap::{Command, CommandFactory}; use itertools::Itertools; use pretty_assertions::StrComparison; use crate::generate_all::Mode; use crate::ROOT_DIR; use uv_cli::Cli; #[derive(clap::Args)] pub(crate) struct Args { /// Write the generated output to stdout (rather than to `settings.md`). #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> Result<()> { let reference_string = generate(); let filename = "cli.md"; let reference_path = PathBuf::from(ROOT_DIR) .join("docs") .join("reference") .join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{comparison}"); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-cli-reference`"); } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}"); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}"); } }, } Ok(()) } fn generate() -> String { let mut output = String::new(); let mut uv = Cli::command(); // It is very important to build the command before beginning inspection or subcommands // will be missing all of the propagated options. uv.build(); let mut parents = Vec::new(); output.push_str("# CLI Reference\n\n"); generate_command(&mut output, &uv, &mut parents); output } fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) { if command.is_hide_set() { return; } // Generate the command header. let name = if parents.is_empty() { command.get_name().to_string() } else { format!( "{} {}", parents.iter().map(|cmd| cmd.get_name()).join(" "), command.get_name() ) }; // Display the top-level `uv` command at the same level as its children let level = max(2, parents.len() + 1); output.push_str(&format!("{} {name}\n\n", "#".repeat(level))); // Display the command description. if let Some(about) = command.get_long_about().or_else(|| command.get_about()) { output.push_str(&about.to_string()); output.push_str("\n\n"); }; // Display the usage { // This appears to be the simplest way to get rendered usage from Clap, // it is complicated to render it manually. It's annoying that it // requires a mutable reference but it doesn't really matter. let mut command = command.clone(); output.push_str("
{subcommand_name}{}",
arg.get_id().to_string().to_uppercase()
));
output.push_str("--{long}"));
if let Some(short) = opt.get_short() {
output.push_str(&format!(", -{short}"));
}
if let Some(values) = opt.get_value_names() {
for value in values {
output.push_str(&format!(
" {}",
value.to_lowercase().replace('_', "-")
));
}
}
output.push_str("