diff --git a/PLAN.md b/PLAN.md index 8da84970d..03c6f49b4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -452,14 +452,10 @@ Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and carries doc-comment or explicit help, hidden canonical values, hidden `alias`, visible `visible_alias`, and case-insensitive matching through its static metadata and lossless KDL emission. -- [x] **`infer_subcommands` / `infer_long_args`** — unambiguous prefixes of - command names, command aliases, long flags, and long aliases are accepted - only when one declaration matches. Both settings are inherited by child - commands and carried through KDL, usage-lib, generated Rust, generated Go, - and Go's runtime spec builder. `#[usage(infer_subcommands, infer_long_args)]` - is the typed spelling. clap exposes setters but no public getters for these - global settings, so `clap_usage` cannot recover them from an existing - `Command`; a migration declares them on the usage type. +- [x] **`infer_subcommands` / `infer_long_args` are intentional non-goals.** Long + flags and subcommands require exact spellings. Diagnostics may suggest what + was probably meant, but accepting a prefix would let a later declaration + change or invalidate an existing invocation. - [x] **`external_subcommand`** — an unmatched word is forwarded with the rest of argv. Spec `external_subcommand`, usage-lib, usage-argv, the derive (`#[usage(external_subcommand)]` on a catch-all `Vec` variant), and the diff --git a/argv/src/help.rs b/argv/src/help.rs index 602e92100..0c1cdcf96 100644 --- a/argv/src/help.rs +++ b/argv/src/help.rs @@ -1249,7 +1249,6 @@ pub fn route_to<'t>( if route.is_empty() { route.push(root); } - let mut infer_subcommands = route.iter().any(|cmd| cmd.infer_subcommands); // Already there for `--help`, whose span is empty. For the `help` word the parse stopped at // the command that *saw* it, and the words naming the one being asked about are exactly the @@ -1270,13 +1269,8 @@ pub fn route_to<'t>( // that reached here did. Matching on name and alias together instead answered with // whichever subcommand came first, which for a colliding word is a different command // than the one the parser selected. - let next = if infer_subcommands { - crate::find_prefixed(here, word)? - } else { - crate::find_named(here, word)? - }; + let next = crate::find_named(here, word)?; route.push(next); - infer_subcommands |= next.infer_subcommands; } // Only if the walk actually arrived: a caller should fall back rather than be handed a // page about some other command. diff --git a/argv/src/lib.rs b/argv/src/lib.rs index 815bfbb85..1fdb4b691 100644 --- a/argv/src/lib.rs +++ b/argv/src/lib.rs @@ -189,12 +189,6 @@ pub struct Command<'a> { /// bound values: an environment variable or default may fill a field, but neither means /// the user supplied an argument to this invocation. pub arg_required_else_help: bool, - /// Accept an unambiguous prefix of a subcommand name or alias. - /// Inherited by nested commands. - pub infer_subcommands: bool, - /// Accept an unambiguous prefix of a long flag or alias. - /// Inherited by nested commands. - pub infer_long_args: bool, /// What an unrecognized flag-like token means here, or `None` to keep whatever the /// enclosing command said. See [`UnknownFlags`]. /// @@ -237,8 +231,6 @@ impl Command<'_> { default_subcommand: ::core::option::Option::None, external_subcommand: false, arg_required_else_help: false, - infer_subcommands: false, - infer_long_args: false, unknown_flags: ::core::option::Option::None, version: false, key: 0, @@ -812,41 +804,6 @@ pub(crate) fn find_named<'t>(cmd: &'t Command<'t>, name: &[u8]) -> Option<&'t Co .or_else(|| subcommands().find(|c| c.aliases.iter().any(|a| a.as_bytes() == name))) } -fn find_prefixed<'t>(cmd: &'t Command<'t>, name: &[u8]) -> Option<&'t Command<'t>> { - if let Some(exact) = find_named(cmd, name) { - return Some(exact); - } - if name.is_empty() { - return None; - } - let mut found = None; - for command in cmd.subcommands { - if !command.name.as_bytes().starts_with(name) - && !command - .aliases - .iter() - .any(|alias| alias.as_bytes().starts_with(name)) - { - continue; - } - if found.is_some() { - return None; - } - found = Some(*command); - } - found -} - -fn has_prefixed_subcommand(cmd: &Command<'_>, name: &[u8]) -> bool { - cmd.subcommands.iter().any(|command| { - command.name.as_bytes().starts_with(name) - || command - .aliases - .iter() - .any(|alias| alias.as_bytes().starts_with(name)) - }) -} - /// What a caller should print for a parse failure, and what to exit with. /// /// The one entry point a generated `parse()` reaches for, and the reason it exists here rather @@ -1051,8 +1008,6 @@ pub struct Parser<'t, 'a, 'v> { /// nothing keeps what the enclosing one said, and walking back up the ancestors on /// every unrecognized token would pay for the inheritance at the wrong moment. unknown_flags: UnknownFlags, - infer_subcommands: bool, - infer_long_args: bool, /// The chain above `cmd`, used to find inherited global flags. Fixed size so /// that nothing is allocated. ancestors: [Option<&'t Command<'t>>; MAX_DEPTH], @@ -1119,8 +1074,6 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { // Nothing above the root to inherit from, so the default stands. ::core::option::Option::None => UnknownFlags::Value, }, - infer_subcommands: root.infer_subcommands, - infer_long_args: root.infer_long_args, ancestors: [None; MAX_DEPTH], depth: 0, bundle: &[], @@ -1357,8 +1310,8 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { None => (body, None), }; - if let Some((flag, negated)) = self.find_long_form(name) { - let value = if flag.takes_value && !negated { + if let Some(flag) = self.find_long(name) { + let value = if flag.takes_value { Some(match attached { Some(v) => v, None => self.take_detached_value(flag)?, @@ -1366,13 +1319,21 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { } else { None }; - if flag.variadic && !negated { + if flag.variadic { self.start_collecting(flag, value.unwrap_or(b""))?; } return Ok(Event::Flag { flag, value, - negated, + negated: false, + }); + } + + if let Some(flag) = self.find_negation(name) { + return Ok(Event::Flag { + flag, + value: None, + negated: true, }); } @@ -1494,20 +1455,7 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { // positional of this command has taken a word, a later word that happens // to equal a subcommand name is just a value. if !self.arg_filled && !self.flags_stopped { - // Exact declared commands outrank the built-in help word. In inferred mode the - // built-in participates as a real candidate, so `he` reaches help only when a - // sibling such as `helper` does not make the prefix ambiguous. - if let Some(sub) = find_named(self.cmd, token) { - self.descend(sub)?; - return Ok(Event::Command(sub)); - } - let help_prefix = - !token.is_empty() && b"help".starts_with(token) && !self.cmd.subcommands.is_empty(); - let inferred = self - .infer_subcommands - .then(|| find_prefixed(self.cmd, token)) - .flatten(); - if let Some(sub) = inferred.filter(|_| !help_prefix) { + if let Some(sub) = self.find_subcommand(token) { self.descend(sub)?; return Ok(Event::Command(sub)); } @@ -1523,25 +1471,14 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { // // The words after it name a command, resolved here rather than descended into: // descending would bind them, and they are a question rather than an invocation. - if (token == b"help" - || (self.infer_subcommands - && help_prefix - && !has_prefixed_subcommand(self.cmd, token))) - && !self.cmd.subcommands.is_empty() - { + if token == b"help" && !self.cmd.subcommands.is_empty() { let mut cmd = self.cmd; - let mut infer_subcommands = self.infer_subcommands; let from = self.pos; while let Some(next) = self.argv.get(self.pos) { - let Some(sub) = (if infer_subcommands { - find_prefixed(cmd, bytes(next)) - } else { - find_named(cmd, bytes(next)) - }) else { + let Some(sub) = find_named(cmd, bytes(next)) else { break; }; cmd = sub; - infer_subcommands |= sub.infer_subcommands; self.pos += 1; } // Kept for `help::route_to`: which mount was asked about is not recoverable @@ -1647,8 +1584,6 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { self.starts[self.depth] = self.cmd_start; self.depth += 1; self.cmd = sub; - self.infer_subcommands |= sub.infer_subcommands; - self.infer_long_args |= sub.infer_long_args; // Only a command that says something changes it, which is what inheriting means. if let ::core::option::Option::Some(mode) = sub.unknown_flags { self.unknown_flags = mode; @@ -1710,89 +1645,14 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { own.chain(inherited) } - fn find_long_form(&self, name: &[u8]) -> Option<(&'t Flag<'t>, bool)> { - if let Some(flag) = self - .in_scope() - .find(|f| f.longs.iter().any(|long| long.as_bytes() == name)) - { - return Some((flag, false)); - } - if let Some(flag) = self - .in_scope() - .find(|f| f.negate.is_some_and(|negate| negate.as_bytes() == name)) - { - return Some((flag, true)); - } - // Built-ins are exact entries too, below an authored spelling and above inference. - if name == b"help" { - return Some((&HELP_LONG, false)); - } - if name == b"version" && self.cmd.version { - return Some((&VERSION_LONG, false)); - } - if !self.infer_long_args || name.is_empty() { - return None; - } - - let mut found: Option<(&Flag<'_>, bool)> = None; - for flag in self.in_scope() { - let positive = flag.longs.iter().any(|long| { - long.as_bytes().starts_with(name) - && !self.long_form_is_shadowed(flag, long.as_bytes()) - }); - let negative = !positive - && flag.negate.is_some_and(|negate| { - negate.as_bytes().starts_with(name) - && !self.long_form_is_shadowed(flag, negate.as_bytes()) - }); - if !positive && !negative { - continue; - } - if found.is_some_and(|(prior, _)| !::core::ptr::eq(prior, flag)) { - return None; - } - found = Some((flag, negative)); - } - for (spelling, flag) in [ - (b"help".as_slice(), &HELP_LONG), - (b"version".as_slice(), &VERSION_LONG), - ] { - if (spelling == b"version" && !self.cmd.version) - || !spelling.starts_with(name) - || self.in_scope().any(|candidate| { - candidate - .longs - .iter() - .any(|long| long.as_bytes() == spelling) - }) - { - continue; - } - if found.is_some_and(|(prior, _)| !::core::ptr::eq(prior, flag)) { - return None; - } - found = Some((flag, false)); - } - found + fn find_long(&self, name: &[u8]) -> Option<&'t Flag<'t>> { + self.in_scope() + .find(|f| f.longs.iter().any(|l| l.as_bytes() == name)) } - /// Whether a nearer declaration already owns this exact long spelling. - /// - /// `in_scope` is ordered by precedence. Prefix inference must apply the same - /// shadowing as exact lookup: redeclaring `--verbose` on a child does not make - /// `--verb` ambiguous merely because an inherited global also spells it that way. - fn long_form_is_shadowed(&self, flag: &Flag<'_>, form: &[u8]) -> bool { - for prior in self.in_scope() { - if ::core::ptr::eq(prior, flag) { - return false; - } - if prior.longs.iter().any(|long| long.as_bytes() == form) - || prior.negate.is_some_and(|negate| negate.as_bytes() == form) - { - return true; - } - } - false + fn find_negation(&self, name: &[u8]) -> Option<&'t Flag<'t>> { + self.in_scope() + .find(|f| f.negate.is_some_and(|n| n.as_bytes() == name)) } fn find_short(&self, byte: u8) -> Option<&'t Flag<'t>> { @@ -1808,6 +1668,12 @@ impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> { None }) } + + fn find_subcommand(&self, name: &[u8]) -> Option<&'t Command<'t>> { + // Shared with `help` rather than spelled out again, so descending into a command and + // asking about one cannot drift apart. + find_named(self.cmd, name) + } } /// View a token as bytes. @@ -2086,39 +1952,6 @@ mod tests { assert_eq!(value, Some(&b"-1"[..])); } - #[test] - fn negation_of_value_flag_does_not_consume_a_value() { - static MODE: Flag = Flag { - key: 9, - name: "mode", - longs: &["mode"], - negate: Some("no-mode"), - ..Flag::VALUE - }; - static NEGATED_VALUE: Command = Command { - name: "ex", - flags: &[&MODE], - args: &[&FILE], - ..Command::EMPTY - }; - - let a = argv(["--no-mode", "input"]); - assert_eq!( - parse(&NEGATED_VALUE, &a).unwrap(), - vec![ - Event::Flag { - flag: &MODE, - value: None, - negated: true - }, - Event::Arg { - arg: &FILE, - value: b"input" - } - ] - ); - } - #[test] fn no_abbreviation() { // A prefix names no flag, so by default it is a value like any other word. @@ -2572,81 +2405,6 @@ mod tests { assert_unique_subcommand_names(&[&INSTALL, &ADD]); } - #[test] - #[cfg(feature = "spec")] - fn inferred_help_keeps_the_route_it_resolved() { - let root = Command { - name: "ex", - subcommands: &[&INSTALL], - infer_subcommands: true, - ..Command::EMPTY - }; - let a = argv(["help", "insta"]); - let Err(Error::Help { cmd, .. }) = parse(&root, &a) else { - panic!("expected inferred help") - }; - let route = crate::help::route_to(&root, &a, cmd).expect("the inferred route"); - assert_eq!(route.len(), 2); - assert!(::core::ptr::eq(route[1], &INSTALL)); - } - - #[test] - fn inferred_builtins_participate_in_exactness_and_ambiguity() { - static HELP_ALL: Flag = Flag { - key: 380, - name: "help-all", - longs: &["help-all"], - ..Flag::BOOL - }; - static HELPER: Command = Command { - key: 381, - name: "helper", - ..Command::EMPTY - }; - let flags = Command { - name: "ex", - flags: &[&HELP_ALL], - infer_long_args: true, - unknown_flags: Some(UnknownFlags::Error), - ..Command::EMPTY - }; - let exact = argv(["--help"]); - assert!(matches!( - parse(&flags, &exact), - Ok(events) if matches!(events.as_slice(), [Event::Flag { flag, .. }] if is_help_flag(flag)) - )); - let ambiguous = argv(["--he"]); - assert!(matches!( - parse(&flags, &ambiguous), - Err(Error::UnknownFlag { .. }) - )); - - let lone = Command { - name: "ex", - infer_long_args: true, - ..Command::EMPTY - }; - let inferred = argv(["--he"]); - assert!(matches!( - parse(&lone, &inferred), - Ok(events) if matches!(events.as_slice(), [Event::Flag { flag, .. }] if is_help_flag(flag)) - )); - - let commands = Command { - name: "ex", - subcommands: &[&HELPER], - infer_subcommands: true, - ..Command::EMPTY - }; - let exact = argv(["help"]); - assert!(matches!(parse(&commands, &exact), Err(Error::Help { .. }))); - let ambiguous = argv(["he"]); - assert!(matches!( - parse(&commands, &ambiguous), - Err(Error::UnexpectedArg { .. }) - )); - } - #[test] fn the_word_is_re_examined_against_the_command_it_reached() { // The reason the cursor steps back rather than the token being consumed: `lint` names diff --git a/argv/src/spec.rs b/argv/src/spec.rs index f3efdd780..c23951abc 100644 --- a/argv/src/spec.rs +++ b/argv/src/spec.rs @@ -1085,12 +1085,6 @@ impl Spec<'_> { if self.root.cmd.arg_required_else_help { writeln!(out, "arg_required_else_help #true")?; } - if self.root.cmd.infer_subcommands { - writeln!(out, "infer_subcommands #true")?; - } - if self.root.cmd.infer_long_args { - writeln!(out, "infer_long_args #true")?; - } // A `complete` block for every completer this CLI declares, naming the command that // asks the binary itself. Written rather than declared, so there is one place a // completer is said to exist: the Rust function. Everything that reads a spec — the @@ -1334,12 +1328,6 @@ fn write_command<'a>( if meta.cmd.arg_required_else_help { out.push_str(" arg_required_else_help=#true"); } - if meta.cmd.infer_subcommands { - out.push_str(" infer_subcommands=#true"); - } - if meta.cmd.infer_long_args { - out.push_str(" infer_long_args=#true"); - } out.push_str(" {\n"); let inner = depth + 1; diff --git a/conformance/src/tables.rs b/conformance/src/tables.rs index 655ec90ab..6b9c4a050 100644 --- a/conformance/src/tables.rs +++ b/conformance/src/tables.rs @@ -110,8 +110,6 @@ pub fn build( unknown_flags, external_subcommand: cmd.external_subcommand, arg_required_else_help: cmd.arg_required_else_help, - infer_subcommands: cmd.infer_subcommands, - infer_long_args: cmd.infer_long_args, key: 0, })); diff --git a/conformance/tests/infer_prefixes.rs b/conformance/tests/infer_prefixes.rs deleted file mode 100644 index d9f11d097..000000000 --- a/conformance/tests/infer_prefixes.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Unambiguous long-flag and subcommand prefixes, through both Rust parsers. - -use std::ffi::OsStr; - -use usage::Spec as LibSpec; -use usage_derive::{Args, Cli, Subcommands}; - -#[derive(Cli)] -#[usage( - name = "ex", - unknown_flags = "error", - infer_subcommands, - infer_long_args -)] -struct Ex { - #[usage(long, global)] - verbose: bool, - #[usage(long)] - verify: bool, - #[usage(long, negate = "--no-color")] - color: bool, - #[usage(subcommand)] - command: Option, -} - -#[derive(Subcommands)] -enum Commands { - Install(Install), - Inspect(Inspect), - #[usage(alias = "uninstall")] - Remove(Remove), -} - -#[derive(Args)] -struct Install { - #[usage(long)] - forceful: bool, - #[usage(long)] - verbose: bool, -} - -#[derive(Args)] -struct Inspect; - -#[derive(Args)] -struct Remove; - -fn argv(tokens: [&str; N]) -> [&OsStr; N] { - tokens.map(OsStr::new) -} - -fn words(tokens: [&str; N]) -> Vec { - tokens.iter().map(|token| token.to_string()).collect() -} - -#[test] -fn the_typed_parser_accepts_only_unique_prefixes() { - let parsed = Ex::parse_from(&argv(["insta", "--for"])) - .expect("root inference should reach the child and stay enabled there"); - let Some(Commands::Install(install)) = parsed.command else { - panic!("expected install") - }; - assert!(install.forceful); - - let parsed = Ex::parse_from(&argv(["install", "--verb"])) - .expect("a child redeclaration should shadow the inherited global for prefixes"); - let Some(Commands::Install(install)) = parsed.command else { - panic!("expected install") - }; - assert!(install.verbose); - assert!(!parsed.verbose); - - assert!( - matches!( - Ex::parse_from(&argv(["uni"])) - .expect("an alias prefix should route") - .command, - Some(Commands::Remove(_)) - ), - "aliases participate in inference" - ); - - assert!(Ex::parse_from(&argv(["ins"])).is_err()); - assert!(Ex::parse_from(&argv(["--ver"])).is_err()); - - let verbose = Ex::parse_from(&argv(["--verb"])).expect("a unique long prefix should bind"); - assert!(verbose.verbose); - assert!(!verbose.verify); - - let color = Ex::parse_from(&argv(["--col"])).expect("a positive prefix should enable"); - assert!(color.color); - let color = Ex::parse_from(&argv(["--no-col"])).expect("a negated prefix should disable"); - assert!(!color.color); - - let exact = Ex::parse_from(&argv(["install"])).expect("exact names still outrank prefixes"); - assert!(matches!(exact.command, Some(Commands::Install(_)))); -} - -#[test] -fn emitted_kdl_gives_usage_lib_the_same_policy() { - let kdl = Ex::to_kdl(); - assert!(kdl.contains("infer_subcommands #true"), "{kdl}"); - assert!(kdl.contains("infer_long_args #true"), "{kdl}"); - let spec: LibSpec = kdl.parse().expect("valid spec"); - - let parsed = usage::parse::parse(&spec, &words(["ex", "insta", "--for"])) - .expect("usage-lib should accept the same unique prefixes"); - assert_eq!(parsed.cmd.name, "install"); - - assert!(usage::parse::parse(&spec, &words(["ex", "ins"])).is_err()); - assert!(usage::parse::parse(&spec, &words(["ex", "--ver"])).is_err()); - - let positive = usage::parse::parse(&spec, &words(["ex", "--col"])) - .expect("a positive prefix should enable"); - assert!(matches!( - positive - .flags - .values() - .find(|value| matches!(value, usage::parse::ParseValue::Bool(true))), - Some(usage::parse::ParseValue::Bool(true)) - )); - let negative = usage::parse::parse(&spec, &words(["ex", "--no-col"])) - .expect("a negated prefix should disable"); - assert!(negative - .flags - .values() - .any(|value| matches!(value, usage::parse::ParseValue::Bool(false)))); -} diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index e2a35ddac..353f3df18 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -134,8 +134,6 @@ pub fn emit(cli: &Cli) -> TokenStream { // version resolved it here and wrote `Value` for every silent command, which made the // root's declaration reach the root alone. let unknown_flags = unknown_flags_tokens(cli); - let infer_subcommands = cli.infer_subcommands; - let infer_long_args = cli.infer_long_args; let default_subcommand = option_str(cli.default_subcommand.as_deref()); let multicall = cli.multicall; @@ -421,8 +419,6 @@ pub fn emit(cli: &Cli) -> TokenStream { // `--version` that answers with nothing is worse than one that is not there. version: #has_version, unknown_flags: #unknown_flags, - infer_subcommands: #infer_subcommands, - infer_long_args: #infer_long_args, arg_required_else_help: #arg_required_else_help, name: #name, key: #root_key, @@ -3314,8 +3310,6 @@ pub fn emit_args(cli: &Cli) -> TokenStream { ) }); let unknown_flags = unknown_flags_tokens(cli); - let infer_subcommands = cli.infer_subcommands; - let infer_long_args = cli.infer_long_args; let arg_required_else_help = cli.arg_required_else_help; let before_help = option_expr(cli.before_help.as_ref()); let before_long_help = option_expr(cli.before_long_help.as_ref()); @@ -3426,8 +3420,6 @@ pub fn emit_args(cli: &Cli) -> TokenStream { aliases: &[#(#aliases),*], key: #command_key, unknown_flags: #unknown_flags, - infer_subcommands: #infer_subcommands, - infer_long_args: #infer_long_args, arg_required_else_help: #arg_required_else_help, flags: #flag_table_ref, args: #arg_table_ref, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index d79d19bdf..56f58f311 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -203,8 +203,6 @@ //! `verbatim_doc_comment` — preserve doc-comment line breaks and whitespace — //! `default_subcommand`, `multicall` — argv[0]'s basename selects a subcommand — //! `arg_required_else_help` — a selected command with no argv of its own shows short help — -//! `infer_subcommands`, `infer_long_args` — accept unambiguous prefixes and inherit that -//! policy through nested commands — //! `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 diff --git a/derive/src/model.rs b/derive/src/model.rs index 6ffed623b..526427e06 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -94,10 +94,6 @@ pub struct Cli { /// Whether a flag-like token that names no flag is a value or an error. Unset /// means the spec's default, which is `value`. pub unknown_flags: Option, - /// Accept unambiguous prefixes of subcommands, inherited by children. - pub infer_subcommands: bool, - /// Accept unambiguous prefixes of long flags, inherited by children. - pub infer_long_args: bool, /// The command a bare invocation means: `mise build` is `mise run build`. /// /// Only the root has one, and it is what mise sets by hand on the emitted spec today. @@ -528,8 +524,6 @@ impl Cli { about: None, long_about: None, unknown_flags: None, - infer_subcommands: false, - infer_long_args: false, default_subcommand: None, multicall: false, no_binary_name: false, @@ -672,8 +666,6 @@ impl Cli { "multicall" => cli.multicall = flag_value(&meta)?, "no_binary_name" => cli.no_binary_name = flag_value(&meta)?, "arg_required_else_help" => cli.arg_required_else_help = flag_value(&meta)?, - "infer_subcommands" => cli.infer_subcommands = flag_value(&meta)?, - "infer_long_args" => cli.infer_long_args = 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)?), @@ -685,8 +677,8 @@ impl Cli { format!( "unknown option `{other}` on a struct; usage::Cli takes \ `name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `usage`, `verbatim_doc_comment`, `unknown_flags`, \ - `default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, `infer_subcommands`, \ - `infer_long_args`, `next_help_heading`, `restart_token`, `mount` and \ + `default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, \ + `next_help_heading`, `restart_token`, `mount` and \ `group` here, and the description comes from the doc \ comment" ), diff --git a/docs/rust/clap-compatibility.md b/docs/rust/clap-compatibility.md index 482588677..8a3dc4a5a 100644 --- a/docs/rust/clap-compatibility.md +++ b/docs/rust/clap-compatibility.md @@ -90,11 +90,11 @@ the Rust declaration, not only from generated KDL, wherever the bridge column sa | positional conflicts | yes | yes | yes | yes | yes | yes | Bare selectors name positionals; dashed selectors name flags. | | other relationships declared on positionals | no | no | no | no | no | no | `requires`, conditional requiredness, and overrides still originate on flags. | | relationships through `flatten` | lossy | lossy | yes | yes | yes | lossy | A declaring type cannot yet validate a selector supplied by a flattened sibling. | -| global flags | yes | yes | yes | yes | yes | yes | Exact lookup and inferred prefixes preserve child shadowing. | +| global flags | yes | yes | yes | yes | yes | yes | Exact lookup preserves child shadowing. | | `allow_external_subcommands` | yes | yes | yes | yes | yes | yes | Use an `#[usage(external_subcommand)]` catch-all variant. | | `multicall` | yes | yes | yes | yes | yes | yes | Process-level entry points route using the executable basename. | | `no_binary_name` | yes | yes | n/a | yes | n/a | yes | `parse_from` is words-only; full-argv helpers honor the command policy. | -| `infer_subcommands`, `infer_long_args` | yes | yes | yes | yes | yes | usage-only | Unambiguous names and aliases are inherited; clap exposes no public getter. | +| `infer_subcommands`, `infer_long_args` | no | no | no | no | no | no | Intentional non-goal: usage requires exact flag and subcommand spellings. | | `arg_required_else_help` | yes | yes | yes | yes | yes | yes | Bare selected commands request short help; defaults and environment values do not count as argv. | | remaining subcommand/argument policies | no | no | no | no | no | no | `args_conflicts_with_subcommands`, precedence, missing positionals, and self-override are not represented. | | unknown flags | different | different | different | different | yes | different | usage is permissive by default; `unknown_flags = "error"` opts into strict parsing. | diff --git a/docs/rust/index.md b/docs/rust/index.md index acfa218b5..665759107 100644 --- a/docs/rust/index.md +++ b/docs/rust/index.md @@ -153,8 +153,8 @@ equivalent yet: binds; arbitrary zero-or-one value ranges from clap are not inferred. - Rust `value_parser` functions are not portable metadata. Values use `FromStr`; use `validate` for a portable expression rule and `validate_error` for its diagnostic. -- Prefix matching is exact by default. `#[usage(infer_long_args, infer_subcommands)]` opts into - unambiguous long-flag and subcommand prefixes, including aliases. +- Long flags and subcommands require exact spellings. Diagnostics can suggest a close match, but + usage does not accept prefixes whose meaning could change when another declaration is added. - On Unix, `PathBuf` and `OsString` fields accept non-UTF-8 argv without changing a byte. String fields still report invalid UTF-8 precisely rather than replacing it; on Windows, values that cannot be converted safely are reported instead of using an unchecked reconstruction. diff --git a/docs/rust/migrating-from-clap.md b/docs/rust/migrating-from-clap.md index e2c0cdea8..bc0dfd817 100644 --- a/docs/rust/migrating-from-clap.md +++ b/docs/rust/migrating-from-clap.md @@ -65,10 +65,6 @@ enum Command { Unknown flags are values by default, which is useful for wrapper CLIs. Add `unknown_flags = "error"` on each command where unknown flag-like words must be rejected. -Clap's global prefix settings migrate as -`#[usage(infer_subcommands, infer_long_args)]`; they are inherited by nested commands. Because -clap exposes setters but not getters for these settings, a `clap::Command` bridge cannot infer -that declaration for you. `#[command(arg_required_else_help)]` migrates in place. usage checks whether the selected command received an argv token; environment and default fallbacks do not count. diff --git a/docs/spec/reference/cmd.md b/docs/spec/reference/cmd.md index 47afd5807..2ac0fe7a1 100644 --- a/docs/spec/reference/cmd.md +++ b/docs/spec/reference/cmd.md @@ -169,27 +169,6 @@ The policy observes argv belonging to the selected command. A global flag before subcommand does not count as that subcommand's argument, and values supplied later by an environment variable or default do not suppress help. -### Inferred prefixes - -A command can accept an unambiguous prefix of a subcommand name or alias, a long -flag, or a long flag alias: - -```kdl -infer_subcommands #true -infer_long_args #true - -cmd "install" { - alias "add" -} -flag "--verbose" -``` - -Here `mycli insta`, `mycli a`, and `mycli --verb` resolve to their full -declarations. A prefix matching two different commands or flags is not accepted; -an exact spelling always wins. Both settings are inherited by nested commands and -may also be enabled for one subtree with `infer_subcommands=#true` or -`infer_long_args=#true` on its `cmd` node. - ### Global flags and mounted commands A mounted command describes a different program, so the flags of the commands it is mounted under diff --git a/go/argv/argv.go b/go/argv/argv.go index b46c8a627..3a42a0b42 100644 --- a/go/argv/argv.go +++ b/go/argv/argv.go @@ -70,10 +70,6 @@ type Command struct { ExternalSubcommand bool // ArgRequiredElseHelp shows this command's help when no argv token follows its name. ArgRequiredElseHelp bool - // InferSubcommands accepts unambiguous prefixes of names and aliases. - InferSubcommands bool - // InferLongArgs accepts unambiguous prefixes of long flag names and aliases. - InferLongArgs bool // UnknownFlags is what an unrecognized flag-like token means here. Already // resolved: inheritance is a question for whoever builds the tables. UnknownFlags UnknownFlags diff --git a/go/argv/parser.go b/go/argv/parser.go index afa5df9e4..f02675c84 100644 --- a/go/argv/parser.go +++ b/go/argv/parser.go @@ -1,7 +1,5 @@ package argv -import "strings" - // Parser reads a command line once, left to right, against static tables. // // There is no backtracking, no reordering, and no second pass: what a token binds @@ -37,9 +35,6 @@ type Parser struct { pos int // cmd is the command currently in scope. cmd *Command - // Prefix policies are inherited as the parser descends. - inferSubcommands bool - inferLongArgs bool // ancestors is the chain above cmd, used to find inherited global flags. // Fixed size so that nothing is allocated. ancestors [MaxDepth]*Command @@ -92,12 +87,7 @@ type Parser struct { // New begins parsing argv against root. argv excludes the program name. func New(root *Command, argv []string) Parser { - return Parser{ - argv: argv, - cmd: root, - inferSubcommands: root.InferSubcommands, - inferLongArgs: root.InferLongArgs, - } + return Parser{argv: argv, cmd: root} } // Next reads the next event, reporting false when argv is exhausted or the parse @@ -296,10 +286,10 @@ func (p *Parser) longFlag(token string) bool { } } - if flag, negated := p.findLongForm(name); flag != nil { + if flag := p.findLong(name); flag != nil { value := "" hasValue := false - if flag.TakesValue && !negated { + if flag.TakesValue { hasValue = true if hasAttached { value = attached @@ -314,10 +304,14 @@ func (p *Parser) longFlag(token string) bool { value = v } } - if flag.Variadic && !negated { + if flag.Variadic { p.startCollecting(flag) } - return p.emit(Event{Kind: KindFlag, Flag: flag, Value: value, HasValue: hasValue, Negated: negated}) + return p.emit(Event{Kind: KindFlag, Flag: flag, Value: value, HasValue: hasValue}) + } + + if flag := p.findNegation(name); flag != nil { + return p.emit(Event{Kind: KindFlag, Flag: flag, Negated: true}) } // Where the CLI declared a version, --version answers with it — asked after the @@ -452,20 +446,12 @@ func (p *Parser) word(token string) bool { // positional of this command has taken a word, a later word that happens to // equal a subcommand name is just a value. if !p.argFilled && !p.flagsStopped { - if sub := findNamed(p.cmd, token); sub != nil { + if sub := p.findSubcommand(token); sub != nil { if !p.descend(sub) { return false } return p.emit(Event{Kind: KindCommand, Command: sub}) } - helpPrefix := token != "" && strings.HasPrefix("help", token) && len(p.cmd.Subcommands) > 0 - inferred := p.findSubcommand(token) - if inferred != nil && !helpPrefix { - if !p.descend(inferred) { - return false - } - return p.emit(Event{Kind: KindCommand, Command: inferred}) - } // `ex help config ls` — the line every page with a Commands section prints. // Asked after the subcommand lookup, so a CLI that declares a help of its own @@ -474,16 +460,14 @@ func (p *Parser) word(token string) bool { // The words after it name a command, resolved here rather than descended // into: descending would bind them, and they are a question rather than an // invocation. - if (token == "help" || (p.inferSubcommands && helpPrefix && !hasPrefixedSubcommand(p.cmd, token))) && len(p.cmd.Subcommands) > 0 { + if token == "help" && len(p.cmd.Subcommands) > 0 { cmd := p.cmd - inferSubcommands := p.inferSubcommands for p.pos < len(p.argv) { - sub := findNamedWithPrefix(cmd, p.argv[p.pos], inferSubcommands) + sub := findNamed(cmd, p.argv[p.pos]) if sub == nil { break } cmd = sub - inferSubcommands = inferSubcommands || sub.InferSubcommands p.pos++ } // The long form, as `ex config --help` gives: someone who typed a whole word @@ -564,8 +548,6 @@ func (p *Parser) descend(sub *Command) bool { p.starts[p.depth] = p.cmdStart p.depth++ p.cmd = sub - p.inferSubcommands = p.inferSubcommands || sub.InferSubcommands - p.inferLongArgs = p.inferLongArgs || sub.InferLongArgs // Where this command's own words start, which is what lets a completion hand a // callback the half-parsed struct of the command it was declared on rather than // of the root. @@ -637,94 +619,21 @@ func (p *Parser) FlagsInScope(fn func(*Flag) bool) { p.eachInScope(fn) } -func (p *Parser) findLongForm(name string) (*Flag, bool) { - if found := p.eachInScope(func(f *Flag) bool { +func (p *Parser) findLong(name string) *Flag { + return p.eachInScope(func(f *Flag) bool { for _, l := range f.Longs { if l == name { return true } } return false - }); found != nil { - return found, false - } - if found := p.eachInScope(func(f *Flag) bool { - return f.Negate != "" && f.Negate == name - }); found != nil { - return found, true - } - if name == "help" { - return HelpLong, false - } - if name == "version" && p.cmd.Version { - return VersionLong, false - } - if !p.inferLongArgs || name == "" { - return nil, false - } - var found *Flag - negated := false - ambiguous := false - p.eachInScope(func(f *Flag) bool { - positive := false - for _, long := range f.Longs { - if len(long) >= len(name) && long[:len(name)] == name && !p.longFormIsShadowed(f, long) { - positive = true - break - } - } - negative := !positive && f.Negate != "" && len(f.Negate) >= len(name) && f.Negate[:len(name)] == name && !p.longFormIsShadowed(f, f.Negate) - if !positive && !negative { - return false - } - if found != nil && found != f { - ambiguous = true - return true - } - found, negated = f, negative - return false }) - for _, builtin := range []struct { - name string - flag *Flag - ok bool - }{{"help", HelpLong, true}, {"version", VersionLong, p.cmd.Version}} { - if !builtin.ok || !strings.HasPrefix(builtin.name, name) || p.longFormIsShadowed(builtin.flag, builtin.name) { - continue - } - if found != nil && found != builtin.flag { - ambiguous = true - break - } - found, negated = builtin.flag, false - } - if ambiguous { - return nil, false - } - return found, negated } -// longFormIsShadowed reports whether a nearer declaration already owns this -// exact spelling. Prefix lookup must preserve exact lookup's shadowing rule. -func (p *Parser) longFormIsShadowed(flag *Flag, form string) bool { - shadowed := false - p.eachInScope(func(prior *Flag) bool { - if prior == flag { - return true - } - for _, long := range prior.Longs { - if long == form { - shadowed = true - return true - } - } - if prior.Negate == form { - shadowed = true - return true - } - return false +func (p *Parser) findNegation(name string) *Flag { + return p.eachInScope(func(f *Flag) bool { + return f.Negate != "" && f.Negate == name }) - return shadowed } func (p *Parser) findShort(b byte) *Flag { @@ -751,42 +660,7 @@ func (p *Parser) findShort(b byte) *Flag { } func (p *Parser) findSubcommand(name string) *Command { - return findNamedWithPrefix(p.cmd, name, p.inferSubcommands) -} - -func findNamedWithPrefix(cmd *Command, name string, infer bool) *Command { - if exact := findNamed(cmd, name); exact != nil || !infer || name == "" { - return exact - } - var found *Command - for _, sub := range cmd.Subcommands { - matches := len(sub.Name) >= len(name) && sub.Name[:len(name)] == name - for _, alias := range sub.Aliases { - matches = matches || len(alias) >= len(name) && alias[:len(name)] == name - } - if !matches { - continue - } - if found != nil { - return nil - } - found = sub - } - return found -} - -func hasPrefixedSubcommand(cmd *Command, name string) bool { - for _, sub := range cmd.Subcommands { - if strings.HasPrefix(sub.Name, name) { - return true - } - for _, alias := range sub.Aliases { - if strings.HasPrefix(alias, name) { - return true - } - } - } - return false + return findNamed(p.cmd, name) } // findNamed resolves a word against a command's subcommands, by name or alias. diff --git a/go/argv/parser_test.go b/go/argv/parser_test.go index 7b4b571af..7b3053e79 100644 --- a/go/argv/parser_test.go +++ b/go/argv/parser_test.go @@ -136,14 +136,6 @@ func TestBinding(t *testing.T) { } } -func TestNegatedValueFlagDoesNotConsumeTheNextWord(t *testing.T) { - mode := &Flag{Key: 20, Name: "mode", Longs: []string{"mode"}, Negate: "no-mode", TakesValue: true} - cmd := &Command{Name: "ex", Flags: []*Flag{mode}, Args: []*Arg{file}} - if got := collect(cmd, "--no-mode", "input"); got != "flag:mode! arg:file=input" { - t.Fatalf("negation consumed the positional: %s", got) - } -} - // TestBundleIsRejectedWhole pins the rule that costs the parser a second scan. // // A token containing an unrecognized letter is not a bundle at all, so none of its @@ -408,78 +400,6 @@ func TestExternalSubcommand(t *testing.T) { } } -func TestInferredPrefixes(t *testing.T) { - verify := &Flag{Key: 21, Name: "verify", Longs: []string{"verify"}} - forceful := &Flag{Key: 22, Name: "forceful", Longs: []string{"forceful"}} - install := &Command{Name: "install", Flags: []*Flag{forceful}} - inspect := &Command{Name: "inspect"} - remove := &Command{Name: "remove", Aliases: []string{"uninstall"}} - cmd := &Command{ - Name: "ex", - Flags: []*Flag{verbose, verify}, - Subcommands: []*Command{install, inspect, remove}, - InferSubcommands: true, - InferLongArgs: true, - UnknownFlags: UnknownFlagsError, - } - - if got := collect(cmd, "insta", "--for"); got != "cmd:install flag:forceful" { - t.Errorf("unique prefixes: got %s", got) - } - if got := collect(cmd, "uni"); got != "cmd:remove" { - t.Errorf("alias prefix: got %s", got) - } - if got := collect(cmd, "ins"); got != "err:unexpected_arg" { - t.Errorf("ambiguous subcommand: got %s", got) - } - if got := collect(cmd, "--ver"); got != "err:unknown_flag" { - t.Errorf("ambiguous long: got %s", got) - } - - helpAll := &Flag{Key: 25, Name: "help-all", Longs: []string{"help-all"}} - withHelpAll := &Command{Name: "ex", Flags: []*Flag{helpAll}, InferLongArgs: true, UnknownFlags: UnknownFlagsError} - if got := collect(withHelpAll, "--help"); got != "flag:help" { - t.Errorf("exact built-in help: got %s", got) - } - if got := collect(withHelpAll, "--he"); got != "err:unknown_flag" { - t.Errorf("help prefix should be ambiguous: got %s", got) - } - if got := collect(&Command{Name: "ex", InferLongArgs: true}, "--he"); got != "flag:help" { - t.Errorf("unique built-in help prefix: got %s", got) - } - - helper := &Command{Name: "helper"} - withHelper := &Command{Name: "ex", Subcommands: []*Command{helper}, InferSubcommands: true} - if got := collect(withHelper, "help"); got != "err:help" { - t.Errorf("exact help command: got %s", got) - } - if got := collect(withHelper, "he"); got != "err:unexpected_arg" { - t.Errorf("help command prefix should be ambiguous: got %s", got) - } - - global := &Flag{Key: 23, Name: "verbose", Longs: []string{"verbose"}, Global: true} - local := &Flag{Key: 24, Name: "verbose", Longs: []string{"verbose"}} - run := &Command{Name: "run", Flags: []*Flag{local}} - shadowed := &Command{Name: "ex", Flags: []*Flag{global}, Subcommands: []*Command{run}, InferLongArgs: true} - if got := collect(shadowed, "run", "--verb"); got != "cmd:run flag:verbose" { - t.Errorf("redeclared global prefix: got %s", got) - } - - nestedInstall := &Command{Name: "install"} - nested := &Command{Name: "nested", Subcommands: []*Command{nestedInstall}, InferSubcommands: true} - nestedRoot := &Command{Name: "ex", Subcommands: []*Command{nested}} - if got := collect(nestedRoot, "nested", "insta"); got != "cmd:nested cmd:install" { - t.Errorf("nested command prefix: got %s", got) - } - p := New(nestedRoot, []string{"help", "nested", "insta"}) - for p.Next() { - } - err, ok := p.Err().(*Error) - if !ok || err.Code != CodeHelp || err.Cmd != nestedInstall { - t.Errorf("nested help prefix: got %v", p.Err()) - } -} - func BenchmarkParse(b *testing.B) { args := []string{"install", "--verbose", "-f", "a", "b", "c"} b.ReportAllocs() diff --git a/go/internal/spec/spec.go b/go/internal/spec/spec.go index b6f1614c7..92a8577dd 100644 --- a/go/internal/spec/spec.go +++ b/go/internal/spec/spec.go @@ -99,8 +99,6 @@ type Cmd struct { UnknownFlags *string `json:"unknown_flags"` ExternalSubcommand bool `json:"external_subcommand"` ArgRequiredElseHelp bool `json:"arg_required_else_help"` - InferSubcommands bool `json:"infer_subcommands"` - InferLongArgs bool `json:"infer_long_args"` } // Subcommands is a command's children, in the order the spec declared them. @@ -418,7 +416,7 @@ func (s *Spec) Build() (*argv.Command, argv.Metadata) { // pass for the same reason the first two were. func (s *Spec) BuildAll() (*argv.Command, argv.Metadata, argv.HelpTable) { b := &builder{complete: s.Complete} - root := b.command(&s.Cmd, unknownFlags(s.UnknownFlags, argv.UnknownFlagsValue), false, false) + root := b.command(&s.Cmd, unknownFlags(s.UnknownFlags, argv.UnknownFlagsValue)) // default_subcommand is a property of the spec rather than of a command, so it // is resolved once, here, against the root's own subcommands. A name that @@ -576,7 +574,7 @@ func (b *builder) next() uint64 { return b.key } -func (b *builder) command(c *Cmd, inherited argv.UnknownFlags, inferSubcommands, inferLongArgs bool) *argv.Command { +func (b *builder) command(c *Cmd, inherited argv.UnknownFlags) *argv.Command { unknown := inherited if c.UnknownFlags != nil { unknown = unknownFlags(*c.UnknownFlags, inherited) @@ -587,8 +585,6 @@ func (b *builder) command(c *Cmd, inherited argv.UnknownFlags, inferSubcommands, UnknownFlags: unknown, ExternalSubcommand: c.ExternalSubcommand, ArgRequiredElseHelp: c.ArgRequiredElseHelp, - InferSubcommands: inferSubcommands || c.InferSubcommands, - InferLongArgs: inferLongArgs || c.InferLongArgs, Key: b.next(), } examples := make([]argv.Example, 0, len(c.Examples)) @@ -635,12 +631,7 @@ func (b *builder) command(c *Cmd, inherited argv.UnknownFlags, inferSubcommands, // In scope for everything below, and out of scope again afterwards. b.scope = append(b.scope, out) for i := range c.Subcommands { - out.Subcommands = append(out.Subcommands, b.command( - &c.Subcommands[i].Cmd, - unknown, - out.InferSubcommands, - out.InferLongArgs, - )) + out.Subcommands = append(out.Subcommands, b.command(&c.Subcommands[i].Cmd, unknown)) } b.scope = b.scope[:len(b.scope)-1] return out diff --git a/lib/src/docs/models.rs b/lib/src/docs/models.rs index 23db481af..093972f96 100644 --- a/lib/src/docs/models.rs +++ b/lib/src/docs/models.rs @@ -494,8 +494,6 @@ impl From<&crate::SpecCommand> for SpecCommand { // Presentational output does not describe relationships between flags, the // way it already does not describe `conflicts`. groups: _, - infer_subcommands: _, - infer_long_args: _, } = cmd; Self { diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index cbffa3b2e..c974a5756 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -377,13 +377,6 @@ impl<'a> Emitter<'a> { if e.cmd.arg_required_else_help { lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into())); } - if effective_command_setting(commands, i, |cmd| cmd.infer_subcommands) { - lines.push(Line::Field("InferSubcommands".into(), "true".into())); - } - if effective_command_setting(commands, i, |cmd| cmd.infer_long_args) { - lines.push(Line::Field("InferLongArgs".into(), "true".into())); - } - if e.root { if let Some(var) = &default_subcommand { lines.push(Line::Field("DefaultSubcommand".into(), var.clone())); @@ -1116,19 +1109,6 @@ fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> Unkn spec.unknown_flags.unwrap_or_default() } -fn effective_command_setting( - commands: &[Emitted], - at: usize, - enabled: impl Fn(&SpecCommand) -> bool, -) -> bool { - let path = &commands[at].cmd.full_cmd; - commands.iter().any(|candidate| { - candidate.cmd.full_cmd.len() <= path.len() - && candidate.cmd.full_cmd[..] == path[..candidate.cmd.full_cmd.len()] - && enabled(&candidate.cmd) - }) -} - fn flag_literal(flag: &SpecFlag, named: &Named) -> String { let mut fields = vec![ format!("Key: {}", named.key), @@ -1784,40 +1764,6 @@ cmd "run" arg_required_else_help=#true { ); } - #[test] - fn inferred_prefix_settings_are_emitted_and_inherited() { - let out = go(r#" -name "ex" -bin "ex" -infer_subcommands #true -infer_long_args #true -cmd "install" {} -"#); - let block = |var: &str| { - let start = out - .find(&format!("var {var} =")) - .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}")); - let rest = &out[start..]; - let end = rest[1..] - .find("\nvar ") - .map(|i| i + 1) - .unwrap_or(rest.len()); - &rest[..end] - }; - for var in ["Root", "cmdInstall"] { - let has_true = |field: &str| { - block(var) - .lines() - .any(|line| line.contains(field) && line.ends_with("true,")) - }; - assert!( - has_true("InferSubcommands:") && has_true("InferLongArgs:"), - "{var} should carry the effective inherited settings:\n{}", - block(var) - ); - } - } - /// A subcommand actually named `root` wants the constant the root has. #[test] fn a_subcommand_named_root_does_not_collide_with_the_root() { diff --git a/lib/src/parse.rs b/lib/src/parse.rs index 5ad4e868a..be0f1cce7 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -266,101 +266,6 @@ fn get_flag_key(word: &str) -> &str { } } -fn resolve_long_flag( - available: &BTreeMap>, - word: &str, - infer: bool, - help_enabled: bool, -) -> Option<(Arc, bool)> { - let key = get_flag_key(word); - if let Some(exact) = available.get(key) { - return Some((Arc::clone(exact), exact.negate.as_deref() == Some(key))); - } - if !infer || !key.starts_with("--") || key.len() == 2 { - return None; - } - let built_in_help = - help_enabled && "--help".starts_with(key) && !available.contains_key("--help"); - let mut found: Option<(Arc, bool)> = None; - for (candidate, flag) in available { - if !candidate.starts_with("--") { - continue; - } - if !candidate.starts_with(key) { - continue; - } - let negated = flag.negate.as_deref() == Some(candidate.as_str()); - if let Some((prior, prior_negated)) = &mut found { - if !Arc::ptr_eq(prior, flag) { - return None; - } - // Several aliases of one declaration are one match. If both a positive and - // negative spelling share the prefix, the positive form wins, matching the - // static Rust and Go parsers. - *prior_negated &= negated; - } else { - found = Some((Arc::clone(flag), negated)); - } - } - if built_in_help { - // The built-in is a candidate too. With a declared match this prefix is - // ambiguous; without one the caller recognizes the built-in itself. - return None; - } - found -} - -fn resolve_subcommand_with_builtins<'a>( - cmd: &'a SpecCommand, - name: &str, - infer: bool, - help_enabled: bool, -) -> Option<&'a SpecCommand> { - if let Some(exact) = cmd.find_subcommand(name) { - return Some(exact); - } - if help_enabled - && !cmd.subcommands.is_empty() - && (name == "help" || (infer && !name.is_empty() && "help".starts_with(name))) - { - return None; - } - cmd.find_subcommand_with_prefix(name, infer) -} - -fn inferred_long_help( - spec: &Spec, - available: &BTreeMap>, - word: &str, - infer: bool, -) -> bool { - let key = get_flag_key(word); - spec.disable_help != Some(true) - && infer - && key.len() > 2 - && "--help".starts_with(key) - && !available - .keys() - .any(|candidate| candidate.starts_with("--") && candidate.starts_with(key)) -} - -fn inferred_help_word(spec: &Spec, cmd: &SpecCommand, word: &str, infer: bool) -> bool { - spec.disable_help != Some(true) - && !word.is_empty() - && !cmd.subcommands.is_empty() - && (word == "help" - || (infer - && "help".starts_with(word) - && !cmd.subcommands.values().any(|sub| { - sub.name.starts_with(word) - || sub.aliases.iter().any(|alias| alias.starts_with(word)) - || sub - .hidden_aliases - .iter() - .any(|alias| alias.starts_with(word)) - }))) -} - pub struct ParseOutput { pub cmd: SpecCommand, pub cmds: Vec, @@ -859,18 +764,11 @@ fn parse_partial_with_env( // once per task invocation. let default_catches_it = spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default); - let infer_subcommands = out.cmds.iter().any(|cmd| cmd.infer_subcommands); if !mounts_resolved && !out.cmd.mounts.is_empty() && !default_catches_it && is_command_word(&input[idx]) - && resolve_subcommand_with_builtins( - &out.cmd, - &input[idx], - infer_subcommands, - spec.disable_help != Some(true), - ) - .is_none() + && out.cmd.find_subcommand(&input[idx]).is_none() { mounts_resolved = true; let mut mounted = out.cmd.clone(); @@ -881,12 +779,7 @@ fn parse_partial_with_env( } out.cmd = mounted; } - if let Some(subcommand) = resolve_subcommand_with_builtins( - &out.cmd, - &input[idx], - infer_subcommands, - spec.disable_help != Some(true), - ) { + if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) { let mut subcommand = subcommand.clone(); // Pass prefix words (global flags before this subcommand) to mount subcommand.mount(&mount_prefix_words(&prefix_flags))?; @@ -920,21 +813,11 @@ fn parse_partial_with_env( // never named it. let is_bundle = word.starts_with("--") || short_bundle_is_known(&out.available_flags, &word); - let infer_long_args = out.cmds.iter().any(|cmd| cmd.infer_long_args); - if let Some((f, negated)) = (if word.starts_with("--") { - resolve_long_flag( - &out.available_flags, - &word, - infer_long_args, - spec.disable_help != Some(true), - ) - } else { - out.available_flags - .get(flag_key) - .cloned() - .map(|flag| (flag, false)) - }) - .filter(|_| is_bundle) + if let Some(f) = out + .available_flags + .get(flag_key) + .cloned() + .filter(|_| is_bundle) { // Skip the flag and keep scanning. Both global and non-global flags may precede // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and @@ -943,7 +826,7 @@ fn parse_partial_with_env( // // Only globals are forwarded to mounts: a non-global flag belongs to the // command that declared it, not to what is mounted below it. - prefix_bindings.push_back(Some((Arc::clone(&f), negated))); + prefix_bindings.push_back(Some((Arc::clone(&f), false))); let mut forwarded = f.global.then(|| vec![word.clone()]); idx += 1; @@ -1173,18 +1056,8 @@ fn parse_partial_with_env( // the flag entirely. let split = w.split_once('='); let word = split.map(|(word, _)| word).unwrap_or(&w); - // Resolve even when Phase 1 already carried the flag binding forward: a binding - // identifies the declaration, but an inferred negation also has to preserve which - // spelling matched. - let resolved = resolve_long_flag( - &out.available_flags, - word, - out.cmds.iter().any(|cmd| cmd.infer_long_args), - spec.disable_help != Some(true), - ); let bound_flag = binding.as_ref().map(|(flag, _)| flag); - let resolved_flag = resolved.as_ref().map(|(flag, _)| flag); - if let Some(f) = bound_flag.or(resolved_flag) { + if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) { parsed_flag_spellings .entry(Arc::as_ptr(f) as usize) .or_default() @@ -1238,32 +1111,17 @@ fn parse_partial_with_env( .unwrap(); arr.push(true); } else { - let negated = binding - .as_ref() - .filter(|(bound, _)| Arc::ptr_eq(bound, f)) - .map(|(_, negated)| *negated) - .or_else(|| { - resolved - .as_ref() - .filter(|(resolved, _)| Arc::ptr_eq(resolved, f)) - .map(|(_, negated)| *negated) - }) - .unwrap_or_else(|| f.negate.as_deref() == Some(word)); - // Exact bindings can compare the typed form. Inferred bindings carry - // which form matched, because a prefix is deliberately not equal to the - // full negation spelling. - out.flags.insert(Arc::clone(f), ParseValue::Bool(!negated)); + let negate = f.negate.clone().unwrap_or_default(); + // Which form was typed is a question about the name, so it is + // asked of `word` rather than the whole token: the attached value + // is dropped just above, and comparing `--no-color=yes` against + // `--no-color` would take the negation down with it. + out.flags + .insert(Arc::clone(f), ParseValue::Bool(word != negate)); } continue; } - if is_help_arg(spec, &w) - || inferred_long_help( - spec, - &out.available_flags, - &w, - out.cmds.iter().any(|cmd| cmd.infer_long_args), - ) - { + if is_help_arg(spec, &w) { out.errors .push(render_help_err(spec, &out.cmd, w.len() > 2)); record_cursor(&mut out, next_arg_idx, seen_double_dash); @@ -1475,17 +1333,9 @@ fn parse_partial_with_env( } continue; } - let help_word = inferred_help_word( - spec, - &out.cmd, - &w, - out.cmds.iter().any(|cmd| cmd.infer_subcommands), - ); - if is_help_arg(spec, &w) || help_word { - // A help *word* is clap's long help action even when inferred from `h` or `he`. - // The length distinction belongs only to the `-h` / `--help` flag spellings. + if is_help_arg(spec, &w) { out.errors - .push(render_help_err(spec, &out.cmd, help_word || w.len() > 2)); + .push(render_help_err(spec, &out.cmd, w.len() > 2)); record_cursor(&mut out, next_arg_idx, seen_double_dash); return Ok((out, overridden_flags)); } @@ -3174,84 +3024,6 @@ mount run="exit 1" assert_eq!(out.cmd.name, "ex"); } - #[test] - fn inferred_builtins_are_exact_and_participate_in_ambiguity() { - let with_help_all: Spec = r#" -name "ex" -bin "ex" -infer_long_args #true -unknown_flags "error" -flag "--help-all" -"# - .parse() - .unwrap(); - let exact = parse(&with_help_all, &input(&["ex", "--help"])) - .unwrap_err() - .to_string(); - assert!(exact.contains("Usage:"), "{exact}"); - let ambiguous = parse(&with_help_all, &input(&["ex", "--he"])) - .unwrap_err() - .to_string(); - assert!(!ambiguous.contains("Usage:"), "{ambiguous}"); - - let lone: Spec = r#" -name "ex" -bin "ex" -infer_long_args #true -"# - .parse() - .unwrap(); - let inferred = parse(&lone, &input(&["ex", "--he"])) - .unwrap_err() - .to_string(); - assert!(inferred.contains("Usage:"), "{inferred}"); - - let disabled: Spec = r#" -name "ex" -bin "ex" -infer_long_args #true -disable_help #true -unknown_flags "error" -flag "--hello" -"# - .parse() - .unwrap(); - parse(&disabled, &input(&["ex", "--he"])) - .expect("disabled built-in help must not make a real flag prefix ambiguous"); - - let with_helper: Spec = r#" -name "ex" -bin "ex" -infer_subcommands #true -cmd "helper" -"# - .parse() - .unwrap(); - let exact = parse(&with_helper, &input(&["ex", "help"])) - .unwrap_err() - .to_string(); - assert!(exact.contains("Usage:"), "{exact}"); - let ambiguous = parse(&with_helper, &input(&["ex", "he"])) - .unwrap_err() - .to_string(); - assert!(!ambiguous.contains("Usage:"), "{ambiguous}"); - - let help_word: Spec = r#" -name "ex" -bin "ex" -about "short description" -long_about "long description marker" -infer_subcommands #true -cmd "run" -"# - .parse() - .unwrap(); - let inferred = parse(&help_word, &input(&["ex", "he"])) - .unwrap_err() - .to_string(); - assert!(inferred.contains("long description marker"), "{inferred}"); - } - #[cfg(unix)] #[test] fn a_declared_subcommand_does_not_run_the_mount() { @@ -4155,19 +3927,6 @@ flag "--file " required_unless="--stdin" ); } - #[test] - fn an_inferred_negation_keeps_its_prefix_binding_across_redeclaration() { - let spec: Spec = "name \"ex\"\nbin \"ex\"\ninfer_long_args #true\nflag \"--clean\" negate=\"--no-clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" negate=\"--no-clean\" global=#true\n}\n" - .parse() - .unwrap(); - - let parsed = parse(&spec, &input(&["ex", "--no-cl", "run"])) - .expect("the prefix should remain bound to the ancestor declaration"); - assert!(parsed.flags.iter().any(|(flag, value)| { - flag.name == "clean" && matches!(value, ParseValue::Bool(false)) - })); - } - #[test] fn a_colliding_alias_does_not_disown_the_child_from_the_rest() { // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that diff --git a/lib/src/spec/builder.rs b/lib/src/spec/builder.rs index fbada8bd8..186e944c7 100644 --- a/lib/src/spec/builder.rs +++ b/lib/src/spec/builder.rs @@ -692,18 +692,6 @@ impl SpecCommandBuilder { self } - /// Accept unambiguous prefixes of subcommand names and aliases. - pub fn infer_subcommands(mut self, enabled: bool) -> Self { - self.inner.infer_subcommands = enabled; - self - } - - /// Accept unambiguous prefixes of long flag names and aliases. - pub fn infer_long_args(mut self, enabled: bool) -> Self { - self.inner.infer_long_args = enabled; - self - } - /// Set what running this command does to the world pub fn effect(mut self, effect: SpecCommandEffect) -> Self { self.inner.effect = Some(effect); diff --git a/lib/src/spec/cmd.rs b/lib/src/spec/cmd.rs index c72ff9fa8..8cb0112ad 100644 --- a/lib/src/spec/cmd.rs +++ b/lib/src/spec/cmd.rs @@ -105,12 +105,6 @@ pub struct SpecCommand { /// Whether a bare invocation of this command shows its help. #[serde(skip_serializing_if = "is_false")] pub arg_required_else_help: bool, - /// Accept unambiguous prefixes of subcommand names and aliases, inherited by children. - #[serde(skip_serializing_if = "is_false")] - pub infer_subcommands: bool, - /// Accept unambiguous prefixes of long flag names and aliases, inherited by children. - #[serde(skip_serializing_if = "is_false")] - pub infer_long_args: bool, /// Token that resets argument parsing, allowing multiple command invocations. /// e.g., `mise run lint ::: test ::: check` with restart_token=":::" #[serde(skip_serializing_if = "Option::is_none")] @@ -181,8 +175,6 @@ impl Default for SpecCommand { subcommand_required: false, external_subcommand: false, arg_required_else_help: false, - infer_subcommands: false, - infer_long_args: false, restart_token: None, help: None, help_long: None, @@ -290,8 +282,6 @@ impl SpecCommand { "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?, "external_subcommand" => cmd.external_subcommand = v.ensure_bool()?, "arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?, - "infer_subcommands" => cmd.infer_subcommands = v.ensure_bool()?, - "infer_long_args" => cmd.infer_long_args = v.ensure_bool()?, "hide" => cmd.hide = v.ensure_bool()?, "unknown_flags" => { let raw = v.ensure_string()?; @@ -420,12 +410,6 @@ impl SpecCommand { cmd.arg_required_else_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()? } - "infer_subcommands" => { - cmd.infer_subcommands = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()? - } - "infer_long_args" => { - cmd.infer_long_args = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()? - } "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?, "effect" => { let arg = child.ensure_arg_len(1..=1)?.arg(0)?; @@ -537,8 +521,6 @@ impl SpecCommand { subcommand_required, external_subcommand, arg_required_else_help, - infer_subcommands, - infer_long_args, restart_token, subcommands, complete, @@ -613,8 +595,6 @@ impl SpecCommand { self.subcommand_required = subcommand_required; self.external_subcommand = external_subcommand; self.arg_required_else_help = arg_required_else_help; - self.infer_subcommands |= infer_subcommands; - self.infer_long_args |= infer_long_args; if effect.is_some() { self.effect = effect; } @@ -668,36 +648,6 @@ impl SpecCommand { self.subcommands.get(name) } - pub(crate) fn find_subcommand_with_prefix( - &self, - name: &str, - infer: bool, - ) -> Option<&SpecCommand> { - if let Some(exact) = self.find_subcommand(name) { - return Some(exact); - } - if !infer || name.is_empty() { - return None; - } - let mut found = None; - for command in self.subcommands.values() { - if !command.name.starts_with(name) - && !command.aliases.iter().any(|alias| alias.starts_with(name)) - && !command - .hidden_aliases - .iter() - .any(|alias| alias.starts_with(name)) - { - continue; - } - if found.is_some() { - return None; - } - found = Some(command); - } - found - } - pub(crate) fn mount(&mut self, global_flag_args: &[String]) -> Result<(), UsageErr> { for mount in self.mounts.iter().cloned().collect_vec() { let cmd = if global_flag_args.is_empty() { @@ -749,8 +699,6 @@ impl From<&SpecCommand> for KdlNode { subcommand_required, external_subcommand, arg_required_else_help, - infer_subcommands, - infer_long_args, restart_token, unknown_flags, aliases, @@ -797,14 +745,6 @@ impl From<&SpecCommand> for KdlNode { node.entries_mut() .push(KdlEntry::new_prop("arg_required_else_help", true)); } - if *infer_subcommands { - node.entries_mut() - .push(KdlEntry::new_prop("infer_subcommands", true)); - } - if *infer_long_args { - node.entries_mut() - .push(KdlEntry::new_prop("infer_long_args", true)); - } if let Some(restart_token) = &restart_token { node.entries_mut() .push(KdlEntry::new_prop("restart_token", restart_token.clone())); diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index 911c0b8d2..982405594 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -301,12 +301,6 @@ impl Spec { "arg_required_else_help" => { schema.cmd.arg_required_else_help = node.arg(0)?.ensure_bool()?; } - "infer_subcommands" => { - schema.cmd.infer_subcommands = node.arg(0)?.ensure_bool()?; - } - "infer_long_args" => { - schema.cmd.infer_long_args = node.arg(0)?.ensure_bool()?; - } "example" => { let code = node.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?; let mut example = SpecExample::new(code.trim().to_string()); @@ -590,16 +584,6 @@ impl Display for Spec { node.push(KdlEntry::new(true)); nodes.push(node); } - if self.cmd.infer_subcommands { - let mut node = KdlNode::new("infer_subcommands"); - node.push(KdlEntry::new(true)); - nodes.push(node); - } - if self.cmd.infer_long_args { - let mut node = KdlNode::new("infer_long_args"); - node.push(KdlEntry::new(true)); - nodes.push(node); - } if !self.usage.is_empty() { let mut node = KdlNode::new("usage"); node.push(string_entry(None, &self.usage));