Skip to content

feat(go): write an answer the way each shell reads it - #989

Merged
jdx merged 4 commits into
go/completefrom
go/complete-shells
Aug 17, 2026
Merged

feat(go): write an answer the way each shell reads it#989
jdx merged 4 commits into
go/completefrom
go/complete-shells

Conversation

@jdx

@jdx jdx commented Aug 17, 2026

Copy link
Copy Markdown
Owner

RenderAnswer turns candidates into the protocol a shell's completion machinery expects. Stacked on #984.

shell shape
bash value
fish, nu, PowerShell value\tdescription
zsh value\tdescription\tinsert

zsh takes a third field because what it displays and what it types are not always the same string. Checked against what usage complete-word --shell zsh already emits:

use^IInstalls a tool and adds the version to mise.toml.^Iuse$

Three rules that look like details and are not

Descriptions are all-or-nothing per answer. A column that appears on some rows and not others reads as missing data rather than as an absent description.

A description is collapsed onto one line, not truncated at the first break, so a two-line description still says both halves. The protocols are line-based and a break would look like another candidate. This is why the one-lining moved here from describe in #984, where it was throwing the second half away — Candidates now carries the whole text and the renderer decides how to fit it.

"Paths belong here too" is a whole line, not a flag on the protocol, because every one of the five shells can already split output into lines and look at the last one. \x01 opens it because no candidate can contain a control character.

Naming

RenderAnswer rather than Render, which already belongs to failures. Two things in one package turning a value into text for a terminal is reason enough to say which.

Still to do

Running the complete scripts a spec can declare, which means subprocesses — a decision for the layer above this one.

🤖 Generated with Claude Code


Note

Low Risk
Additive completion-output formatting with broad unit tests; no changes to parsing, binding, or security-sensitive paths.

Overview
Adds argv.RenderAnswer so completion Answer values serialize to the line protocol each shell expects: bash values only; fish, nu, and PowerShell use value + tab + description; zsh adds a third insert field with zshQuote for safe typing.

Rendering enforces all-or-nothing description columns (only among candidates that actually emit), collapses multi-line help with oneLine instead of truncating, drops candidates whose values contain control chars/tabs/newlines rather than repairing them, and appends \x01files / \x01dirs marker lines when paths should complete too.

describe in complete.go now attaches the full short help (including newlines); one-lining moved here from candidate building. README documents RenderAnswer; tests cover per-shell shapes and edge cases.

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

@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: ea912eb0-d9d9-422f-a43d-98f669de77c3

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 shell-specific rendering for Go completion answers and preserves complete help descriptions until the renderer collapses them safely.

  • Emits each shell’s expected value, description, and insertion fields.
  • Drops candidate values containing protocol-conflicting control characters.
  • Adds path markers and focused tests for formatting, quoting, filtering, and descriptions.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; current code drops control-bearing candidate values instead of corrupting protocol framing or silently changing inserted arguments.

Important Files Changed

Filename Overview
go/argv/complete_shell.go Adds shell-specific answer rendering, path markers, description collapsing, unsafe-value filtering, and zsh insertion quoting; the previously reported delimiter and value-mutation issues are fixed at current HEAD.
go/argv/complete_shell_test.go Covers protocol shapes, all-or-nothing descriptions, multiline collapsing, zsh quoting, marker placement, and rejection of values that cannot safely travel through the protocols.
go/argv/complete.go Preserves full multiline help text in candidates so protocol-specific rendering can collapse it without truncation.
go/argv/complete_test.go Updates candidate-generation coverage to require preservation of the complete description.
go/README.md Documents the shell-specific output contracts exposed by RenderAnswer.

Reviews (8): Last reviewed commit: "fix(go): decide the description column o..." | Re-trigger Greptile

Comment thread go/argv/complete_shell.go
@jdx
jdx force-pushed the go/complete-shells branch from 4c70937 to 1109ff7 Compare August 17, 2026 17:22
Comment thread go/argv/complete_shell.go Outdated

