Skip to main content

usage_derive/
lib.rs

1//! A derive that compiles a CLI definition into parse tables and a spec.
2//!
3//! `#[derive(usage::Cli)]` reads a struct and emits three things: `static` parse
4//! tables for [usage-argv](https://docs.rs/usage-argv), `static` metadata for
5//! spec emission, and a parse function that assigns values straight into the
6//! struct's fields. Nothing is constructed at run time — there is no command tree
7//! to build before a parse can start — and a successful parse touches only the
8//! first of the three.
9//!
10//! Not compiled here, because this crate deliberately does not depend on
11//! usage-argv — see the note in its `Cargo.toml`. The same example runs as a test
12//! in `conformance/tests/derive.rs`, as `the_crate_level_example_from_the_docs`.
13//!
14//! ```ignore
15//! # use usage_derive::Cli;
16//! /// A tool that does things
17//! #[derive(Cli)]
18//! #[usage(bin = "ex", version = "1.0")]
19//! struct Cli {
20//!     /// How many jobs to run at once
21//!     #[usage(short = 'j', long, env = "EX_JOBS", default = "4")]
22//!     jobs: Option<String>,
23//!
24//!     /// Print more
25//!     #[usage(short = 'v', long, count)]
26//!     verbose: u8,
27//!
28//!     /// Colorize output
29//!     #[usage(long, negate = "--no-color", default = "true")]
30//!     color: bool,
31//!
32//!     /// Files to process
33//!     files: Vec<String>,
34//! }
35//!
36//! let argv = ["-j8", "--no-color", "a.txt"].map(std::ffi::OsStr::new);
37//! let cli = Cli::parse_from(&argv).unwrap();
38//! assert_eq!(cli.jobs.as_deref(), Some("8"));
39//! assert!(!cli.color);
40//! assert_eq!(cli.files, ["a.txt"]);
41//!
42//! // The same declaration is also the spec, which is what generates docs,
43//! // manpages, and completions.
44//! assert!(Cli::to_kdl().contains(r#"flag "-j --jobs""#));
45//! ```
46//!
47//! # Declaring
48//!
49//! A field with `long` or `short` is a flag; anything else is a positional
50//! argument. Help text comes from the doc comment: the first paragraph is the
51//! short form, and the whole comment is the long form.
52//!
53//! | option | meaning |
54//! | --- | --- |
55//! | `long`, `long = "x"` | a long form, defaulting to the field name |
56//! | `short`, `short = 'x'` | a short form, defaulting to the field's first letter |
57//! | `name = "x"` | the name used in the spec and in help output |
58//! | `negate = "--no-x"` | a second long form that sets a `bool` false |
59//! | `count` | count occurrences instead of collecting values |
60//! | `var` | the flag may be repeated, taking one value each time |
61//! | `variadic` | one occurrence keeps taking values, until a flag-like token or `--` |
62//! | `global` | subcommands inherit the flag |
63//! | `env = "X"` | an environment variable that can supply the value |
64//! | `default = "x"` | the value when the command line does not supply one |
65//! | `help_heading = "x"` | the section to list this under in help output |
66//! | `hide` | keep it out of help and completions |
67//! | `double_dash = "required"` | a positional only fillable after `--` |
68//! | `arg` | force a field to be positional |
69//!
70//! # What this version does not do
71//!
72//! Published early on purpose, so it can be used and argued with — but these are
73//! real limits, not omissions from the docs.
74//!
75//! - **Subcommands.** One command per struct for now.
76//! - **Typed values.** Fields are `bool`, `String`, `Option<String>`,
77//!   `Vec<String>`, or an unsigned integer with `count`. Anything else is a
78//!   compile error rather than a surprise, because converting a value is also
79//!   where required-ness, `choices`, and bounds get enforced, and that layer does
80//!   not exist yet.
81//! - **Enforce what it records.** `default` and `env` are written into the spec
82//!   and `default` is applied, but `env` is not read, and a missing required value
83//!   is not reported. Same reason.
84
85use proc_macro::TokenStream;
86use syn::{parse_macro_input, DeriveInput};
87
88mod codegen;
89mod model;
90
91/// Compile a struct into a parser and a spec. See the [crate docs](crate).
92#[proc_macro_derive(Cli, attributes(usage))]
93pub fn derive_cli(input: TokenStream) -> TokenStream {
94    let input = parse_macro_input!(input as DeriveInput);
95    match model::Cli::from_input(&input) {
96        Ok(cli) => codegen::emit(&cli).into(),
97        // Reporting the error as tokens rather than panicking is what puts it on
98        // the offending line instead of on the derive.
99        Err(e) => e.to_compile_error().into(),
100    }
101}