usage_rs/lib.rs
1//! The facade for building compiled Rust CLIs with usage.
2//!
3//! Depend on `usage-rs`. That is the one package an
4//! application needs: derive macros, the argv runtime, help, and clap-shaped errors ship in the
5//! defaults. Completions stay behind a feature; low-level adopters that want only the binding
6//! runtime keep depending on `usage-argv` directly.
7//!
8//! ```toml
9//! [dependencies]
10//! usage-rs = { version = "7" }
11//! ```
12//!
13//! # Smaller binaries
14//!
15//! CLIs without `flatten_help` or `HelpAll` actions can omit `help-advanced`:
16//!
17//! ```toml
18//! usage-rs = { version = "7", default-features = false, features = ["help", "diagnostics", "completions"] }
19//! ```
20//!
21//! Advanced help remains enabled by default. Declaring those actions without its feature
22//! fails at compile time; hand-written metadata requesting advanced help panics when rendered.
23//! Cargo features are additive: another dependency enabling it restores advanced help.
24//!
25//! To keep the spec endpoint without linking the KDL writer, use
26//! `#[usage(spec_endpoint_file = "cli.usage.kdl")]`. The file is included at compile time,
27//! relative to the declaring crate. Generate it with `Cli::to_kdl()` in a development tool,
28//! and test that `Cli::spec_request(&[usage_rs::SPEC_REQUEST.as_ref()])` equals `Cli::to_kdl()`.
29//! Regenerate after changing CLI metadata or its version. `to_kdl()` still uses the live
30//! metadata, including settings and `spec_extra`; only the endpoint uses the file.
31//!
32//! What happens after a parse can come from the same declaration: a command implements [`Run`],
33//! the subcommand enum says `#[usage(run)]`, and the `match` that routes argv to the code
34//! carrying it out is generated rather than written. [`RunWith`] under `#[usage(run_with)]` hands
35//! each command shared state, and [`RunAsync`] / [`RunAsyncWith`] under `#[usage(run_async)]` /
36//! `#[usage(run_async_with)]` are the async pair. Nothing about any of it reaches the spec.
37//!
38//! Enable portable expression validation only when a CLI declares `validate` rules:
39//!
40//! ```toml
41//! usage-rs = { version = "7", features = ["validation"] }
42//! ```
43//!
44//! ```
45//! # #[cfg(feature = "spec")]
46//! use usage_rs::Cli;
47//!
48//! # #[cfg(not(feature = "spec"))]
49//! # fn main() {}
50//! # #[cfg(feature = "spec")]
51//! # fn main() {
52//! #[derive(Cli)]
53//! #[usage(bin = "ex")]
54//! struct Ex {
55//! #[usage(long, value_hint = usage_rs::ValueHint::FilePath)]
56//! file: Option<std::path::PathBuf>,
57//! }
58//!
59//! let argv = [std::ffi::OsStr::new("--file"), std::ffi::OsStr::new("input.txt")];
60//! let ex = Ex::parse_from(&argv).expect("valid command line");
61//! assert_eq!(ex.file.as_deref(), Some(std::path::Path::new("input.txt")));
62//! # }
63//! ```
64//!
65//! A derived [`Args`] type can also be parsed on its own. This lets an adapter
66//! reuse one command's exact argument contract without constructing or parsing
67//! the application's root CLI:
68//!
69//! ```
70//! # #[cfg(feature = "spec")]
71//! use usage_rs::Args;
72//! #
73//! # #[cfg(not(feature = "spec"))]
74//! # fn main() {}
75//! # #[cfg(feature = "spec")]
76//! # fn main() {
77//!
78//! #[derive(Debug, Args)]
79//! struct Install {
80//! #[usage(short, long)]
81//! force: bool,
82//! tools: Vec<String>,
83//! }
84//!
85//! let argv = ["--force", "node@24"].map(std::ffi::OsStr::new);
86//! let install = usage_rs::parse_args_from::<Install>(&argv).unwrap();
87//! assert!(install.force);
88//! assert_eq!(install.tools, ["node@24"]);
89//! # }
90//! ```
91
92#![forbid(unsafe_code)]
93
94// Generated absolute paths must also work if a derive is used inside this crate. Integration
95// targets already receive this name through Cargo; the library target needs the self alias.
96extern crate self as usage_rs;
97
98pub use usage_argv as argv;
99pub use usage_argv::*;
100#[cfg(feature = "config")]
101pub use usage_config as config;
102#[cfg(feature = "config")]
103pub use usage_derive::Config;
104#[cfg(feature = "spec")]
105pub use usage_derive::{ArgGroup, Args, Cli, Subcommands, ValueEnum};
106#[cfg(feature = "test")]
107pub use usage_test as test;
108#[cfg(feature = "validation")]
109pub use usage_validation as validation;
110#[cfg(feature = "response-files")]
111pub mod response;
112
113#[cfg(all(test, feature = "spec"))]
114mod tests {
115 #[derive(crate::Cli)]
116 #[usage(bin = "internal")]
117 struct Internal {}
118
119 #[cfg(feature = "validation")]
120 #[derive(Debug, crate::Cli)]
121 #[usage(bin = "validated")]
122 struct Validated {
123 #[usage(
124 long,
125 validate = "int(value) >= 1 && int(value) <= 65535",
126 validate_error = "must be a valid port"
127 )]
128 port: Option<u16>,
129 }
130
131 #[cfg(feature = "validation")]
132 #[derive(Debug, crate::Args)]
133 struct ValidatedArgs {
134 #[usage(long, validate = "value == 'ok'", validate_error = "must be ok")]
135 token: Option<String>,
136 }
137
138 #[cfg(feature = "validation")]
139 #[derive(Debug, crate::Cli)]
140 #[usage(bin = "validated-args")]
141 struct ValidatedArgsCli {
142 #[usage(flatten)]
143 args: ValidatedArgs,
144 }
145
146 #[test]
147 fn derives_resolve_the_facade_from_inside_the_facade() {
148 assert_eq!(Internal::spec().bin, Some("internal"));
149 }
150
151 #[cfg(feature = "config")]
152 #[derive(crate::Config)]
153 struct InternalSettings {
154 /// How many jobs to run at once
155 #[usage(env = "INTERNAL_JOBS", default = 4)]
156 jobs: u64,
157 }
158
159 #[cfg(feature = "config")]
160 #[test]
161 fn the_config_derive_resolves_the_facade_from_inside_the_facade() {
162 let resolved = crate::config::resolve(
163 InternalSettings::SETTINGS_REGISTRY,
164 crate::config::Layers::new(),
165 )
166 .expect("resolves");
167 let settings = InternalSettings::read(&resolved).expect("reads");
168 assert_eq!(settings.jobs, 4);
169 assert!(InternalSettings::spec_kdl().contains(r#"prop "jobs""#));
170 }
171
172 #[cfg(feature = "validation")]
173 #[test]
174 fn derives_evaluate_portable_validation_expressions() {
175 let valid = [
176 ::std::ffi::OsStr::new("--port"),
177 ::std::ffi::OsStr::new("9229"),
178 ];
179 assert_eq!(Validated::parse_from(&valid).unwrap().port, Some(9229));
180
181 let invalid = [
182 ::std::ffi::OsStr::new("--port"),
183 ::std::ffi::OsStr::new("0"),
184 ];
185 let crate::Error::InvalidValue(error) = Validated::parse_from(&invalid).unwrap_err() else {
186 panic!("expected invalid value");
187 };
188 assert_eq!(error.reason, "must be a valid port");
189
190 let invalid_args = [
191 ::std::ffi::OsStr::new("--token"),
192 ::std::ffi::OsStr::new("bad"),
193 ];
194 let crate::Error::InvalidValue(error) =
195 ValidatedArgsCli::parse_from(&invalid_args).unwrap_err()
196 else {
197 panic!("expected invalid value from flattened Args");
198 };
199 assert_eq!(error.reason, "must be ok");
200
201 let valid_args = [
202 ::std::ffi::OsStr::new("--token"),
203 ::std::ffi::OsStr::new("ok"),
204 ];
205 assert_eq!(
206 ValidatedArgsCli::parse_from(&valid_args)
207 .unwrap()
208 .args
209 .token
210 .as_deref(),
211 Some("ok")
212 );
213
214 let kdl = Validated::to_kdl();
215 assert!(
216 kdl.contains(r#"validate="int(value) >= 1 && int(value) <= 65535""#),
217 "{kdl}"
218 );
219 }
220}