@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 4e301ec. Configure here.

Comment thread go/argv/complete_shell.go
@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       199       203       209  ns
argh: argv -> struct                          285       293       300       309  ns
clap: build tree + parse -> struct         481068    484502    493754    508004  ns
bpaf: build parser + parse -> struct      1568080   1568080   1585072   1653370  ns

usage: argv -> struct                             214 ns      0.21 µs
clap: build tree + parse -> struct             501175 ns    501.17 µs
clap: parse -> struct, tree reused              24092 ns     24.09 µs
clap: build tree only                          310469 ns    310.47 µs

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

@jdx
jdx force-pushed the go/complete-shells branch from 4e301ec to ae888f0 Compare August 17, 2026 19:57
@jdx
jdx force-pushed the go/complete-shells branch from ae888f0 to 15f78f5 Compare August 17, 2026 20:17
jdx and others added 4 commits August 17, 2026 20:29
`RenderAnswer` turns candidates into the protocol a shell's completion machinery
expects, which is where the five differ: bash reads values only; fish, nu and
PowerShell take a description after a tab; zsh takes a third field, the text to
insert, because what it displays and what it types are not always the same
string. Checked against what `usage complete-word --shell zsh` already emits.

Three rules that look like details and are not:

Descriptions are all-or-nothing per answer. A column that appears on some rows
and not others reads as missing data rather than as an absent description.

A description is collapsed onto one line rather than truncated at the first
break, so a two-line description still says both halves. The protocols are
line-based and a break would look like another candidate — which is why the
one-lining moved here from `describe`, where it was throwing the second half
away. `Candidates` now carries the whole text and the renderer decides how to fit
it.

And "paths belong here too" is a whole line rather than a flag on the protocol,
because every one of the five shells can already split output into lines and look
at the last one. `\x01` opens it because no candidate can contain a control
character.

`RenderAnswer` rather than `Render`, which already belongs to failures: two
things in one package turning a value into text for a terminal is reason enough
to say which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A candidate carrying a tab or a newline was written straight into the output.
Both protocols are lines with tab-separated fields, so such a value reads as
extra rows or extra columns — zsh in particular would take the text after the tab
as the description and the text after that as what to insert.

A candidate normally comes from a spec and contains neither. A `complete` script
can produce anything, though, and a completion that rearranges the protocol is a
worse failure than a missing candidate: the shell inserts something nobody
offered.

Values go through the same one-lining descriptions already did, and the test
asserts the field and row counts each shell expects rather than the text, since
that is what the protocol is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit's parent collapsed a tab or a break inside a candidate onto a space
so that it could not be read as another field or another row. That protects the
protocol and breaks the candidate: a value is the text that gets typed onto the
command line, so a repaired one inserts an argument nobody offered — the shell
reports success and the CLI receives something else.

Dropping it is the honest failure. The user types the value themselves and it
works, and the file's own reasoning already said as much: a missing candidate is
the lesser of the two.

Every control character, not just the three that delimit. The marker lines that
say "paths belong here too" open with `\x01`, so a value beginning with one was
read as a marker.

Descriptions still collapse. They are prose, nothing is typed from them, and a
two-line help should still say both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Descriptions are all-or-nothing per answer, and the decision was made over every
candidate — including the ones dropped a few lines below for carrying something
that cannot travel. A description on a dropped row turned the column on for
everyone else, putting an empty field on every surviving row: the rule broken by
the answer it was deciding for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the go/complete-shells branch from 15f78f5 to f37999a Compare August 17, 2026 20:31
@jdx
jdx merged commit 77b387d into main Aug 17, 2026
11 checks passed
@jdx
jdx deleted the go/complete-shells branch August 17, 2026 22:42
jdx added a commit that referenced this pull request Aug 17, 2026
#990)

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.

```go
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](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!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 import` → `brew`), and
choice validation (`--log-level chatty` → `invalid_choice`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2fedce2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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