diff --git a/Cargo.lock b/Cargo.lock index e6dea89fc..3f15a72ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,7 +319,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_usage" -version = "2.0.3" +version = "4.0.0" dependencies = [ "clap", "insta", diff --git a/Cargo.toml b/Cargo.toml index faba13617..4fffe9b61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ authors = ["Jeff Dickey @jdx"] license = "MIT" [workspace.dependencies] -clap_usage = { path = "./clap_usage", version = "2.0.3" } +clap_usage = { path = "./clap_usage", version = "4.0.0" } usage-cli = { path = "./cli" } usage-lib = { path = "./lib", version = "4.0.0", features = ["clap"] } diff --git a/clap_usage/Cargo.toml b/clap_usage/Cargo.toml index 60b2c984c..70ae7a866 100644 --- a/clap_usage/Cargo.toml +++ b/clap_usage/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap_usage" edition = "2021" -version = "2.0.3" +version = "4.0.0" include = [ "/Cargo.toml", "/Cargo.lock", diff --git a/clap_usage/src/generate.rs b/clap_usage/src/generate.rs index df2864a40..39a579773 100644 --- a/clap_usage/src/generate.rs +++ b/clap_usage/src/generate.rs @@ -1,9 +1,33 @@ use clap::Command; use std::io::Write; -pub fn generate>(cmd: &mut Command, bin_name: S, buf: &mut dyn Write) { +/// The usage spec for a clap command, ready to inspect or annotate. +/// +/// [`generate`] writes this straight out. Take it here instead when you need +/// to set something clap cannot express — most often `effect=`, which says +/// whether a command reads, writes or destroys: +/// +/// ```no_run +/// # use clap::Command; +/// # use clap_usage::usage::SpecCommandEffect; +/// # let mut cmd = Command::new("mycli").subcommand(Command::new("rm")); +/// let mut spec = clap_usage::spec(&mut cmd, "mycli"); +/// if let Some(rm) = spec.cmd.subcommands.get_mut("rm") { +/// rm.effect = Some(SpecCommandEffect::Destructive); +/// } +/// println!("{spec}"); +/// ``` +pub fn spec>(cmd: &mut Command, bin_name: S) -> usage::Spec { let mut spec: usage::Spec = cmd.clone().into(); spec.bin = bin_name.into(); + spec +} + +/// Write the usage spec for a clap command, with the `@generated` header. +/// +/// Use [`spec`] instead if you need to modify the spec before writing it. +pub fn generate>(cmd: &mut Command, bin_name: S, buf: &mut dyn Write) { + let spec = spec(cmd, bin_name); writeln!(buf, "// @generated by usage-cli from clap metadata") .expect("write @generated comment"); diff --git a/clap_usage/src/lib.rs b/clap_usage/src/lib.rs index 1f05b7fb4..222c0319f 100644 --- a/clap_usage/src/lib.rs +++ b/clap_usage/src/lib.rs @@ -1,3 +1,22 @@ +//! Build [`usage`] specs from [`clap`] commands. +//! +//! [`generate`] writes a spec straight out; [`spec`] hands you the [`usage::Spec`] +//! first so you can set things clap cannot express. +//! +//! The `usage` crate is re-exported, so depending on `clap_usage` alone is +//! enough to name the spec types you get back: +//! +//! ```no_run +//! use clap_usage::usage::SpecCommandEffect; +//! ``` + mod generate; -pub use crate::generate::generate; +/// The [`usage`] crate, re-exported. +/// +/// [`spec`] returns `usage` types, so consumers need to name them. Re-exporting +/// the whole crate means `clap_usage` on its own is a sufficient dependency, +/// and nothing here goes stale as `usage` grows. +pub use usage; + +pub use crate::generate::{generate, spec}; diff --git a/clap_usage/tests/spec_accessor.rs b/clap_usage/tests/spec_accessor.rs new file mode 100644 index 000000000..43d36c937 --- /dev/null +++ b/clap_usage/tests/spec_accessor.rs @@ -0,0 +1,35 @@ +use clap::Command; +// Deliberately reached through clap_usage, proving a consumer needs no +// direct usage-lib dependency to annotate the spec it gets back. +use clap_usage::usage::SpecCommandEffect; + +/// The reason `spec` exists: `generate` writes straight to a writer, so there +/// is no way to set something clap cannot express, such as `effect=`. +#[test] +fn spec_can_be_annotated_before_rendering() { + let mut cmd = Command::new("mycli") + .subcommand(Command::new("ls").about("List things")) + .subcommand(Command::new("rm").about("Remove things")); + + let mut spec = clap_usage::spec(&mut cmd, "mycli"); + assert_eq!(spec.bin, "mycli"); + + spec.cmd.subcommands.get_mut("ls").unwrap().effect = Some(SpecCommandEffect::Read); + spec.cmd.subcommands.get_mut("rm").unwrap().effect = Some(SpecCommandEffect::Destructive); + + let rendered = spec.to_string(); + assert!(rendered.contains("cmd ls"), "{rendered}"); + assert!(rendered.contains("effect=read"), "{rendered}"); + assert!(rendered.contains("effect=destructive"), "{rendered}"); +} + +/// `generate` must keep producing exactly what it always did. +#[test] +fn generate_still_writes_the_header_and_spec() { + let mut cmd = Command::new("mycli").subcommand(Command::new("ls")); + let mut buf = vec![]; + clap_usage::generate(&mut cmd, "mycli", &mut buf); + let out = String::from_utf8(buf).unwrap(); + assert!(out.starts_with("// @generated by usage-cli from clap metadata\n")); + assert!(out.contains("bin mycli")); +}