Skip to content

feat(go): generate a struct per command, and the Parse that fills them - #990

Merged
jdx merged 1 commit into
go/complete-shellsfrom
go/generate-structs
Aug 17, 2026
Merged

feat(go): generate a struct per command, and the Parse that fills them#990
jdx merged 1 commit into
go/complete-shellsfrom
go/generate-structs

Conversation

@jdx

@jdx jdx commented Aug 17, 2026

Copy link
Copy Markdown
Owner

The front door. An author calls Parse and gets a value with fields rather than a loop over events — binding, the post-binding rules and the three tables are unchanged underneath.

cli, err := mycli.Parse(os.Args[1:])
if cli.Run != nil {
    fmt.Println(cli.Run.Task, cli.Run.Args)
}

Stacked on #989.

Fields are strings and bools, on purpose

That is what a usage spec knows: it says what a value is called and never what type it is. Turning "8" into an int stays the caller's business — the conversions in #978 exist for exactly that, and inferring a type from an argument's name would be guessing.

Two things mise found that a small fixture could not

Field names collide within a struct. A command can declare a --shell flag beside a shell subcommand, and mise does it with shell, version, command, env and tool. The kind disambiguates — Shell and ShellCmd — because that says which one it is where Shell2 would say only that there were two. The assignment is worked out once and shared by the declarations and by Parse, so the two cannot disagree about where a value goes.

A subcommand's defaults were dropped. The fallback assignment started as a function taking the root struct, which cannot reach a subcommand's — that lives in a local of Parse. mise's bootstrap packages import --manager defaults to brew and came back empty. It is inline now, where the variables are; only the keys of commands the words selected are in scope, so the variable is never nil when its key is.

Both are tested against mise's real command lines, including the [ARGS]… [-- ARGS_LAST]… split through the generated structs.

🤖 Generated with Claude Code


Note

Low Risk
Changes are documentation, a thin env helper, and shadow integration tests; generated Parse behavior is exercised on real mise argv but does not alter the core parser’s hot path.

Overview
Go CLI authors are steered toward generated Parse instead of hand-rolling argv.New event loops. go/README.md now shows mycli.Parse(os.Args[1:]), nested command structs with nil pointers for unselected branches, and notes that binding, post-rules, env/default fill, and validation all happen inside Parse with string / bool / []string fields.

argv.LookupEnv is added as the process-environment hook that generated Parse passes into Fill, keeping tests injectable while real CLIs read os.LookupEnv.

The mise shadow package gains integration tests for the generated front door: struct fill (use -g node@20), -- arg splitting on tasks run, subcommand defaults (bootstrap packages importbrew), and choice validation (--log-level chattyinvalid_choice).

Reviewed by Cursor Bugbot for commit 2fedce2. Bugbot is set up for automated code reviews on this repo. Configure here.

@jdx
jdx force-pushed the go/generate-structs branch from ac119a9 to 388490e Compare August 17, 2026 15:06
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ea03f4f0-dbde-4e84-8344-d31021ad9bf6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds generated Go command structs and a Parse entry point that binds arguments, applies post-binding validation and fallback values, and returns the selected command tree.

  • Generates one typed Go struct per command with collision-safe field names.
  • Populates command, flag, argument, environment, and default values through generated key-based dispatch.
  • Adds mise-scale integration coverage and documents the new API.
  • Exposes argv.LookupEnv for generated parsers.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
lib/src/go/structs.rs Generates command structs and the Parse implementation; investigated command selection, inherited flags, and fallback assignment without finding an eligible defect.
lib/src/go/mod.rs Integrates struct generation and adds safe exported-field identifier conversion.
go/argv/post.go Exposes the process-environment lookup helper used by generated Parse functions.
go/internal/shadow/mise/meta_test.go Adds end-to-end coverage for struct population, separator handling, subcommand defaults, and choices validation.
go/internal/shadow/mise/tables.go Regenerates the mise-scale fixture with command structs and key-dispatched parsing.
go/README.md Documents the generated Parse API, returned struct shape, validation, and fallback behavior.

Reviews (9): Last reviewed commit: "feat(go): generate a struct per command,..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: markdown on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1, startup on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

framework instructions, cold parse vs usage
usage 4161
argh 6292 1.5x
clap 5895248 1416x
bpaf 21917778 5267x
                                              min       p01       p10    median
usage-rs: argv -> struct                      193       195       198       201  ns
argh: argv -> struct                          276       281       285       290  ns
clap: build tree + parse -> struct         480250    481846    485234    491711  ns
bpaf: build parser + parse -> struct      1576314   1576314   1586014   1599872  ns

usage: argv -> struct                             224 ns      0.22 µs
clap: build tree + parse -> struct             498789 ns    498.79 µs
clap: parse -> struct, tree reused              25014 ns     25.01 µs
clap: build tree only                          306282 ns    306.28 µs

2fedce2bfd67 vs c5d9ad605c95 · measured on the runner, not pushed to the history.

@jdx
jdx force-pushed the go/generate-structs branch from 388490e to 324e329 Compare August 17, 2026 17:23
@jdx
jdx force-pushed the go/generate-structs branch from 324e329 to 00cf140 Compare August 17, 2026 17:36
Comment thread lib/src/go/structs.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3742a3d. Configure here.

Comment thread lib/src/go/structs.rs
The front door. An author calls `Parse(os.Args[1:])` and gets a value with fields
rather than a loop over events; binding, the post-binding rules and the three
tables are unchanged underneath.

    cli, err := mycli.Parse(os.Args[1:])
    if cli.Run != nil {
        fmt.Println(cli.Run.Task, cli.Run.Args)
    }

Fields are `string`, `bool` and `[]string`, because that is what a usage spec
knows. A spec says what a value is *called* and never what type it is, so turning
`"8"` into an `int` stays the caller's business — the conversions added earlier in
this stack are there for exactly that, and inferring a type from an argument's
name would be guessing.

Two things mise found that a small fixture could not:

Field names collide within a struct. A command can declare a `--shell` flag
beside a `shell` subcommand, and mise does that with `shell`, `version`,
`command`, `env` and `tool`. The kind disambiguates — `Shell` and `ShellCmd` —
because that says which one it is where `Shell2` would say only that there were
two. The assignment is worked out once and shared by the declarations and by
`Parse`, so the two cannot disagree about where a value goes.

And a subcommand's defaults were being dropped. The fallback assignment started
life as a function taking the root struct, which cannot reach a subcommand's —
that lives in a local of `Parse`. mise's `bootstrap packages import --manager`
defaults to `brew` and was coming back empty. It is written inline now, where the
variables are, and only the keys of commands the words selected are in scope, so
the variable is never nil when its key is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the go/generate-structs branch from 3742a3d to 2fedce2 Compare August 17, 2026 20:31
@jdx
jdx merged commit 9834b07 into main Aug 17, 2026
11 checks passed
@jdx
jdx deleted the go/generate-structs branch August 17, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant