Skip to content

Add shell completion for lstk aws - #424

Draft
skyrpex wants to merge 2 commits into
mainfrom
claude/lstk-aws-shell-completion-30f9ed
Draft

Add shell completion for lstk aws#424
skyrpex wants to merge 2 commits into
mainfrom
claude/lstk-aws-shell-completion-30f9ed

Conversation

@skyrpex

@skyrpex skyrpex commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Motivation

aws and awslocal both have rich Tab completion; lstk aws had none. Reported in Slack while checking lstk aws for parity with awslocal.

Changes

  • lstk aws <TAB> now completes AWS services, operations and parameters by delegating to the AWS CLI's own aws_completer.
  • New internal/awscli/complete.go: CompleterPath() (PATH, falling back to a sibling of the symlink-resolved aws binary) and Complete(ctx, args, toComplete), which speaks the same COMP_LINE/COMP_POINT protocol bash's complete -C uses.
  • Wired as a ValidArgsFunction on the aws command in cmd/aws.go, with the Tab-press timeout owned at the command boundary.
  • lstk aws --help mentions that completion comes with lstk completion <shell>.

Delegating through Cobra rather than registering complete -C aws_completer lstk is what makes this work in every shell lstk completion supports — the native AWS registration is bash/zsh-only — and it needs no additions to the self-contained bash 3.2 fallback from DEVX-950.

Three protocol details the implementation has to respect, all documented in comments:

  • COMP_LINE must start with the literal word aws: the completer drops the first word before matching, so handing it lstk aws s3 l returns nothing at all.
  • COMP_POINT is a character offset, not a byte offset.
  • A trailing space is significant — it is what tells the completer the cursor has started a new word.

Cobra does not run PreRunE on the __complete path, so completion stays fully offline: no config load, no Docker health check, no endpoint resolution. That matches aws_completer, which completes commands and parameters only and never contacts an endpoint. A missing or failing completer returns ShellCompDirectiveDefault and prints nothing — any output on this path would be read by the shell as a candidate.

On cmd.WaitDelay

Worth a look during review, since the reason isn't visible from the call site. exec.CommandContext kills the completer when the deadline expires, but it does not close a pipe the completer's own children still hold — so cmd.Output() keeps blocking until every holder of stdout exits. A completer that forks instead of exec'ing its payload would therefore hang a Tab press for as long as that grandchild lives, deadline or no deadline. The pip-installed v1 aws_completer is a script, so this is not hypothetical.

This is platform-dependent, which is how it got past a first green local run: on macOS /bin/sh is bash 3.2 and exec's the payload (killing the child is enough), while on Linux it is dash, which forks. The deadline test consequently blocked for 30s against a 100ms deadline on CI and 100ms on macOS. cmd.WaitDelay caps the post-deadline drain, then closes the pipes.

The test's fake completer now forks explicitly (sleep N & wait) rather than relying on which shell /bin/sh happens to be, so it exercises the grandchild case on every platform. Verified in a golang:1.25 container that it fails (30.0s) with only the WaitDelay line removed and passes (0.21s) with it — the test is not vacuous.

Review

Human review advised — new user-facing behavior, and the shell-completion protocol has failure modes (stray stdout corrupting candidates) that don't surface as test failures.

Tests

  • 10 unit tests in internal/awscli/complete_test.go: COMP_LINE construction, output parsing, completer discovery (PATH and aws-sibling fallback), and the deadline bound described above.
  • 6 integration tests in test/integration/aws_completion_test.go, including one that drives the real generated bash script the way a Tab press does.
  • runBashCompletionDriver grew a variadic extraPath so a stand-in aws_completer is visible to the lstk __complete subprocess.
  • make test (1266 pass), make lint (0 issues), and all 23 TestAWSCommand integration tests pass.

Manually verified against the real aws_completer (aws-cli 2.35.11) in stock macOS bash 3.2. ~140 ms per Tab press.

Manually testing this

Build first:

make build

Confirm the AWS CLI's completer is installed — without it this feature is a deliberate no-op:

which aws_completer

Ask for completions directly. This is exactly what every shell's completion script calls, so it needs no shell setup:

./bin/lstk __complete aws s3 l
./bin/lstk __complete aws dynamodb desc
./bin/lstk __complete aws s3api list-buckets --pre

Each prints the candidates, then a :4 directive line.

Now the real thing. In bash:

export PATH="$PWD/bin:$PATH" && eval "$(lstk completion bash)"

In zsh:

export PATH="$PWD/bin:$PATH" && mkdir -p /tmp/lstk-comp && lstk completion zsh > /tmp/lstk-comp/_lstk && fpath=(/tmp/lstk-comp $fpath) && autoload -Uz compinit && compinit

Then type these and press Tab:

lstk aws s3 l<TAB>          ->  ls
lstk aws sq<TAB>            ->  sqs
lstk aws dynamodb desc<TAB> ->  describe-backup, describe-table, ... (13 candidates)
lstk st<TAB>                ->  start, status, stop  (lstk's own completion, unchanged)

Completion must not need Docker or a running emulator — this still returns ls:

DOCKER_HOST=tcp://localhost:1 ./bin/lstk __complete aws s3 l

And on a machine without aws_completer it must degrade silently, printing only :0 (hand the word back to the shell's file completion) and never an error:

env -i PATH="$(mktemp -d)" HOME="$HOME" ./bin/lstk __complete aws s3 l

Todo

  • lstk az needs the same treatment, but a different protocol (argcomplete: _ARGCOMPLETE=1, output on fd 8).

Closes DEVX-846

Co-Authored-By: Claude <noreply@anthropic.com>
@skyrpex skyrpex added semver: minor docs: needed Pull request requires documentation updates labels Jul 31, 2026
exec.CommandContext kills the completer on deadline but does not close a
pipe its own children still hold, so a completer leaving a grandchild on
stdout blocked cmd.Output() for that grandchild's full lifetime. On Linux
/bin/sh is dash, which forks rather than execs, so the deadline test hung
for 30s instead of 100ms.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs: needed Pull request requires documentation updates semver: minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant