Skip to content

feat: catalog-driven bash-completion installation#121

Merged
CybotTM merged 5 commits into
mainfrom
feat/bash-completions
Jul 22, 2026
Merged

feat: catalog-driven bash-completion installation#121
CybotTM merged 5 commits into
mainfrom
feat/bash-completions

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Adds catalog-driven bash-completion installation. A tool declares how to get its
completion; the toolset installs it as part of the tool's normal lifecycle and can
backfill everything already installed.

Independent of #120.

Schema

A catalog entry may declare an optional bash_completion object with exactly one of:

"bash_completion": { "command": "gh completion -s bash" }        // generate to stdout
"bash_completion": { "source_path": "completions/rbenv.bash" }   // copy, relative to clone_path

Files land in ${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/, named after
binary_name so the framework lazy-loads them for that command.

Changes

  • lib/completion.sh — install/remove/post-install, framework bootstrap, output validation
  • lib/bashrc.sh — reusable idempotent managed-block insert/remove
  • install_tool.sh — installs completion after a successful install/update/reconcile,
    removes on uninstall; best-effort, never fails the surrounding tool operation
  • install_completion.sh — CLI wrapper + --all backfill
  • make completions, make completion-<tool>
  • 40 catalog entries declare bash_completion (39 command, 1 source_path)
  • catalog/COVERAGE.md records the full classification

Verification

All 112 catalog entries were audited: each candidate generator was executed, its output
validated, and checked to confirm it registers the entry's own binary_name.
End-to-end: install_completion.sh --all installs 36 completions; every file registers
its own command. pytest → 728 passed, 1 skipped.

Findings that shaped the implementation

  1. Validation had to be strict. A permissive check (any output containing "complete")
    accepted bandit complete bash, which actually ran a security scan. The predicate now
    requires complete -… / compgen / COMPREPLY.
  2. Six tools have a working generator that must not be used — the file is named after
    binary_name, so a script registering a different command silently shadows another tool:
    gh-aw emits gh's completion, rga returns ripgrep's verbatim, compose is
    binary_name: docker, fzf --bash is shell integration (key bindings + global
    complete -D), rustup never registers rustc, gcloud has no clone_path.
    A test enforces that no two declaring entries share a binary_name.

Robustness fixes (post-review, each reproduced then verified)

  • _completion_catalog_file returned 1 for an unknown tool, aborting set -e callers before
    their own no-op guard (make completion-<typo> failed with a bare Error 1).
  • mkdir/mv were unchecked and the trailing echo forced a 0 return — an unwritable target
    reported "installed", wrote nothing, and leaked a temp file.
  • The generator inherited stdin, so a generator that reads stdin would hang the install
    forever. Now </dev/null (+ a timeout where the platform has one).
  • The ~/.bashrc block lacked an interactive guard; the distro bash_completion has none of
    its own and enables extglob/progcomp at top level, so a non-interactive shell sourcing
    ~/.bashrc silently got extended globbing.

codex, pip, pipx are not installed on the audit machine, so their generators come from
official docs — runtime validation makes that safe (an invalid generator writes nothing).

Note on size

Exceeds the ~300 net-LOC guideline. The framework is small; the bulk is 40 mechanical
one-line catalog declarations plus the coverage table, done in one sweep.

CybotTM added 3 commits July 22, 2026 10:51
Tools may declare an optional `bash_completion` object in their catalog entry
with exactly one of `command` (generate to stdout) or `source_path` (copy from
clone_path). Completions install to the XDG user completions dir, named after
binary_name so the bash-completion framework lazy-loads them.

- lib/completion.sh: install/remove/post-install hooks, framework bootstrap,
  and validation that rejects non-completion output (a bare "complete" in help
  text is not enough — some tools treat an unknown `completion` argument as a
  scan target and their output would otherwise pass).
- lib/bashrc.sh: reusable idempotent managed-block insert/remove, hardened
  against unbalanced markers.
- install_tool.sh: installs completion after a successful install/update/
  reconcile, removes it on uninstall. Best-effort — never fails the tool op.
- install_completion.sh: CLI wrapper, plus `--all` backfill.
- make completions / make completion-<tool>.
- catalog/gh.json: first declaration, verified end-to-end.
- tests: both shapes, validation rejection, removal, --all, and catalog
  invariants (one shape per entry; no two entries share a binary_name).

Claude-Session: https://claude.ai/code/session_01E3fMneH9KcaT7UYaHpfsHs
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
The completion lib is POSIX shell; invoking it via `bash` fails on
windows-latest. Use the repo's existing skip_on_windows idiom for the
shell-invoking classes; the catalog-schema invariants stay platform-independent.

Claude-Session: https://claude.ai/code/session_01E3fMneH9KcaT7UYaHpfsHs
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Audited all 112 catalog entries for a bash-completion generator. Each candidate
generator was executed and its output validated (complete -…/compgen/COMPREPLY),
then checked to confirm it registers the entry's own binary_name.

40 entries now declare bash_completion (39 command, 1 source_path for rbenv).

Six tools have a generator that must NOT be used, because the completion file is
named after binary_name and would shadow another tool:
- gh-aw emits gh's completion (registers gh, never gh-aw)
- rga forwards to ripgrep and returns ripgrep's script verbatim (registers rg)
- compose has binary_name docker; docker already covers it
- fzf --bash is shell integration (key bindings, global `complete -D`), not a
  completion script — belongs in .bashrc via eval
- rustup completions registers rustup/cargo, never rustc
- gcloud has no clone_path to resolve source_path against, and its file
  registers three commands in one lazily-loaded file

codex, pip and pipx are not installed on the audit machine; their generators come
from official docs. Runtime validation makes that safe — an invalid generator
writes nothing.

catalog/COVERAGE.md records the full classification.

Claude-Session: https://claude.ai/code/session_01E3fMneH9KcaT7UYaHpfsHs
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copilot AI review requested due to automatic review settings July 22, 2026 09:14
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CybotTM added 2 commits July 22, 2026 11:30
Found by adversarial review of #121; each was reproduced before fixing and
verified after.

- _completion_catalog_file returned 1 for an unknown tool. Callers capture it in
  an assignment, so a `set -e` caller aborted before its own no-op guard:
  `install_completion.sh <unknown>` exited 1 with no output, and
  `make completion-<typo>` failed with a bare "Error 1". Always return 0.
- mkdir/mv were unchecked and the trailing echo forced a 0 return, so an
  unwritable target reported "installed" while writing nothing and leaking the
  temp file. Check both, clean up, return 1. In --all this had inflated the
  installed count.
- The generator ran with stdin inherited from make/the terminal and no timeout,
  so a generator that reads stdin would hang the install forever. No shipped
  entry triggers it today (all 38 local generators were re-run to confirm), but
  the blast radius warrants `timeout 30 … </dev/null`.
- The ~/.bashrc block lacked an interactive guard. The distro bash_completion
  has none of its own and enables extglob/progcomp at top level, so a
  non-interactive shell sourcing ~/.bashrc (`ssh host somescript`) silently got
  extended globbing, changing how !(…)/@(…) parse. Guard on `$- == *i*` and
  skip POSIX mode, as distro rc files do.

Also make the post_install_completion call in install_tool.sh `|| true`, so a
successful install can never exit non-zero, matching the documented contract and
the sibling remove_completion call.

Adds regressions for all four, plus direct unit tests for lib/bashrc.sh.

Claude-Session: https://claude.ai/code/session_01E3fMneH9KcaT7UYaHpfsHs
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
The previous commit hard-coded `timeout 30` around the completion generator.
macOS ships no GNU `timeout`, so on macos-latest the command failed, the
generator produced no output, and every completion was rejected as invalid —
three tests failed in CI.

Route the generator through _completion_run, which uses `timeout` (GNU) or
`gtimeout` (Homebrew coreutils) when present and otherwise runs the command
directly. Verified both branches: with timeout absent (PATH built from the real
bin dirs minus timeout/gtimeout) the completion installs correctly, and a
generator that reads stdin still returns immediately — `</dev/null`, not the
timeout, is what prevents the hang. The timeout remains as insurance for a
generator that blocks on something else.

Claude-Session: https://claude.ai/code/session_01E3fMneH9KcaT7UYaHpfsHs
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CybotTM
CybotTM merged commit c262105 into main Jul 22, 2026
24 of 25 checks passed
@CybotTM
CybotTM deleted the feat/bash-completions branch July 22, 2026 10:13
CybotTM added a commit that referenced this pull request Jul 22, 2026
…ts (#122)

CI runs the suite on `ubuntu-latest`, `macos-latest` **and
`windows-latest`**, while
everything under `scripts/` is POSIX Bash. Any test that invokes `bash`,
sources a
`scripts/lib/*.sh` helper, or executes an installer fails on the Windows
leg.

Thirteen test files already use the `skip_on_windows` marker, but the
convention was
undocumented — so it gets re-derived the hard way. It is invisible
locally: the suite
is green on Linux and only the Windows CI leg turns red (this happened
on #120 and
again on #121 during this work).

Documents the marker, and that it applies to the shell-invoking class
only — pure
Python assertions (catalog JSON structure, schema invariants) must keep
running on
every platform.

Docs only; no code or test changes.

Came from /retro: yes
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.

2 participants