//! Generate a Markdown-compatible reference for the ty command-line interface. use std::cmp::max; use std::path::PathBuf; use anyhow::{Result, bail}; use clap::{Command, CommandFactory}; use itertools::Itertools; use pretty_assertions::StrComparison; use crate::ROOT_DIR; use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; use ty::Cli; const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"]; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> Result<()> { let reference_string = generate(); let filename = "crates/ty/docs/cli.md"; let reference_path = PathBuf::from(ROOT_DIR).join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match std::fs::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 `{REGENERATE_ALL_COMMAND}`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`"); } Err(err) => { bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}"); } }, Mode::Write => match std::fs::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); std::fs::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); std::fs::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 ty = Cli::command(); // It is very important to build the command before beginning inspection or subcommands // will be missing all of the propagated options. ty.build(); let mut parents = Vec::new(); output.push_str("\n\n"); output.push_str("# CLI Reference\n\n"); generate_command(&mut output, &ty, &mut parents); output } #[allow(clippy::format_push_string)] fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) { if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) { 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 `ty` 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}
"));
for long_alias in opt.get_all_aliases().into_iter().flatten() {
output.push_str(&format!(", --{long_alias}
"));
}
if let Some(short) = opt.get_short() {
output.push_str(&format!(", -{short}
"));
}
for short_alias in opt.get_all_short_aliases().into_iter().flatten() {
output.push_str(&format!(", -{short_alias}
"));
}
// Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts`
if opt
.get_num_args()
.unwrap_or_else(|| 1.into())
.takes_values()
{
if let Some(values) = opt.get_value_names() {
for value in values {
output.push_str(&format!(
" {}",
value.to_lowercase().replace('_', "-")
));
}
}
}
output.push_str("