Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,10 @@ Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and
(`Command::is_multicall_set`). Path components and a trailing `.exe`
are stripped. **Not used by the fleet today**; clap's own applets and
busybox-style binaries are the reason it exists.
- [ ] **`no_binary_name`** — parsing an argv that has no `argv[0]`. usage-argv
already takes argv without the program name; this is clap's setter that
skips stripping it. Out of scope until a fleet CLI needs it.
- [x] **`no_binary_name`** — parsing an argv that has no `argv[0]`. The
allocation-free `parse_from` primitive always has that contract, while
clap-shaped `try_parse_from` includes argv0 by default and honors
`#[command(no_binary_name)]` by routing directly to the primitive.
- [ ] **Command parsing policy** — `arg_required_else_help`,
`args_conflicts_with_subcommands`, `subcommand_negates_reqs`,
`subcommand_precedence_over_arg`, `allow_missing_positional`,
Expand Down
26 changes: 26 additions & 0 deletions conformance/tests/program_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ struct Busy {
command: Option<BusyCommand>,
}

#[derive(Cli)]
#[command(no_binary_name)]
struct WordsOnly {
#[usage(long)]
plain: bool,
}

#[derive(Subcommands)]
enum BusyCommand {
#[usage(alias = "r")]
Expand Down Expand Up @@ -117,3 +124,22 @@ fn a_full_argv_helper_matches_clap_shaped_tests_and_multicall() {
assert!(matches!(parsed.command, Some(BusyCommand::Run)));
assert!(parsed.verbose, "root globals remain in scope for applets");
}

#[test]
fn clap_shaped_parsing_honors_no_binary_name() {
use std::ffi::OsStr;

let full = [OsStr::new("communique"), OsStr::new("--plain")];
assert!(
Cli_::try_parse_from(&full)
.expect("argv0 is stripped")
.plain
);

let words = [OsStr::new("--plain")];
assert!(
WordsOnly::try_parse_from(&words)
.expect("every word is parsed")
.plain
);
}
15 changes: 15 additions & 0 deletions derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ pub fn emit(cli: &Cli) -> TokenStream {

let default_subcommand = option_str(cli.default_subcommand.as_deref());
let multicall = cli.multicall;
let no_binary_name = cli.no_binary_name;
let usage = option_str(cli.usage.as_deref());
let restart_token = option_str(cli.restart_token.as_deref());
let mount = option_str(cli.mount.as_deref());
Expand Down Expand Up @@ -589,6 +590,20 @@ pub fn emit(cli: &Cli) -> TokenStream {
Self::parse_from(__usage_words)
}

/// Parse using clap's `try_parse_from` argv contract.
///
/// Input includes argv0 by default. `#[command(no_binary_name)]`
/// opts into treating every supplied word as an argument.
pub fn try_parse_from<'v>(
argv: &[&'v ::std::ffi::OsStr],
) -> ::std::result::Result<Self, usage_argv::Error<'static, 'v>> {
if #no_binary_name {
Self::parse_from(argv)
} else {
Self::parse_from_argv(argv)
}
}

/// Parse the process's own arguments.
#completion

Expand Down
4 changes: 2 additions & 2 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ mod crate_name;
mod model;

/// Compile a struct into a parser and a spec. See the [crate docs](crate).
#[proc_macro_derive(Cli, attributes(usage))]
#[proc_macro_derive(Cli, attributes(usage, command))]
pub fn derive_cli(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let parsed = model::Cli::from_input(&input)
Expand All @@ -335,7 +335,7 @@ pub fn derive_cli(input: TokenStream) -> TokenStream {
/// tables and metadata as [`Cli`], minus the program-level parts a subcommand does
/// not have — a name, a version, an entry point — plus the trait a parent uses to
/// route events into it.
#[proc_macro_derive(Args, attributes(usage))]
#[proc_macro_derive(Args, attributes(usage, command))]
pub fn derive_args(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// `restart_token` and `mount` are per-command and belong here; `default_subcommand` is
Expand Down
22 changes: 18 additions & 4 deletions derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub struct Cli {
/// whole program. `parse()` rewrites the process's argv[0]; `parse_from` is
/// unchanged, because the caller already decided the words.
pub multicall: bool,
/// Whether clap-shaped `try_parse_from` input omits argv0.
pub no_binary_name: bool,
/// Declared descriptions, for the case a doc comment cannot express: a long form that does
/// not contain the short one.
pub about_attr: Option<proc_macro2::TokenStream>,
Expand Down Expand Up @@ -477,7 +479,7 @@ impl Cli {
attr_span: input
.attrs
.iter()
.find(|a| a.path().is_ident("usage"))
.find(|a| a.path().is_ident("usage") || a.path().is_ident("command"))
.map(|a| a.path().span()),
version: None,
runtime_version: None,
Expand All @@ -486,6 +488,7 @@ impl Cli {
unknown_flags: None,
default_subcommand: None,
multicall: false,
no_binary_name: false,
about_attr: None,
long_about_attr: None,
before_help: None,
Expand Down Expand Up @@ -579,6 +582,7 @@ impl Cli {
cli.default_subcommand = Some(strip_dashes(&string_value(&meta)?))
}
"multicall" => cli.multicall = flag_value(&meta)?,
"no_binary_name" => cli.no_binary_name = flag_value(&meta)?,
"restart_token" => cli.restart_token = Some(string_value(&meta)?),
"mount" => cli.mount = Some(string_value(&meta)?),
"group" => cli.groups.push(group_decl(&meta)?),
Expand All @@ -588,7 +592,7 @@ impl Cli {
format!(
"unknown option `{other}` on a struct; usage::Cli takes \
`name`, `bin`, `version`, `version_spec`, `usage`, `verbatim_doc_comment`, `unknown_flags`, \
`default_subcommand`, `multicall`, `restart_token`, `mount` and \
`default_subcommand`, `multicall`, `no_binary_name`, `restart_token`, `mount` and \
`group` here, and the description comes from the doc \
comment"
),
Expand Down Expand Up @@ -720,6 +724,13 @@ impl Cli {
declares it once for the whole program, not one per command",
));
}
if self.no_binary_name {
return Err(self.misplaced(
ident,
"`no_binary_name` belongs on the root, where `#[derive(Cli)]` is: it \
selects the input contract of the whole CLI's clap-shaped parser",
));
}
return Ok(());
}

Expand Down Expand Up @@ -2524,9 +2535,12 @@ pub fn type_name(ty: &Type) -> String {
}
}

/// The `#[usage(...)]` attributes on an item.
/// The native `#[usage(...)]` or clap-compatible `#[command(...)]`
/// attributes on a command struct.
fn attrs(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
attrs.iter().filter(|a| a.path().is_ident("usage"))
attrs
.iter()
.filter(|a| a.path().is_ident("usage") || a.path().is_ident("command"))
}

/// Value metadata accepts clap's `#[value(...)]` spelling so an enum can keep its
Expand Down