Skip to main content

usage_rs/
lib.rs

1//! The facade for building compiled Rust CLIs with usage.
2//!
3//! Depend on `usage-rs` under the short crate name `usage`. 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 = { package = "usage-rs", version = "6" }
11//! ```
12//!
13//! # Smaller binaries
14//!
15//! CLIs without `flatten_help` or `HelpAll` actions can omit `help-advanced`:
16//!
17//! ```toml
18//! usage = { package = "usage-rs", version = "6", 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::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 = { package = "usage-rs", version = "6", features = ["validation"] }
42//! ```
43//!
44//! ```
45//! use usage_rs as usage;
46//! # #[cfg(feature = "spec")]
47//! use usage::Cli;
48//!
49//! # #[cfg(not(feature = "spec"))]
50//! # fn main() {}
51//! # #[cfg(feature = "spec")]
52//! # fn main() {
53//! #[derive(Cli)]
54//! #[usage(bin = "ex")]
55//! struct Ex {
56//!     #[usage(long, value_hint = usage::ValueHint::FilePath)]
57//!     file: Option<std::path::PathBuf>,
58//! }
59//!
60//! let argv = [std::ffi::OsStr::new("--file"), std::ffi::OsStr::new("input.txt")];
61//! let ex = Ex::parse_from(&argv).expect("valid command line");
62//! assert_eq!(ex.file.as_deref(), Some(std::path::Path::new("input.txt")));
63//! # }
64//! ```
65//!
66//! A derived [`Args`] type can also be parsed on its own. This lets an adapter
67//! reuse one command's exact argument contract without constructing or parsing
68//! the application's root CLI:
69//!
70//! ```
71//! use usage_rs as usage;
72//! # #[cfg(feature = "spec")]
73//! use usage::Args;
74//! #
75//! # #[cfg(not(feature = "spec"))]
76//! # fn main() {}
77//! # #[cfg(feature = "spec")]
78//! # fn main() {
79//!
80//! #[derive(Debug, Args)]
81//! struct Install {
82//!     #[usage(short, long)]
83//!     force: bool,
84//!     tools: Vec<String>,
85//! }
86//!
87//! let argv = ["--force", "node@24"].map(std::ffi::OsStr::new);
88//! let install = usage::parse_args_from::<Install>(&argv).unwrap();
89//! assert!(install.force);
90//! assert_eq!(install.tools, ["node@24"]);
91//! # }
92//! ```
93
94#![forbid(unsafe_code)]
95
96// Generated absolute paths must also work if a derive is used inside this crate. Integration
97// targets already receive this name through Cargo; the library target needs the self alias.
98extern crate self as usage_rs;
99
100pub use usage_argv as argv;
101pub use usage_argv::*;
102#[cfg(feature = "config")]
103pub use usage_config as config;
104#[cfg(feature = "config")]
105pub use usage_derive::Config;
106#[cfg(feature = "spec")]
107pub use usage_derive::{ArgGroup, Args, Cli, Subcommands, ValueEnum};
108#[cfg(feature = "test")]
109pub use usage_test as test;
110#[cfg(feature = "validation")]
111pub use usage_validation as validation;
112#[cfg(feature = "response-files")]
113pub mod response;
114
115#[cfg(all(test, feature = "spec"))]
116mod tests {
117    #[derive(crate::Cli)]
118    #[usage(bin = "internal")]
119    struct Internal {}
120
121    #[cfg(feature = "validation")]
122    #[derive(Debug, crate::Cli)]
123    #[usage(bin = "validated")]
124    struct Validated {
125        #[usage(
126            long,
127            validate = "int(value) >= 1 && int(value) <= 65535",
128            validate_error = "must be a valid port"
129        )]
130        port: Option<u16>,
131    }
132
133    #[cfg(feature = "validation")]
134    #[derive(Debug, crate::Args)]
135    struct ValidatedArgs {
136        #[usage(long, validate = "value == 'ok'", validate_error = "must be ok")]
137        token: Option<String>,
138    }
139
140    #[cfg(feature = "validation")]
141    #[derive(Debug, crate::Cli)]
142    #[usage(bin = "validated-args")]
143    struct ValidatedArgsCli {
144        #[usage(flatten)]
145        args: ValidatedArgs,
146    }
147
148    #[test]
149    fn derives_resolve_the_facade_from_inside_the_facade() {
150        assert_eq!(Internal::spec().bin, Some("internal"));
151    }
152
153    #[cfg(feature = "config")]
154    #[derive(crate::Config)]
155    struct InternalSettings {
156        /// How many jobs to run at once
157        #[usage(env = "INTERNAL_JOBS", default = 4)]
158        jobs: u64,
159    }
160
161    #[cfg(feature = "config")]
162    #[test]
163    fn the_config_derive_resolves_the_facade_from_inside_the_facade() {
164        let resolved = crate::config::resolve(
165            InternalSettings::SETTINGS_REGISTRY,
166            crate::config::Layers::new(),
167        )
168        .expect("resolves");
169        let settings = InternalSettings::read(&resolved).expect("reads");
170        assert_eq!(settings.jobs, 4);
171        assert!(InternalSettings::spec_kdl().contains(r#"prop "jobs""#));
172    }
173
174    #[cfg(feature = "validation")]
175    #[test]
176    fn derives_evaluate_portable_validation_expressions() {
177        let valid = [
178            ::std::ffi::OsStr::new("--port"),
179            ::std::ffi::OsStr::new("9229"),
180        ];
181        assert_eq!(Validated::parse_from(&valid).unwrap().port, Some(9229));
182
183        let invalid = [
184            ::std::ffi::OsStr::new("--port"),
185            ::std::ffi::OsStr::new("0"),
186        ];
187        let crate::Error::InvalidValue(error) = Validated::parse_from(&invalid).unwrap_err() else {
188            panic!("expected invalid value");
189        };
190        assert_eq!(error.reason, "must be a valid port");
191
192        let invalid_args = [
193            ::std::ffi::OsStr::new("--token"),
194            ::std::ffi::OsStr::new("bad"),
195        ];
196        let crate::Error::InvalidValue(error) =
197            ValidatedArgsCli::parse_from(&invalid_args).unwrap_err()
198        else {
199            panic!("expected invalid value from flattened Args");
200        };
201        assert_eq!(error.reason, "must be ok");
202
203        let valid_args = [
204            ::std::ffi::OsStr::new("--token"),
205            ::std::ffi::OsStr::new("ok"),
206        ];
207        assert_eq!(
208            ValidatedArgsCli::parse_from(&valid_args)
209                .unwrap()
210                .args
211                .token
212                .as_deref(),
213            Some("ok")
214        );
215
216        let kdl = Validated::to_kdl();
217        assert!(
218            kdl.contains(r#"validate="int(value) >= 1 && int(value) <= 65535""#),
219            "{kdl}"
220        );
221    }
222}