From ae19f7bbca20bfd71ada8fa13860e1579283e365 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:05:40 +0000 Subject: [PATCH 01/21] fix(argv): show choices when a subcommand is required --- argv/src/diagnostic.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/argv/src/diagnostic.rs b/argv/src/diagnostic.rs index 8fb024c1a..a6709fd2a 100644 --- a/argv/src/diagnostic.rs +++ b/argv/src/diagnostic.rs @@ -508,6 +508,13 @@ pub fn render( ); } Error::MissingSubcommand => { + // A bare command that can do nothing on its own is a request for orientation. + // clap prints the command's help page here, including the available subcommands, + // while keeping exit 2; an error plus only `` tells the reader what is + // missing and withholds the list they need to fix it. + if let Some(help) = crate::help::render_at(spec, &taken, false) { + return help; + } with_usage = true; let _ = writeln!( out, @@ -811,6 +818,15 @@ mod tests { assert_eq!(line, crate::help::usage_line(&["ex", "use"], &USE_META)); } + #[test] + fn a_missing_subcommand_prints_the_choices() { + let message = rendered(&[], Error::MissingSubcommand); + assert!(message.contains("Commands:"), "{message}"); + assert!(message.contains("use"), "{message}"); + assert!(message.contains("user"), "{message}"); + assert!(!message.contains("requires a subcommand"), "{message}"); + } + #[test] fn a_missing_value_names_what_it_wanted() { // No usage block: the shape of the command line was right, one value was missing — which From 7ec6ca26b2e19b9696601f294ed8513f444709a6 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:17:42 +0000 Subject: [PATCH 02/21] feat(derive): preserve verbatim doc comments --- conformance/tests/metadata.rs | 66 +++++++++++++++++++++++++++++++++++ derive/src/lib.rs | 2 ++ derive/src/model.rs | 59 ++++++++++++++++++++++++------- 3 files changed, 114 insertions(+), 13 deletions(-) diff --git a/conformance/tests/metadata.rs b/conformance/tests/metadata.rs index 80258ef36..77f9fbca9 100644 --- a/conformance/tests/metadata.rs +++ b/conformance/tests/metadata.rs @@ -212,6 +212,72 @@ struct Verbatim { command: Option, } +/// First root line +/// second root line +/// +/// root example +#[derive(Cli)] +#[usage(bin = "verbatim-comments", verbatim_doc_comment)] +struct VerbatimComments { + /// First field line + /// second field line + /// + /// field example + #[usage(long, verbatim_doc_comment)] + layout: bool, + #[usage(subcommand)] + command: Option, +} + +#[derive(Args)] +struct Paint {} + +#[derive(Subcommands)] +enum VerbatimCommands { + /// First command line + /// second command line + #[usage(verbatim_doc_comment)] + Paint(Paint), +} + +#[test] +fn doc_comments_can_preserve_their_layout() { + let spec: LibSpec = VerbatimComments::to_kdl().parse().expect("valid spec"); + assert_eq!( + spec.about.as_deref(), + Some("First root line\nsecond root line") + ); + assert_eq!( + spec.about_long.as_deref(), + Some("First root line\nsecond root line\n\n root example") + ); + + let layout = spec.cmd.flags.iter().find(|f| f.name == "layout").unwrap(); + assert_eq!( + layout.help.as_deref(), + Some("First field line\nsecond field line") + ); + assert_eq!( + layout.help_long.as_deref(), + Some("First field line\nsecond field line\n\n field example") + ); + + let paint = spec.cmd.subcommands.get("paint").expect("paint"); + assert_eq!( + paint.help.as_deref(), + Some("First command line\nsecond command line") + ); + assert!(paint.help_long.is_none()); + + let argv = [ + std::ffi::OsStr::new("--layout"), + std::ffi::OsStr::new("paint"), + ]; + let parsed = VerbatimComments::parse_from(&argv).expect("the metadata still parses"); + assert!(parsed.layout); + assert!(matches!(parsed.command, Some(VerbatimCommands::Paint(_)))); +} + #[test] fn help_text_can_keep_line_breaks_a_comment_would_flow() { // A doc comment's first paragraph is read the way Rust reads one, so a line break inside diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 6f237419b..8e7ab3e11 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -179,6 +179,7 @@ //! here the declaration simply wins and the other spelling still answers. //! //! On the struct itself: `bin`, `version`, `about`, `long_about`, `before_help`, `after_help`, +//! `verbatim_doc_comment` — preserve doc-comment line breaks and whitespace — //! `default_subcommand`, `min_usage_version` — the oldest `usage` that can read the emitted //! spec, declared rather than worked out — `effect` — what running this command does to the world, on an `Args` //! rather than on the root, which does nothing itself — `completion`, which adds the hidden command a generated shell @@ -201,6 +202,7 @@ //! | `env = "X"` | an environment variable that can supply the value | //! | `default = "x"` | the value when the command line does not supply one; a `Vec` may be given several, and starts out holding all of them | //! | `help_heading = "x"` | the section to list this under in help output | +//! | `verbatim_doc_comment` | preserve line breaks and whitespace in the doc comment instead of flowing its first paragraph | //! | `hide` | keep it out of help and completions | //! | `effect = "write"` | what supplying this flag does to the world: `read`, `write` or `destructive`. Also goes on an `Args`, where it says what *running* the command does | //! | `double_dash = "…"` | how a positional relates to `--`: `optional` (the default), `required` (fillable only after one), `preserve` (the `--` is a value), `automatic` (filling it ends flag parsing, so a wrapper forwards) | diff --git a/derive/src/model.rs b/derive/src/model.rs index a4395a747..60aca02f1 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -336,7 +336,7 @@ impl Cli { } let mut name_given = false; - let (about, long_about) = doc_comment(&input.attrs)?; + let mut verbatim_doc_comment = false; let mut cli = Cli { ident: input.ident.clone(), fingerprint: quote::ToTokens::to_token_stream(input).to_string(), @@ -354,8 +354,8 @@ impl Cli { .find(|a| a.path().is_ident("usage")) .map(|a| a.path().span()), version: None, - about, - long_about, + about: None, + long_about: None, unknown_flags: None, default_subcommand: None, about_attr: None, @@ -383,6 +383,7 @@ impl Cli { // decorative after it. "completion" => cli.completion = flag_value(&meta)?, "settings" => cli.settings = flag_value(&meta)?, + "verbatim_doc_comment" => verbatim_doc_comment = flag_value(&meta)?, "effect" => cli.effect = Some(effect_value(&meta)?), "alias" => cli.aliases.extend(selectors(&meta)?), "alias_hidden" => cli.hidden_aliases.extend(selectors(&meta)?), @@ -436,7 +437,7 @@ impl Cli { path, format!( "unknown option `{other}` on a struct; usage::Cli takes \ - `name`, `bin`, `version`, `unknown_flags`, \ + `name`, `bin`, `version`, `verbatim_doc_comment`, `unknown_flags`, \ `default_subcommand`, `restart_token`, and `mount` here, \ and the description comes from the doc comment" ), @@ -446,6 +447,8 @@ impl Cli { } } + (cli.about, cli.long_about) = doc_comment(&input.attrs, verbatim_doc_comment)?; + // Declared descriptions win over the comment, which is the point of declaring them. if let Some(about) = cli.about_attr.take() { cli.about = Some(about); @@ -1010,8 +1013,6 @@ impl Field { .clone() .expect("named fields were checked by the caller"); let span = field.span(); - let (help, long_help) = doc_comment(&field.attrs)?; - // A subcommand field is neither a flag nor an argument, and shares none of // their options, so it is recognized before any of them are read. if let Some(subcommand) = Self::subcommand(field, &ident, span)? { @@ -1042,6 +1043,7 @@ impl Field { let mut required_collection = false; let mut help_attr: Option = None; let mut long_help_attr: Option = None; + let mut verbatim_doc_comment = false; let mut hide = false; let mut is_arg = false; let mut choices: Vec = Vec::new(); @@ -1153,6 +1155,7 @@ impl Field { // help whose breaks are meant literally has to be given directly. "help" => help_attr = Some(string_value(&meta)?), "long_help" => long_help_attr = Some(string_value(&meta)?), + "verbatim_doc_comment" => verbatim_doc_comment = flag_value(&meta)?, "required" => required_collection = flag_value(&meta)?, "double_dash" => { let mode = string_value(&meta)?; @@ -1183,6 +1186,7 @@ impl Field { `var_min`, `var_max`, `value_enum`, `overrides`, \ `conflicts`, `requires`, `required_if`, \ `required_unless`, `help_heading`, `value_name`, \ + `verbatim_doc_comment`, \ `required`, and `double_dash`" ), )); @@ -1191,6 +1195,8 @@ impl Field { } } + let (help, long_help) = doc_comment(&field.attrs, verbatim_doc_comment)?; + // A bare `long` or `short` written before `name` would have captured the // field name rather than the renamed one, so resolve both once everything // has been read. Counted rather than rewritten, so a field carrying both a @@ -1971,10 +1977,13 @@ fn flag_value(meta: &Meta) -> syn::Result { /// Split a doc comment into the short help and the long help. /// -/// The first paragraph is the short form, matching what every Rust CLI framework -/// does and what an author expects from writing one; the whole comment is the long -/// form, and is only reported when it says more than the short one. -fn doc_comment(attrs: &[Attribute]) -> syn::Result<(Option, Option)> { +/// The first paragraph is the short form; the whole comment is the long form and is only +/// reported when it says more than the short one. Prose is flowed by default, while +/// `verbatim` keeps line breaks and whitespace for tables, examples, and ASCII art. +fn doc_comment( + attrs: &[Attribute], + verbatim: bool, +) -> syn::Result<(Option, Option)> { let mut lines: Vec = Vec::new(); for attr in attrs.iter().filter(|a| a.path().is_ident("doc")) { if let Meta::NameValue(nv) = &attr.meta { @@ -1987,14 +1996,35 @@ fn doc_comment(attrs: &[Attribute]) -> syn::Result<(Option, Option syn::Result { - let (help, long_help) = doc_comment(&variant.attrs)?; + let mut verbatim_doc_comment = false; // `unraw` first: `r#type` is how a variant named after a keyword prints, and a command // called `r#type` is one no user could type. `type` is what they meant. let mut name = to_kebab(&variant.ident.unraw().to_string()); @@ -2247,12 +2277,14 @@ impl Variant { // breaks matter is declared instead. "help" => help_attr = Some(string_value(&meta)?), "long_help" => long_help_attr = Some(string_value(&meta)?), + "verbatim_doc_comment" => verbatim_doc_comment = flag_value(&meta)?, other => { return Err(syn::Error::new_spanned( path, format!( "unknown option `{other}` on a variant; a subcommand \ - variant takes `name`, `alias` and `alias_hidden` here, \ + variant takes `name`, `alias`, `alias_hidden` and \ + `verbatim_doc_comment` here, \ and its description comes from the doc comment" ), )); @@ -2260,6 +2292,7 @@ impl Variant { } } } + let (help, long_help) = doc_comment(&variant.attrs, verbatim_doc_comment)?; for alias in aliases.iter().chain(&hidden_aliases) { if alias.is_empty() { return Err(syn::Error::new_spanned( From 522909b555d11dc1e63ed24f97c1c690921fe3ba Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:33:07 +0000 Subject: [PATCH 03/21] fix(derive): preserve ordinary doc indentation --- conformance/tests/metadata.rs | 18 ++++++++++++++++++ derive/src/model.rs | 19 +++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/conformance/tests/metadata.rs b/conformance/tests/metadata.rs index 77f9fbca9..127be0f99 100644 --- a/conformance/tests/metadata.rs +++ b/conformance/tests/metadata.rs @@ -212,6 +212,11 @@ struct Verbatim { command: Option, } +#[doc = " Ordinary first line\n indented continuation"] +#[derive(Cli)] +#[usage(bin = "ordinary-comments")] +struct OrdinaryComments {} + /// First root line /// second root line /// @@ -278,6 +283,19 @@ fn doc_comments_can_preserve_their_layout() { assert!(matches!(parsed.command, Some(VerbatimCommands::Paint(_)))); } +#[test] +fn ordinary_multiline_doc_attributes_keep_their_indentation() { + let spec: LibSpec = OrdinaryComments::to_kdl().parse().expect("valid spec"); + assert_eq!( + spec.about.as_deref(), + Some("Ordinary first line indented continuation") + ); + assert_eq!( + spec.about_long.as_deref(), + Some("Ordinary first line\n indented continuation") + ); +} + #[test] fn help_text_can_keep_line_breaks_a_comment_would_flow() { // A doc comment's first paragraph is read the way Rust reads one, so a line break inside diff --git a/derive/src/model.rs b/derive/src/model.rs index 60aca02f1..81a743c33 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -1996,14 +1996,17 @@ fn doc_comment( // mise's help is full of them, since an indented block is how a spec shows a // command to type. let raw = s.value(); - lines.extend(raw.split('\n').map(|line| { - let line = line.strip_prefix(' ').unwrap_or(line); - if verbatim { - line.to_string() - } else { - line.trim_end().to_string() - } - })); + if verbatim { + lines.extend( + raw.split('\n') + .map(|line| line.strip_prefix(' ').unwrap_or(line).to_string()), + ); + } else { + // Preserve the pre-verbatim behaviour for an explicitly written, + // multiline `#[doc = "..."]`: only `///` contributes one leading + // space per attribute. A newline inside one attribute does not. + lines.push(raw.strip_prefix(' ').unwrap_or(&raw).trim_end().to_string()); + } } } } From 641c80c6efb6933c09cf23e83a1205f933263e04 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 04:34:29 +0000 Subject: [PATCH 04/21] fix(derive): preserve verbatim continuation indentation --- conformance/tests/metadata.rs | 17 +++++++++++++++++ derive/src/model.rs | 9 +++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/conformance/tests/metadata.rs b/conformance/tests/metadata.rs index 127be0f99..62d124c99 100644 --- a/conformance/tests/metadata.rs +++ b/conformance/tests/metadata.rs @@ -217,6 +217,11 @@ struct Verbatim { #[usage(bin = "ordinary-comments")] struct OrdinaryComments {} +#[doc = " Verbatim first line\n indented continuation"] +#[derive(Cli)] +#[usage(bin = "verbatim-attribute-comments", verbatim_doc_comment)] +struct VerbatimAttributeComments {} + /// First root line /// second root line /// @@ -296,6 +301,18 @@ fn ordinary_multiline_doc_attributes_keep_their_indentation() { ); } +#[test] +fn verbatim_multiline_doc_attributes_keep_their_indentation() { + let spec: LibSpec = VerbatimAttributeComments::to_kdl() + .parse() + .expect("valid spec"); + assert_eq!( + spec.about.as_deref(), + Some("Verbatim first line\n indented continuation") + ); + assert!(spec.about_long.is_none()); +} + #[test] fn help_text_can_keep_line_breaks_a_comment_would_flow() { // A doc comment's first paragraph is read the way Rust reads one, so a line break inside diff --git a/derive/src/model.rs b/derive/src/model.rs index 81a743c33..91cb47eec 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -1997,10 +1997,11 @@ fn doc_comment( // command to type. let raw = s.value(); if verbatim { - lines.extend( - raw.split('\n') - .map(|line| line.strip_prefix(' ').unwrap_or(line).to_string()), - ); + let mut raw_lines = raw.split('\n'); + if let Some(first) = raw_lines.next() { + lines.push(first.strip_prefix(' ').unwrap_or(first).to_string()); + } + lines.extend(raw_lines.map(str::to_string)); } else { // Preserve the pre-verbatim behaviour for an explicitly written, // multiline `#[doc = "..."]`: only `///` contributes one leading From 4df0208bfd0f7f23dfd7b8492fb67b2c05cd7790 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:24:39 +0000 Subject: [PATCH 05/21] feat(derive): support path value hints --- argv/src/complete.rs | 15 +++++++--- argv/src/spec.rs | 37 +++++++++++++++++++++++ conformance/tests/completion.rs | 43 ++++++++++++++++++++++++++ derive/src/codegen.rs | 4 +++ derive/src/lib.rs | 1 + derive/src/model.rs | 53 ++++++++++++++++++++++++++++++++- 6 files changed, 148 insertions(+), 5 deletions(-) diff --git a/argv/src/complete.rs b/argv/src/complete.rs index 3a17ee7a3..b7f6f4725 100644 --- a/argv/src/complete.rs +++ b/argv/src/complete.rs @@ -428,19 +428,26 @@ pub fn complete<'a>(spec: &'a Spec<'a>, split: &Split) -> Completions<'a> { // The name a value here would have, which is what says whether paths belong, and whether // that value declares its own set. - let (named, declares_choices) = if let Some(flag) = position.awaiting_value { + let (named, declares_choices, complete_type) = if let Some(flag) = position.awaiting_value { let meta = flag_meta(spec.root, flag); ( meta.and_then(|m| m.value_name).or(Some(flag.name)), meta.is_some_and(|m| !m.choices.is_empty()), + meta.and_then(|m| m.complete_type), ) } else if let Some(arg) = at_cursor { let meta = arg_meta(spec.root, arg); - (Some(arg.name), meta.is_some_and(|m| !m.choices.is_empty())) + ( + Some(arg.name), + meta.is_some_and(|m| !m.choices.is_empty()), + meta.and_then(|m| m.complete_type), + ) } else { - (None, false) + (None, false, None) }; - let asked_for = named.and_then(files_for); + let asked_for = complete_type + .and_then(files_for) + .or_else(|| named.and_then(files_for)); // An argument that requires a separator is not fillable yet, so nothing else belongs here — // not even a path, which the parser would reject exactly as it rejects a value. diff --git a/argv/src/spec.rs b/argv/src/spec.rs index 565eef04d..4335b5ac0 100644 --- a/argv/src/spec.rs +++ b/argv/src/spec.rs @@ -485,6 +485,8 @@ pub struct FlagMeta<'a> { /// that asks *this binary*, so a spec stays complete for every other consumer while the /// binary answers itself. pub complete: Option, + /// A built-in completion class such as `path` or `dir`. + pub complete_type: Option<&'a str>, /// Whether the flag may be given more than once. Distinct from /// [`Flag::variadic`], which is one occurrence taking several values. pub repeatable: bool, @@ -518,6 +520,7 @@ impl FlagMeta<'_> { /// Metadata for a flag with nothing declared, for struct update syntax. pub const EMPTY: FlagMeta<'static> = FlagMeta { complete: None, + complete_type: None, flag: &Flag::BOOL, help: None, long_help: None, @@ -561,12 +564,15 @@ pub struct ArgMeta<'a> { pub help_heading: Option<&'a str>, /// What answers for this argument when a shell asks. See [`FlagMeta::complete`]. pub complete: Option, + /// A built-in completion class such as `path` or `dir`. + pub complete_type: Option<&'a str>, } impl ArgMeta<'_> { /// Metadata for an argument with nothing declared, for struct update syntax. pub const EMPTY: ArgMeta<'static> = ArgMeta { complete: None, + complete_type: None, arg: &Arg::REQUIRED, help: None, long_help: None, @@ -785,6 +791,7 @@ fn write_body( ); write_arg(out, arg, depth)?; } + write_completion_types(out, meta, depth)?; #[cfg(feature = "complete")] write_completers(out, meta, bin, depth)?; for sub in meta.subcommands { @@ -793,6 +800,36 @@ fn write_body( Ok(()) } +/// Built-in completion types declared by this command, written in the spec's vocabulary. +fn write_completion_types( + out: &mut String, + meta: &CommandMeta<'_>, + depth: usize, +) -> core::fmt::Result { + for arg in meta.args { + if let Some(type_) = arg.complete_type { + indent(out, depth)?; + writeln!( + out, + "complete {} type={}", + quoted(&arg.arg.name.to_ascii_lowercase()), + quoted(type_) + )?; + } + } + for flag in meta.flags { + if let Some(type_) = flag.complete_type { + let name = flag + .value_name + .unwrap_or(flag.flag.name) + .to_ascii_lowercase(); + indent(out, depth)?; + writeln!(out, "complete {} type={}", quoted(&name), quoted(type_))?; + } + } + Ok(()) +} + fn write_command( out: &mut String, meta: &CommandMeta<'_>, diff --git a/conformance/tests/completion.rs b/conformance/tests/completion.rs index 988eaf086..c811f920e 100644 --- a/conformance/tests/completion.rs +++ b/conformance/tests/completion.rs @@ -59,6 +59,49 @@ fn ask(shell: &str, line: &str) -> String { Ex::completion_request(&argv).expect("this is a completion request") } +#[derive(Cli)] +#[usage(bin = "hinted", completion)] +struct Hinted { + /// A file to read + #[usage(long, value_hint = clap::ValueHint::FilePath)] + file: Option, + /// A directory to write + #[usage(long, value_hint = clap::ValueHint::DirPath)] + dir: Option, +} + +fn ask_hinted(line: &str) -> String { + let argv: Vec = ["__complete_word__", "--shell", "bash", "--line", line] + .iter() + .map(OsString::from) + .collect(); + Hinted::completion_request(&argv).expect("this is a completion request") +} + +#[test] +fn clap_path_value_hints_reach_native_and_emitted_completions() { + assert_eq!( + ask_hinted("hinted --file "), + format!("{}\n", usage_argv::complete::FILES_MARKER) + ); + assert_eq!( + ask_hinted("hinted --dir "), + format!("{}\n", usage_argv::complete::DIRS_MARKER) + ); + + let kdl = Hinted::to_kdl(); + assert!(kdl.contains("complete \"file\" type=\"path\""), "{kdl}"); + assert!(kdl.contains("complete \"dir\" type=\"dir\""), "{kdl}"); + + let argv = [OsStr::new("--file"), OsStr::new("input.kdl")]; + let parsed = Hinted::parse_from(&argv).expect("the hinted flag still parses"); + assert_eq!( + parsed.file.as_deref(), + Some(std::path::Path::new("input.kdl")) + ); + assert!(parsed.dir.is_none()); +} + #[test] fn a_request_is_answered_from_the_same_tables_the_parse_uses() { assert_eq!(ask("bash", "ex "), "install\nrm\nrun\nuninstall\n"); diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index d72f07511..374af1784 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -802,6 +802,7 @@ fn flag_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { let env = option_str(field.env.as_deref()); let help_heading = option_str(field.help_heading.as_deref()); let value_name = option_str(field.value_name.as_deref()); + let complete_type = option_str(field.complete_type.as_deref()); let defaults = &field.default; let default = quote!(&[#(#defaults),*]); let hide = field.hide; @@ -836,6 +837,7 @@ fn flag_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { pub static #name: ::usage_argv::spec::FlagMeta = ::usage_argv::spec::FlagMeta { effect: #effect, complete: #completer, + complete_type: #complete_type, flag: &#table, help: #help, long_help: #long_help, @@ -867,6 +869,7 @@ fn arg_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { let long_help = option_str(field.long_help.as_deref()); let env = option_str(field.env.as_deref()); let help_heading = option_str(field.help_heading.as_deref()); + let complete_type = option_str(field.complete_type.as_deref()); let defaults = &field.default; let default = quote!(&[#(#defaults),*]); let hide = field.hide; @@ -882,6 +885,7 @@ fn arg_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { #completer_decl pub static #name: ::usage_argv::spec::ArgMeta = ::usage_argv::spec::ArgMeta { complete: #completer, + complete_type: #complete_type, arg: &#table, help: #help, long_help: #long_help, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 8e7ab3e11..b9f8fa532 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -208,6 +208,7 @@ //! | `double_dash = "…"` | how a positional relates to `--`: `optional` (the default), `required` (fillable only after one), `preserve` (the `--` is a value), `automatic` (filling it ends flag parsing, so a wrapper forwards) | //! | `complete = my_fn` | a function that answers for this value when a shell asks | //! | `value_enum` | the words come from the field's type, which derives [`ValueEnum`] | +//! | `value_hint = clap::ValueHint::FilePath` | ask the shell for paths; `AnyPath` and `DirPath` are also supported without retaining a clap dependency | //! | `arg` | force a field to be positional | //! | `overrides = "--other"` | a flag this one displaces, the last given winning | //! | `conflicts = "--other"` | a flag this one cannot be given with | diff --git a/derive/src/model.rs b/derive/src/model.rs index 91cb47eec..f3f39e351 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -163,6 +163,8 @@ pub struct Field { /// The counterpart of a spec's `run=`, and the source it is generated from: declaring the /// function is the only place a completer is said to exist. pub complete: Option, + /// A built-in completion class, in the spec's vocabulary (`path` or `dir`). + pub complete_type: Option, pub var_min: Option, pub var_max: Option, /// Flags this one displaces. Applied while parsing rather than after it: the @@ -243,6 +245,38 @@ fn effect_value(meta: &Meta) -> syn::Result { ))) } +/// Clap's path-oriented `ValueHint`s lowered into the completion types the usage spec has. +/// +/// The path is consumed syntactically, so accepting `clap::ValueHint::FilePath` does not keep +/// clap as a dependency after a declaration migrates to `#[usage(...)]`. +fn value_hint(meta: &Meta) -> syn::Result { + let value = &meta.require_name_value()?.value; + let Expr::Path(path) = value else { + return Err(syn::Error::new_spanned( + value, + "`value_hint` takes a ValueHint variant, as in \ + `value_hint = clap::ValueHint::FilePath`", + )); + }; + let Some(variant) = path.path.segments.last() else { + return Err(syn::Error::new_spanned( + value, + "`value_hint` needs a variant", + )); + }; + match variant.ident.to_string().as_str() { + "FilePath" | "AnyPath" => Ok("path".to_string()), + "DirPath" => Ok("dir".to_string()), + other => Err(syn::Error::new_spanned( + value, + format!( + "`ValueHint::{other}` has no usage completion type yet; supported hints are \ + `FilePath`, `AnyPath`, and `DirPath`" + ), + )), + } +} + /// Whether a field is a flag or a positional, and how it is addressed. pub enum Kind { Flag { @@ -887,6 +921,7 @@ impl Field { // place rather than a command. effect: None, complete: None, + complete_type: None, // A flattened field holds declarations, not a value, so none of what describes a // value applies — the same as a subcommand field. shape: Shape::Bool, @@ -979,6 +1014,7 @@ impl Field { kind: Kind::Subcommand { ty, optional }, effect: None, complete: None, + complete_type: None, // A subcommand field holds a command, not a value, so none of what // describes a value applies to it. shape: Shape::Bool, @@ -1048,6 +1084,7 @@ impl Field { let mut is_arg = false; let mut choices: Vec = Vec::new(); let mut complete: Option = None; + let mut complete_type: Option = None; let mut value_enum = false; let mut var_min: Option = None; let mut var_max: Option = None; @@ -1114,6 +1151,7 @@ impl Field { }; complete = Some(path.path.clone()); } + "value_hint" => complete_type = Some(value_hint(&meta)?), "choices" => { let Meta::List(list) = &meta else { return Err(syn::Error::new_spanned( @@ -1183,7 +1221,7 @@ impl Field { "unknown option `{other}`; a field takes `name`, `long`, \ `short`, `negate`, `global`, `var`, `variadic`, \ `count`, `hide`, `arg`, `env`, `default`, `choices`, \ - `var_min`, `var_max`, `value_enum`, `overrides`, \ + `var_min`, `var_max`, `value_enum`, `value_hint`, `overrides`, \ `conflicts`, `requires`, `required_if`, \ `required_unless`, `help_heading`, `value_name`, \ `verbatim_doc_comment`, \ @@ -1340,6 +1378,18 @@ impl Field { "a `bool` or counting field has no value to check against `choices`", )); } + if complete_type.is_some() && matches!(shape, Shape::Bool | Shape::Count) { + return Err(syn::Error::new( + span, + "`value_hint` describes a value to complete, and this field takes no value", + )); + } + if complete_type.is_some() && complete.is_some() { + return Err(syn::Error::new( + span, + "`value_hint` and `complete` both answer completion for this value; use one", + )); + } if let (Some(min), Some(max)) = (var_min, var_max) { if min > max { return Err(syn::Error::new( @@ -1689,6 +1739,7 @@ impl Field { required_collection, choices, complete, + complete_type, value_enum, var_min, var_max, From 26fa45574d488b369f7c32fdbc683d21b76cada6 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:39:11 +0000 Subject: [PATCH 06/21] refactor(derive): use usage value hints --- argv/src/lib.rs | 15 +++++++++++++++ conformance/tests/completion.rs | 6 +++--- derive/src/lib.rs | 2 +- derive/src/model.rs | 9 +++------ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/argv/src/lib.rs b/argv/src/lib.rs index 5bf1850d3..fd077e237 100644 --- a/argv/src/lib.rs +++ b/argv/src/lib.rs @@ -83,6 +83,21 @@ use std::ffi::{OsStr, OsString}; +/// A value's filesystem completion class for `#[usage(value_hint = ...)]`. +/// +/// This lives in the runtime crate so a declaration never needs clap merely to describe what +/// kind of path a shell should offer. It is metadata only and adds no work to a successful +/// parse. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ValueHint { + /// A path to a file. + FilePath, + /// A path to either a file or a directory. + AnyPath, + /// A path to a directory. + DirPath, +} + #[cfg(feature = "complete")] pub mod complete; #[cfg(feature = "diagnostics")] diff --git a/conformance/tests/completion.rs b/conformance/tests/completion.rs index c811f920e..ea18b0d1b 100644 --- a/conformance/tests/completion.rs +++ b/conformance/tests/completion.rs @@ -63,10 +63,10 @@ fn ask(shell: &str, line: &str) -> String { #[usage(bin = "hinted", completion)] struct Hinted { /// A file to read - #[usage(long, value_hint = clap::ValueHint::FilePath)] + #[usage(long, value_hint = usage_argv::ValueHint::FilePath)] file: Option, /// A directory to write - #[usage(long, value_hint = clap::ValueHint::DirPath)] + #[usage(long, value_hint = usage_argv::ValueHint::DirPath)] dir: Option, } @@ -79,7 +79,7 @@ fn ask_hinted(line: &str) -> String { } #[test] -fn clap_path_value_hints_reach_native_and_emitted_completions() { +fn usage_path_value_hints_reach_native_and_emitted_completions() { assert_eq!( ask_hinted("hinted --file "), format!("{}\n", usage_argv::complete::FILES_MARKER) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b9f8fa532..b6d99cb7b 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -208,7 +208,7 @@ //! | `double_dash = "…"` | how a positional relates to `--`: `optional` (the default), `required` (fillable only after one), `preserve` (the `--` is a value), `automatic` (filling it ends flag parsing, so a wrapper forwards) | //! | `complete = my_fn` | a function that answers for this value when a shell asks | //! | `value_enum` | the words come from the field's type, which derives [`ValueEnum`] | -//! | `value_hint = clap::ValueHint::FilePath` | ask the shell for paths; `AnyPath` and `DirPath` are also supported without retaining a clap dependency | +//! | `value_hint = usage_argv::ValueHint::FilePath` | ask the shell for paths; `AnyPath` and `DirPath` are also supported | //! | `arg` | force a field to be positional | //! | `overrides = "--other"` | a flag this one displaces, the last given winning | //! | `conflicts = "--other"` | a flag this one cannot be given with | diff --git a/derive/src/model.rs b/derive/src/model.rs index f3f39e351..8bbe8673a 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -245,17 +245,14 @@ fn effect_value(meta: &Meta) -> syn::Result { ))) } -/// Clap's path-oriented `ValueHint`s lowered into the completion types the usage spec has. -/// -/// The path is consumed syntactically, so accepting `clap::ValueHint::FilePath` does not keep -/// clap as a dependency after a declaration migrates to `#[usage(...)]`. +/// usage's path-oriented `ValueHint`s lowered into the completion types the spec has. fn value_hint(meta: &Meta) -> syn::Result { let value = &meta.require_name_value()?.value; let Expr::Path(path) = value else { return Err(syn::Error::new_spanned( value, - "`value_hint` takes a ValueHint variant, as in \ - `value_hint = clap::ValueHint::FilePath`", + "`value_hint` takes a usage ValueHint variant, as in \ + `value_hint = usage_argv::ValueHint::FilePath`", )); }; let Some(variant) = path.path.segments.last() else { From 567d41aa9eec567a483b679232fba1565b03dccc Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:53:41 +0000 Subject: [PATCH 07/21] feat(lib): add usage-rs facade --- Cargo.lock | 44 ++++- Cargo.toml | 2 + derive/Cargo.toml | 1 + derive/src/codegen.rs | 418 +++++++++++++++++++++------------------ derive/src/lib.rs | 2 +- derive/src/model.rs | 4 +- usage-rs/Cargo.toml | 28 +++ usage-rs/src/lib.rs | 32 +++ usage-rs/tests/facade.rs | 38 ++++ 9 files changed, 376 insertions(+), 193 deletions(-) create mode 100644 usage-rs/Cargo.toml create mode 100644 usage-rs/src/lib.rs create mode 100644 usage-rs/tests/facade.rs diff --git a/Cargo.lock b/Cargo.lock index c05992993..e9f3b17f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,6 +1321,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1965,7 +1974,7 @@ dependencies = [ "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", @@ -1980,6 +1989,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + [[package]] name = "toml_parser" version = "1.1.3+spec-1.1.0" @@ -2119,6 +2149,7 @@ dependencies = [ name = "usage-derive" version = "5.1.0" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.3", @@ -2152,6 +2183,14 @@ dependencies = [ "xx", ] +[[package]] +name = "usage-rs" +version = "5.1.0" +dependencies = [ + "usage-argv", + "usage-derive", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -2535,6 +2574,9 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "xtask" diff --git a/Cargo.toml b/Cargo.toml index e82693b74..1dc771e7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "config", "config-build", "derive", + "usage-rs", "clap_usage", "cli", "conformance", @@ -33,6 +34,7 @@ usage-argv = { path = "./argv", version = "5.1.0" } usage-config = { path = "./config", version = "5.1.0" } usage-derive = { path = "./derive", version = "5.1.0" } usage-lib = { path = "./lib", version = "5.1.0", features = ["clap"] } +usage-rs = { path = "./usage-rs", version = "5.1.0" } [workspace.metadata.release] allow-branch = ["main"] diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 910f37f46..cad5f848f 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -19,6 +19,7 @@ release = true [dependencies] proc-macro2 = "1" +proc-macro-crate = "3" quote = "1" # syn 2 rather than 3, which is already in the tree via clap_derive: nothing here # needs the newer API, and matching what is there avoids a second copy. diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index 374af1784..b3020763d 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -13,12 +13,38 @@ //! do not collide with anything, and so `cargo expand` shows them together. use proc_macro2::TokenStream; +use proc_macro_crate::{crate_name, FoundCrate}; use quote::{format_ident, quote}; use crate::model::{rendered_path, Cli, DoubleDash, Field, Kind, Shape, Subcommands, ValueEnum}; +/// The runtime as the adopter depended on it. +/// +/// A direct `usage-argv` dependency remains supported for the low-level crates and existing +/// users. The `usage-rs` facade re-exports that runtime as `usage::argv`, which lets an +/// application keep derives, tables, and their versions behind one dependency. +fn runtime_path() -> TokenStream { + match crate_name("usage-rs") { + Ok(FoundCrate::Itself) => quote!(::usage_rs::argv), + Ok(FoundCrate::Name(name)) => { + let facade = format_ident!("{}", name.replace('-', "_")); + quote!(::#facade::argv) + } + Err(_) => match crate_name("usage-argv") { + Ok(FoundCrate::Name(name)) => { + let runtime = format_ident!("{}", name.replace('-', "_")); + quote!(::#runtime) + } + // Deriving inside an integration target of `usage-argv`, or preserving the old + // useful compiler error when neither dependency was declared. + _ => quote!(::usage_argv), + }, + } +} + pub fn emit(cli: &Cli) -> TokenStream { let ident = &cli.ident; + let runtime = runtime_path(); let flags: Vec<&Field> = cli .fields @@ -151,7 +177,7 @@ pub fn emit(cli: &Cli) -> TokenStream { argv: &'v [&'v ::std::ffi::OsStr], ) -> ::std::result::Result< (Self, ::usage_config::CliLayer), - ::usage_argv::Error<'static, 'v>, + usage_argv::Error<'static, 'v>, > { // The layer from what argv left, and only then the rest: `check` fills a field // from its `env` and marks it given, and a variable's value contributed here @@ -198,13 +224,15 @@ pub fn emit(cli: &Cli) -> TokenStream { clippy::needless_update )] const _: () = { + use #runtime as usage_argv; + #flatten_checks #keys #(#flag_tables)* #(#arg_tables)* #table_decls - pub static ROOT: ::usage_argv::Command = ::usage_argv::Command { + pub static ROOT: usage_argv::Command = usage_argv::Command { // Only where a version was declared, which is when clap adds the flag: a // `--version` that answers with nothing is worse than one that is not there. version: #has_version, @@ -215,14 +243,14 @@ pub fn emit(cli: &Cli) -> TokenStream { args: #arg_table_ref, #sub_commands #sub_default - ..::usage_argv::Command::EMPTY + ..usage_argv::Command::EMPTY }; #(#flag_metas)* #(#arg_metas)* #meta_table_decls - pub static ROOT_META: ::usage_argv::spec::CommandMeta = ::usage_argv::spec::CommandMeta { + pub static ROOT_META: usage_argv::spec::CommandMeta = usage_argv::spec::CommandMeta { cmd: &ROOT, about: #about, long_about: #long_about, @@ -236,7 +264,7 @@ pub fn emit(cli: &Cli) -> TokenStream { flags: #flag_meta_table_ref, args: #arg_meta_table_ref, #sub_metas - ..::usage_argv::spec::CommandMeta::EMPTY + ..usage_argv::spec::CommandMeta::EMPTY }; // Values arrive as the bytes that were on the command line. This version @@ -263,7 +291,7 @@ pub fn emit(cli: &Cli) -> TokenStream { /// nested command generate the same code here. pub fn check<'t, 'v>( partial: &mut Partial, - ) -> ::std::result::Result<(), ::usage_argv::Error<'t, 'v>> { + ) -> ::std::result::Result<(), usage_argv::Error<'t, 'v>> { // Read unconditionally: a command that declares nothing to check would // otherwise leave the parameter unused in the user's crate, where // nobody can silence it. @@ -281,12 +309,12 @@ pub fn emit(cli: &Cli) -> TokenStream { /// and marks them given, which would hand a variable's value to the layer that /// outranks every other and name it after a flag nobody typed. pub fn read_argv<'v>( - command: &'static ::usage_argv::Command<'static>, + command: &'static usage_argv::Command<'static>, argv: &'v [&'v ::std::ffi::OsStr], - ) -> ::std::result::Result> { + ) -> ::std::result::Result> { #defaults - let mut __usage_parser = ::usage_argv::Parser::new(command, argv); + let mut __usage_parser = usage_argv::Parser::new(command, argv); while let ::std::option::Option::Some(__usage_event) = __usage_parser.next_event() { @@ -294,20 +322,20 @@ pub fn emit(cli: &Cli) -> TokenStream { // Asked *before* the event is applied, and answered with the command in // scope: `mise config --help` is a question about `config`, and the parser // is what knows how deep the words reached. - if let ::usage_argv::Event::Flag { flag, .. } = &__usage_event { - if flag.key == ::usage_argv::HELP_LONG_KEY - || flag.key == ::usage_argv::HELP_SHORT_KEY + if let usage_argv::Event::Flag { flag, .. } = &__usage_event { + if flag.key == usage_argv::HELP_LONG_KEY + || flag.key == usage_argv::HELP_SHORT_KEY { - return ::std::result::Result::Err(::usage_argv::Error::Help { + return ::std::result::Result::Err(usage_argv::Error::Help { cmd: __usage_parser.command(), - long: flag.key == ::usage_argv::HELP_LONG_KEY, + long: flag.key == usage_argv::HELP_LONG_KEY, }); } // Same shape, and for the same reason: a question rather than a // failure, answered by whoever knows the version string. - if ::usage_argv::is_version_flag(flag) { + if usage_argv::is_version_flag(flag) { return ::std::result::Result::Err( - ::usage_argv::Error::Version, + usage_argv::Error::Version, ); } } @@ -327,9 +355,9 @@ pub fn emit(cli: &Cli) -> TokenStream { /// whether the flag was absent or negated — so the entry point that wants that reads /// the two halves apart instead. pub fn read<'v>( - command: &'static ::usage_argv::Command<'static>, + command: &'static usage_argv::Command<'static>, argv: &'v [&'v ::std::ffi::OsStr], - ) -> ::std::result::Result> { + ) -> ::std::result::Result> { let mut partial = read_argv(command, argv)?; check(&mut partial)?; ::std::result::Result::Ok(partial) @@ -340,7 +368,7 @@ pub fn emit(cli: &Cli) -> TokenStream { #settings_layer #settings_guard - pub static SPEC: ::usage_argv::spec::Spec = ::usage_argv::spec::Spec { + pub static SPEC: usage_argv::spec::Spec = usage_argv::spec::Spec { name: #name, bin: #bin, version: #version, @@ -356,12 +384,12 @@ pub fn emit(cli: &Cli) -> TokenStream { /// /// `static`, so reaching them costs nothing: there is no command tree /// to build before a parse can start. - pub fn command() -> &'static ::usage_argv::Command<'static> { + pub fn command() -> &'static usage_argv::Command<'static> { &ROOT } /// This CLI's spec, for emitting, documenting, or completing. - pub fn spec() -> &'static ::usage_argv::spec::Spec<'static> { + pub fn spec() -> &'static usage_argv::spec::Spec<'static> { &SPEC } @@ -377,7 +405,7 @@ pub fn emit(cli: &Cli) -> TokenStream { /// Parse a command line, excluding the program name. pub fn parse_from<'v>( argv: &'v [&'v ::std::ffi::OsStr], - ) -> ::std::result::Result> { + ) -> ::std::result::Result> { let partial = read(Self::command(), argv)?; ::std::result::Result::Ok(Self { #sub_build @@ -401,7 +429,7 @@ pub fn emit(cli: &Cli) -> TokenStream { match Self::parse_from(&__usage_argv) { ::std::result::Result::Ok(parsed) => parsed, // Not failures: someone asked a question, and the answer goes to stdout. - ::std::result::Result::Err(::usage_argv::Error::Version) => { + ::std::result::Result::Err(usage_argv::Error::Version) => { match (Self::spec().bin.unwrap_or(Self::spec().name), Self::spec().version) { (bin, ::std::option::Option::Some(version)) => { ::std::println!("{bin} {version}"); @@ -414,21 +442,21 @@ pub fn emit(cli: &Cli) -> TokenStream { } } } - ::std::result::Result::Err(::usage_argv::Error::Help { cmd, long }) => { + ::std::result::Result::Err(usage_argv::Error::Help { cmd, long }) => { // By the route the words took, not by the command's address: one // `Subcommands` type mounted under two parents is one address, and a // page found by searching for it carries the first mount's path and // globals. Falls back where the route cannot be rebuilt. - let __usage_page = match ::usage_argv::help::route_to( + let __usage_page = match usage_argv::help::route_to( Self::command(), &__usage_argv, cmd, ) { ::std::option::Option::Some(route) => { - ::usage_argv::help::render_at(Self::spec(), &route, long) + usage_argv::help::render_at(Self::spec(), &route, long) } ::std::option::Option::None => { - ::usage_argv::help::render(Self::spec(), cmd, long) + usage_argv::help::render(Self::spec(), cmd, long) } }; match __usage_page { @@ -443,7 +471,7 @@ pub fn emit(cli: &Cli) -> TokenStream { ::std::result::Result::Err(e) => { ::std::eprint!( "{}", - ::usage_argv::render_failure(Self::spec(), &__usage_argv, &e) + usage_argv::render_failure(Self::spec(), &__usage_argv, &e) ); // clap's, so a script that checks for it keeps working. ::std::process::exit(2); @@ -476,7 +504,7 @@ fn flatten_checks(cli: &Cli) -> TokenStream { }; Some(quote! { const _: () = ::core::assert!( - <#ty as ::usage_argv::spec::CommandArgs>::COMMAND.subcommands.is_empty(), + <#ty as usage_argv::spec::CommandArgs>::COMMAND.subcommands.is_empty(), "a flattened group cannot declare subcommands: flatten joins flags and \ arguments into the parent's tables and leaves subcommands behind, so the \ command would require one that no word could select. Declare the \ @@ -495,10 +523,10 @@ fn flatten_checks(cli: &Cli) -> TokenStream { fn unknown_flags_tokens(cli: &Cli) -> TokenStream { match cli.unknown_flags.as_deref() { Some("error") => quote!(::core::option::Option::Some( - ::usage_argv::UnknownFlags::Error + usage_argv::UnknownFlags::Error )), Some(_) => quote!(::core::option::Option::Some( - ::usage_argv::UnknownFlags::Value + usage_argv::UnknownFlags::Value )), None => quote!(::core::option::Option::None), } @@ -516,7 +544,7 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { let functions = quote! { // Says what is missing, where the alternative is `unresolved module complete` — which // names the symptom and not the attribute that asked for it. - ::usage_argv::__usage_needs_complete_feature!(); + usage_argv::__usage_needs_complete_feature!(); /// This CLI's completion script for `shell`, to be written to a file or sourced. /// @@ -524,10 +552,10 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { /// script that names a command the binary does not answer a compile error instead of a /// silence at the prompt. pub fn completion_script( - shell: ::usage_argv::complete::Shell, + shell: usage_argv::complete::Shell, ) -> ::std::string::String { let spec = Self::spec(); - ::usage_argv::script::script(spec.bin.unwrap_or(spec.name), shell) + usage_argv::script::script(spec.bin.unwrap_or(spec.name), shell) } /// The word a shell is completing, answered from this CLI's own tables. @@ -544,7 +572,7 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { } // Its own flags, read by hand: three of them, and reading them with the parser // would mean putting them in the tables this is deliberately outside of. - let mut shell = ::usage_argv::complete::Shell::Bash; + let mut shell = usage_argv::complete::Shell::Bash; let mut line = ::std::string::String::new(); let mut cursor = ::std::option::Option::None; let mut candidates_for: ::std::option::Option<::std::string::String> = @@ -555,7 +583,7 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { "--shell" => { if let ::std::option::Option::Some(name) = rest.next() { if let ::std::option::Option::Some(found) = - ::usage_argv::complete::Shell::from_name( + usage_argv::complete::Shell::from_name( &name.to_string_lossy(), ) { @@ -589,23 +617,23 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { // No cursor means the end of the line, which is where a shell puts it when it has // no way to say — nushell, whose completer only ever sees the words. let cursor = cursor.unwrap_or(line.len()); - let split = ::usage_argv::complete::split(&line, cursor, shell); + let split = usage_argv::complete::split(&line, cursor, shell); if let ::std::option::Option::Some(name) = candidates_for { // Walked here as well, because a `--candidates` request names a completer and // says nothing about where the cursor is — and the completer still wants the // words its own command was given. let position = - ::usage_argv::complete::walk(Self::spec().root.cmd, split.argv()); + usage_argv::complete::walk(Self::spec().root.cmd, split.argv()); let __usage_words = split.argv(); let __usage_path: ::std::vec::Vec<( - &::usage_argv::Command<'_>, + &usage_argv::Command<'_>, &[::std::string::String], )> = position .path .iter() .map(|(cmd, start)| (*cmd, __usage_words.get(*start..).unwrap_or(&[]))) .collect(); - let ctx = ::usage_argv::complete::CompleteCtx { + let ctx = usage_argv::complete::CompleteCtx { words: &split.words, cword: split.cword, prefix: &split.prefix, @@ -618,15 +646,15 @@ fn completion_fns(cli: &Cli) -> (TokenStream, TokenStream) { // against a newer version of this CLI is a stale script, and a stale script // should complete nothing rather than print a message into the user's prompt. let found = - ::usage_argv::complete::for_name(Self::spec(), &name, &ctx).unwrap_or_default(); - let answer = ::usage_argv::complete::Completions { + usage_argv::complete::for_name(Self::spec(), &name, &ctx).unwrap_or_default(); + let answer = usage_argv::complete::Completions { candidates: found, files: ::std::option::Option::None, }; - return ::std::option::Option::Some(::usage_argv::complete::render(&answer, shell)); + return ::std::option::Option::Some(usage_argv::complete::render(&answer, shell)); } - let answer = ::usage_argv::complete::complete(Self::spec(), &split); - ::std::option::Option::Some(::usage_argv::complete::render(&answer, shell)) + let answer = usage_argv::complete::complete(Self::spec(), &split); + ::std::option::Option::Some(usage_argv::complete::render(&answer, shell)) } }; let intercept = quote! { @@ -676,7 +704,7 @@ fn flag_table(i: usize, field: &Field) -> TokenStream { }; quote! { - pub static #name: ::usage_argv::Flag = ::usage_argv::Flag { + pub static #name: usage_argv::Flag = usage_argv::Flag { key: #key, name: #field_name, longs: &[#(#longs),*], @@ -699,10 +727,10 @@ fn arg_table(i: usize, field: &Field) -> TokenStream { unreachable!("filtered by the caller"); }; let double_dash = match double_dash { - DoubleDash::Optional => quote!(::usage_argv::DoubleDash::Optional), - DoubleDash::Required => quote!(::usage_argv::DoubleDash::Required), - DoubleDash::Preserve => quote!(::usage_argv::DoubleDash::Preserve), - DoubleDash::Automatic => quote!(::usage_argv::DoubleDash::Automatic), + DoubleDash::Optional => quote!(usage_argv::DoubleDash::Optional), + DoubleDash::Required => quote!(usage_argv::DoubleDash::Required), + DoubleDash::Preserve => quote!(usage_argv::DoubleDash::Preserve), + DoubleDash::Automatic => quote!(usage_argv::DoubleDash::Automatic), }; // A bound stops the variadic while binding, so the argument after it is reachable. let var_max = match field.var_max.filter(|_| var) { @@ -715,7 +743,7 @@ fn arg_table(i: usize, field: &Field) -> TokenStream { }; quote! { - pub static #name: ::usage_argv::Arg = ::usage_argv::Arg { + pub static #name: usage_argv::Arg = usage_argv::Arg { key: #key, name: #field_name, var: #var, @@ -750,8 +778,8 @@ fn completer_tokens( let completer_path = path; let decl = quote! { fn #wrapper( - ctx: &::usage_argv::complete::CompleteCtx<'_>, - ) -> ::std::vec::Vec<::usage_argv::complete::Candidate<'static>> { + ctx: &usage_argv::complete::CompleteCtx<'_>, + ) -> ::std::vec::Vec> { // The words this command was given, parsed against this command's own tables — so // what the callback reads is what the parser would have bound, rather than a slice // of the line it has to interpret itself. @@ -760,7 +788,7 @@ fn completer_tokens( // subcommand's words against the ancestor's tables would drop everything the // ancestor was given before the subcommand's name. let __usage_declaration = - <#owner as ::usage_argv::spec::CommandArgs>::COMMAND; + <#owner as usage_argv::spec::CommandArgs>::COMMAND; let (__usage_command, __usage_words) = ctx .command_for(__usage_declaration) .unwrap_or((__usage_declaration, ctx.command_words)); @@ -770,8 +798,8 @@ fn completer_tokens( .collect(); let __usage_argv: ::std::vec::Vec<&::std::ffi::OsStr> = __usage_owned.iter().map(|a| a.as_os_str()).collect(); - let mut partial = <#owner as ::usage_argv::spec::CommandArgs>::start(); - let mut parser = ::usage_argv::Parser::new( + let mut partial = <#owner as usage_argv::spec::CommandArgs>::start(); + let mut parser = usage_argv::Parser::new( __usage_command, &__usage_argv, ); @@ -780,7 +808,7 @@ fn completer_tokens( while let ::std::option::Option::Some(event) = parser.next_event() { match event { ::std::result::Result::Ok(event) => { - let _ = <#owner as ::usage_argv::spec::CommandArgs>::apply( + let _ = <#owner as usage_argv::spec::CommandArgs>::apply( &mut partial, &event, ); @@ -834,7 +862,7 @@ fn flag_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { .unwrap_or_else(|| quote!(::core::option::Option::None)); quote! { #completer_decl - pub static #name: ::usage_argv::spec::FlagMeta = ::usage_argv::spec::FlagMeta { + pub static #name: usage_argv::spec::FlagMeta = usage_argv::spec::FlagMeta { effect: #effect, complete: #completer, complete_type: #complete_type, @@ -857,7 +885,7 @@ fn flag_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { requires: &[#(#requires),*], required_if: &[#(#required_if),*], required_unless: &[#(#required_unless),*], - ..::usage_argv::spec::FlagMeta::EMPTY + ..usage_argv::spec::FlagMeta::EMPTY }; } } @@ -883,7 +911,7 @@ fn arg_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { quote! { #completer_decl - pub static #name: ::usage_argv::spec::ArgMeta = ::usage_argv::spec::ArgMeta { + pub static #name: usage_argv::spec::ArgMeta = usage_argv::spec::ArgMeta { complete: #completer, complete_type: #complete_type, arg: &#table, @@ -897,7 +925,7 @@ fn arg_meta(i: usize, field: &Field, owner: &syn::Ident) -> TokenStream { choices: #choices, var_min: #var_min, var_max: #var_max, - ..::usage_argv::spec::ArgMeta::EMPTY + ..usage_argv::spec::ArgMeta::EMPTY }; } } @@ -907,7 +935,7 @@ fn choices_tokens(field: &Field) -> TokenStream { // From the type when the field says `value_enum`, so the spec, the help and the check // all read the list the type declares rather than a copy of it. if let (true, Some(ty)) = (field.value_enum, field.value_ty.as_ref()) { - return quote!(<#ty as ::usage_argv::spec::ValueEnum>::CHOICES); + return quote!(<#ty as usage_argv::spec::ValueEnum>::CHOICES); } let choices = &field.choices; quote!(&[#(#choices),*]) @@ -971,7 +999,7 @@ fn key_consts(fingerprint: &str, flags: usize, args: usize) -> TokenStream { }); quote! { const __USAGE_KEY_BASE: u64 = - ::usage_argv::key_base(::core::module_path!(), #declaration); + usage_argv::key_base(::core::module_path!(), #declaration); const #command: u64 = __USAGE_KEY_BASE | #KIND_COMMAND; #(#flag_keys)* #(#arg_keys)* @@ -1071,10 +1099,10 @@ fn tables(cli: &Cli) -> Tables { flattened = true; flush_flags(&mut own_flags, &mut flag_groups, &mut flag_meta_groups); flush_args(&mut own_args, &mut arg_groups, &mut arg_meta_groups); - flag_groups.push(quote!(<#ty as ::usage_argv::spec::CommandArgs>::COMMAND.flags)); - arg_groups.push(quote!(<#ty as ::usage_argv::spec::CommandArgs>::COMMAND.args)); - flag_meta_groups.push(quote!(<#ty as ::usage_argv::spec::CommandArgs>::META.flags)); - arg_meta_groups.push(quote!(<#ty as ::usage_argv::spec::CommandArgs>::META.args)); + flag_groups.push(quote!(<#ty as usage_argv::spec::CommandArgs>::COMMAND.flags)); + arg_groups.push(quote!(<#ty as usage_argv::spec::CommandArgs>::COMMAND.args)); + flag_meta_groups.push(quote!(<#ty as usage_argv::spec::CommandArgs>::META.flags)); + arg_meta_groups.push(quote!(<#ty as usage_argv::spec::CommandArgs>::META.args)); } Kind::Subcommand { .. } => {} } @@ -1106,25 +1134,25 @@ fn tables(cli: &Cli) -> Tables { Tables { decls: quote! { - const FLAG_GROUPS: &[&[&::usage_argv::Flag<'static>]] = &[#(#flag_groups),*]; - const ARG_GROUPS: &[&[&::usage_argv::Arg<'static>]] = &[#(#arg_groups),*]; - static FLAGS: [&::usage_argv::Flag<'static>; - ::usage_argv::table_len(FLAG_GROUPS)] = - ::usage_argv::concat_flags(FLAG_GROUPS); - static ARGS: [&::usage_argv::Arg<'static>; ::usage_argv::table_len(ARG_GROUPS)] = - ::usage_argv::concat_args(ARG_GROUPS); + const FLAG_GROUPS: &[&[&usage_argv::Flag<'static>]] = &[#(#flag_groups),*]; + const ARG_GROUPS: &[&[&usage_argv::Arg<'static>]] = &[#(#arg_groups),*]; + static FLAGS: [&usage_argv::Flag<'static>; + usage_argv::table_len(FLAG_GROUPS)] = + usage_argv::concat_flags(FLAG_GROUPS); + static ARGS: [&usage_argv::Arg<'static>; usage_argv::table_len(ARG_GROUPS)] = + usage_argv::concat_args(ARG_GROUPS); }, meta_decls: quote! { - const FLAG_META_GROUPS: &[&[::usage_argv::spec::FlagMeta<'static>]] = + const FLAG_META_GROUPS: &[&[usage_argv::spec::FlagMeta<'static>]] = &[#(#flag_meta_groups),*]; - const ARG_META_GROUPS: &[&[::usage_argv::spec::ArgMeta<'static>]] = + const ARG_META_GROUPS: &[&[usage_argv::spec::ArgMeta<'static>]] = &[#(#arg_meta_groups),*]; - static FLAG_METAS: [::usage_argv::spec::FlagMeta<'static>; - ::usage_argv::table_len(FLAG_META_GROUPS)] = - ::usage_argv::spec::concat_flag_metas(FLAG_META_GROUPS); - static ARG_METAS: [::usage_argv::spec::ArgMeta<'static>; - ::usage_argv::table_len(ARG_META_GROUPS)] = - ::usage_argv::spec::concat_arg_metas(ARG_META_GROUPS); + static FLAG_METAS: [usage_argv::spec::FlagMeta<'static>; + usage_argv::table_len(FLAG_META_GROUPS)] = + usage_argv::spec::concat_flag_metas(FLAG_META_GROUPS); + static ARG_METAS: [usage_argv::spec::ArgMeta<'static>; + usage_argv::table_len(ARG_META_GROUPS)] = + usage_argv::spec::concat_arg_metas(ARG_META_GROUPS); }, flags: quote!(&FLAGS), args: quote!(&ARGS), @@ -1321,7 +1349,7 @@ fn partial_struct(cli: &Cli) -> TokenStream { if let Kind::Flatten { ty } = &f.kind { let ident = &f.ident; return Some(quote! { - pub #ident: <#ty as ::usage_argv::spec::CommandArgs>::Partial, + pub #ident: <#ty as usage_argv::spec::CommandArgs>::Partial, }); } let ident = &f.ident; @@ -1395,10 +1423,10 @@ fn given_value(field: &Field) -> TokenStream { // The bool the parser landed on, which is `false` for a negation and `true` for the flag // itself: what the user said, rather than that they said something. Shape::Bool => quote! { - ::usage_argv::spec::SettingGiven::Bool(partial.#ident) + usage_argv::spec::SettingGiven::Bool(partial.#ident) }, Shape::Count => quote! { - ::usage_argv::spec::SettingGiven::Int( + usage_argv::spec::SettingGiven::Int( ::std::convert::TryFrom::try_from(partial.#ident) .unwrap_or(::std::primitive::i64::MAX), ) @@ -1406,21 +1434,21 @@ fn given_value(field: &Field) -> TokenStream { Shape::Optional => quote! { match ::std::str::from_utf8(partial.#ident.as_deref().unwrap_or_default()) { ::std::result::Result::Ok(__usage_text) => { - ::usage_argv::spec::SettingGiven::Text( + usage_argv::spec::SettingGiven::Text( ::std::string::ToString::to_string(__usage_text), ) } - ::std::result::Result::Err(_) => ::usage_argv::spec::SettingGiven::NotText, + ::std::result::Result::Err(_) => usage_argv::spec::SettingGiven::NotText, } }, Shape::Required => quote! { match ::std::str::from_utf8(&partial.#ident) { ::std::result::Result::Ok(__usage_text) => { - ::usage_argv::spec::SettingGiven::Text( + usage_argv::spec::SettingGiven::Text( ::std::string::ToString::to_string(__usage_text), ) } - ::std::result::Result::Err(_) => ::usage_argv::spec::SettingGiven::NotText, + ::std::result::Result::Err(_) => usage_argv::spec::SettingGiven::NotText, } }, // Item by item, rather than joined and re-split: an item holding the separator would come @@ -1438,9 +1466,9 @@ fn given_value(field: &Field) -> TokenStream { .collect::<::std::option::Option<::std::vec::Vec<_>>>() { ::std::option::Option::Some(__usage_items) => { - ::usage_argv::spec::SettingGiven::List(__usage_items) + usage_argv::spec::SettingGiven::List(__usage_items) } - ::std::option::Option::None => ::usage_argv::spec::SettingGiven::NotText, + ::std::option::Option::None => usage_argv::spec::SettingGiven::NotText, } }, } @@ -1486,17 +1514,17 @@ fn children(cli: &Cli) -> Vec<(TokenStream, TokenStream)> { let ident = &field.ident; match &field.kind { Kind::Flatten { ty } => Some(( - quote!(<#ty as ::usage_argv::spec::CommandArgs>::SETTINGS_BINDINGS), + quote!(<#ty as usage_argv::spec::CommandArgs>::SETTINGS_BINDINGS), quote! { - <#ty as ::usage_argv::spec::CommandArgs>::settings_given( + <#ty as usage_argv::spec::CommandArgs>::settings_given( &partial.#ident, ) }, )), Kind::Subcommand { ty, .. } => Some(( - quote!(<#ty as ::usage_argv::spec::Subcommands>::SETTINGS_BINDINGS), + quote!(<#ty as usage_argv::spec::Subcommands>::SETTINGS_BINDINGS), quote! { - <#ty as ::usage_argv::spec::Subcommands>::settings_given( + <#ty as usage_argv::spec::Subcommands>::settings_given( &partial.__usage_sub, partial.__usage_selected, ) @@ -1522,7 +1550,7 @@ fn joined_bindings(own: &[TokenStream], children: &[TokenStream]) -> TokenStream const PARTS: &[&'static [(&'static str, &'static str)]] = &[OWN #(, #children)*]; const N: usize = OWN.len() #(+ #children.len())*; const JOINED: [(&'static str, &'static str); N] = - ::usage_argv::spec::concat_bindings(PARTS); + usage_argv::spec::concat_bindings(PARTS); &JOINED } } @@ -1562,7 +1590,7 @@ fn settings(cli: &Cli) -> Option { /// The settings this command line gave values for. pub fn settings_given( partial: &Partial, - ) -> ::std::vec::Vec<(&'static str, ::usage_argv::spec::SettingGiven)> { + ) -> ::std::vec::Vec<(&'static str, usage_argv::spec::SettingGiven)> { let mut __usage_given = ::std::vec::Vec::new(); #(#contributions)* #(#from_children)* @@ -1597,17 +1625,17 @@ fn settings_layer() -> TokenStream { ); for (__usage_key, __usage_given) in settings_given(partial) { __usage_layer = match __usage_given { - ::usage_argv::spec::SettingGiven::Bool(__usage_value) => { + usage_argv::spec::SettingGiven::Bool(__usage_value) => { __usage_layer.with_value(__usage_key, ::usage_config::Value::Bool(__usage_value)) } - ::usage_argv::spec::SettingGiven::Int(__usage_value) => { + usage_argv::spec::SettingGiven::Int(__usage_value) => { __usage_layer.with_value(__usage_key, ::usage_config::Value::Int(__usage_value)) } - ::usage_argv::spec::SettingGiven::Text(__usage_value) => { + usage_argv::spec::SettingGiven::Text(__usage_value) => { __usage_layer .with_value(__usage_key, ::usage_config::Value::String(__usage_value)) } - ::usage_argv::spec::SettingGiven::List(__usage_items) => __usage_layer.with_value( + usage_argv::spec::SettingGiven::List(__usage_items) => __usage_layer.with_value( __usage_key, ::usage_config::Value::List( __usage_items @@ -1616,7 +1644,7 @@ fn settings_layer() -> TokenStream { .collect(), ), ), - ::usage_argv::spec::SettingGiven::NotText => { + usage_argv::spec::SettingGiven::NotText => { __usage_layer.with_unrepresentable(__usage_key) } }; @@ -1668,7 +1696,7 @@ fn partial_defaults(cli: &Cli) -> TokenStream { if let Kind::Flatten { ty } = &f.kind { let ident = &f.ident; return Some(quote! { - #ident: <#ty as ::usage_argv::spec::CommandArgs>::start(), + #ident: <#ty as usage_argv::spec::CommandArgs>::start(), }); } let ident = &f.ident; @@ -1718,7 +1746,7 @@ fn field_final(field: &Field) -> TokenStream { // the same call at every level. // return quote! { - #ident: <#ty as ::usage_argv::spec::CommandArgs>::build(partial.#ident)? + #ident: <#ty as usage_argv::spec::CommandArgs>::build(partial.#ident)? }; } let Some(ty) = field.value_ty.as_ref() else { @@ -1767,12 +1795,12 @@ fn field_final(field: &Field) -> TokenStream { // replaced by a different filename. let one = |value: TokenStream| { quote! { - match ::usage_argv::os_string_from_bytes(#value) { + match usage_argv::os_string_from_bytes(#value) { ::std::result::Result::Ok(__usage_os) => #build(__usage_os), ::std::result::Result::Err(__usage_bytes) => { return ::std::result::Result::Err( - ::usage_argv::Error::InvalidValue(::std::boxed::Box::new( - ::usage_argv::InvalidValue { + usage_argv::Error::InvalidValue(::std::boxed::Box::new( + usage_argv::InvalidValue { name: #name, value: ::std::string::String::from_utf8_lossy( &__usage_bytes, @@ -1846,8 +1874,8 @@ fn field_final(field: &Field) -> TokenStream { ::std::result::Result::Ok(text) => text, ::std::result::Result::Err(bad) => { return ::std::result::Result::Err( - ::usage_argv::Error::InvalidValue(::std::boxed::Box::new( - ::usage_argv::InvalidValue { + usage_argv::Error::InvalidValue(::std::boxed::Box::new( + usage_argv::InvalidValue { name: #name, value: ::std::string::String::from_utf8_lossy( bad.as_bytes(), @@ -1869,8 +1897,8 @@ fn field_final(field: &Field) -> TokenStream { ::std::result::Result::Ok(parsed) => parsed, ::std::result::Result::Err(reason) => { return ::std::result::Result::Err( - ::usage_argv::Error::InvalidValue(::std::boxed::Box::new( - ::usage_argv::InvalidValue { + usage_argv::Error::InvalidValue(::std::boxed::Box::new( + usage_argv::InvalidValue { name: #name, value: __usage_text, reason: ::std::string::ToString::to_string(&reason), @@ -1990,7 +2018,7 @@ fn apply_fn(cli: &Cli) -> TokenStream { }; let ident = &f.ident; Some(quote! { - if <#ty as ::usage_argv::spec::CommandArgs>::apply(&mut partial.#ident, event) { + if <#ty as usage_argv::spec::CommandArgs>::apply(&mut partial.#ident, event) { return true; } }) @@ -2011,9 +2039,9 @@ fn apply_fn(cli: &Cli) -> TokenStream { quote! { pub fn apply( partial: &mut Partial, - event: &::usage_argv::Event<'_, '_>, + event: &usage_argv::Event<'_, '_>, ) -> bool { - use ::usage_argv::Event; + use usage_argv::Event; #route #(#flattened)* // Each arm evaluates to whether it claimed the event, rather than @@ -2080,7 +2108,7 @@ fn subcommand_parts(cli: &Cli) -> Option { let selected = quote! { match partial.__usage_selected { ::std::option::Option::Some(__usage_at) => { - <#ty as ::usage_argv::spec::Subcommands>::select( + <#ty as usage_argv::spec::Subcommands>::select( partial.__usage_sub, __usage_at, )? @@ -2096,7 +2124,7 @@ fn subcommand_parts(cli: &Cli) -> Option { ::std::option::Option::Some(__usage_cmd) => __usage_cmd, ::std::option::Option::None => { return ::std::result::Result::Err( - ::usage_argv::Error::MissingSubcommand, + usage_argv::Error::MissingSubcommand, ); } }, @@ -2104,7 +2132,7 @@ fn subcommand_parts(cli: &Cli) -> Option { }; Some(SubcommandParts { - commands: quote!(subcommands: <#ty as ::usage_argv::spec::Subcommands>::COMMANDS,), + commands: quote!(subcommands: <#ty as usage_argv::spec::Subcommands>::COMMANDS,), // Resolved from the name at compile time. The variants are another expansion, so the // name is all there is to go on here — but `find_subcommand` searches the list during // const evaluation, which means a name no subcommand answers to fails to compile @@ -2112,17 +2140,17 @@ fn subcommand_parts(cli: &Cli) -> Option { default: match cli.default_subcommand.as_deref() { ::std::option::Option::Some(name) => quote! { default_subcommand: ::std::option::Option::Some( - ::usage_argv::find_subcommand( - <#ty as ::usage_argv::spec::Subcommands>::COMMANDS, + usage_argv::find_subcommand( + <#ty as usage_argv::spec::Subcommands>::COMMANDS, #name, ), ), }, ::std::option::Option::None => TokenStream::new(), }, - metas: quote!(subcommands: <#ty as ::usage_argv::spec::Subcommands>::METAS,), + metas: quote!(subcommands: <#ty as usage_argv::spec::Subcommands>::METAS,), partial_fields: quote! { - pub __usage_sub: <#ty as ::usage_argv::spec::Subcommands>::Partial, + pub __usage_sub: <#ty as usage_argv::spec::Subcommands>::Partial, /// Which of this command's subcommands was reached, as a position in /// `COMMANDS`. Found from the table's own address, so it cannot be /// confused by a key collision. @@ -2137,9 +2165,9 @@ fn subcommand_parts(cli: &Cli) -> Option { // subcommands answers to it: a deeper descent belongs to whoever owns // that command, and recording it here would make the wrong variant look // selected. - if let ::usage_argv::Event::Command(__usage_cmd) = event { + if let usage_argv::Event::Command(__usage_cmd) = event { if let ::std::option::Option::Some(__usage_at) = - <#ty as ::usage_argv::spec::Subcommands>::COMMANDS + <#ty as usage_argv::spec::Subcommands>::COMMANDS .iter() .position(|candidate| ::core::ptr::eq(*candidate, *__usage_cmd)) { @@ -2149,7 +2177,7 @@ fn subcommand_parts(cli: &Cli) -> Option { // Only the selected one is asked — see `Subcommands::apply`. The selection is // set just above, so a command word reaches the command it named on the same // event that selected it. - if <#ty as ::usage_argv::spec::Subcommands>::apply( + if <#ty as usage_argv::spec::Subcommands>::apply( &mut partial.__usage_sub, partial.__usage_selected, event, @@ -2159,7 +2187,7 @@ fn subcommand_parts(cli: &Cli) -> Option { }, check: quote! { if let ::std::option::Option::Some(__usage_at) = partial.__usage_selected { - <#ty as ::usage_argv::spec::Subcommands>::check( + <#ty as usage_argv::spec::Subcommands>::check( &mut partial.__usage_sub, __usage_at, )?; @@ -2173,6 +2201,7 @@ fn subcommand_parts(cli: &Cli) -> Option { /// parent reach them. pub fn emit_args(cli: &Cli) -> TokenStream { let ident = &cli.ident; + let runtime = runtime_path(); // A group carries settings the same way a root does, minus the layer: `SettingGiven` is // usage-argv's own vocabulary, so a flattened group can hand its parent what it was given // without either of them naming the config crate. Emitted whenever it has anything to say — @@ -2190,7 +2219,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { fn settings_given( partial: &Self::Partial, - ) -> ::std::vec::Vec<(&'static str, ::usage_argv::spec::SettingGiven)> { + ) -> ::std::vec::Vec<(&'static str, usage_argv::spec::SettingGiven)> { settings_given(partial) } } @@ -2291,13 +2320,15 @@ pub fn emit_args(cli: &Cli) -> TokenStream { clippy::needless_update )] const _: () = { + use #runtime as usage_argv; + #flatten_checks #keys #(#flag_tables)* #(#arg_tables)* #table_decls - pub static COMMAND: ::usage_argv::Command = ::usage_argv::Command { + pub static COMMAND: usage_argv::Command = usage_argv::Command { name: #name, aliases: &[#(#aliases),*], key: #command_key, @@ -2305,14 +2336,14 @@ pub fn emit_args(cli: &Cli) -> TokenStream { flags: #flag_table_ref, args: #arg_table_ref, #sub_commands - ..::usage_argv::Command::EMPTY + ..usage_argv::Command::EMPTY }; #(#flag_metas)* #(#arg_metas)* #meta_table_decls - pub static COMMAND_META: ::usage_argv::spec::CommandMeta = ::usage_argv::spec::CommandMeta { + pub static COMMAND_META: usage_argv::spec::CommandMeta = usage_argv::spec::CommandMeta { cmd: &COMMAND, effect: #effect, about: #about, @@ -2328,7 +2359,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { flags: #flag_meta_table_ref, args: #arg_meta_table_ref, #sub_metas - ..::usage_argv::spec::CommandMeta::EMPTY + ..usage_argv::spec::CommandMeta::EMPTY }; pub fn __usage_text(value: &[u8]) -> ::std::vec::Vec { @@ -2356,7 +2387,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { /// an invocation that ran `run`. pub fn check<'t, 'v>( partial: &mut Partial, - ) -> ::std::result::Result<(), ::usage_argv::Error<'t, 'v>> { + ) -> ::std::result::Result<(), usage_argv::Error<'t, 'v>> { // Read unconditionally: a command that declares nothing to check would // otherwise leave the parameter unused in the user's crate, where // nobody can silence it. @@ -2367,11 +2398,11 @@ pub fn emit_args(cli: &Cli) -> TokenStream { #settings_defs - impl ::usage_argv::spec::CommandArgs for #ident { + impl usage_argv::spec::CommandArgs for #ident { type Partial = Partial; - const COMMAND: &'static ::usage_argv::Command<'static> = &COMMAND; - const META: &'static ::usage_argv::spec::CommandMeta<'static> = + const COMMAND: &'static usage_argv::Command<'static> = &COMMAND; + const META: &'static usage_argv::spec::CommandMeta<'static> = &COMMAND_META; fn start() -> Self::Partial { @@ -2380,14 +2411,14 @@ pub fn emit_args(cli: &Cli) -> TokenStream { fn apply( partial: &mut Self::Partial, - event: &::usage_argv::Event<'_, '_>, + event: &usage_argv::Event<'_, '_>, ) -> bool { apply(partial, event) } fn check<'t, 'v>( partial: &mut Self::Partial, - ) -> ::std::result::Result<(), ::usage_argv::Error<'t, 'v>> { + ) -> ::std::result::Result<(), usage_argv::Error<'t, 'v>> { check(partial) } @@ -2395,7 +2426,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { fn build<'t, 'v>( partial: Self::Partial, - ) -> ::std::result::Result> { + ) -> ::std::result::Result> { ::std::result::Result::Ok(Self { #sub_build #(#field_finals),* @@ -2410,6 +2441,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { /// parent uses to route events into them. pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let ident = &subs.ident; + let runtime = runtime_path(); // The structs the bare variants imply, written here so everything downstream keeps // speaking to a struct. `Args` is derived on them rather than the impl being written out: @@ -2442,19 +2474,19 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let partial_fields = subs.variants.iter().enumerate().map(|(i, v)| { let field = format_ident!("v{i}"); let ty = &v.ty; - quote!(pub #field: <#ty as ::usage_argv::spec::CommandArgs>::Partial,) + quote!(pub #field: <#ty as usage_argv::spec::CommandArgs>::Partial,) }); let partial_starts = subs.variants.iter().enumerate().map(|(i, v)| { let field = format_ident!("v{i}"); let ty = &v.ty; - quote!(#field: <#ty as ::usage_argv::spec::CommandArgs>::start(),) + quote!(#field: <#ty as usage_argv::spec::CommandArgs>::start(),) }); let applies = subs.variants.iter().enumerate().map(|(i, v)| { let field = format_ident!("v{i}"); let ty = &v.ty; quote! { ::std::option::Option::Some(#i) => { - <#ty as ::usage_argv::spec::CommandArgs>::apply(&mut partial.#field, event) + <#ty as usage_argv::spec::CommandArgs>::apply(&mut partial.#field, event) } } }); @@ -2473,15 +2505,15 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let aliases = v.aliases.iter().chain(&v.hidden_aliases); quote! { const #alias_groups: &[&[&str]] = &[ - <#ty as ::usage_argv::spec::CommandArgs>::COMMAND.aliases, + <#ty as usage_argv::spec::CommandArgs>::COMMAND.aliases, &[#(#aliases),*], ]; - static #aliases_name: [&str; ::usage_argv::table_len(#alias_groups)] = - ::usage_argv::spec::concat_aliases(#alias_groups); - pub static #name: ::usage_argv::Command = ::usage_argv::Command { + static #aliases_name: [&str; usage_argv::table_len(#alias_groups)] = + usage_argv::spec::concat_aliases(#alias_groups); + pub static #name: usage_argv::Command = usage_argv::Command { name: #cmd_name, aliases: &#aliases_name, - ..*<#ty as ::usage_argv::spec::CommandArgs>::COMMAND + ..*<#ty as usage_argv::spec::CommandArgs>::COMMAND }; } }); @@ -2512,11 +2544,11 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { // long form went missing from help for every command written that way. let about = match v.help.as_deref() { Some(help) => option_str(Some(help)), - None => quote!(<#ty as ::usage_argv::spec::CommandArgs>::META.about), + None => quote!(<#ty as usage_argv::spec::CommandArgs>::META.about), }; let long_about = match v.long_help.as_deref() { Some(long) => option_str(Some(long)), - None => quote!(<#ty as ::usage_argv::spec::CommandArgs>::META.long_about), + None => quote!(<#ty as usage_argv::spec::CommandArgs>::META.long_about), }; // Which of the table's aliases are hidden. The visible ones are not listed // anywhere: `cmd.aliases` minus these is what help and completions show. @@ -2526,19 +2558,19 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let hide = v.hide; quote! { const #hidden_groups: &[&[&str]] = &[ - <#ty as ::usage_argv::spec::CommandArgs>::META.hidden_aliases, + <#ty as usage_argv::spec::CommandArgs>::META.hidden_aliases, &[#(#hidden),*], ]; - static #hidden_name: [&str; ::usage_argv::table_len(#hidden_groups)] = - ::usage_argv::spec::concat_aliases(#hidden_groups); - pub static #name: ::usage_argv::spec::CommandMeta = - ::usage_argv::spec::CommandMeta { + static #hidden_name: [&str; usage_argv::table_len(#hidden_groups)] = + usage_argv::spec::concat_aliases(#hidden_groups); + pub static #name: usage_argv::spec::CommandMeta = + usage_argv::spec::CommandMeta { cmd: &#cmd, about: #about, long_about: #long_about, hide: #hide, hidden_aliases: &#hidden_name, - ..*<#ty as ::usage_argv::spec::CommandArgs>::META + ..*<#ty as usage_argv::spec::CommandArgs>::META }; } }); @@ -2552,7 +2584,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let field = format_ident!("v{i}"); let ty = &v.ty; quote! { - #i => <#ty as ::usage_argv::spec::CommandArgs>::check(&mut partial.#field), + #i => <#ty as usage_argv::spec::CommandArgs>::check(&mut partial.#field), } }); // Every variant's bindings, because a table says what the CLI *can* do and is compared @@ -2560,7 +2592,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { // those are about one invocation. A command nobody ran did not give anything. let binding_parts = subs.variants.iter().map(|v| { let ty = &v.ty; - quote!(<#ty as ::usage_argv::spec::CommandArgs>::SETTINGS_BINDINGS) + quote!(<#ty as usage_argv::spec::CommandArgs>::SETTINGS_BINDINGS) }); let binding_lens = binding_parts.clone().map(|part| quote!(+ #part.len())); let givens = subs.variants.iter().enumerate().map(|(i, v)| { @@ -2568,7 +2600,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let ty = &v.ty; quote! { ::std::option::Option::Some(#i) => { - <#ty as ::usage_argv::spec::CommandArgs>::settings_given(&partial.#field) + <#ty as usage_argv::spec::CommandArgs>::settings_given(&partial.#field) } } }); @@ -2578,7 +2610,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let ty = &v.ty; // The one place the box matters: everything else — tables, partial, `build` — // speaks to the struct itself. - let built = quote!(<#ty as ::usage_argv::spec::CommandArgs>::build(partial.#field)?); + let built = quote!(<#ty as usage_argv::spec::CommandArgs>::build(partial.#field)?); let built = if v.boxed { quote!(::std::boxed::Box::new(#built)) } else { @@ -2615,6 +2647,8 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { clippy::needless_update )] const _: () = { + use #runtime as usage_argv; + pub struct Partial { #(#partial_fields)* } @@ -2628,20 +2662,20 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { #(#command_overrides)* #(#meta_overrides)* - const _: () = ::usage_argv::assert_unique_subcommand_names(&[#(#unique_commands),*]); + const _: () = usage_argv::assert_unique_subcommand_names(&[#(#unique_commands),*]); - impl ::usage_argv::spec::Subcommands for #ident { + impl usage_argv::spec::Subcommands for #ident { type Partial = Partial; - const COMMANDS: &'static [&'static ::usage_argv::Command<'static>] = + const COMMANDS: &'static [&'static usage_argv::Command<'static>] = &[#(#commands),*]; - const METAS: &'static [&'static ::usage_argv::spec::CommandMeta<'static>] = + const METAS: &'static [&'static usage_argv::spec::CommandMeta<'static>] = &[#(#metas),*]; fn apply( partial: &mut Self::Partial, selected: ::std::option::Option, - event: &::usage_argv::Event<'_, '_>, + event: &usage_argv::Event<'_, '_>, ) -> bool { match selected { #(#applies)* @@ -2655,14 +2689,14 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { const PARTS: &[&'static [(&'static str, &'static str)]] = &[#(#binding_parts),*]; const N: usize = 0 #(#binding_lens)*; const JOINED: [(&'static str, &'static str); N] = - ::usage_argv::spec::concat_bindings(PARTS); + usage_argv::spec::concat_bindings(PARTS); &JOINED }; fn settings_given( partial: &Self::Partial, selected: ::std::option::Option, - ) -> ::std::vec::Vec<(&'static str, ::usage_argv::spec::SettingGiven)> { + ) -> ::std::vec::Vec<(&'static str, usage_argv::spec::SettingGiven)> { match selected { #(#givens)* // No subcommand was reached, so none of them was given anything. @@ -2673,7 +2707,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { fn check<'t, 'v>( partial: &mut Self::Partial, selected: usize, - ) -> ::std::result::Result<(), ::usage_argv::Error<'t, 'v>> { + ) -> ::std::result::Result<(), usage_argv::Error<'t, 'v>> { match selected { #(#checks)* // A position that is not one of these cannot be produced: it comes @@ -2687,7 +2721,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { selected: usize, ) -> ::std::result::Result< ::std::option::Option, - ::usage_argv::Error<'t, 'v>, + usage_argv::Error<'t, 'v>, > { match selected { #(#selects)* @@ -2735,7 +2769,7 @@ fn post_binding(cli: &Cli) -> TokenStream { }; let ident = &f.ident; Some(quote! { - <#ty as ::usage_argv::spec::CommandArgs>::check(&mut partial.#ident)?; + <#ty as usage_argv::spec::CommandArgs>::check(&mut partial.#ident)?; }) }); let duplicate_checks = cli.fields.iter().filter(|f| rejects_duplicate(f)).map(|f| { @@ -2744,7 +2778,7 @@ fn post_binding(cli: &Cli) -> TokenStream { quote! { if partial.#duplicated { return ::std::result::Result::Err( - ::usage_argv::Error::DuplicateFlag { name: #name }, + usage_argv::Error::DuplicateFlag { name: #name }, ); } } @@ -2846,7 +2880,7 @@ fn post_binding(cli: &Cli) -> TokenStream { Some(quote! { if !partial.#given #standing { return ::std::result::Result::Err( - ::usage_argv::Error::MissingRequired { name: #name }, + usage_argv::Error::MissingRequired { name: #name }, ); } }) @@ -2863,7 +2897,7 @@ fn post_binding(cli: &Cli) -> TokenStream { // lists what was expected, instead of a message about a type the user did not name. let choices: TokenStream = match (f.value_enum, f.value_ty.as_ref()) { (true, Some(ty)) => { - quote!(<#ty as ::usage_argv::spec::ValueEnum>::CHOICES) + quote!(<#ty as usage_argv::spec::ValueEnum>::CHOICES) } _ => { let list = &f.choices; @@ -2893,7 +2927,7 @@ fn post_binding(cli: &Cli) -> TokenStream { }; if !#choices.contains(&__usage_text) { return ::std::result::Result::Err( - ::usage_argv::Error::InvalidChoice { + usage_argv::Error::InvalidChoice { name: #name, choices: #choices, }, @@ -2913,7 +2947,7 @@ fn post_binding(cli: &Cli) -> TokenStream { Some(min) => quote! { if got < #min { return ::std::result::Result::Err( - ::usage_argv::Error::VarTooFew { name: #name, min: #min, got }, + usage_argv::Error::VarTooFew { name: #name, min: #min, got }, ); } }, @@ -2938,7 +2972,7 @@ fn post_binding(cli: &Cli) -> TokenStream { Some(max) => quote! { if got > #max { return ::std::result::Result::Err( - ::usage_argv::Error::VarTooMany { name: #name, max: #max, got }, + usage_argv::Error::VarTooMany { name: #name, max: #max, got }, ); } }, @@ -2975,7 +3009,7 @@ fn post_binding(cli: &Cli) -> TokenStream { Some(quote! { if partial.#given && partial.#other_given { return ::std::result::Result::Err( - ::usage_argv::Error::ConflictingFlags { + usage_argv::Error::ConflictingFlags { name: #name, other: #other_name, }, @@ -3012,7 +3046,7 @@ fn post_binding(cli: &Cli) -> TokenStream { Some(quote! { if partial.#given && !partial.#other_given { return ::std::result::Result::Err( - ::usage_argv::Error::MissingRequired { name: #other_name }, + usage_argv::Error::MissingRequired { name: #other_name }, ); } }) @@ -3049,7 +3083,7 @@ fn post_binding(cli: &Cli) -> TokenStream { // variable has already filled the field and set `__given_*`. let missing = quote! { return ::std::result::Result::Err( - ::usage_argv::Error::MissingRequired { name: #name }, + usage_argv::Error::MissingRequired { name: #name }, ); }; let required_if = (!if_given.is_empty()).then(|| { @@ -3103,6 +3137,7 @@ fn post_binding(cli: &Cli) -> TokenStream { /// hand-written list. pub fn emit_value_enum(value_enum: &ValueEnum) -> TokenStream { let ident = &value_enum.ident; + let runtime = runtime_path(); let words: Vec<&String> = value_enum.variants.iter().map(|(_, name)| name).collect(); let arms = value_enum .variants @@ -3118,22 +3153,27 @@ pub fn emit_value_enum(value_enum: &ValueEnum) -> TokenStream { .join(", "); quote! { - impl ::usage_argv::spec::ValueEnum for #ident { - const CHOICES: &'static [&'static str] = &[#(#words),*]; - } + #[doc(hidden)] + const _: () = { + use #runtime as usage_argv; + + impl usage_argv::spec::ValueEnum for #ident { + const CHOICES: &'static [&'static str] = &[#(#words),*]; + } - impl ::std::str::FromStr for #ident { - type Err = ::std::string::String; + impl ::std::str::FromStr for #ident { + type Err = ::std::string::String; - fn from_str(value: &str) -> ::std::result::Result { - match value { - #(#arms)* - other => ::std::result::Result::Err(::std::format!( - "`{other}` is not one of: {}", - #expected - )), + fn from_str(value: &str) -> ::std::result::Result { + match value { + #(#arms)* + other => ::std::result::Result::Err(::std::format!( + "`{other}` is not one of: {}", + #expected + )), + } } } - } + }; } } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b6d99cb7b..1dba460a5 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -208,7 +208,7 @@ //! | `double_dash = "…"` | how a positional relates to `--`: `optional` (the default), `required` (fillable only after one), `preserve` (the `--` is a value), `automatic` (filling it ends flag parsing, so a wrapper forwards) | //! | `complete = my_fn` | a function that answers for this value when a shell asks | //! | `value_enum` | the words come from the field's type, which derives [`ValueEnum`] | -//! | `value_hint = usage_argv::ValueHint::FilePath` | ask the shell for paths; `AnyPath` and `DirPath` are also supported | +//! | `value_hint = usage::ValueHint::FilePath` | ask the shell for paths; `AnyPath` and `DirPath` are also supported | //! | `arg` | force a field to be positional | //! | `overrides = "--other"` | a flag this one displaces, the last given winning | //! | `conflicts = "--other"` | a flag this one cannot be given with | diff --git a/derive/src/model.rs b/derive/src/model.rs index 8bbe8673a..0a138835f 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -241,7 +241,7 @@ fn effect_value(meta: &Meta) -> syn::Result { } }; Ok(quote::quote!(::core::option::Option::Some( - ::usage_argv::spec::Effect::#variant + usage_argv::spec::Effect::#variant ))) } @@ -252,7 +252,7 @@ fn value_hint(meta: &Meta) -> syn::Result { return Err(syn::Error::new_spanned( value, "`value_hint` takes a usage ValueHint variant, as in \ - `value_hint = usage_argv::ValueHint::FilePath`", + `value_hint = usage::ValueHint::FilePath`", )); }; let Some(variant) = path.path.segments.last() else { diff --git a/usage-rs/Cargo.toml b/usage-rs/Cargo.toml new file mode 100644 index 000000000..753cc9290 --- /dev/null +++ b/usage-rs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "usage-rs" +description = "A compiled CLI parser for Rust, built on usage specs" +version = "5.1.0" +edition = "2021" +rust-version = "1.91" +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[dependencies] +usage-argv = { workspace = true } +usage-derive = { workspace = true, optional = true } + +[features] +default = ["spec", "help"] +spec = ["usage-argv/spec", "dep:usage-derive"] +help = ["spec"] +completions = ["spec", "usage-argv/complete"] +# Kept as a spelling close to the runtime feature for low-level adopters. +complete = ["completions"] +diagnostics = ["spec", "usage-argv/diagnostics"] + +[package.metadata.release] +shared-version = true +release = true diff --git a/usage-rs/src/lib.rs b/usage-rs/src/lib.rs new file mode 100644 index 000000000..b0be59389 --- /dev/null +++ b/usage-rs/src/lib.rs @@ -0,0 +1,32 @@ +//! The facade for building compiled Rust CLIs with usage. +//! +//! Depend on `usage-rs` under the short crate name `usage`; the derive macros and their runtime +//! then come from one versioned package, while cold-path functionality stays behind features: +//! +//! ```toml +//! [dependencies] +//! usage = { package = "usage-rs", version = "5.1" } +//! ``` +//! +//! ``` +//! use usage_rs as usage; +//! use usage::Cli; +//! +//! #[derive(Cli)] +//! #[usage(bin = "ex")] +//! struct Ex { +//! #[usage(long, value_hint = usage::ValueHint::FilePath)] +//! file: Option, +//! } +//! +//! let argv = [std::ffi::OsStr::new("--file"), std::ffi::OsStr::new("input.txt")]; +//! let ex = Ex::parse_from(&argv).expect("valid command line"); +//! assert_eq!(ex.file.as_deref(), Some(std::path::Path::new("input.txt"))); +//! ``` + +#![forbid(unsafe_code)] + +pub use usage_argv as argv; +pub use usage_argv::*; +#[cfg(feature = "spec")] +pub use usage_derive::{Args, Cli, Subcommands, ValueEnum}; diff --git a/usage-rs/tests/facade.rs b/usage-rs/tests/facade.rs new file mode 100644 index 000000000..cca41397a --- /dev/null +++ b/usage-rs/tests/facade.rs @@ -0,0 +1,38 @@ +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use usage_rs as usage; +use usage_rs::{Args, Cli, Subcommands}; + +#[derive(Cli)] +#[usage(bin = "ex")] +struct Ex { + #[usage(subcommand)] + command: Command, +} + +#[derive(Subcommands)] +enum Command { + Show(Show), +} + +/// Show one file +#[derive(Args)] +struct Show { + #[usage(long, value_hint = usage::ValueHint::FilePath)] + file: PathBuf, +} + +#[test] +fn one_dependency_provides_derives_runtime_and_value_hints() { + let _hint_from_facade = usage::ValueHint::FilePath; + let argv = [ + OsStr::new("show"), + OsStr::new("--file"), + OsStr::new("input.txt"), + ]; + let cli = Ex::parse_from(&argv).expect("valid command line"); + let Command::Show(show) = cli.command; + assert_eq!(show.file, Path::new("input.txt")); + assert!(Ex::to_kdl().contains("complete \"file\" type=\"path\"")); +} From b352cef44cb53419d4bbe3befcc439811fb5185a Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:58:01 +0000 Subject: [PATCH 08/21] chore: include usage-rs in msrv checks --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e81cb40f4..f7f164ca0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: matrix: include: - version: "1.91" - crates: usage-argv usage-derive usage-config + crates: usage-argv usage-derive usage-config usage-rs - version: "1.95" crates: usage-lib usage-config-build clap_usage usage-cli steps: From 5c280bf4f04e63abaf44073b8a4419708e8a5cfa Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:23:16 +0000 Subject: [PATCH 09/21] fix(derive): route unit commands through facade --- derive/src/codegen.rs | 25 ++++++++++++++++++++++++- usage-rs/tests/facade.rs | 12 +++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index b3020763d..b392cae15 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -42,6 +42,28 @@ fn runtime_path() -> TokenStream { } } +/// The derive package as the adopter depended on it. +/// +/// Most emitted code only needs the runtime path. Unit subcommands synthesize an empty `Args` +/// struct, though, so that derive must come through the facade too when it is the application's +/// only dependency. +fn derive_path() -> TokenStream { + match crate_name("usage-rs") { + Ok(FoundCrate::Itself) => quote!(::usage_rs), + Ok(FoundCrate::Name(name)) => { + let facade = format_ident!("{}", name.replace('-', "_")); + quote!(::#facade) + } + Err(_) => match crate_name("usage-derive") { + Ok(FoundCrate::Name(name)) => { + let derive = format_ident!("{}", name.replace('-', "_")); + quote!(::#derive) + } + _ => quote!(::usage_derive), + }, + } +} + pub fn emit(cli: &Cli) -> TokenStream { let ident = &cli.ident; let runtime = runtime_path(); @@ -2442,6 +2464,7 @@ pub fn emit_args(cli: &Cli) -> TokenStream { pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { let ident = &subs.ident; let runtime = runtime_path(); + let derive = derive_path(); // The structs the bare variants imply, written here so everything downstream keeps // speaking to a struct. `Args` is derived on them rather than the impl being written out: @@ -2461,7 +2484,7 @@ pub fn emit_subcommands(subs: &Subcommands) -> TokenStream { .map(|word| quote!(#[usage(effect = #word)])); quote! { #[doc(hidden)] - #[derive(::usage_derive::Args)] + #[derive(#derive::Args)] #effect pub struct #name {} } diff --git a/usage-rs/tests/facade.rs b/usage-rs/tests/facade.rs index cca41397a..ff9b51b63 100644 --- a/usage-rs/tests/facade.rs +++ b/usage-rs/tests/facade.rs @@ -14,6 +14,8 @@ struct Ex { #[derive(Subcommands)] enum Command { Show(Show), + /// Print version information + Version, } /// Show one file @@ -32,7 +34,15 @@ fn one_dependency_provides_derives_runtime_and_value_hints() { OsStr::new("input.txt"), ]; let cli = Ex::parse_from(&argv).expect("valid command line"); - let Command::Show(show) = cli.command; + let Command::Show(show) = cli.command else { + panic!("show command should be selected"); + }; assert_eq!(show.file, Path::new("input.txt")); assert!(Ex::to_kdl().contains("complete \"file\" type=\"path\"")); } + +#[test] +fn unit_subcommands_use_the_facade_derive() { + let cli = Ex::parse_from(&[OsStr::new("version")]).expect("valid unit subcommand"); + assert!(matches!(cli.command, Command::Version)); +} From 9eec6227d807d77ca0a86baea5b0a533d40c4be3 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:01:00 +0000 Subject: [PATCH 10/21] feat(cli): parse usage's own command line with the parser usage ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `usage` is now its own first adopter. The ten command structs, the root and the two command enums are declared with `usage-derive` instead of clap, and `--usage-spec` prints `Cli::to_kdl()` — the same tables that parsed the command line, rather than a transcription of a clap `Command` through `clap_usage`. Smaller than mise and far less forgiving of a lossy spec, because this CLI's spec is what generates its own docs, manpage and completions: anything the derive cannot say shows up in the checked-in output. - `clap`, `clap_usage` and the `clap-sort` dev-dependency, plus `tests/clap_sort.rs` — declaration order is held by the spec since #915. - `command_effects.rs`'s two tables, 60 lines that existed because "clap has no way to express this". Each command declares `#[usage(effect = "…")]` where it is defined; the file keeps `UNCLASSIFIED` and the coverage tests, which now read the derived metadata. A stale entry is no longer possible for the effects themselves — an effect moves with the command it is written on. - The four shell commands shared one `Shell` struct, which the derive refuses: a command collects into the struct that declares it. They are four structs flattening a shared group now, written by a macro so the paragraph of long help is not copied four times. Their docs improve as a side effect — all four used to say "Execute a shell script with the specified shell". - `sponsors` is a bare variant (#923), so its empty struct is gone. - `requires` has no positive form in the spec, so the two constraints that used it are stated as `required_if` on the other flag. `--out-dir requires --multi` is a positive requirement on a `bool` and has no spelling at all; jdx/usage#925 adds `requires`, and it belongs here when it lands. Gains `JDX_USAGE_BIN` on `--usage-bin` (the bridge dropped `env`), long help that keeps its line breaks, an `about` for `generate manpage`, and `name "usage"` rather than `name "usage-cli"`. Loses `subcommand_required`, which the spec can hold and the derive knows from a bare `T` subcommand field but does not emit, and strictness on subcommands: `unknown_flags` is accepted on an `Args` and ignored, and the root's is not inherited, so only the root is strict. Both are derive gaps worth their own fix rather than a workaround here. Workspace suite green, clippy clean, docs and assets re-rendered. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 16 +- cli/Cargo.toml | 9 +- cli/assets/fig.ts | 1 + cli/assets/usage.1 | 78 ++-- cli/src/cli/complete_word.rs | 16 +- cli/src/cli/exec.rs | 13 +- cli/src/cli/generate/completion.rs | 18 +- cli/src/cli/generate/completion_init.rs | 8 +- cli/src/cli/generate/fig.rs | 10 +- cli/src/cli/generate/json.rs | 8 +- cli/src/cli/generate/json_schema.rs | 14 +- cli/src/cli/generate/manpage.rs | 11 +- cli/src/cli/generate/markdown.rs | 25 +- cli/src/cli/generate/mod.rs | 36 +- cli/src/cli/generate/sdk.rs | 18 +- cli/src/cli/lint.rs | 12 +- cli/src/cli/mcp.rs | 8 +- cli/src/cli/mod.rs | 115 ++++-- cli/src/cli/shell.rs | 58 ++- cli/src/cli/sponsors.rs | 20 +- cli/src/command_effects.rs | 194 +++------ cli/src/lib.rs | 3 + cli/src/usage_spec.rs | 19 +- cli/tests/clap_sort.rs | 7 - cli/tests/shell_completions_integration.rs | 8 +- cli/usage.usage.kdl | 372 +++++++----------- docs/cli/reference/bash.md | 9 +- docs/cli/reference/commands.json | 86 ++-- docs/cli/reference/complete-word.md | 3 +- docs/cli/reference/fish.md | 9 +- .../cli/reference/generate/completion-init.md | 6 +- docs/cli/reference/generate/completion.md | 6 +- docs/cli/reference/generate/manpage.md | 9 +- docs/cli/reference/lint.md | 4 +- docs/cli/reference/powershell.md | 9 +- docs/cli/reference/zsh.md | 9 +- 36 files changed, 647 insertions(+), 600 deletions(-) delete mode 100644 cli/tests/clap_sort.rs diff --git a/Cargo.lock b/Cargo.lock index e9f3b17f3..1172b5a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -290,15 +290,6 @@ dependencies = [ "clap_derive", ] -[[package]] -name = "clap-sort" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9f374a541bd277ba6f4ccd08d955024ba09fda8dfc69ca1a750799ebed97a9" -dependencies = [ - "clap", -] - [[package]] name = "clap_builder" version = "4.6.6" @@ -2089,9 +2080,6 @@ name = "usage-cli" version = "5.1.0" dependencies = [ "assert_cmd", - "clap", - "clap-sort", - "clap_usage", "ctor", "env_logger", "exec", @@ -2111,6 +2099,8 @@ dependencies = [ "tera", "thiserror", "tokio", + "usage-argv", + "usage-derive", "usage-lib", "xx", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index bb414550b..ecff73933 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,8 +26,6 @@ name = "usage_cli" path = "src/lib.rs" [dependencies] -clap = { version = "4", features = ["derive", "string", "env"] } -clap_usage = { workspace = true } env_logger = "0.11" indexmap = "2" itertools = "0.15" @@ -43,6 +41,12 @@ serde_with = "3" tera = "2" thiserror = "2" tokio = { version = "1", features = ["rt", "macros", "io-std"] } +# The CLI is its own first adopter: `usage` parses its own command line with the +# parser it ships. `spec` because `--usage-spec` emits this CLI's own KDL, and +# `diagnostics` because a person types these commands and has to be told what +# went wrong. +usage-argv = { workspace = true, features = ["spec", "diagnostics"] } +usage-derive = { workspace = true } usage-lib = { workspace = true, features = ["clap", "docs", "unstable_choices_env"] } xx = "2" @@ -51,7 +55,6 @@ exec = "0.3" [dev-dependencies] assert_cmd = { version = "2", features = ["color-auto"] } -clap-sort = "1" ctor = "1" insta = "1" predicates = "3" diff --git a/cli/assets/fig.ts b/cli/assets/fig.ts index c2b857f02..8f426aeb9 100644 --- a/cli/assets/fig.ts +++ b/cli/assets/fig.ts @@ -460,6 +460,7 @@ const completionSpec: Fig.Spec = { }, { name: ["manpage", "man"], + description: "Generate a manpage from a usage spec", options: [ { name: ["-f", "--file"], diff --git a/cli/assets/usage.1 b/cli/assets/usage.1 index 5948c8c75..d1f080954 100644 --- a/cli/assets/usage.1 +++ b/cli/assets/usage.1 @@ -1,6 +1,6 @@ -.TH USAGE-CLI 1 +.TH USAGE 1 .SH NAME -usage\-cli \- CLI for working with usage\-based CLIs +usage \- CLI for working with usage\-based CLIs .SH SYNOPSIS \fBusage\fR [OPTIONS] [] [COMMAND] .SH DESCRIPTION @@ -17,7 +17,7 @@ Outputs completions for the specified shell for completing the `usage` CLI itsel .SH COMMANDS .TP \fBbash\fR -Execute a shell script with the specified shell +Execute a shell script using bash .TP \fBcomplete\-word\fR Generate shell completion candidates for a partial command line @@ -32,7 +32,7 @@ Execute a script, parsing args and exposing them as environment variables .RE .TP \fBfish\fR -Execute a shell script with the specified shell +Execute a shell script using fish .TP \fBgenerate\fR Generate completions, documentation, and other artifacts from usage specs @@ -65,6 +65,7 @@ Outputs a usage spec in json format Generate a JSON Schema for a CLI's config file from its usage spec .TP \fBgenerate manpage\fR +Generate a manpage from a usage spec .RS \fIAliases: \fRman .RE @@ -88,20 +89,20 @@ Serve a usage spec over the Model Context Protocol .RE .TP \fBpowershell\fR -Execute a shell script with the specified shell +Execute a shell script using PowerShell .TP \fBsponsors\fR Show the companies sponsoring usage and the jdx.dev open source tools .TP \fBzsh\fR -Execute a shell script with the specified shell +Execute a shell script using zsh .SH "USAGE BASH" -Execute a shell script with the specified shell +Execute a shell script using bash Typically, this will be called by a script's shebang. -If using `var=#true` on args/flags, they will be joined with spaces using `shell_words::join()` -to properly escape and quote values with spaces in them. +If using `var=#true` on args/flags, they will be joined with spaces using +`shell_words::join()` to properly escape and quote values with spaces in them. .PP \fBUsage:\fR usage bash [OPTIONS]