From dccd808b912cda39f689294c8fd6fb25ed0fcab0 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 04:35:44 +0000 Subject: [PATCH 1/5] feat(go): emit the cold table too, so generated code can apply the rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `usage generate go` emitted the tables binding reads and stopped there, which left the post-binding rules reachable only from a spec lowered at run time — the harness could use them and a generated CLI could not. Now it emits `Meta` alongside: required, choices, default, env, the var bounds, and the four that compare one entry against another, with their names already resolved to keys. Indexed by key, which is what makes a lookup an index rather than a map — and a Go map would have to be built at init, which is the one thing these tables exist to avoid. Commands take keys and have no cold half, so their slots are empty entries rather than gaps: `Lookup` checks the key it finds and reports nothing when it does not match, so an empty slot answers correctly and the index stays dense. **It costs nothing unless used.** Go's linker drops an unreferenced package-level table entirely: a binary that only binds does not contain `Meta` at all, and one that references it carries 217 KB for mise's 989 entries and still has no init function. That is what Rust gets from putting the equivalent behind a feature flag, except nobody has to remember the flag. Both halves measured with `go tool nm` rather than assumed. The tests that matter here are the join. `argv`'s unit tests prove the rules against tables written by hand and the corpus proves them against tables built at run time; neither exercises the emitter, which is where a field can be dropped, misnamed, or filed under the wrong key with everything still green. So the shadow now checks that all 989 entries have metadata describing *themselves*, that every relationship points at a real entry, and that mise's own declarations behave: `bootstrap packages import` fills `--manager` from its default, `--log-level` enforces the choices declared on the value it takes, and a value on the command line beats the default. Co-Authored-By: Claude Opus 5 --- go/README.md | 20 +- go/internal/shadow/mise/meta_test.go | 179 +++ go/internal/shadow/mise/tables.go | 1060 +++++++++++++++++ lib/src/go/mod.rs | 197 ++- ...fault_subcommand_points_into_the_tree.snap | 14 + .../usage__go__tests__a_whole_cli.snap | 23 + ...liding_names_get_distinct_identifiers.snap | 16 + ...n_flags_are_inherited_and_overridable.snap | 15 + 8 files changed, 1507 insertions(+), 17 deletions(-) create mode 100644 go/internal/shadow/mise/meta_test.go diff --git a/go/README.md b/go/README.md index 0ac104103..6cd6cb60b 100644 --- a/go/README.md +++ b/go/README.md @@ -96,9 +96,16 @@ a derive macro: //go:generate usage generate go -f mycli.usage.kdl -o tables.go ``` -The generated file exports `Root` to pass to `argv.New`, and a key constant per -command, flag and argument. Dispatch on those rather than on `Name`: it costs no -string comparison, and a flag renamed in the spec then fails to compile instead of +The generated file exports `Root` to pass to `argv.New`, `Meta` for the rules +decided after the last token, and a key constant per command, flag and argument. + +`Meta` costs nothing if you do not use it: Go's linker drops an unreferenced +package-level table entirely, so a CLI that only binds does not carry it. mise's +is 217 KB when something does reference it. That is the same split Rust gets from +a feature flag, without needing one. + +Dispatch on the key constants rather than on `Name`: it costs no string +comparison, and a flag renamed in the spec then fails to compile instead of silently never matching. Writing tables by hand is supported too, and is what @@ -111,9 +118,6 @@ var ( ) ``` -Dispatch on `Key` rather than `Name` in generated code: it is what the field -identifiers are for, and it costs no string comparison. - ## Conformance The [corpus](../corpus) is the definition of correct, and it is plain JSON so that @@ -146,10 +150,6 @@ claim is measured at real scale rather than against a fixture with four flags: ## What is missing -- **The cold table in generated code.** `usage generate go` emits the parse tables - but not the `Meta` ones yet, so the post-binding rules are reachable today from a - spec lowered at run time rather than from a generated package. Proving them - against the corpus came first, the way the binder did before the generator. - **Typed values.** Binding collects text. Something still has to turn `"8"` into an `int` and `"1m"` into a `time.Duration`, and report the ones that will not convert. diff --git a/go/internal/shadow/mise/meta_test.go b/go/internal/shadow/mise/meta_test.go new file mode 100644 index 000000000..b9e5916ea --- /dev/null +++ b/go/internal/shadow/mise/meta_test.go @@ -0,0 +1,179 @@ +package mise + +import ( + "testing" + + "github.com/jdx/usage/go/argv" +) + +// The generated cold table, driving the rules it exists for. +// +// The unit tests in `argv` prove the rules against tables written by hand; the +// corpus proves them against tables built from a spec at run time. Neither +// exercises the emitter's half, which is where a field can be dropped, misnamed, +// or filed under the wrong key without anything noticing. These are the join. + +// resolve runs a parse and applies the rules to one named entry, returning what +// it ended up with. +func resolve(t *testing.T, name string, args []string, + environ map[string]string) (values []string, source argv.Source, err *argv.Error) { + t.Helper() + + given := map[uint64][]string{} + seen := map[uint64]bool{} + path := []*argv.Command{Root} + + p := argv.New(Root, args) + for p.Next() { + ev := p.Event() + switch ev.Kind { + case argv.KindCommand: + path = append(path, ev.Command) + case argv.KindFlag: + seen[ev.Flag.Key] = true + if ev.HasValue { + given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + } + case argv.KindArg: + seen[ev.Arg.Key] = true + given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) + } + } + if e := p.Err(); e != nil { + t.Fatalf("binding failed: %v", e) + } + + lookup := func(k string) (string, bool) { v, ok := environ[k]; return v, ok } + + for _, cmd := range path { + for _, f := range cmd.Flags { + v, src := argv.Fill(Meta.Lookup(f.Key), given[f.Key], lookup) + if e := argv.Check(Meta.Lookup(f.Key), v, len(given[f.Key])); e != nil && err == nil { + err = e + } + if f.Name == name { + values, source = v, src + } + } + for _, a := range cmd.Args { + v, src := argv.Fill(Meta.Lookup(a.Key), given[a.Key], lookup) + if e := argv.Check(Meta.Lookup(a.Key), v, 0); e != nil && err == nil { + err = e + } + if a.Name == name { + values, source = v, src + } + } + } + return values, source, err +} + +// mise declares `--manager` on `bootstrap packages import` with both a default +// and the single choice that default names, which makes it the one entry that +// exercises the whole cold table at once. +func TestAGeneratedDefaultFills(t *testing.T) { + values, source, err := resolve(t, "manager", + []string{"bootstrap", "packages", "import"}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != argv.FromDefault { + t.Errorf("want the default, got %v", source) + } + if len(values) != 1 || values[0] != "brew" { + t.Errorf("want [brew], got %q", values) + } +} + +// The choices came from a `choices` block on the flag's *value*, which is a level +// of nesting the emitter has to read through. +func TestGeneratedChoicesAreEnforced(t *testing.T) { + if _, _, err := resolve(t, "log-level", []string{"--log-level", "debug"}, nil); err != nil { + t.Fatalf("a declared choice should be accepted: %v", err) + } + _, _, err := resolve(t, "log-level", []string{"--log-level", "chatty"}, nil) + if err == nil { + t.Fatal("a value outside the choices should be refused") + } + if err.Code != argv.CodeInvalidChoice || err.Name != "log-level" { + t.Errorf("want invalid_choice for log-level, got %q for %q", err.Code, err.Name) + } +} + +// The command line beats the default, which is the ordering the whole fallback +// rests on. +func TestTheCommandLineBeatsAGeneratedDefault(t *testing.T) { + values, source, err := resolve(t, "manager", + []string{"bootstrap", "packages", "import", "--manager", "brew"}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != argv.FromArgv || len(values) != 1 || values[0] != "brew" { + t.Errorf("want brew from argv, got %q from %v", values, source) + } +} + +// The two tables are separate data joined only by key, and the emitter writes +// them in two passes. If those ever disagree, every rule reads the wrong +// declaration — so it is checked across all 989 entries rather than sampled. +func TestEveryEntryHasMetadataDescribingItself(t *testing.T) { + var checked int + var walk func(*argv.Command) + walk = func(c *argv.Command) { + // A command takes a key and has no cold half, so its slot is empty and + // Lookup should report nothing rather than a neighbour's entry. + if m := Meta.Lookup(c.Key); m != nil { + t.Errorf("command %q has metadata %+v", c.Name, m) + } + for _, f := range c.Flags { + m := Meta.Lookup(f.Key) + if m == nil { + t.Errorf("flag %q of %q has no metadata", f.Name, c.Name) + continue + } + if m.Name != f.Name || !m.Flag { + t.Errorf("flag %q of %q got metadata for %q (flag=%v)", + f.Name, c.Name, m.Name, m.Flag) + } + checked++ + } + for _, a := range c.Args { + m := Meta.Lookup(a.Key) + if m == nil { + t.Errorf("arg %q of %q has no metadata", a.Name, c.Name) + continue + } + if m.Name != a.Name || m.Flag { + t.Errorf("arg %q of %q got metadata for %q (flag=%v)", + a.Name, c.Name, m.Name, m.Flag) + } + checked++ + } + for _, sub := range c.Subcommands { + walk(sub) + } + } + walk(Root) + + if checked < 800 { + t.Errorf("only %d entries checked: the tables are probably truncated", checked) + } +} + +// Every key a relationship points at must exist, or the rule silently does +// nothing. The emitter drops names it cannot resolve, so this is where a spec +// that names a flag which does not exist would show up. +func TestRelationshipsPointAtRealEntries(t *testing.T) { + for i := range Meta { + m := &Meta[i] + for _, group := range [][]uint64{ + m.Conflicts, m.Overrides, m.RequiredUnless, m.RequiredIf, + } { + for _, key := range group { + if Meta.Lookup(key) == nil { + t.Errorf("%q points at key %d, which is not an entry", m.Name, key) + } + } + } + } +} diff --git a/go/internal/shadow/mise/tables.go b/go/internal/shadow/mise/tables.go index e75d010a7..f071cba44 100644 --- a/go/internal/shadow/mise/tables.go +++ b/go/internal/shadow/mise/tables.go @@ -3774,3 +3774,1063 @@ var cmdWhich = &argv.Command{ {Key: ArgWhichBinName, Name: "BIN_NAME"}, }, } + +// Meta is the cold table, read only by the rules that are decided once the +// last token has been read: required, choices, the env-then-default fallback, +// the var bounds, and the four that compare one entry against another. A parse +// never touches it. +// +// Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty: +// commands take keys too, and have no cold half. +var Meta = argv.Metadata{ + {}, + {Key: FlagContinueOnError, Name: "continue-on-error", Flag: true}, + {Key: FlagCd, Name: "cd", Flag: true}, + {Key: FlagEnv, Name: "env", Flag: true}, + {Key: FlagForce, Name: "force", Flag: true}, + {Key: FlagJobs, Name: "jobs", Flag: true}, + {Key: FlagDryRun, Name: "dry-run", Flag: true}, + {Key: FlagProfile, Name: "profile", Flag: true}, + {Key: FlagQuiet, Name: "quiet", Flag: true}, + {Key: FlagShell, Name: "shell", Flag: true}, + {Key: FlagTool, Name: "tool", Flag: true}, + {Key: FlagVerbose, Name: "verbose", Flag: true}, + {Key: FlagVersion, Name: "version", Flag: true}, + {Key: FlagYes, Name: "yes", Flag: true}, + {Key: FlagDebug, Name: "debug", Flag: true}, + {Key: FlagLogLevel, Name: "log-level", Flag: true, Choices: []string{"trace", "debug", "info", "warning", "error"}}, + {Key: FlagNoConfig, Name: "no-config", Flag: true}, + {Key: FlagNoEnv, Name: "no-env", Flag: true}, + {Key: FlagNoHooks, Name: "no-hooks", Flag: true}, + {Key: FlagNoTimings, Name: "no-timings", Flag: true}, + {Key: FlagOutput, Name: "output", Flag: true}, + {Key: FlagRaw, Name: "raw", Flag: true}, + {Key: FlagLocked, Name: "locked", Flag: true}, + {Key: FlagSilent, Name: "silent", Flag: true}, + {Key: FlagTimings, Name: "timings", Flag: true}, + {Key: FlagTrace, Name: "trace", Flag: true}, + {Key: ArgTask, Name: "TASK"}, + {Key: ArgTaskArgs, Name: "TASK_ARGS"}, + {Key: ArgTaskArgsLast, Name: "TASK_ARGS_LAST"}, + {}, + {Key: FlagActivateQuiet, Name: "quiet", Flag: true}, + {Key: FlagActivateShell, Name: "shell", Flag: true, Choices: []string{"bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"}}, + {Key: FlagActivateNoHookEnv, Name: "no-hook-env", Flag: true}, + {Key: FlagActivateShims, Name: "shims", Flag: true}, + {Key: FlagActivateStatus, Name: "status", Flag: true}, + {Key: ArgActivateShellType, Name: "SHELL_TYPE", Choices: []string{"bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"}}, + {}, + {Key: FlagToolAliasTool, Name: "tool", Flag: true}, + {Key: FlagToolAliasNoHeader, Name: "no-header", Flag: true}, + {}, + {Key: ArgToolAliasGetTool, Name: "TOOL", Required: true}, + {Key: ArgToolAliasGetAlias, Name: "ALIAS", Required: true}, + {}, + {Key: FlagToolAliasLsNoHeader, Name: "no-header", Flag: true}, + {Key: ArgToolAliasLsTool, Name: "TOOL"}, + {}, + {Key: ArgToolAliasSetTool, Name: "TOOL", Required: true}, + {Key: ArgToolAliasSetAlias, Name: "ALIAS", Required: true}, + {Key: ArgToolAliasSetValue, Name: "VALUE"}, + {}, + {Key: ArgToolAliasUnsetTool, Name: "TOOL", Required: true}, + {Key: ArgToolAliasUnsetAlias, Name: "ALIAS"}, + {}, + {Key: ArgAsdfArgs, Name: "ARGS"}, + {}, + {}, + {}, + {Key: FlagBinPathsBinNames, Name: "bin-names", Flag: true}, + {Key: FlagBinPathsJson, Name: "json", Flag: true}, + {Key: ArgBinPathsToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagBootstrapDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapYes, Name: "yes", Flag: true}, + {Key: FlagBootstrapForceDotfiles, Name: "force-dotfiles", Flag: true}, + {Key: FlagBootstrapOnly, Name: "only", Flag: true, Choices: []string{"plugins", "packages", "accounts", "files", "services", "firewall", "compose", "repos", "dotfiles", "mise-shell-activate", "shell", "macos-defaults", "defaults", "macos-launchd-agents", "launchd", "linux-systemd-units", "systemd", "user", "tools", "task", "final-hook"}}, + {Key: FlagBootstrapPromptSecrets, Name: "prompt-secrets", Flag: true}, + {Key: FlagBootstrapSkip, Name: "skip", Flag: true, Choices: []string{"plugins", "packages", "accounts", "files", "services", "firewall", "compose", "repos", "dotfiles", "mise-shell-activate", "shell", "macos-defaults", "defaults", "macos-launchd-agents", "launchd", "linux-systemd-units", "systemd", "user", "tools", "task", "final-hook"}}, + {Key: FlagBootstrapUpdate, Name: "update", Flag: true}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {Key: FlagBootstrapAccountsApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapAccountsApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapAccountsStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapAccountsStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapComposeApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapComposeApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapComposeStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapComposeStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapDotfilesAddForce, Name: "force", Flag: true}, + {Key: FlagBootstrapDotfilesAddGlobal, Name: "global", Flag: true}, + {Key: FlagBootstrapDotfilesAddLocal, Name: "local", Flag: true}, + {Key: FlagBootstrapDotfilesAddMode, Name: "mode", Flag: true}, + {Key: FlagBootstrapDotfilesAddDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapDotfilesAddNoApply, Name: "no-apply", Flag: true}, + {Key: FlagBootstrapDotfilesAddPath, Name: "path", Flag: true}, + {Key: FlagBootstrapDotfilesAddSource, Name: "source", Flag: true}, + {Key: FlagBootstrapDotfilesAddYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapDotfilesAddTarget, Name: "TARGET", Required: true}, + {}, + {Key: FlagBootstrapDotfilesApplyForce, Name: "force", Flag: true}, + {Key: FlagBootstrapDotfilesApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapDotfilesApplyYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapDotfilesApplyTarget, Name: "TARGET"}, + {}, + {Key: FlagBootstrapDotfilesEditApply, Name: "apply", Flag: true}, + {Key: FlagBootstrapDotfilesEditMode, Name: "mode", Flag: true}, + {Key: FlagBootstrapDotfilesEditSource, Name: "source", Flag: true}, + {Key: FlagBootstrapDotfilesEditYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapDotfilesEditTarget, Name: "TARGET", Required: true}, + {}, + {Key: FlagBootstrapDotfilesStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapDotfilesStatusMissing, Name: "missing", Flag: true}, + {Key: ArgBootstrapDotfilesStatusTarget, Name: "TARGET"}, + {}, + {Key: FlagBootstrapDotfilesUnapplyForce, Name: "force", Flag: true}, + {Key: FlagBootstrapDotfilesUnapplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapDotfilesUnapplyYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapDotfilesUnapplyTarget, Name: "TARGET"}, + {}, + {}, + {Key: FlagBootstrapFilesApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapFilesApplyYes, Name: "yes", Flag: true}, + {Key: FlagBootstrapFilesApplyPromptSecrets, Name: "prompt-secrets", Flag: true}, + {}, + {Key: FlagBootstrapFilesStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapFilesStatusMissing, Name: "missing", Flag: true}, + {Key: FlagBootstrapFilesStatusPromptSecrets, Name: "prompt-secrets", Flag: true}, + {}, + {}, + {Key: FlagBootstrapFirewallApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapFirewallApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapFirewallStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapFirewallStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapLaunchdApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapLaunchdApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapLaunchdStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapLaunchdStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {}, + {Key: FlagBootstrapLinuxSystemdUnitsApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapLinuxSystemdUnitsApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapLinuxSystemdUnitsStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapLinuxSystemdUnitsStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {}, + {Key: FlagBootstrapMacosDefaultsApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapMacosDefaultsApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapMacosDefaultsStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapMacosDefaultsStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapMacosLaunchdAgentsApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapMacosLaunchdAgentsApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapMacosLaunchdAgentsStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapMacosLaunchdAgentsStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapMacosDefaultsApplyDryRun2, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapMacosDefaultsApplyYes2, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapMacosDefaultsStatusJson2, Name: "json", Flag: true}, + {Key: FlagBootstrapMacosDefaultsStatusMissing2, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapMiseShellActivateApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapMiseShellActivateApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapMiseShellActivateStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapMiseShellActivateStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapPackagesApplyManager, Name: "manager", Flag: true}, + {Key: FlagBootstrapPackagesApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesApplyYes, Name: "yes", Flag: true}, + {Key: FlagBootstrapPackagesApplyUpdate, Name: "update", Flag: true}, + {Key: ArgBootstrapPackagesApplyPackage, Name: "PACKAGE"}, + {}, + {}, + {Key: FlagBootstrapPackagesBrewTapLocal, Name: "local", Flag: true}, + {Key: FlagBootstrapPackagesBrewTapDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesBrewTapPath, Name: "path", Flag: true}, + {Key: ArgBootstrapPackagesBrewTapTap, Name: "TAP", Required: true}, + {Key: ArgBootstrapPackagesBrewTapUrl, Name: "URL"}, + {}, + {Key: FlagBootstrapPackagesBrewUntapLocal, Name: "local", Flag: true}, + {Key: FlagBootstrapPackagesBrewUntapDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesBrewUntapPath, Name: "path", Flag: true}, + {Key: ArgBootstrapPackagesBrewUntapTaps, Name: "TAPS", Required: true}, + {}, + {Key: FlagBootstrapPackagesImportEnv, Name: "env", Flag: true}, + {Key: FlagBootstrapPackagesImportGlobal, Name: "global", Flag: true}, + {Key: FlagBootstrapPackagesImportManager, Name: "manager", Flag: true, Choices: []string{"brew"}, Default: []string{"brew"}}, + {Key: FlagBootstrapPackagesImportAll, Name: "all", Flag: true}, + {Key: FlagBootstrapPackagesImportDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesImportPath, Name: "path", Flag: true}, + {}, + {Key: FlagBootstrapPackagesPruneManager, Name: "manager", Flag: true, Choices: []string{"brew"}, Default: []string{"brew"}}, + {Key: FlagBootstrapPackagesPruneDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesPruneYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapPackagesStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapPackagesStatusMissing, Name: "missing", Flag: true}, + {}, + {Key: FlagBootstrapPackagesUpgradeManager, Name: "manager", Flag: true}, + {Key: FlagBootstrapPackagesUpgradeDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesUpgradeYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapPackagesUpgradePackage, Name: "PACKAGE"}, + {}, + {Key: FlagBootstrapPackagesUseEnv, Name: "env", Flag: true}, + {Key: FlagBootstrapPackagesUseGlobal, Name: "global", Flag: true}, + {Key: FlagBootstrapPackagesUseDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapPackagesUsePath, Name: "path", Flag: true}, + {Key: FlagBootstrapPackagesUseYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapPackagesUsePackage, Name: "PACKAGE", Required: true}, + {}, + {Key: FlagBootstrapPlanJson, Name: "json", Flag: true}, + {Key: FlagBootstrapPlanDetailedExitcode, Name: "detailed-exitcode", Flag: true}, + {Key: FlagBootstrapPlanPromptSecrets, Name: "prompt-secrets", Flag: true}, + {}, + {}, + {Key: FlagBootstrapPluginsApplyDryRun, Name: "dry-run", Flag: true}, + {}, + {Key: FlagBootstrapPluginsStatusMissing, Name: "missing", Flag: true}, + {}, + {Key: FlagBootstrapRemoteAll, Name: "all", Flag: true}, + {Key: FlagBootstrapRemoteBootstrapCommand, Name: "bootstrap-command", Flag: true}, + {Key: FlagBootstrapRemoteConnectTimeout, Name: "connect-timeout", Flag: true, Default: []string{"10"}}, + {Key: FlagBootstrapRemoteExclude, Name: "exclude", Flag: true}, + {Key: FlagBootstrapRemoteFailFast, Name: "fail-fast", Flag: true}, + {Key: FlagBootstrapRemoteForceDotfiles, Name: "force-dotfiles", Flag: true}, + {Key: FlagBootstrapRemoteHost, Name: "host", Flag: true}, + {Key: FlagBootstrapRemoteIdentityFile, Name: "identity-file", Flag: true}, + {Key: FlagBootstrapRemoteDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapRemoteKeepStaging, Name: "keep-staging", Flag: true}, + {Key: FlagBootstrapRemoteMiseBin, Name: "mise-bin", Flag: true}, + {Key: FlagBootstrapRemoteOnly, Name: "only", Flag: true, Choices: []string{"plugins", "packages", "accounts", "files", "services", "firewall", "compose", "repos", "dotfiles", "mise-shell-activate", "shell", "macos-defaults", "defaults", "macos-launchd-agents", "launchd", "linux-systemd-units", "systemd", "user", "tools", "task", "final-hook"}}, + {Key: FlagBootstrapRemotePort, Name: "port", Flag: true}, + {Key: FlagBootstrapRemotePromptSecrets, Name: "prompt-secrets", Flag: true}, + {Key: FlagBootstrapRemoteRemoteMise, Name: "remote-mise", Flag: true}, + {Key: FlagBootstrapRemoteSkip, Name: "skip", Flag: true, Choices: []string{"plugins", "packages", "accounts", "files", "services", "firewall", "compose", "repos", "dotfiles", "mise-shell-activate", "shell", "macos-defaults", "defaults", "macos-launchd-agents", "launchd", "linux-systemd-units", "systemd", "user", "tools", "task", "final-hook"}}, + {Key: FlagBootstrapRemoteSource, Name: "source", Flag: true}, + {Key: FlagBootstrapRemoteSshOption, Name: "ssh-option", Flag: true}, + {Key: FlagBootstrapRemoteTag, Name: "tag", Flag: true}, + {Key: FlagBootstrapRemoteUpdate, Name: "update", Flag: true}, + {Key: FlagBootstrapRemoteYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapRemoteTarget, Name: "TARGET"}, + {}, + {}, + {Key: FlagBootstrapReposApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapReposApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapReposExecContinueOnError, Name: "continue-on-error", Flag: true}, + {Key: FlagBootstrapReposExecDryRun, Name: "dry-run", Flag: true}, + {Key: ArgBootstrapReposExecPath, Name: "PATH"}, + {Key: ArgBootstrapReposExecCommand, Name: "COMMAND", Required: true}, + {}, + {Key: FlagBootstrapReposStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapReposStatusMissing, Name: "missing", Flag: true}, + {}, + {Key: FlagBootstrapReposUpdateDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapReposUpdateYes, Name: "yes", Flag: true}, + {Key: ArgBootstrapReposUpdatePath, Name: "PATH"}, + {}, + {}, + {Key: FlagBootstrapSecretsStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapSecretsStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapServicesApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapServicesApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapServicesStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapServicesStatusMissing, Name: "missing", Flag: true}, + {}, + {Key: FlagBootstrapStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapStatusMissing, Name: "missing", Flag: true}, + {Key: FlagBootstrapStatusPromptSecrets, Name: "prompt-secrets", Flag: true}, + {}, + {}, + {Key: FlagBootstrapSystemdApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapSystemdApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapSystemdStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapSystemdStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagBootstrapUserApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagBootstrapUserApplyYes, Name: "yes", Flag: true}, + {}, + {Key: FlagBootstrapUserStatusJson, Name: "json", Flag: true}, + {Key: FlagBootstrapUserStatusMissing, Name: "missing", Flag: true}, + {}, + {}, + {Key: FlagCacheClearOutdate, Name: "outdate", Flag: true}, + {Key: FlagCacheClearTask, Name: "task", Flag: true}, + {Key: ArgCacheClearTool, Name: "TOOL"}, + {}, + {}, + {Key: FlagCachePruneVerbose, Name: "verbose", Flag: true}, + {Key: FlagCachePruneDryRun, Name: "dry-run", Flag: true}, + {Key: ArgCachePruneTool, Name: "TOOL"}, + {}, + {Key: FlagCacheTaskJson, Name: "json", Flag: true}, + {Key: ArgCacheTaskTask, Name: "TASK", Required: true}, + {}, + {Key: FlagCompletionShell, Name: "shell", Flag: true, Choices: []string{"bash", "fish", "powershell", "zsh"}}, + {Key: FlagCompletionIncludeBashCompletionLib, Name: "include-bash-completion-lib", Flag: true}, + {Key: FlagCompletionUsage, Name: "usage", Flag: true}, + {Key: ArgCompletionShell, Name: "SHELL", Choices: []string{"bash", "fish", "powershell", "zsh"}}, + {}, + {Key: FlagConfigJson, Name: "json", Flag: true}, + {Key: FlagConfigNoHeader, Name: "no-header", Flag: true}, + {Key: FlagConfigTrackedConfigs, Name: "tracked-configs", Flag: true}, + {}, + {Key: FlagConfigGetFile, Name: "file", Flag: true}, + {Key: ArgConfigGetKey, Name: "KEY"}, + {}, + {Key: FlagConfigLsJson, Name: "json", Flag: true}, + {Key: FlagConfigLsNoHeader, Name: "no-header", Flag: true}, + {Key: FlagConfigLsTrackedConfigs, Name: "tracked-configs", Flag: true}, + {}, + {Key: FlagConfigSetFile, Name: "file", Flag: true}, + {Key: FlagConfigSetType, Name: "type", Flag: true, Choices: []string{"infer", "string", "integer", "float", "bool", "list", "set"}, Default: []string{"infer"}}, + {Key: ArgConfigSetKey, Name: "KEY", Required: true}, + {Key: ArgConfigSetValue, Name: "VALUE"}, + {}, + {Key: ArgCurrentPlugin, Name: "PLUGIN"}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {Key: FlagDotfilesAddForce, Name: "force", Flag: true}, + {Key: FlagDotfilesAddGlobal, Name: "global", Flag: true}, + {Key: FlagDotfilesAddLocal, Name: "local", Flag: true}, + {Key: FlagDotfilesAddMode, Name: "mode", Flag: true}, + {Key: FlagDotfilesAddDryRun, Name: "dry-run", Flag: true}, + {Key: FlagDotfilesAddNoApply, Name: "no-apply", Flag: true}, + {Key: FlagDotfilesAddPath, Name: "path", Flag: true}, + {Key: FlagDotfilesAddSource, Name: "source", Flag: true}, + {Key: FlagDotfilesAddYes, Name: "yes", Flag: true}, + {Key: ArgDotfilesAddTarget, Name: "TARGET", Required: true}, + {}, + {Key: FlagDotfilesApplyForce, Name: "force", Flag: true}, + {Key: FlagDotfilesApplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagDotfilesApplyYes, Name: "yes", Flag: true}, + {Key: ArgDotfilesApplyTarget, Name: "TARGET"}, + {}, + {Key: FlagDotfilesEditApply, Name: "apply", Flag: true}, + {Key: FlagDotfilesEditMode, Name: "mode", Flag: true}, + {Key: FlagDotfilesEditSource, Name: "source", Flag: true}, + {Key: FlagDotfilesEditYes, Name: "yes", Flag: true}, + {Key: ArgDotfilesEditTarget, Name: "TARGET", Required: true}, + {}, + {Key: FlagDotfilesStatusJson, Name: "json", Flag: true}, + {Key: FlagDotfilesStatusMissing, Name: "missing", Flag: true}, + {Key: ArgDotfilesStatusTarget, Name: "TARGET"}, + {}, + {Key: FlagDotfilesUnapplyForce, Name: "force", Flag: true}, + {Key: FlagDotfilesUnapplyDryRun, Name: "dry-run", Flag: true}, + {Key: FlagDotfilesUnapplyYes, Name: "yes", Flag: true}, + {Key: ArgDotfilesUnapplyTarget, Name: "TARGET"}, + {}, + {Key: FlagDoctorJson, Name: "json", Flag: true}, + {}, + {Key: FlagDoctorPathFull, Name: "full", Flag: true}, + {}, + {Key: FlagEnShell, Name: "shell", Flag: true}, + {Key: ArgEnDir, Name: "DIR", Default: []string{"."}}, + {}, + {Key: FlagEnvDotenv, Name: "dotenv", Flag: true}, + {Key: FlagEnvJson, Name: "json", Flag: true}, + {Key: FlagEnvShell, Name: "shell", Flag: true, Choices: []string{"bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"}}, + {Key: FlagEnvJsonExtended, Name: "json-extended", Flag: true}, + {Key: FlagEnvRedacted, Name: "redacted", Flag: true}, + {Key: FlagEnvValues, Name: "values", Flag: true}, + {Key: ArgEnvToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagExecCommand, Name: "command", Flag: true}, + {Key: FlagExecJobs, Name: "jobs", Flag: true}, + {Key: FlagExecAllowEnv, Name: "allow-env", Flag: true}, + {Key: FlagExecAllowNet, Name: "allow-net", Flag: true}, + {Key: FlagExecAllowRead, Name: "allow-read", Flag: true}, + {Key: FlagExecAllowWrite, Name: "allow-write", Flag: true}, + {Key: FlagExecDenyAll, Name: "deny-all", Flag: true}, + {Key: FlagExecDenyEnv, Name: "deny-env", Flag: true}, + {Key: FlagExecDenyNet, Name: "deny-net", Flag: true}, + {Key: FlagExecDenyRead, Name: "deny-read", Flag: true}, + {Key: FlagExecDenyWrite, Name: "deny-write", Flag: true}, + {Key: FlagExecFreshEnv, Name: "fresh-env", Flag: true}, + {Key: FlagExecNoDeps, Name: "no-deps", Flag: true}, + {Key: FlagExecRaw, Name: "raw", Flag: true}, + {Key: ArgExecToolVersion, Name: "TOOL@VERSION"}, + {Key: ArgExecCommand, Name: "COMMAND"}, + {}, + {Key: FlagFmtAll, Name: "all", Flag: true}, + {Key: FlagFmtCheck, Name: "check", Flag: true}, + {Key: FlagFmtStdin, Name: "stdin", Flag: true}, + {}, + {}, + {Key: FlagGenerateBootstrapLocalize, Name: "localize", Flag: true}, + {Key: FlagGenerateBootstrapVersion, Name: "version", Flag: true}, + {Key: FlagGenerateBootstrapWrite, Name: "write", Flag: true}, + {Key: FlagGenerateBootstrapLocalizedDir, Name: "localized-dir", Flag: true, Default: []string{".mise"}}, + {}, + {Key: FlagGenerateConfigGlobal, Name: "global", Flag: true}, + {Key: FlagGenerateConfigDryRun, Name: "dry-run", Flag: true}, + {Key: FlagGenerateConfigToolVersions, Name: "tool-versions", Flag: true}, + {Key: ArgGenerateConfigPath, Name: "PATH"}, + {}, + {Key: FlagGenerateDevcontainerImage, Name: "image", Flag: true}, + {Key: FlagGenerateDevcontainerMountMiseData, Name: "mount-mise-data", Flag: true}, + {Key: FlagGenerateDevcontainerName, Name: "name", Flag: true}, + {Key: FlagGenerateDevcontainerWrite, Name: "write", Flag: true}, + {}, + {Key: FlagGenerateGitPreCommitTask, Name: "task", Flag: true, Default: []string{"pre-commit"}}, + {Key: FlagGenerateGitPreCommitWrite, Name: "write", Flag: true}, + {Key: FlagGenerateGitPreCommitHook, Name: "hook", Flag: true, Default: []string{"pre-commit"}}, + {}, + {Key: FlagGenerateGithubActionTask, Name: "task", Flag: true, Default: []string{"ci"}}, + {Key: FlagGenerateGithubActionWrite, Name: "write", Flag: true}, + {Key: FlagGenerateGithubActionName, Name: "name", Flag: true, Default: []string{"ci"}}, + {}, + {Key: FlagGenerateTaskDocsInject, Name: "inject", Flag: true}, + {Key: FlagGenerateTaskDocsIndex, Name: "index", Flag: true}, + {Key: FlagGenerateTaskDocsMulti, Name: "multi", Flag: true}, + {Key: FlagGenerateTaskDocsOutput, Name: "output", Flag: true}, + {Key: FlagGenerateTaskDocsRoot, Name: "root", Flag: true}, + {Key: FlagGenerateTaskDocsStyle, Name: "style", Flag: true, Choices: []string{"simple", "detailed"}, Default: []string{"simple"}}, + {}, + {Key: FlagGenerateTaskStubsDir, Name: "dir", Flag: true, Default: []string{"bin"}}, + {Key: FlagGenerateTaskStubsMiseBin, Name: "mise-bin", Flag: true, Default: []string{"mise"}}, + {}, + {Key: FlagGenerateToolStubBin, Name: "bin", Flag: true}, + {Key: FlagGenerateToolStubBootstrap, Name: "bootstrap", Flag: true}, + {Key: FlagGenerateToolStubBootstrapVersion, Name: "bootstrap-version", Flag: true}, + {Key: FlagGenerateToolStubFetch, Name: "fetch", Flag: true}, + {Key: FlagGenerateToolStubHttp, Name: "http", Flag: true, Default: []string{"http"}}, + {Key: FlagGenerateToolStubLock, Name: "lock", Flag: true}, + {Key: FlagGenerateToolStubPlatformBin, Name: "platform-bin", Flag: true}, + {Key: FlagGenerateToolStubPlatformUrl, Name: "platform-url", Flag: true}, + {Key: FlagGenerateToolStubSkipDownload, Name: "skip-download", Flag: true}, + {Key: FlagGenerateToolStubUrl, Name: "url", Flag: true}, + {Key: FlagGenerateToolStubVersion, Name: "version", Flag: true, Default: []string{"latest"}}, + {Key: ArgGenerateToolStubOutput, Name: "OUTPUT", Required: true}, + {}, + {}, + {Key: FlagGithubTokenOauth, Name: "oauth", Flag: true}, + {Key: FlagGithubTokenRaw, Name: "raw", Flag: true}, + {Key: FlagGithubTokenRefresh, Name: "refresh", Flag: true}, + {Key: FlagGithubTokenUnmask, Name: "unmask", Flag: true}, + {Key: ArgGithubTokenHost, Name: "HOST", Default: []string{"github.com"}}, + {}, + {Key: FlagGlobalFuzzy, Name: "fuzzy", Flag: true}, + {Key: FlagGlobalPath, Name: "path", Flag: true}, + {Key: FlagGlobalPin, Name: "pin", Flag: true}, + {Key: FlagGlobalRemove, Name: "remove", Flag: true}, + {Key: ArgGlobalToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagHookEnvForce, Name: "force", Flag: true}, + {Key: FlagHookEnvQuiet, Name: "quiet", Flag: true}, + {Key: FlagHookEnvShell, Name: "shell", Flag: true, Choices: []string{"bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"}}, + {Key: FlagHookEnvReason, Name: "reason", Flag: true, Choices: []string{"precmd", "chpwd"}}, + {Key: FlagHookEnvStatus, Name: "status", Flag: true}, + {}, + {Key: FlagHookNotFoundShell, Name: "shell", Flag: true, Choices: []string{"bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"}}, + {Key: ArgHookNotFoundBin, Name: "BIN", Required: true}, + {}, + {Key: FlagImplodeDryRun, Name: "dry-run", Flag: true}, + {Key: FlagImplodeConfig, Name: "config", Flag: true}, + {}, + {Key: FlagEditGlobal, Name: "global", Flag: true}, + {Key: FlagEditDryRun, Name: "dry-run", Flag: true}, + {Key: FlagEditToolVersions, Name: "tool-versions", Flag: true}, + {Key: ArgEditPath, Name: "PATH"}, + {}, + {Key: FlagInstallForce, Name: "force", Flag: true}, + {Key: FlagInstallJobs, Name: "jobs", Flag: true}, + {Key: FlagInstallDryRun, Name: "dry-run", Flag: true}, + {Key: FlagInstallVerbose, Name: "verbose", Flag: true}, + {Key: FlagInstallDryRunCode, Name: "dry-run-code", Flag: true}, + {Key: FlagInstallMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: FlagInstallMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagInstallRaw, Name: "raw", Flag: true}, + {Key: FlagInstallShared, Name: "shared", Flag: true}, + {Key: FlagInstallSystem, Name: "system", Flag: true}, + {Key: ArgInstallToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: ArgInstallIntoToolVersion, Name: "TOOL@VERSION", Required: true}, + {Key: ArgInstallIntoPath, Name: "PATH", Required: true}, + {}, + {Key: FlagLatestInstalled, Name: "installed", Flag: true}, + {Key: FlagLatestMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: ArgLatestToolVersion, Name: "TOOL@VERSION", Required: true}, + {Key: ArgLatestAsdfVersion, Name: "ASDF_VERSION"}, + {}, + {Key: FlagLinkForce, Name: "force", Flag: true}, + {Key: ArgLinkToolVersion, Name: "TOOL@VERSION", Required: true}, + {Key: ArgLinkPath, Name: "PATH", Required: true}, + {}, + {Key: FlagLocalParent, Name: "parent", Flag: true}, + {Key: FlagLocalFuzzy, Name: "fuzzy", Flag: true}, + {Key: FlagLocalPath, Name: "path", Flag: true}, + {Key: FlagLocalPin, Name: "pin", Flag: true}, + {Key: FlagLocalRemove, Name: "remove", Flag: true}, + {Key: ArgLocalToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagLockGlobal, Name: "global", Flag: true}, + {Key: FlagLockJobs, Name: "jobs", Flag: true}, + {Key: FlagLockDryRun, Name: "dry-run", Flag: true}, + {Key: FlagLockPlatform, Name: "platform", Flag: true}, + {Key: FlagLockBump, Name: "bump", Flag: true}, + {Key: FlagLockJson, Name: "json", Flag: true}, + {Key: FlagLockLocal, Name: "local", Flag: true}, + {Key: FlagLockMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: ArgLockTool, Name: "TOOL"}, + {}, + {Key: FlagLsCurrent, Name: "current", Flag: true}, + {Key: FlagLsGlobal, Name: "global", Flag: true}, + {Key: FlagLsInstalled, Name: "installed", Flag: true}, + {Key: FlagLsJson, Name: "json", Flag: true}, + {Key: FlagLsLocal, Name: "local", Flag: true}, + {Key: FlagLsMissing, Name: "missing", Flag: true}, + {Key: FlagLsOffline, Name: "offline", Flag: true}, + {Key: FlagLsPlugin, Name: "plugin", Flag: true}, + {Key: FlagLsAllSources, Name: "all-sources", Flag: true}, + {Key: FlagLsMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagLsNoHeader, Name: "no-header", Flag: true}, + {Key: FlagLsOutdated, Name: "outdated", Flag: true}, + {Key: FlagLsPrefix, Name: "prefix", Flag: true}, + {Key: FlagLsPrunable, Name: "prunable", Flag: true}, + {Key: ArgLsInstalledTool, Name: "INSTALLED_TOOL"}, + {}, + {Key: FlagLsRemoteAll, Name: "all", Flag: true}, + {Key: FlagLsRemoteMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: FlagLsRemoteJson, Name: "json", Flag: true}, + {Key: FlagLsRemoteNoVersionsHost, Name: "no-versions-host", Flag: true}, + {Key: FlagLsRemotePrerelease, Name: "prerelease", Flag: true}, + {Key: FlagLsRemoteStrictMetadata, Name: "strict-metadata", Flag: true}, + {Key: ArgLsRemoteToolVersion, Name: "TOOL@VERSION"}, + {Key: ArgLsRemotePrefix, Name: "PREFIX"}, + {}, + {}, + {}, + {Key: FlagOciBuildCopy, Name: "copy", Flag: true}, + {Key: FlagOciBuildOutput, Name: "output", Flag: true, Default: []string{"./mise-oci"}}, + {Key: FlagOciBuildFrom, Name: "from", Flag: true}, + {Key: FlagOciBuildIncludeGlobal, Name: "include-global", Flag: true}, + {Key: FlagOciBuildTag, Name: "tag", Flag: true}, + {Key: FlagOciBuildMountPoint, Name: "mount-point", Flag: true}, + {Key: FlagOciBuildNoMise, Name: "no-mise", Flag: true}, + {Key: FlagOciBuildOwner, Name: "owner", Flag: true}, + {}, + {Key: FlagOciPushCacheFrom, Name: "cache-from", Flag: true}, + {Key: FlagOciPushFrom, Name: "from", Flag: true}, + {Key: FlagOciPushImageDir, Name: "image-dir", Flag: true}, + {Key: FlagOciPushIncludeGlobal, Name: "include-global", Flag: true}, + {Key: FlagOciPushMountPoint, Name: "mount-point", Flag: true}, + {Key: FlagOciPushNoCache, Name: "no-cache", Flag: true}, + {Key: FlagOciPushNoMise, Name: "no-mise", Flag: true}, + {Key: FlagOciPushOwner, Name: "owner", Flag: true}, + {Key: FlagOciPushUpdateIndex, Name: "update-index", Flag: true}, + {Key: ArgOciPushRef, Name: "REF", Required: true}, + {}, + {Key: FlagOciRunEngine, Name: "engine", Flag: true, Choices: []string{"auto", "podman", "docker"}, Default: []string{"auto"}}, + {Key: FlagOciRunFrom, Name: "from", Flag: true}, + {Key: FlagOciRunImageDir, Name: "image-dir", Flag: true}, + {Key: FlagOciRunIncludeGlobal, Name: "include-global", Flag: true}, + {Key: FlagOciRunKeep, Name: "keep", Flag: true}, + {Key: FlagOciRunMountPoint, Name: "mount-point", Flag: true}, + {Key: FlagOciRunNoMise, Name: "no-mise", Flag: true}, + {Key: FlagOciRunOwner, Name: "owner", Flag: true}, + {Key: FlagOciRunVolume, Name: "volume", Flag: true}, + {Key: FlagOciRunEnv, Name: "env", Flag: true}, + {Key: FlagOciRunInteractive, Name: "interactive", Flag: true}, + {Key: FlagOciRunTty, Name: "tty", Flag: true}, + {Key: FlagOciRunWorkdir, Name: "workdir", Flag: true}, + {Key: ArgOciRunCmd, Name: "CMD"}, + {}, + {Key: FlagOutdatedJson, Name: "json", Flag: true}, + {Key: FlagOutdatedBump, Name: "bump", Flag: true}, + {Key: FlagOutdatedInactive, Name: "inactive", Flag: true}, + {Key: FlagOutdatedLocal, Name: "local", Flag: true}, + {Key: FlagOutdatedMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagOutdatedNoHeader, Name: "no-header", Flag: true}, + {Key: ArgOutdatedToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagPatronsJson, Name: "json", Flag: true}, + {Key: FlagPatronsRefresh, Name: "refresh", Flag: true}, + {}, + {Key: FlagPluginsAll, Name: "all", Flag: true}, + {Key: FlagPluginsCore, Name: "core", Flag: true}, + {Key: FlagPluginsUrls, Name: "urls", Flag: true}, + {Key: FlagPluginsRefs, Name: "refs", Flag: true}, + {Key: FlagPluginsUser, Name: "user", Flag: true}, + {}, + {Key: FlagPluginsInstallAll, Name: "all", Flag: true}, + {Key: FlagPluginsInstallForce, Name: "force", Flag: true}, + {Key: FlagPluginsInstallJobs, Name: "jobs", Flag: true}, + {Key: FlagPluginsInstallVerbose, Name: "verbose", Flag: true}, + {Key: ArgPluginsInstallNewPlugin, Name: "NEW_PLUGIN"}, + {Key: ArgPluginsInstallGitUrl, Name: "GIT_URL"}, + {Key: ArgPluginsInstallRest, Name: "REST"}, + {}, + {Key: FlagPluginsLinkForce, Name: "force", Flag: true}, + {Key: ArgPluginsLinkName, Name: "NAME", Required: true}, + {Key: ArgPluginsLinkDir, Name: "DIR"}, + {}, + {Key: FlagPluginsLsAll, Name: "all", Flag: true}, + {Key: FlagPluginsLsCore, Name: "core", Flag: true}, + {Key: FlagPluginsLsOutdated, Name: "outdated", Flag: true}, + {Key: FlagPluginsLsUrls, Name: "urls", Flag: true}, + {Key: FlagPluginsLsRefs, Name: "refs", Flag: true}, + {Key: FlagPluginsLsUser, Name: "user", Flag: true}, + {}, + {Key: FlagPluginsLsRemoteUrls, Name: "urls", Flag: true}, + {Key: FlagPluginsLsRemoteOnlyNames, Name: "only-names", Flag: true}, + {}, + {Key: FlagPluginsUninstallAll, Name: "all", Flag: true}, + {Key: FlagPluginsUninstallPurge, Name: "purge", Flag: true}, + {Key: ArgPluginsUninstallPlugin, Name: "PLUGIN"}, + {}, + {Key: FlagPluginsUpdateJobs, Name: "jobs", Flag: true}, + {Key: ArgPluginsUpdatePlugin, Name: "PLUGIN"}, + {}, + {Key: FlagDepsExplain, Name: "explain", Flag: true}, + {Key: FlagDepsForce, Name: "force", Flag: true}, + {Key: FlagDepsDryRun, Name: "dry-run", Flag: true}, + {Key: FlagDepsList, Name: "list", Flag: true}, + {Key: FlagDepsMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagDepsOnly, Name: "only", Flag: true}, + {Key: FlagDepsSkip, Name: "skip", Flag: true}, + {Key: ArgDepsProvider, Name: "PROVIDER"}, + {}, + {Key: FlagDepsAddDev, Name: "dev", Flag: true}, + {Key: ArgDepsAddPackages, Name: "PACKAGES", Required: true}, + {}, + {Key: FlagDepsInstallExplain, Name: "explain", Flag: true}, + {Key: FlagDepsInstallForce, Name: "force", Flag: true}, + {Key: FlagDepsInstallDryRun, Name: "dry-run", Flag: true}, + {Key: FlagDepsInstallList, Name: "list", Flag: true}, + {Key: FlagDepsInstallMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagDepsInstallOnly, Name: "only", Flag: true}, + {Key: FlagDepsInstallSkip, Name: "skip", Flag: true}, + {Key: ArgDepsInstallProvider, Name: "PROVIDER"}, + {}, + {Key: ArgDepsRemovePackages, Name: "PACKAGES", Required: true}, + {}, + {Key: FlagPruneDryRun, Name: "dry-run", Flag: true}, + {Key: FlagPruneConfigs, Name: "configs", Flag: true}, + {Key: FlagPruneDryRunCode, Name: "dry-run-code", Flag: true}, + {Key: FlagPruneMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagPruneTools, Name: "tools", Flag: true}, + {Key: ArgPruneInstalledTool, Name: "INSTALLED_TOOL"}, + {}, + {Key: FlagRegistryBackend, Name: "backend", Flag: true}, + {Key: FlagRegistryComplete, Name: "complete", Flag: true}, + {Key: FlagRegistryHideAliased, Name: "hide-aliased", Flag: true}, + {Key: FlagRegistryJson, Name: "json", Flag: true}, + {Key: FlagRegistrySecurity, Name: "security", Flag: true}, + {Key: ArgRegistryName, Name: "NAME"}, + {}, + {}, + {Key: FlagReshimForce, Name: "force", Flag: true}, + {Key: ArgReshimTool, Name: "TOOL"}, + {Key: ArgReshimVersion, Name: "VERSION"}, + {}, + {Key: FlagRunAffected, Name: "affected", Flag: true}, + {Key: FlagRunAffectedBase, Name: "affected-base", Flag: true}, + {Key: FlagRunAffectedExplain, Name: "affected-explain", Flag: true}, + {Key: FlagRunAffectedHead, Name: "affected-head", Flag: true}, + {Key: FlagRunAffectedJson, Name: "affected-json", Flag: true}, + {Key: FlagRunContinueOnError, Name: "continue-on-error", Flag: true}, + {Key: FlagRunCd, Name: "cd", Flag: true}, + {Key: FlagRunForce, Name: "force", Flag: true}, + {Key: FlagRunJobs, Name: "jobs", Flag: true}, + {Key: FlagRunDryRun, Name: "dry-run", Flag: true}, + {Key: FlagRunOutput, Name: "output", Flag: true}, + {Key: FlagRunQuiet, Name: "quiet", Flag: true}, + {Key: FlagRunRaw, Name: "raw", Flag: true}, + {Key: FlagRunShell, Name: "shell", Flag: true}, + {Key: FlagRunSilent, Name: "silent", Flag: true}, + {Key: FlagRunTool, Name: "tool", Flag: true}, + {Key: FlagRunAllowEnv, Name: "allow-env", Flag: true}, + {Key: FlagRunAllowNet, Name: "allow-net", Flag: true}, + {Key: FlagRunAllowRead, Name: "allow-read", Flag: true}, + {Key: FlagRunAllowWrite, Name: "allow-write", Flag: true}, + {Key: FlagRunDenyAll, Name: "deny-all", Flag: true}, + {Key: FlagRunDenyEnv, Name: "deny-env", Flag: true}, + {Key: FlagRunDenyNet, Name: "deny-net", Flag: true}, + {Key: FlagRunDenyRead, Name: "deny-read", Flag: true}, + {Key: FlagRunDenyWrite, Name: "deny-write", Flag: true}, + {Key: FlagRunFreshEnv, Name: "fresh-env", Flag: true}, + {Key: FlagRunNoCache, Name: "no-cache", Flag: true}, + {Key: FlagRunNoDeps, Name: "no-deps", Flag: true}, + {Key: FlagRunNoTimings, Name: "no-timings", Flag: true}, + {Key: FlagRunSkipDeps, Name: "skip-deps", Flag: true}, + {Key: FlagRunSkipTools, Name: "skip-tools", Flag: true}, + {Key: FlagRunTaskCache, Name: "task-cache", Flag: true, Choices: []string{"read-write", "read-only", "write-only", "off", "local-only"}, Default: []string{"read-write"}}, + {Key: FlagRunTaskCacheExplain, Name: "task-cache-explain", Flag: true}, + {Key: FlagRunTaskCacheExplainJson, Name: "task-cache-explain-json", Flag: true}, + {Key: FlagRunTaskCacheStats, Name: "task-cache-stats", Flag: true}, + {Key: FlagRunTimeout, Name: "timeout", Flag: true}, + {Key: FlagRunTimings, Name: "timings", Flag: true}, + {}, + {Key: FlagSearchInteractive, Name: "interactive", Flag: true}, + {Key: FlagSearchMatchType, Name: "match-type", Flag: true, Choices: []string{"equal", "contains", "fuzzy"}, Default: []string{"fuzzy"}}, + {Key: FlagSearchNoHeader, Name: "no-header", Flag: true}, + {Key: ArgSearchName, Name: "NAME"}, + {}, + {Key: FlagSelfUpdateForce, Name: "force", Flag: true}, + {Key: FlagSelfUpdateYes, Name: "yes", Flag: true}, + {Key: FlagSelfUpdateNoPlugins, Name: "no-plugins", Flag: true}, + {Key: ArgSelfUpdateVersion, Name: "VERSION"}, + {}, + {Key: FlagSetEnv, Name: "env", Flag: true}, + {Key: FlagSetGlobal, Name: "global", Flag: true}, + {Key: FlagSetAgeEncrypt, Name: "age-encrypt", Flag: true}, + {Key: FlagSetAgeKeyFile, Name: "age-key-file", Flag: true}, + {Key: FlagSetAgeRecipient, Name: "age-recipient", Flag: true}, + {Key: FlagSetAgeSshRecipient, Name: "age-ssh-recipient", Flag: true}, + {Key: FlagSetComplete, Name: "complete", Flag: true}, + {Key: FlagSetFile, Name: "file", Flag: true}, + {Key: FlagSetNoRedact, Name: "no-redact", Flag: true}, + {Key: FlagSetPrompt, Name: "prompt", Flag: true}, + {Key: FlagSetRemove, Name: "remove", Flag: true}, + {Key: FlagSetStdin, Name: "stdin", Flag: true}, + {Key: ArgSetEnvVar, Name: "ENV_VAR"}, + {}, + {Key: FlagSettingsAll, Name: "all", Flag: true}, + {Key: FlagSettingsJson, Name: "json", Flag: true}, + {Key: FlagSettingsLocal, Name: "local", Flag: true}, + {Key: FlagSettingsToml, Name: "toml", Flag: true}, + {Key: FlagSettingsComplete, Name: "complete", Flag: true}, + {Key: FlagSettingsJsonExtended, Name: "json-extended", Flag: true}, + {Key: ArgSettingsSetting, Name: "SETTING"}, + {Key: ArgSettingsValue, Name: "VALUE"}, + {}, + {Key: FlagSettingsAddLocal, Name: "local", Flag: true}, + {Key: ArgSettingsAddSetting, Name: "SETTING", Required: true}, + {Key: ArgSettingsAddValue, Name: "VALUE"}, + {}, + {Key: FlagSettingsGetLocal, Name: "local", Flag: true}, + {Key: ArgSettingsGetSetting, Name: "SETTING", Required: true}, + {}, + {Key: FlagSettingsLsAll, Name: "all", Flag: true}, + {Key: FlagSettingsLsJson, Name: "json", Flag: true}, + {Key: FlagSettingsLsLocal, Name: "local", Flag: true}, + {Key: FlagSettingsLsToml, Name: "toml", Flag: true}, + {Key: FlagSettingsLsComplete, Name: "complete", Flag: true}, + {Key: FlagSettingsLsJsonExtended, Name: "json-extended", Flag: true}, + {Key: ArgSettingsLsSetting, Name: "SETTING"}, + {}, + {Key: FlagSettingsSetLocal, Name: "local", Flag: true}, + {Key: ArgSettingsSetSetting, Name: "SETTING", Required: true}, + {Key: ArgSettingsSetValue, Name: "VALUE"}, + {}, + {Key: FlagSettingsUnsetLocal, Name: "local", Flag: true}, + {Key: ArgSettingsUnsetKey, Name: "KEY", Required: true}, + {}, + {Key: FlagShellJobs, Name: "jobs", Flag: true}, + {Key: FlagShellUnset, Name: "unset", Flag: true}, + {Key: FlagShellRaw, Name: "raw", Flag: true}, + {Key: ArgShellToolVersion, Name: "TOOL@VERSION", Required: true}, + {}, + {Key: FlagShellAliasNoHeader, Name: "no-header", Flag: true}, + {}, + {Key: ArgShellAliasGetShellAlias, Name: "shell_alias", Required: true}, + {}, + {Key: FlagShellAliasLsNoHeader, Name: "no-header", Flag: true}, + {}, + {Key: ArgShellAliasSetShellAlias, Name: "shell_alias", Required: true}, + {Key: ArgShellAliasSetCommand, Name: "COMMAND"}, + {}, + {Key: ArgShellAliasUnsetShellAlias, Name: "shell_alias", Required: true}, + {}, + {}, + {}, + {Key: FlagSyncNodeBrew, Name: "brew", Flag: true}, + {Key: FlagSyncNodeNodenv, Name: "nodenv", Flag: true}, + {Key: FlagSyncNodeNvm, Name: "nvm", Flag: true}, + {}, + {Key: FlagSyncPythonPyenv, Name: "pyenv", Flag: true}, + {Key: FlagSyncPythonUv, Name: "uv", Flag: true}, + {}, + {Key: FlagSyncRubyBrew, Name: "brew", Flag: true}, + {}, + {Key: FlagTasksGlobal, Name: "global", Flag: true}, + {Key: FlagTasksJson, Name: "json", Flag: true}, + {Key: FlagTasksLocal, Name: "local", Flag: true}, + {Key: FlagTasksExtended, Name: "extended", Flag: true}, + {Key: FlagTasksAll, Name: "all", Flag: true}, + {Key: FlagTasksComplete, Name: "complete", Flag: true}, + {Key: FlagTasksHidden, Name: "hidden", Flag: true}, + {Key: FlagTasksNameOnly, Name: "name-only", Flag: true}, + {Key: FlagTasksNoHeader, Name: "no-header", Flag: true}, + {Key: FlagTasksSort, Name: "sort", Flag: true, Choices: []string{"name", "alias", "description", "source"}}, + {Key: FlagTasksSortOrder, Name: "sort-order", Flag: true, Choices: []string{"asc", "desc"}}, + {Key: FlagTasksUsage, Name: "usage", Flag: true}, + {Key: ArgTasksTask, Name: "TASK"}, + {}, + {Key: FlagTasksAddAlias, Name: "alias", Flag: true}, + {Key: FlagTasksAddDepends, Name: "depends", Flag: true}, + {Key: FlagTasksAddDir, Name: "dir", Flag: true}, + {Key: FlagTasksAddFile, Name: "file", Flag: true}, + {Key: FlagTasksAddHide, Name: "hide", Flag: true}, + {Key: FlagTasksAddQuiet, Name: "quiet", Flag: true}, + {Key: FlagTasksAddRaw, Name: "raw", Flag: true}, + {Key: FlagTasksAddSources, Name: "sources", Flag: true}, + {Key: FlagTasksAddWaitFor, Name: "wait-for", Flag: true}, + {Key: FlagTasksAddDependsPost, Name: "depends-post", Flag: true}, + {Key: FlagTasksAddDescription, Name: "description", Flag: true}, + {Key: FlagTasksAddOutputs, Name: "outputs", Flag: true}, + {Key: FlagTasksAddRunWindows, Name: "run-windows", Flag: true}, + {Key: FlagTasksAddShell, Name: "shell", Flag: true}, + {Key: FlagTasksAddSilent, Name: "silent", Flag: true}, + {Key: ArgTasksAddTask, Name: "TASK", Required: true}, + {Key: ArgTasksAddRun, Name: "RUN"}, + {}, + {Key: FlagTasksDepsCompact, Name: "compact", Flag: true}, + {Key: FlagTasksDepsDot, Name: "dot", Flag: true}, + {Key: FlagTasksDepsHidden, Name: "hidden", Flag: true}, + {Key: ArgTasksDepsTasks, Name: "TASKS"}, + {}, + {Key: FlagTasksEditPath, Name: "path", Flag: true}, + {Key: ArgTasksEditTask, Name: "TASK", Required: true}, + {}, + {Key: FlagTasksGraphJson, Name: "json", Flag: true}, + {Key: FlagTasksGraphExplain, Name: "explain", Flag: true}, + {Key: FlagTasksGraphNoHeader, Name: "no-header", Flag: true}, + {}, + {Key: FlagTasksInfoJson, Name: "json", Flag: true}, + {Key: ArgTasksInfoTask, Name: "TASK", Required: true}, + {}, + {Key: FlagTasksLsGlobal, Name: "global", Flag: true}, + {Key: FlagTasksLsJson, Name: "json", Flag: true}, + {Key: FlagTasksLsLocal, Name: "local", Flag: true}, + {Key: FlagTasksLsExtended, Name: "extended", Flag: true}, + {Key: FlagTasksLsAll, Name: "all", Flag: true}, + {Key: FlagTasksLsComplete, Name: "complete", Flag: true}, + {Key: FlagTasksLsHidden, Name: "hidden", Flag: true}, + {Key: FlagTasksLsNameOnly, Name: "name-only", Flag: true}, + {Key: FlagTasksLsNoHeader, Name: "no-header", Flag: true}, + {Key: FlagTasksLsSort, Name: "sort", Flag: true, Choices: []string{"name", "alias", "description", "source"}}, + {Key: FlagTasksLsSortOrder, Name: "sort-order", Flag: true, Choices: []string{"asc", "desc"}}, + {Key: FlagTasksLsUsage, Name: "usage", Flag: true}, + {}, + {Key: FlagTasksRunAffected, Name: "affected", Flag: true}, + {Key: FlagTasksRunAffectedBase, Name: "affected-base", Flag: true}, + {Key: FlagTasksRunAffectedExplain, Name: "affected-explain", Flag: true}, + {Key: FlagTasksRunAffectedHead, Name: "affected-head", Flag: true}, + {Key: FlagTasksRunAffectedJson, Name: "affected-json", Flag: true}, + {Key: FlagTasksRunContinueOnError, Name: "continue-on-error", Flag: true}, + {Key: FlagTasksRunCd, Name: "cd", Flag: true}, + {Key: FlagTasksRunForce, Name: "force", Flag: true}, + {Key: FlagTasksRunJobs, Name: "jobs", Flag: true}, + {Key: FlagTasksRunDryRun, Name: "dry-run", Flag: true}, + {Key: FlagTasksRunOutput, Name: "output", Flag: true}, + {Key: FlagTasksRunQuiet, Name: "quiet", Flag: true}, + {Key: FlagTasksRunRaw, Name: "raw", Flag: true}, + {Key: FlagTasksRunShell, Name: "shell", Flag: true}, + {Key: FlagTasksRunSilent, Name: "silent", Flag: true}, + {Key: FlagTasksRunTool, Name: "tool", Flag: true}, + {Key: FlagTasksRunAllowEnv, Name: "allow-env", Flag: true}, + {Key: FlagTasksRunAllowNet, Name: "allow-net", Flag: true}, + {Key: FlagTasksRunAllowRead, Name: "allow-read", Flag: true}, + {Key: FlagTasksRunAllowWrite, Name: "allow-write", Flag: true}, + {Key: FlagTasksRunDenyAll, Name: "deny-all", Flag: true}, + {Key: FlagTasksRunDenyEnv, Name: "deny-env", Flag: true}, + {Key: FlagTasksRunDenyNet, Name: "deny-net", Flag: true}, + {Key: FlagTasksRunDenyRead, Name: "deny-read", Flag: true}, + {Key: FlagTasksRunDenyWrite, Name: "deny-write", Flag: true}, + {Key: FlagTasksRunFreshEnv, Name: "fresh-env", Flag: true}, + {Key: FlagTasksRunNoCache, Name: "no-cache", Flag: true}, + {Key: FlagTasksRunNoDeps, Name: "no-deps", Flag: true}, + {Key: FlagTasksRunNoTimings, Name: "no-timings", Flag: true}, + {Key: FlagTasksRunSkipDeps, Name: "skip-deps", Flag: true}, + {Key: FlagTasksRunSkipTools, Name: "skip-tools", Flag: true}, + {Key: FlagTasksRunTaskCache, Name: "task-cache", Flag: true, Choices: []string{"read-write", "read-only", "write-only", "off", "local-only"}, Default: []string{"read-write"}}, + {Key: FlagTasksRunTaskCacheExplain, Name: "task-cache-explain", Flag: true}, + {Key: FlagTasksRunTaskCacheExplainJson, Name: "task-cache-explain-json", Flag: true}, + {Key: FlagTasksRunTaskCacheStats, Name: "task-cache-stats", Flag: true}, + {Key: FlagTasksRunTimeout, Name: "timeout", Flag: true}, + {Key: FlagTasksRunTimings, Name: "timings", Flag: true}, + {Key: ArgTasksRunTask, Name: "TASK", Default: []string{"default"}}, + {Key: ArgTasksRunArgs, Name: "ARGS"}, + {Key: ArgTasksRunArgsLast, Name: "ARGS_LAST"}, + {}, + {Key: FlagTasksValidateErrorsOnly, Name: "errors-only", Flag: true}, + {Key: FlagTasksValidateJson, Name: "json", Flag: true}, + {Key: ArgTasksValidateTasks, Name: "TASKS"}, + {}, + {Key: FlagTestToolAll, Name: "all", Flag: true}, + {Key: FlagTestToolJobs, Name: "jobs", Flag: true}, + {Key: FlagTestToolAllConfig, Name: "all-config", Flag: true}, + {Key: FlagTestToolIncludeNonDefined, Name: "include-non-defined", Flag: true}, + {Key: FlagTestToolRaw, Name: "raw", Flag: true}, + {Key: ArgTestToolTools, Name: "TOOLS"}, + {}, + {}, + {Key: FlagTokenForgejoUnmask, Name: "unmask", Flag: true}, + {Key: ArgTokenForgejoHost, Name: "HOST", Default: []string{"codeberg.org"}}, + {}, + {Key: FlagTokenGithubOauth, Name: "oauth", Flag: true}, + {Key: FlagTokenGithubRaw, Name: "raw", Flag: true}, + {Key: FlagTokenGithubRefresh, Name: "refresh", Flag: true}, + {Key: FlagTokenGithubUnmask, Name: "unmask", Flag: true}, + {Key: ArgTokenGithubHost, Name: "HOST", Default: []string{"github.com"}}, + {}, + {Key: FlagTokenGitlabUnmask, Name: "unmask", Flag: true}, + {Key: ArgTokenGitlabHost, Name: "HOST", Default: []string{"gitlab.com"}}, + {}, + {Key: FlagToolJson, Name: "json", Flag: true}, + {Key: FlagToolActive, Name: "active", Flag: true}, + {Key: FlagToolBackend, Name: "backend", Flag: true}, + {Key: FlagToolConfigSource, Name: "config-source", Flag: true}, + {Key: FlagToolDescription, Name: "description", Flag: true}, + {Key: FlagToolInstalled, Name: "installed", Flag: true}, + {Key: FlagToolRequested, Name: "requested", Flag: true}, + {Key: FlagToolToolOptions, Name: "tool-options", Flag: true}, + {Key: ArgToolTool, Name: "TOOL", Required: true}, + {}, + {Key: ArgToolStubFile, Name: "FILE", Required: true}, + {Key: ArgToolStubArgs, Name: "ARGS"}, + {}, + {Key: FlagTrustAll, Name: "all", Flag: true}, + {Key: FlagTrustIgnore, Name: "ignore", Flag: true}, + {Key: FlagTrustShow, Name: "show", Flag: true}, + {Key: FlagTrustUntrust, Name: "untrust", Flag: true}, + {Key: ArgTrustConfigFile, Name: "CONFIG_FILE"}, + {}, + {Key: FlagUninstallAll, Name: "all", Flag: true}, + {Key: FlagUninstallDryRun, Name: "dry-run", Flag: true}, + {Key: FlagUninstallDryRunCode, Name: "dry-run-code", Flag: true}, + {Key: ArgUninstallInstalledToolVersion, Name: "INSTALLED_TOOL@VERSION"}, + {}, + {Key: FlagUnsetFile, Name: "file", Flag: true}, + {Key: FlagUnsetGlobal, Name: "global", Flag: true}, + {Key: ArgUnsetEnvKey, Name: "ENV_KEY"}, + {}, + {Key: ArgUntrustConfigFile, Name: "CONFIG_FILE"}, + {}, + {Key: FlagUnuseEnv, Name: "env", Flag: true}, + {Key: FlagUnuseGlobal, Name: "global", Flag: true}, + {Key: FlagUnusePath, Name: "path", Flag: true}, + {Key: FlagUnuseNoPrune, Name: "no-prune", Flag: true}, + {Key: ArgUnuseInstalledToolVersion, Name: "INSTALLED_TOOL@VERSION", Required: true}, + {}, + {Key: FlagUpgradeInteractive, Name: "interactive", Flag: true}, + {Key: FlagUpgradeJobs, Name: "jobs", Flag: true}, + {Key: FlagUpgradeBump, Name: "bump", Flag: true}, + {Key: FlagUpgradeDryRun, Name: "dry-run", Flag: true}, + {Key: FlagUpgradeExclude, Name: "exclude", Flag: true}, + {Key: FlagUpgradeDryRunCode, Name: "dry-run-code", Flag: true}, + {Key: FlagUpgradeInactive, Name: "inactive", Flag: true}, + {Key: FlagUpgradeLocal, Name: "local", Flag: true}, + {Key: FlagUpgradeMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: FlagUpgradeMonorepo, Name: "monorepo", Flag: true}, + {Key: FlagUpgradeNoPrune, Name: "no-prune", Flag: true}, + {Key: FlagUpgradeRaw, Name: "raw", Flag: true}, + {Key: ArgUpgradeInstalledToolVersion, Name: "INSTALLED_TOOL@VERSION"}, + {}, + {}, + {Key: FlagUseEnv, Name: "env", Flag: true}, + {Key: FlagUseForce, Name: "force", Flag: true}, + {Key: FlagUseGlobal, Name: "global", Flag: true}, + {Key: FlagUseJobs, Name: "jobs", Flag: true}, + {Key: FlagUseDryRun, Name: "dry-run", Flag: true}, + {Key: FlagUsePath, Name: "path", Flag: true}, + {Key: FlagUseDryRunCode, Name: "dry-run-code", Flag: true}, + {Key: FlagUseFuzzy, Name: "fuzzy", Flag: true}, + {Key: FlagUseMinimumReleaseAge, Name: "minimum-release-age", Flag: true}, + {Key: FlagUsePin, Name: "pin", Flag: true}, + {Key: FlagUseRaw, Name: "raw", Flag: true}, + {Key: FlagUseRemove, Name: "remove", Flag: true}, + {Key: ArgUseToolVersion, Name: "TOOL@VERSION"}, + {}, + {Key: FlagVersionJson, Name: "json", Flag: true}, + {}, + {Key: FlagWatchTaskFlag, Name: "task-flag", Flag: true}, + {Key: FlagWatchGlob, Name: "glob", Flag: true}, + {Key: FlagWatchSkipDeps, Name: "skip-deps", Flag: true}, + {Key: FlagWatchWatch, Name: "watch", Flag: true}, + {Key: FlagWatchWatchNonRecursive, Name: "watch-non-recursive", Flag: true}, + {Key: FlagWatchWatchFile, Name: "watch-file", Flag: true}, + {Key: FlagWatchClear, Name: "clear", Flag: true, Choices: []string{"clear", "reset"}}, + {Key: FlagWatchOnBusyUpdate, Name: "on-busy-update", Flag: true, Choices: []string{"queue", "do-nothing", "restart", "signal"}, Default: []string{"do-nothing"}}, + {Key: FlagWatchRestart, Name: "restart", Flag: true}, + {Key: FlagWatchSignal, Name: "signal", Flag: true}, + {Key: FlagWatchStopSignal, Name: "stop-signal", Flag: true}, + {Key: FlagWatchStopTimeout, Name: "stop-timeout", Flag: true, Default: []string{"10s"}}, + {Key: FlagWatchMapSignal, Name: "map-signal", Flag: true}, + {Key: FlagWatchDebounce, Name: "debounce", Flag: true, Default: []string{"50ms"}}, + {Key: FlagWatchStdinQuit, Name: "stdin-quit", Flag: true}, + {Key: FlagWatchNoVcsIgnore, Name: "no-vcs-ignore", Flag: true}, + {Key: FlagWatchNoProjectIgnore, Name: "no-project-ignore", Flag: true}, + {Key: FlagWatchNoGlobalIgnore, Name: "no-global-ignore", Flag: true}, + {Key: FlagWatchNoDefaultIgnore, Name: "no-default-ignore", Flag: true}, + {Key: FlagWatchNoDiscoverIgnore, Name: "no-discover-ignore", Flag: true}, + {Key: FlagWatchIgnoreNothing, Name: "ignore-nothing", Flag: true}, + {Key: FlagWatchPostpone, Name: "postpone", Flag: true}, + {Key: FlagWatchDelayRun, Name: "delay-run", Flag: true}, + {Key: FlagWatchPoll, Name: "poll", Flag: true}, + {Key: FlagWatchShell, Name: "shell", Flag: true}, + {Key: FlagWatchN, Name: "n", Flag: true}, + {Key: FlagWatchEmitEventsTo, Name: "emit-events-to", Flag: true, Choices: []string{"environment", "stdio", "file", "json-stdio", "json-file", "none"}, Default: []string{"none"}}, + {Key: FlagWatchOnlyEmitEvents, Name: "only-emit-events", Flag: true}, + {Key: FlagWatchEnv, Name: "env", Flag: true}, + {Key: FlagWatchWrapProcess, Name: "wrap-process", Flag: true, Choices: []string{"group", "session", "none"}}, + {Key: FlagWatchNotify, Name: "notify", Flag: true}, + {Key: FlagWatchColor, Name: "color", Flag: true, Choices: []string{"auto", "always", "never"}, Default: []string{"auto"}}, + {Key: FlagWatchTimings, Name: "timings", Flag: true}, + {Key: FlagWatchQuiet, Name: "quiet", Flag: true}, + {Key: FlagWatchBell, Name: "bell", Flag: true}, + {Key: FlagWatchProjectOrigin, Name: "project-origin", Flag: true}, + {Key: FlagWatchWorkdir, Name: "workdir", Flag: true}, + {Key: FlagWatchExts, Name: "exts", Flag: true}, + {Key: FlagWatchFilter, Name: "filter", Flag: true}, + {Key: FlagWatchFilterFile, Name: "filter-file", Flag: true}, + {Key: FlagWatchFilterProg, Name: "filter-prog", Flag: true}, + {Key: FlagWatchIgnore, Name: "ignore", Flag: true}, + {Key: FlagWatchIgnoreFile, Name: "ignore-file", Flag: true}, + {Key: FlagWatchFsEvents, Name: "fs-events", Flag: true, Choices: []string{"access", "create", "remove", "rename", "modify", "metadata"}, Default: []string{"create", "remove", "rename", "modify", "metadata"}}, + {Key: FlagWatchNoMeta, Name: "no-meta", Flag: true}, + {Key: FlagWatchPrintEvents, Name: "print-events", Flag: true}, + {Key: FlagWatchManual, Name: "manual", Flag: true}, + {Key: ArgWatchTask, Name: "TASK"}, + {Key: ArgWatchArgs, Name: "ARGS"}, + {}, + {Key: ArgWhereToolVersion, Name: "TOOL@VERSION", Required: true}, + {Key: ArgWhereAsdfVersion, Name: "ASDF_VERSION"}, + {}, + {Key: FlagWhichTool, Name: "tool", Flag: true}, + {Key: FlagWhichComplete, Name: "complete", Flag: true}, + {Key: FlagWhichPlugin, Name: "plugin", Flag: true}, + {Key: FlagWhichVersion, Name: "version", Flag: true}, + {Key: ArgWhichBinName, Name: "BIN_NAME"}, +} diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index b1c11f403..3610381a0 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -7,12 +7,15 @@ //! //! # What it emits, and what it does not //! -//! Binding tables only — which token becomes which flag or argument. Help text, -//! choices, defaults, `env`, and every other thing that needs a value's type are -//! deliberately absent, for the same reason they are absent from the Rust hot -//! path: a successful parse never touches them, and a table that carried them -//! would put mise's several hundred kilobytes of help strings in front of the -//! parser. They belong in a second, cold table, which is a separate piece of work. +//! Two tables, kept apart on purpose. The hot one is what binding reads: which +//! token becomes which flag or argument, and nothing else. The cold one — `Meta` — +//! carries what the rules decided after the last token need: `required`, +//! `choices`, `default`, `env`, the var bounds, and the four that compare one +//! entry against another. A parse never touches the second. +//! +//! Help text is in neither. mise's runs to several hundred kilobytes, and a table +//! carrying it would put all of that in front of the parser; rendering help is its +//! own cold table and its own piece of work. //! //! # Why package-level `var` and not `const` //! @@ -27,7 +30,7 @@ //! because `default_subcommand` has to point at a node inside the tree, and a //! composite literal cannot refer to its own interior. -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Write as _; use heck::AsPascalCase; @@ -145,6 +148,7 @@ impl<'a> Emitter<'a> { self.header(); self.constants(&commands); self.tables(&commands); + self.metadata(&commands); // Each command is followed by a blank line, which leaves one at the end of // the file. gofmt strips it, and a generated file that is not gofmt-clean @@ -380,6 +384,185 @@ impl<'a> Emitter<'a> { } } +impl Emitter<'_> { + /// Emit the cold table: everything binding deliberately does not know. + /// + /// Indexed by key, which is what makes a lookup an index rather than a map — + /// and a Go map would have to be built at init, which is the one thing these + /// tables are for avoiding. Keys are handed out to commands as well as to + /// flags and arguments, and a command has no cold half, so its slot is an + /// empty entry rather than a gap: `Metadata.Lookup` checks the key it finds + /// and reports nothing when it does not match, so an empty slot answers + /// correctly and the index stays dense. + fn metadata(&mut self, commands: &[Emitted]) { + // By key, so the slice can be written in one pass in index order. + let mut by_key: BTreeMap = BTreeMap::new(); + for e in commands { + for (flag, named) in &e.flags { + by_key.insert(named.number, self.flag_meta(flag, named, e, commands)); + } + for (arg, named) in &e.args { + by_key.insert(named.number, arg_meta(arg, named)); + } + } + + let total = commands + .iter() + .map(|e| 1 + e.flags.len() + e.args.len()) + .sum::() as u64; + + let _ = writeln!( + self.out, + "// Meta is the cold table, read only by the rules that are decided once the\n\ + // last token has been read: required, choices, the env-then-default fallback,\n\ + // the var bounds, and the four that compare one entry against another. A parse\n\ + // never touches it.\n\ + //\n\ + // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\ + // commands take keys too, and have no cold half.\n\ + var Meta = argv.Metadata{{" + ); + for key in 1..=total { + match by_key.get(&key) { + Some(entry) => { + let _ = writeln!(self.out, "\t{entry},"); + } + None => { + let _ = writeln!(self.out, "\t{{}},"); + } + } + } + let _ = writeln!(self.out, "}}\n"); + } + + /// The cold half of a flag. + fn flag_meta( + &self, + flag: &SpecFlag, + named: &Named, + owner: &Emitted, + commands: &[Emitted], + ) -> String { + let mut fields = vec![ + format!("Key: {}", named.key), + format!("Name: {}", go_string(&flag.name)), + "Flag: true".to_string(), + ]; + if flag.required { + fields.push("Required: true".to_string()); + } + // Written on the value a flag takes, never on the flag. + if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) { + fields.push(format!("Choices: {}", string_slice(&choices.choices))); + } + // A default can be written in either place, and usage-lib falls back to + // the one on the value. `env` deliberately does not follow the same + // nesting, because usage-lib does not read it there either. + let default = if !flag.default.is_empty() { + &flag.default + } else { + flag.arg + .as_ref() + .map(|a| &a.default) + .unwrap_or(&flag.default) + }; + if !default.is_empty() { + fields.push(format!("Default: {}", string_slice(default))); + } + if let Some(env) = &flag.env { + fields.push(format!("Env: {}", go_string(env))); + } + if let Some(min) = flag.var_min { + fields.push(format!("VarMin: {}", clamp_var_max(min))); + } + // Occurrences. The per-occurrence value bound is a limit binding applies + // and lives on the parse table. + if let Some(max) = flag.var_max { + fields.push(format!("VarMax: {}", clamp_var_max(max))); + } + + for (label, names) in [ + ("Conflicts", &flag.conflicts), + ("Overrides", &flag.overrides), + ("RequiredUnless", &flag.required_unless), + ("RequiredIf", &flag.required_if), + ] { + let keys = resolve_relationship(names, owner, commands); + if !keys.is_empty() { + fields.push(format!("{label}: {}", key_slice(&keys))); + } + } + + format!("{{{}}}", fields.join(", ")) + } +} + +/// The cold half of a positional argument. +fn arg_meta(arg: &SpecArg, named: &Named) -> String { + let mut fields = vec![ + format!("Key: {}", named.key), + format!("Name: {}", go_string(&arg.name)), + ]; + if arg.required { + fields.push("Required: true".to_string()); + } + if let Some(choices) = &arg.choices { + fields.push(format!("Choices: {}", string_slice(&choices.choices))); + } + if !arg.default.is_empty() { + fields.push(format!("Default: {}", string_slice(&arg.default))); + } + if let Some(env) = &arg.env { + fields.push(format!("Env: {}", go_string(env))); + } + if let Some(min) = arg.var_min { + fields.push(format!("VarMin: {}", clamp_var_max(min))); + } + // No VarMax: for an argument the bound is a limit binding applies, which is + // what makes `[a]… [b]` fillable at all, so judging it again would fail an + // invocation that never broke it. + format!("{{{}}}", fields.join(", ")) +} + +/// Turn the names in a relationship into the keys they refer to. +/// +/// Resolved here, where the whole command is visible, so that nothing downstream +/// searches by name on a path it would repeat per parse. The names arrive as +/// written — `--stdin`, dashes and all — so they are matched against a flag's long +/// forms, its shorts, and the name the spec gives it. +/// +/// A name nothing answers to is dropped. That is a spec bug worth reporting, but +/// this function has no way to; the check belongs beside the duplicate-form and +/// duplicate-key checks that already run where the whole tree is visible. +fn resolve_relationship(names: &[String], owner: &Emitted, _commands: &[Emitted]) -> Vec { + let mut out = Vec::new(); + for name in names { + let bare = name.trim_start_matches('-'); + let found = owner.flags.iter().find(|(flag, _)| { + flag.name == bare + || flag.long.iter().any(|l| l == bare) + || (bare.chars().count() == 1 && flag.short.iter().any(|c| c.to_string() == bare)) + }); + if let Some((_, named)) = found { + out.push(named.key.clone()); + } + } + out +} + +fn string_slice(values: &[String]) -> String { + let list = values + .iter() + .map(|v| go_string(v)) + .collect::>() + .join(", "); + format!("[]string{{{list}}}") +} + +fn key_slice(keys: &[String]) -> String { + format!("[]uint64{{{}}}", keys.join(", ")) +} + /// A line inside a `const` block or a composite literal. /// /// The distinction exists only to reproduce gofmt's alignment, which pads within diff --git a/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap b/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap index 19dbb90ad..b1381bd72 100644 --- a/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap +++ b/lib/src/go/snapshots/usage__go__tests__a_default_subcommand_points_into_the_tree.snap @@ -43,3 +43,17 @@ var cmdRun = &argv.Command{ {Key: ArgRunArgs, Name: "args", Var: true}, }, } + +// Meta is the cold table, read only by the rules that are decided once the +// last token has been read: required, choices, the env-then-default fallback, +// the var bounds, and the four that compare one entry against another. A parse +// never touches it. +// +// Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty: +// commands take keys too, and have no cold half. +var Meta = argv.Metadata{ + {}, + {Key: ArgTask, Name: "task"}, + {}, + {Key: ArgRunArgs, Name: "args"}, +} diff --git a/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap b/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap index 070023e75..d0e91c791 100644 --- a/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap +++ b/lib/src/go/snapshots/usage__go__tests__a_whole_cli.snap @@ -83,3 +83,26 @@ var cmdConfigLs = &argv.Command{ {Key: FlagConfigLsNoHeader, Name: "no-header", Longs: []string{"no-header"}}, }, } + +// Meta is the cold table, read only by the rules that are decided once the +// last token has been read: required, choices, the env-then-default fallback, +// the var bounds, and the four that compare one entry against another. A parse +// never touches it. +// +// Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty: +// commands take keys too, and have no cold half. +var Meta = argv.Metadata{ + {}, + {Key: FlagVerbose, Name: "verbose", Flag: true}, + {Key: FlagColor, Name: "color", Flag: true}, + {Key: FlagJobs, Name: "jobs", Flag: true}, + {Key: FlagInclude, Name: "include", Flag: true, VarMax: 3}, + {Key: ArgFile, Name: "file", Required: true}, + {Key: ArgRest, Name: "rest"}, + {}, + {Key: FlagInstallForce, Name: "force", Flag: true}, + {Key: ArgInstallPkg, Name: "pkg", Required: true}, + {}, + {}, + {Key: FlagConfigLsNoHeader, Name: "no-header", Flag: true}, +} diff --git a/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap b/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap index 7a100cc9e..0a5f32d40 100644 --- a/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap +++ b/lib/src/go/snapshots/usage__go__tests__colliding_names_get_distinct_identifiers.snap @@ -57,3 +57,19 @@ var cmdMacosDefaults2 = &argv.Command{ {Key: FlagMacosDefaultsApply2, Name: "apply", Longs: []string{"apply"}}, }, } + +// Meta is the cold table, read only by the rules that are decided once the +// last token has been read: required, choices, the env-then-default fallback, +// the var bounds, and the four that compare one entry against another. A parse +// never touches it. +// +// Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty: +// commands take keys too, and have no cold half. +var Meta = argv.Metadata{ + {}, + {}, + {Key: FlagMacosDefaultsApply, Name: "apply", Flag: true}, + {}, + {}, + {Key: FlagMacosDefaultsApply2, Name: "apply", Flag: true}, +} diff --git a/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap b/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap index c5536ba52..012f6aa63 100644 --- a/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap +++ b/lib/src/go/snapshots/usage__go__tests__unknown_flags_are_inherited_and_overridable.snap @@ -60,3 +60,18 @@ var cmdExecNested = &argv.Command{ Name: "nested", Key: CmdExecNested, } + +// Meta is the cold table, read only by the rules that are decided once the +// last token has been read: required, choices, the env-then-default fallback, +// the var bounds, and the four that compare one entry against another. A parse +// never touches it. +// +// Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty: +// commands take keys too, and have no cold half. +var Meta = argv.Metadata{ + {}, + {}, + {}, + {}, + {}, +} From f84ceee8432c71f3f9d4343bc19397713a9065b0 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 04:46:40 +0000 Subject: [PATCH 2/5] fix(go): resolve a generated relationship against the flags actually in scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same hole as the commit before this one, on the emitter's side: a relationship naming an inherited global or a negation resolved to nothing, so the key never reached `Meta` and the generated CLI skipped the rule entirely while usage-lib enforced it. Own flags first, then any ancestor's globals, which is the scope a token has and the order it gets it — so a subcommand redeclaring an inherited name shadows it here as at parse time. A flag that is not global still resolves to nothing from below, and the test checks that half too, since a looser search would get it wrong in the other direction. A negation names the flag it belongs to, matching usage-lib: `conflicts = "--no-color"` is about the `color` entry, and the conflict is reported whichever spelling was typed. mise's tables are unchanged — every relationship it declares is local — so the regenerated file is byte-identical. The fix is real regardless; it just was not reachable from the one large spec in the repository. Co-Authored-By: Claude Opus 5 --- lib/src/go/mod.rs | 87 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index 3610381a0..4a4c59733 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -534,22 +534,57 @@ fn arg_meta(arg: &SpecArg, named: &Named) -> String { /// A name nothing answers to is dropped. That is a spec bug worth reporting, but /// this function has no way to; the check belongs beside the duplicate-form and /// duplicate-key checks that already run where the whole tree is visible. -fn resolve_relationship(names: &[String], owner: &Emitted, _commands: &[Emitted]) -> Vec { +fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec { let mut out = Vec::new(); for name in names { let bare = name.trim_start_matches('-'); - let found = owner.flags.iter().find(|(flag, _)| { - flag.name == bare - || flag.long.iter().any(|l| l == bare) - || (bare.chars().count() == 1 && flag.short.iter().any(|c| c.to_string() == bare)) - }); - if let Some((_, named)) = found { - out.push(named.key.clone()); + // The declaring command's own flags first, then any ancestor's globals — + // the scope a token has, in the order a token gets it, so a subcommand + // redeclaring an inherited name shadows it here as it does at parse time. + let mut found = match_flag(owner, bare, false); + if found.is_none() { + let path = &owner.cmd.full_cmd; + for depth in (0..path.len()).rev() { + let ancestor = commands + .iter() + .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]); + if let Some(key) = ancestor.and_then(|a| match_flag(a, bare, true)) { + found = Some(key); + break; + } + } + } + if let Some(key) = found { + out.push(key); } } out } +/// Find a flag by any spelling a declaration may use for it. +/// +/// The negation counts, and resolves to the same entry: usage-lib treats +/// `conflicts = "--no-color"` as naming the `color` flag and reports the conflict +/// whichever of the two spellings was typed. The relationship is between entries +/// rather than between tokens, which is what the key model already assumes. +fn match_flag(cmd: &Emitted, bare: &str, globals_only: bool) -> Option { + cmd.flags + .iter() + .find(|(flag, _)| { + if globals_only && !flag.global { + return false; + } + flag.name == bare + || flag + .negate + .as_deref() + .is_some_and(|n| n.trim_start_matches('-') == bare) + || flag.long.iter().any(|l| l == bare) + || (bare.chars().count() == 1 && flag.short.iter().any(|c| c.to_string() == bare)) + }) + .map(|(_, named)| named.key.clone()) +} + fn string_slice(values: &[String]) -> String { let list = values .iter() @@ -1082,6 +1117,42 @@ flag "--tag " var=#true var_max=1 assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}"); } + /// A relationship names a flag by any spelling that reaches it, and from + /// anywhere the flag is in scope. + /// + /// Both halves were silently resolving to nothing, which is worse than an + /// error: the rule simply never fired, while usage-lib enforced it. + #[test] + fn a_relationship_resolves_through_scope_and_negation() { + let out = go(r#" +name "ex" +bin "ex" +flag "--quiet" global=#true +flag "--color" negate="--no-color" +flag "--plain" conflicts="--no-color" +cmd "run" { + flag "--loud" conflicts="--quiet" + flag "--solo" conflicts="--plain" +} +"#); + // A negation names the flag it belongs to. + assert!( + out.contains("Name: \"plain\", Flag: true, Conflicts: []uint64{FlagColor}"), + "{out}" + ); + // An inherited global is in scope from below. + assert!( + out.contains("Name: \"loud\", Flag: true, Conflicts: []uint64{FlagQuiet}"), + "{out}" + ); + // `--plain` is not global, so from a subcommand it names nothing — the + // other half, and the one a looser search would get wrong. + assert!( + out.contains("{Key: FlagRunSolo, Name: \"solo\", Flag: true},"), + "a non-global should not resolve from below:\n{out}" + ); + } + #[test] fn strings_are_escaped_to_go_rules() { assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#); From a7900d9a07b3315fe4452ab27c8d61e728262ab0 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:21:30 +0000 Subject: [PATCH 3/5] fix(go): match a relationship's form in the emitter too, and count a typed boolean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The form-matching fix from the commit before this one, applied to the emitter: `--q` reached the short `-q` and `-color` reached the long `--color`, so a generated CLI enforced rules usage-lib resolves to nothing. `--x` matches long forms and the negation, `-x` matches shorts, an undashed word matches the name. And a real bug in this PR's own test helper, which is the more embarrassing half. It recorded value-less flags in a `seen` map and then never read it, so a typed boolean left `given` nil — which `Fill` reads as "the command line said nothing" — and fell through to `env` and `default`. Nothing failed, because the tests here all use flags that take values. It records the empty slice now, which is the distinction `Fill` actually draws, and counts occurrences properly rather than inferring them from the number of values. `TestATypedBooleanCountsAsGiven` is the test that would have caught it, using mise's own `--quiet`. mise's regenerated tables are unchanged: every relationship it declares names a local flag by its long form. Co-Authored-By: Claude Opus 5 --- go/internal/shadow/mise/meta_test.go | 36 ++++++++++++-- lib/src/go/mod.rs | 74 ++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/go/internal/shadow/mise/meta_test.go b/go/internal/shadow/mise/meta_test.go index b9e5916ea..175bb67ac 100644 --- a/go/internal/shadow/mise/meta_test.go +++ b/go/internal/shadow/mise/meta_test.go @@ -19,8 +19,12 @@ func resolve(t *testing.T, name string, args []string, environ map[string]string) (values []string, source argv.Source, err *argv.Error) { t.Helper() + // A value-less flag that was given has no values, and nil would read as "the + // command line said nothing" — so it is recorded as an empty slice, which is + // the distinction Fill draws. Getting this wrong makes a typed boolean fall + // through to env and default. given := map[uint64][]string{} - seen := map[uint64]bool{} + occurrences := map[uint64]int{} path := []*argv.Command{Root} p := argv.New(Root, args) @@ -30,12 +34,14 @@ func resolve(t *testing.T, name string, args []string, case argv.KindCommand: path = append(path, ev.Command) case argv.KindFlag: - seen[ev.Flag.Key] = true + occurrences[ev.Flag.Key]++ if ev.HasValue { given[ev.Flag.Key] = append(given[ev.Flag.Key], ev.Value) + } else if given[ev.Flag.Key] == nil { + given[ev.Flag.Key] = []string{} } case argv.KindArg: - seen[ev.Arg.Key] = true + occurrences[ev.Arg.Key]++ given[ev.Arg.Key] = append(given[ev.Arg.Key], ev.Value) } } @@ -48,7 +54,7 @@ func resolve(t *testing.T, name string, args []string, for _, cmd := range path { for _, f := range cmd.Flags { v, src := argv.Fill(Meta.Lookup(f.Key), given[f.Key], lookup) - if e := argv.Check(Meta.Lookup(f.Key), v, len(given[f.Key])); e != nil && err == nil { + if e := argv.Check(Meta.Lookup(f.Key), v, occurrences[f.Key]); e != nil && err == nil { err = e } if f.Name == name { @@ -177,3 +183,25 @@ func TestRelationshipsPointAtRealEntries(t *testing.T) { } } } + +// A value-less flag typed on the command line must not fall through to the +// fallbacks. mise's `--quiet` is one, and the distinction is invisible unless +// something asks: `Fill` reads a nil `given` as "the command line said nothing", +// so a helper that only records values reports a typed boolean as unset. +func TestATypedBooleanCountsAsGiven(t *testing.T) { + _, source, err := resolve(t, "quiet", []string{"--quiet"}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != argv.FromArgv { + t.Errorf("a typed boolean should come from argv, got %v", source) + } + + _, source, err = resolve(t, "quiet", nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != argv.Unset { + t.Errorf("untyped, with nothing declared to fill it, should be unset: %v", source) + } +} diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index 4a4c59733..5aa78dbf3 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -537,18 +537,17 @@ fn arg_meta(arg: &SpecArg, named: &Named) -> String { fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec { let mut out = Vec::new(); for name in names { - let bare = name.trim_start_matches('-'); // The declaring command's own flags first, then any ancestor's globals — // the scope a token has, in the order a token gets it, so a subcommand // redeclaring an inherited name shadows it here as it does at parse time. - let mut found = match_flag(owner, bare, false); + let mut found = match_flag(owner, name, false); if found.is_none() { let path = &owner.cmd.full_cmd; for depth in (0..path.len()).rev() { let ancestor = commands .iter() .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]); - if let Some(key) = ancestor.and_then(|a| match_flag(a, bare, true)) { + if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) { found = Some(key); break; } @@ -567,20 +566,42 @@ fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) /// `conflicts = "--no-color"` as naming the `color` flag and reports the conflict /// whichever of the two spellings was typed. The relationship is between entries /// rather than between tokens, which is what the key model already assumes. -fn match_flag(cmd: &Emitted, bare: &str, globals_only: bool) -> Option { +fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option { + // The form is part of the name. `--q` does not reach the short `-q` and + // `-color` does not reach the long `--color`: usage-lib resolves neither, and + // resolving them here would have a generated CLI enforcing a rule the + // reference does not. + let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") { + (Some(rest), None, None) + } else if let Some(rest) = name.strip_prefix('-') { + let mut chars = rest.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => (None, Some(c), None), + _ => (None, None, None), + } + } else { + (None, None, Some(name)) + }; + cmd.flags .iter() .find(|(flag, _)| { if globals_only && !flag.global { return false; } - flag.name == bare - || flag + if let Some(bare) = bare { + return flag.name == bare; + } + if let Some(long) = long { + // The negation is a long form of the flag it belongs to and names + // the same entry. + return flag .negate .as_deref() - .is_some_and(|n| n.trim_start_matches('-') == bare) - || flag.long.iter().any(|l| l == bare) - || (bare.chars().count() == 1 && flag.short.iter().any(|c| c.to_string() == bare)) + .is_some_and(|n| n.trim_start_matches('-') == long) + || flag.long.iter().any(|l| l == long); + } + short.is_some_and(|c| flag.short.contains(&c)) }) .map(|(_, named)| named.key.clone()) } @@ -1153,6 +1174,41 @@ cmd "run" { ); } + /// The form is part of the name, and usage-lib resolves neither of the + /// mismatched ones — so resolving them would have a generated CLI enforcing a + /// rule the reference does not. + #[test] + fn a_relationship_needs_the_right_form() { + let out = go(r#" +name "ex" +bin "ex" +flag "-q --quiet" +flag "--color" +flag "--a" conflicts="--q" +flag "--b" conflicts="-color" +flag "--c" conflicts="-q" +flag "--d" conflicts="--color" +"#); + // `--q` is not a long form of anything, and `-color` is not a short. + assert!( + out.contains("{Key: FlagA, Name: \"a\", Flag: true},"), + "{out}" + ); + assert!( + out.contains("{Key: FlagB, Name: \"b\", Flag: true},"), + "{out}" + ); + // The forms the flags actually have. + assert!( + out.contains("Name: \"c\", Flag: true, Conflicts: []uint64{FlagQuiet}"), + "{out}" + ); + assert!( + out.contains("Name: \"d\", Flag: true, Conflicts: []uint64{FlagColor}"), + "{out}" + ); + } + #[test] fn strings_are_escaped_to_go_rules() { assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#); From dc4dfab74e81c44235d75502e54cf436855061a5 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 06:02:08 +0000 Subject: [PATCH 4/5] fix(go): compare a generated negation as the spec wrote it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The emitter half of the previous commit. `negate="-no-color"` is a form nobody can type as `--no-color`, and usage-lib does not resolve a relationship naming the latter to the flag declaring the former — so trimming the dashes before comparing had a generated CLI enforcing a rule the reference does not. Compared exactly now, which the emitter can do directly because it reads the spec rather than the parse table, where the negation is stored bare for the parser's benefit. mise's tables are unchanged: it declares no negations that any relationship names. Co-Authored-By: Claude Opus 5 --- lib/src/go/mod.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index 5aa78dbf3..407009921 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -593,13 +593,11 @@ fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option { return flag.name == bare; } if let Some(long) = long { - // The negation is a long form of the flag it belongs to and names - // the same entry. - return flag - .negate - .as_deref() - .is_some_and(|n| n.trim_start_matches('-') == long) - || flag.long.iter().any(|l| l == long); + // The negation is a form of the flag it belongs to and names the + // same entry — compared as the spec wrote it, dashes and all, so a + // `negate = "-no-color"` is not reached by `--no-color`. usage-lib + // does not resolve that either. + return flag.negate.as_deref() == Some(name) || flag.long.iter().any(|l| l == long); } short.is_some_and(|c| flag.short.contains(&c)) }) @@ -1209,6 +1207,29 @@ flag "--d" conflicts="--color" ); } + /// A negation is matched as the spec wrote it, dashes and all. + #[test] + fn a_negation_is_matched_as_written() { + let out = go(r#" +name "ex" +bin "ex" +flag "--color" negate="--no-color" +flag "--tint" negate="-no-tint" +flag "--a" conflicts="--no-color" +flag "--b" conflicts="--no-tint" +"#); + assert!( + out.contains("Name: \"a\", Flag: true, Conflicts: []uint64{FlagColor}"), + "{out}" + ); + // `--no-tint` is not the form `-no-tint`, so it names nothing — as in + // usage-lib, which does not resolve it either. + assert!( + out.contains("{Key: FlagB, Name: \"b\", Flag: true},"), + "{out}" + ); + } + #[test] fn strings_are_escaped_to_go_rules() { assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#); From da5120a835681a276c971ccb56772ee508f6df6c Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:02:27 +0000 Subject: [PATCH 5/5] fix(go): look for a relationship's target the way the parser looks for a token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings on the same function, and the second has a sharper justification than the report gave it. A negation was only matched inside the `--` branch, so `negate="-no-tint"` named by `conflicts="-no-tint"` — its own exact form — resolved to nothing. usage-lib resolves it and reports the conflict. Negations are compared as both sides were written now, dashes included, so the exact form matches and `--no-tint` still does not. And an ordinary form now beats another flag's negation, because searching per candidate meant an earlier flag's `negate` could win over a later flag's `long`. The argument is not precedence in the abstract: the parser tries every long form before it tries any negation, so with `--a` declaring `negate="--zap"` and a separate `--zap`, typing `--zap` binds *zap* — checked, not assumed. The table was pointing the relationship at `a`, which would have enforced the rule against a flag the command line never binds. The table has to agree with the binder it feeds, so it looks in the same two passes and the same order. usage-lib fires on both spellings here, because it compares declared strings against the given flags rather than resolving to an entry, so it cannot settle which key is right. The binder can, and did. Both fixed in the emitter and in the table builder the corpus uses, each with a test. mise's tables are unchanged: it declares no negation any relationship names. Co-Authored-By: Claude Opus 5 --- go/internal/spec/spec.go | 41 ++++++++------- go/internal/spec/spec_test.go | 50 ++++++++++++++++++ lib/src/go/mod.rs | 96 +++++++++++++++++++++++++++-------- 3 files changed, 148 insertions(+), 39 deletions(-) diff --git a/go/internal/spec/spec.go b/go/internal/spec/spec.go index 2785be49f..5ba32003d 100644 --- a/go/internal/spec/spec.go +++ b/go/internal/spec/spec.go @@ -356,12 +356,19 @@ func (b *builder) resolveRelationships(c *Cmd, out *argv.Command) { // whichever of the two spellings was typed. The relationship is between entries // rather than between tokens, which is what this key model already assumes. func (b *builder) matchFlag(flags []*argv.Flag, name string, globalsOnly bool) (uint64, bool) { - // The form is part of the name. `--q` does not reach the short `-q`, and - // `-color` does not reach the long `--color`: usage-lib resolves neither, and - // resolving them here would have a generated CLI enforcing a rule the - // reference does not. A declaration that names a flag by the wrong form is a - // typo, and the useful failure is the rule not existing rather than a rule - // nobody wrote. + // Two passes, in the order the parser itself looks: every ordinary form + // first, then negations. + // + // That order is not a nicety. `longFlag` tries `findLong` across all flags + // before it tries `findNegation`, so with `--a` declaring `negate="--zap"` + // and a separate `--zap`, typing `--zap` binds *zap*. A per-candidate search + // would hand the relationship to `a`, and the table would then enforce a rule + // against a flag the command line never binds. The table has to agree with + // the binder it feeds. + eligible := func(f *argv.Flag) bool { return !globalsOnly || f.Global } + + // The form is part of the name: `--q` does not reach the short `-q`, and + // `-color` does not reach the long `--color`. usage-lib resolves neither. long, short, bare := "", byte(0), "" switch { case strings.HasPrefix(name, "--"): @@ -373,25 +380,15 @@ func (b *builder) matchFlag(flags []*argv.Flag, name string, globalsOnly bool) ( // it can be typed as. bare = name } - if long == "" && short == 0 && bare == "" { - return 0, false - } for _, f := range flags { - if globalsOnly && !f.Global { + if !eligible(f) { continue } if bare != "" && f.Name == bare { return f.Key, true } if long != "" { - // The negation is a form of the flag it belongs to and names the same - // entry: usage-lib reports a conflict declared against `--no-color` - // whichever of the two spellings was typed. Compared as written, so a - // `negate="-no-color"` is not reached by `--no-color`. - if b.negation[f.Key] == name { - return f.Key, true - } for _, l := range f.Longs { if l == long { return f.Key, true @@ -406,9 +403,17 @@ func (b *builder) matchFlag(flags []*argv.Flag, name string, globalsOnly bool) ( } } } + + // Negations, compared exactly as both sides were written — dashes included. + // `negate="-no-tint"` is named by `-no-tint` and not by `--no-tint`, and + // usage-lib resolves it that way round too. + for _, f := range flags { + if eligible(f) && b.negation[f.Key] != "" && b.negation[f.Key] == name { + return f.Key, true + } + } return 0, false } - func (b *builder) flag(f *Flag) *argv.Flag { out := &argv.Flag{ Key: b.next(), diff --git a/go/internal/spec/spec_test.go b/go/internal/spec/spec_test.go index 929880de8..c08328b20 100644 --- a/go/internal/spec/spec_test.go +++ b/go/internal/spec/spec_test.go @@ -313,3 +313,53 @@ func TestANegationIsMatchedAsWritten(t *testing.T) { t.Errorf("--no-tint is not the form `-no-tint`, so nothing: got %v", got) } } + +// The table has to agree with the binder it feeds. +// +// The parser tries every long form before any negation, so with `--a` declaring +// `negate="--zap"` and a separate `--zap`, typing `--zap` binds *zap*. Resolving +// per candidate handed the relationship to `a`, and the rule would then have been +// enforced against a flag the command line never binds. +func TestAnOrdinaryFormBeatsAnotherFlagsNegation(t *testing.T) { + root, meta := build(&Spec{ + Name: "ex", Bin: "ex", + Cmd: Cmd{Name: "ex", Flags: []Flag{ + {Name: "a", Long: []string{"a"}, Negate: "--zap"}, + {Name: "zap", Long: []string{"zap"}}, + {Name: "p", Long: []string{"p"}, Conflicts: []string{"--zap"}}, + }}, + }) + var zap uint64 + for _, f := range root.Flags { + if f.Name == "zap" { + zap = f.Key + } + } + if got := metaFor(t, meta, root, "p").Conflicts; len(got) != 1 || got[0] != zap { + t.Errorf("should name the flag --zap binds (%d), got %v", zap, got) + } +} + +// A negation is named by the form it was written as, whatever the dashes. +func TestASingleDashNegationIsNamedByItsOwnForm(t *testing.T) { + root, meta := build(&Spec{ + Name: "ex", Bin: "ex", + Cmd: Cmd{Name: "ex", Flags: []Flag{ + {Name: "tint", Long: []string{"tint"}, Negate: "-no-tint"}, + {Name: "plain", Long: []string{"plain"}, Conflicts: []string{"-no-tint"}}, + {Name: "other", Long: []string{"other"}, Conflicts: []string{"--no-tint"}}, + }}, + }) + var tint uint64 + for _, f := range root.Flags { + if f.Name == "tint" { + tint = f.Key + } + } + if got := metaFor(t, meta, root, "plain").Conflicts; len(got) != 1 || got[0] != tint { + t.Errorf("the exact form should resolve to tint (%d), got %v", tint, got) + } + if got := metaFor(t, meta, root, "other").Conflicts; len(got) != 0 { + t.Errorf("--no-tint is not how it was declared, got %v", got) + } +} diff --git a/lib/src/go/mod.rs b/lib/src/go/mod.rs index 407009921..8cd9ed307 100644 --- a/lib/src/go/mod.rs +++ b/lib/src/go/mod.rs @@ -567,10 +567,19 @@ fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) /// whichever of the two spellings was typed. The relationship is between entries /// rather than between tokens, which is what the key model already assumes. fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option { - // The form is part of the name. `--q` does not reach the short `-q` and - // `-color` does not reach the long `--color`: usage-lib resolves neither, and - // resolving them here would have a generated CLI enforcing a rule the - // reference does not. + // Two passes, in the order the parser itself looks: every ordinary form + // first, then negations. + // + // That order is not a nicety. The parser tries every long form before it + // tries any negation, so with `--a` declaring `negate = "--zap"` and a + // separate `--zap`, typing `--zap` binds *zap*. A per-candidate search hands + // the relationship to `a`, and the table then enforces a rule against a flag + // the command line never binds. The table has to agree with the binder it + // feeds. + let eligible = |flag: &SpecFlag| !globals_only || flag.global; + + // The form is part of the name: `--q` does not reach the short `-q`, and + // `-color` does not reach the long `--color`. usage-lib resolves neither. let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") { (Some(rest), None, None) } else if let Some(rest) = name.strip_prefix('-') { @@ -583,27 +592,30 @@ fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option { (None, None, Some(name)) }; + let ordinary = cmd.flags.iter().find(|(flag, _)| { + if !eligible(flag) { + return false; + } + if let Some(bare) = bare { + return flag.name == bare; + } + if let Some(long) = long { + return flag.long.iter().any(|l| l == long); + } + short.is_some_and(|c| flag.short.contains(&c)) + }); + if let Some((_, named)) = ordinary { + return Some(named.key.clone()); + } + + // Negations, compared exactly as both sides were written — dashes included. + // `negate = "-no-tint"` is named by `-no-tint` and not by `--no-tint`, and + // usage-lib resolves it that way round too. cmd.flags .iter() - .find(|(flag, _)| { - if globals_only && !flag.global { - return false; - } - if let Some(bare) = bare { - return flag.name == bare; - } - if let Some(long) = long { - // The negation is a form of the flag it belongs to and names the - // same entry — compared as the spec wrote it, dashes and all, so a - // `negate = "-no-color"` is not reached by `--no-color`. usage-lib - // does not resolve that either. - return flag.negate.as_deref() == Some(name) || flag.long.iter().any(|l| l == long); - } - short.is_some_and(|c| flag.short.contains(&c)) - }) + .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name)) .map(|(_, named)| named.key.clone()) } - fn string_slice(values: &[String]) -> String { let list = values .iter() @@ -1207,6 +1219,48 @@ flag "--d" conflicts="--color" ); } + /// The table has to agree with the binder it feeds. + /// + /// The parser tries every long form before any negation, so with `--a` + /// declaring `negate="--zap"` and a separate `--zap`, typing `--zap` binds + /// *zap*. A per-candidate search handed the relationship to `a`, which would + /// have enforced the rule against a flag the command line never binds. + #[test] + fn an_ordinary_form_beats_another_flags_negation() { + let out = go(r#" +name "ex" +bin "ex" +flag "--a" negate="--zap" +flag "--zap" +flag "--p" conflicts="--zap" +"#); + assert!( + out.contains("Name: \"p\", Flag: true, Conflicts: []uint64{FlagZap}"), + "should name the flag `--zap` binds, not the one negating to it:\n{out}" + ); + } + + /// A negation is named by the form it was written as, whatever the dashes. + #[test] + fn a_single_dash_negation_is_named_by_its_own_form() { + let out = go(r#" +name "ex" +bin "ex" +flag "--tint" negate="-no-tint" +flag "--plain" conflicts="-no-tint" +flag "--other" conflicts="--no-tint" +"#); + assert!( + out.contains("Name: \"plain\", Flag: true, Conflicts: []uint64{FlagTint}"), + "the exact form should resolve:\n{out}" + ); + // And the form it was not written as does not. + assert!( + out.contains("{Key: FlagOther, Name: \"other\", Flag: true},"), + "`--no-tint` is not how it was declared:\n{out}" + ); + } + /// A negation is matched as the spec wrote it, dashes and all. #[test] fn a_negation_is_matched_as_written() {