fix(completion): return suggestions instead of the help page - #135
fix(completion): return suggestions instead of the help page#135vrobert78 wants to merge 2 commits into
Conversation
Tab completion produced nothing in zsh, bash and fish. The completion
scripts fetch suggestions by calling the hidden `_complete` command of
the legacy CLI, passing the shell as a glued short option (-szsh, -sbash,
-sfish). The Go layer parses those arguments with Cobra, which splits a
glued short option into single-letter flags; as every supported shell
name contains an "h", the bundle always ended with -h, so the CLI
answered with its help page:
$ upsun _complete --no-interaction -szsh -c1 -iupsun -ienv
Command: _complete
Description: Internal command to provide shell completion suggestions
The completion function silently discarded that output. The legacy CLI
itself parses the glued form correctly, so only the Go layer was at
fault.
Proxy `_complete` through a hidden Cobra command with flag parsing
disabled, so the request reaches the legacy CLI unchanged. This also
fixes completion files that users already have installed, and it covers
the input tokens, which have the same problem: completing
"upsun ssh --pro<TAB>" passes -issh, whose "h" was bundled in too.
Additionally, emit the shell as --shell=<shell> in the generated scripts,
so a completion request no longer depends on how glued short options are
parsed.
Completion requests now also skip the update check and the shell config
leftovers notice: they run on every Tab, and the bash script captures
stderr along with stdout, so any message would end up in the suggestions.
Verified with real Tab presses in zsh, bash and fish (newly generated
scripts, previously installed scripts, and eval "$(upsun completion)").
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes shell tab-completion for the Upsun CLI by preventing Cobra/pflag from mis-parsing legacy _complete requests (notably the glued -s<shell> form that previously produced a bundled -h and triggered help output instead of suggestions). It does so by proxying _complete through a hidden Cobra command with flag parsing disabled, updating generated completion scripts to use --shell=<shell>, and adding regression coverage via unit + integration tests.
Changes:
- Add a hidden
_completeCobra command withDisableFlagParsingto pass completion arguments through to the legacy CLI unchanged. - Rewrite generated completion scripts to emit
--shell=<shell>instead of-s<shell>. - Skip update checks and shell-config-leftovers notices for completion requests; add unit and integration tests to cover both short/long forms.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
commands/root.go |
Skips pre/post-run update/notice behaviors for completion requests; registers the new hidden _complete proxy command. |
commands/completion.go |
Rewrites generated completion scripts’ shell option; implements the hidden _complete proxy and completion-request detection. |
commands/completion_test.go |
Unit tests for script shell-option rewriting and for ensuring _complete args reach the command unparsed. |
integration-tests/completion_test.go |
Integration tests validating generated scripts and end-to-end _complete suggestion output for short/long option forms. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…fails exitWithError writes the message to stderr for any failure that is not an exit code, and the bash completion script captures stderr along with stdout, so it would be offered as a suggestion. Report the exit code without writing anything instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRnasDG9njauyLG5BGfkKz
There was a problem hiding this comment.
Note
Reviewed — No blocking findings · 🔵 1 minor point · ⚪ 2 nitpicks
🔍 Full review · 4 files reviewed
🔍 What this review checked
- With DisableFlagParsing, cobra passes
a(args after removing only the_completetoken) to Run, so-szshand input tokens such as-isshreach the legacy CLI verbatim. isCompletionRequestmatches the executed subcommand'sName(), and only the root defines PersistentPreRun/PersistentPostRun, so both guards fire for_completeand for cobra's__complete.- Skipping PersistentPreRun means the update-check goroutine is never started, so the skipped
selectin PersistentPostRun leaves nothing blocked or leaked. --shell=<shell>is the long form of Symfony's_complete-soption and contains no bundled-h, so newly generated scripts also work against older binaries via the root fallthrough.- The shell-option rewrite runs after the phar-path/basename→executable substitution, so the two replacements cannot interfere.
Verification. Covered by the new commands/completion_test.go (run by the test CI job via make test) and integration-tests/completion_test.go (run by the integration-test job, which builds platform.phar and the binary before make integration-test); no automated test drives a real shell, so the pty matrix in the description is manual only, and the stderr-passthrough behaviour of the _complete proxy is untested.
Review details
- Commit: 2e8c151
- Model: claude-opus-5
🔵 Minor point
commands/completion.go:85— The_completeproxy wires the legacy child's stderr tocmd.ErrOrStderr()(the real stderr). Symfony's bash completion script runs the request assfcomplete=$("${completecmd[@]}" 2>&1)and uses the captured text as the suggestion list whenever the exit status is 0 — which is exactly the reasoning given forexitSilently. So any line the legacy CLI (or PHP itself, e.g. a deprecation notice or a TLS/cert warning) writes to stderr while still exiting 0 is offered to the user as a completion candidate. Redirecting the child's stderr toio.Discardfor this command (or buffering it and only emitting it under--debug) closes the same hole the exit path already guards.
⚪ Nitpicks
commands/completion.go:24—shellOptionReplaceris a literal allowlist of-szsh,-sbashand-sfish. Symfony'scompletioncommand dumps one template per supported shell, so a script generated for any other shell it supports keeps the glued short form and depends entirely on the new_completeproxy for correctness; the integration test'sNotContains("-s"+shell)assertion likewise only covers these three. Deriving the replacement from the requested shell argument (or rewriting-s<name>generically) would keep the template fix in step with the legacy CLI.commands/completion.go:78— Registering_completeas a Cobra command changesupsun help _complete:newHelpCommandresolves it viacmd.Root().Find(Hidden does not exclude it) and the root help func then prints the Cobra usage stub, instead of delegating to the legacy CLI's own_completehelp as before. Harmless for an internal command, but it is a behaviour change worth knowing about.
Review 1 of 10 for this pull request · View the full run
Problem
Tab completion returns nothing in zsh, bash and fish, out of the box and via
eval "$(upsun completion)". The completion function is registered correctly; the request it makes comes back empty.The generated scripts fetch suggestions from the hidden
_completecommand of the legacy CLI, passing the shell as a glued short option:The Go layer parses these with Cobra before handing them to the legacy CLI. Cobra's root command uses
FParseErrWhitelist{UnknownFlags: true}, which makes pflag walk a glued short option letter by letter (-s,-z,-s,-h). Every supported shell name contains anh, so the bundle always ends with-h, and the root help function rewrites the invocation ashelp _complete …:The completion function silently discards that. The legacy PHP CLI parses the glued form correctly on its own (
php legacy/bin/platform _complete … -szshreturns suggestions), so the Go layer was the only thing at fault.The same applies to the input tokens the scripts pass, which is why fixing only the templates is not enough: completing
upsun ssh --pro<TAB>sends-issh, and thathgets bundled in too.Fix
_completethrough a hidden Cobra command withDisableFlagParsing, so completion requests reach the legacy CLI unchanged. This also repairs completion files users already have installed, and covers input tokens containing anh.--shell=<shell>in the generated scripts, so a request no longer depends on how glued short options are parsed.Verification
Real Tab presses driven through a pty in zsh, bash and fish (Debian container, plus zsh on macOS), for each install method — 27/27 pass with the fix, and the table below shows why the template change alone would not have been sufficient:
upsun env⇥… --for⇥upsun ssh --pro⇥eval "$(upsun completion)"OEM (vendorized) CLIs
Same bug, same fix. Verified with the Akeneo Extension Platform CLI, installed in a container with its own installer (
curl -fsSL https://cli.extension.akeneo.cloud/installer | sh), which downloads the standardupsunrelease binary and points it at the vendorconfig.yamlthrough a wrapper script:env⇥… --for⇥ssh --pro⇥-tags vendorbuild with the Akeneo config embeddedIdentical results in zsh, bash and fish for every row above. OEM users get the fix through the normal release, since the installer pulls the standard release binary.
Tests added:
commands/completion_test.go(script rewrite, and completion arguments reaching the command unparsed) andintegration-tests/completion_test.go(generated scripts, and a real_completerequest in short form, long form, and with an input token containing anh). All fail onmainand pass here.🤖 Generated with Claude Code