mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Generate CLI reference for documentation (#5685)
Loosely based on [Cargo's format](https://github.com/rust-lang/cargo/blob/master/src/doc/src/commands/cargo-build.md) <img width="896" alt="Screenshot 2024-08-01 at 9 44 03 AM" src="https://github.com/user-attachments/assets/7c016bb3-2b54-46af-8ea8-ce82e07a0e30"> Future work includes: - Grouping options - Enforcing some sort of specific command ordering - Showing possible values for enums - Adding "long_about" to commands for more context
This commit is contained in:
parent
9788496c51
commit
f107406727
8 changed files with 1889 additions and 2 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -2062,6 +2062,17 @@ dependencies = [
|
|||
"quoted_printable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef3aab6a1d529b112695f72beec5ee80e729cb45af58663ec902c8fac764ecdd"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"pipeline",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
|
@ -2563,6 +2574,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pipeline"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
|
@ -2711,7 +2728,7 @@ dependencies = [
|
|||
"indexmap",
|
||||
"log",
|
||||
"priority-queue",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustc-hash 2.0.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -4689,6 +4706,7 @@ dependencies = [
|
|||
"fs-err",
|
||||
"install-wheel-rs",
|
||||
"itertools 0.13.0",
|
||||
"markdown",
|
||||
"mimalloc",
|
||||
"owo-colors",
|
||||
"pep508_rs",
|
||||
|
@ -4709,6 +4727,7 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
"uv-build",
|
||||
"uv-cache",
|
||||
"uv-cli",
|
||||
"uv-client",
|
||||
"uv-configuration",
|
||||
"uv-dispatch",
|
||||
|
|
|
@ -29,7 +29,7 @@ uv-warnings = { workspace = true }
|
|||
|
||||
anstream = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap = { workspace = true, features = ["derive", "string"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
|
|
@ -23,6 +23,7 @@ pep508_rs = { workspace = true }
|
|||
pypi-types = { workspace = true }
|
||||
uv-build = { workspace = true }
|
||||
uv-cache = { workspace = true, features = ["clap"] }
|
||||
uv-cli = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-dispatch = { workspace = true }
|
||||
|
@ -43,6 +44,7 @@ anyhow = { workspace = true }
|
|||
clap = { workspace = true, features = ["derive", "wrap_help"] }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
itertools = { workspace = true }
|
||||
markdown = "0.3.0"
|
||||
owo-colors = { workspace = true }
|
||||
poloto = { version = "19.1.2", optional = true }
|
||||
pretty_assertions = { version = "1.4.0" }
|
||||
|
|
262
crates/uv-dev/src/generate_cli_reference.rs
Normal file
262
crates/uv-dev/src/generate_cli_reference.rs
Normal file
|
@ -0,0 +1,262 @@
|
|||
//! 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("<h3 class=\"cli-reference\">Usage</h3>\n\n");
|
||||
output.push_str(&format!(
|
||||
"```\n{}\n```",
|
||||
command
|
||||
.render_usage()
|
||||
.to_string()
|
||||
.trim_start_matches("Usage: "),
|
||||
));
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
// Display a list of child commands
|
||||
let mut subcommands = command.get_subcommands().peekable();
|
||||
let has_subcommands = subcommands.peek().is_some();
|
||||
if has_subcommands {
|
||||
output.push_str("<h3 class=\"cli-reference\">Commands</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for subcommand in subcommands {
|
||||
if subcommand.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
let subcommand_name = format!("{name} {}", subcommand.get_name());
|
||||
output.push_str(&format!(
|
||||
"<dt><a href=\"{}\"><code>{subcommand_name}</code></a></dt>",
|
||||
subcommand_name.replace(' ', "-")
|
||||
));
|
||||
if let Some(about) = subcommand.get_about() {
|
||||
output.push_str(&format!(
|
||||
"<dd>{}</dd>\n",
|
||||
markdown::to_html(&about.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Do not display options for commands with children
|
||||
if !has_subcommands {
|
||||
// Display positional arguments
|
||||
let mut arguments = command
|
||||
.get_positionals()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.peekable();
|
||||
|
||||
if arguments.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Arguments</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for arg in arguments {
|
||||
output.push_str("<dt>");
|
||||
output.push_str(&format!(
|
||||
"<code>{}</code>",
|
||||
arg.get_id().to_string().to_uppercase()
|
||||
));
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Display options
|
||||
let mut options = command
|
||||
.get_opts()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.sorted_by_key(|opt| opt.get_id())
|
||||
.peekable();
|
||||
|
||||
if options.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Options</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
for opt in command.get_opts() {
|
||||
if opt.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(long) = opt.get_long() else { continue };
|
||||
|
||||
output.push_str("<dt>");
|
||||
output.push_str(&format!("<code>--{long}</code>"));
|
||||
if let Some(short) = opt.get_short() {
|
||||
output.push_str(&format!(", <code>-{short}</code>"));
|
||||
}
|
||||
if let Some(values) = opt.get_value_names() {
|
||||
for value in values {
|
||||
output.push_str(&format!(
|
||||
" <i>{}</i>",
|
||||
value.to_lowercase().replace('_', "-")
|
||||
));
|
||||
}
|
||||
}
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>");
|
||||
}
|
||||
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
parents.push(command);
|
||||
|
||||
// Recurse to all of the subcommands.
|
||||
for subcommand in command.get_subcommands() {
|
||||
generate_command(output, subcommand, parents);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn test_generate_cli_reference() -> Result<()> {
|
||||
let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") {
|
||||
Mode::Write
|
||||
} else {
|
||||
Mode::Check
|
||||
};
|
||||
main(&Args { mode })
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ use crate::build::{build, BuildArgs};
|
|||
use crate::clear_compile::ClearCompileArgs;
|
||||
use crate::compile::CompileArgs;
|
||||
use crate::generate_all::Args as GenerateAllArgs;
|
||||
use crate::generate_cli_reference::Args as GenerateCliReferenceArgs;
|
||||
use crate::generate_json_schema::Args as GenerateJsonSchemaArgs;
|
||||
use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs;
|
||||
#[cfg(feature = "render")]
|
||||
|
@ -46,6 +47,7 @@ mod build;
|
|||
mod clear_compile;
|
||||
mod compile;
|
||||
mod generate_all;
|
||||
mod generate_cli_reference;
|
||||
mod generate_json_schema;
|
||||
mod generate_options_reference;
|
||||
mod render_benchmarks;
|
||||
|
@ -69,6 +71,8 @@ enum Cli {
|
|||
GenerateJSONSchema(GenerateJsonSchemaArgs),
|
||||
/// Generate the options reference for the documentation.
|
||||
GenerateOptionsReference(GenerateOptionsReferenceArgs),
|
||||
/// Generate the CLI reference for the documentation.
|
||||
GenerateCliReference(GenerateCliReferenceArgs),
|
||||
#[cfg(feature = "render")]
|
||||
/// Render the benchmarks.
|
||||
RenderBenchmarks(RenderBenchmarksArgs),
|
||||
|
@ -88,6 +92,7 @@ async fn run() -> Result<()> {
|
|||
Cli::GenerateAll(args) => generate_all::main(&args)?,
|
||||
Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
|
||||
Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?,
|
||||
Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?,
|
||||
#[cfg(feature = "render")]
|
||||
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
|
||||
}
|
||||
|
|
1580
docs/reference/cli.md
Normal file
1580
docs/reference/cli.md
Normal file
File diff suppressed because it is too large
Load diff
|
@ -147,3 +147,20 @@ See https://github.com/astral-sh/uv/issues/5130 */
|
|||
.md-nav__link--active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Styling for the generated CLI reference page */
|
||||
.cli-reference dd {
|
||||
margin-top: 0.1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.cli-reference dd p {
|
||||
margin-block-start: 0.2em;
|
||||
margin-block-end: 0.3em;
|
||||
}
|
||||
.cli-reference ul {
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
h3.cli-reference {
|
||||
font-size: 1.1em;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
|
|
@ -120,6 +120,8 @@ nav:
|
|||
- Declaring dependencies: pip/dependencies.md
|
||||
- Locking environments: pip/compile.md
|
||||
- Compatibility with pip: pip/compatibility.md
|
||||
- Reference:
|
||||
- Commands: reference/cli.md
|
||||
- Policies:
|
||||
- Versioning: versioning.md
|
||||
- Platform support: platforms.md
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue