Skip to content

feat: add shell completion generation (bash, fish, zsh)#8

Open
johanvx wants to merge 8 commits into
mcpplibs:mainfrom
johanvx:feat/completions
Open

feat: add shell completion generation (bash, fish, zsh)#8
johanvx wants to merge 8 commits into
mcpplibs:mainfrom
johanvx:feat/completions

Conversation

@johanvx

@johanvx johanvx commented Jul 11, 2026

Copy link
Copy Markdown

TL;DR

Add Shell enum, completions::Command snapshot, and three shell-completion generators (bash, fish, zsh) that produce native completion scripts for a cmdline::App. The implementation is stripped-down from clap_complete, matching what our data model (App/Option/Arg) actually supports: no aliases, no possible-values, no value-hints, no conflict annotations.

Problem

mcpplibs.cmdline had no built-in mechanism to generate shell completions. Users who wanted tab-completion had to hand-write scripts for each shell (fish complete -c, bash complete -F, zsh _arguments), duplicating the option definitions already present in their App setup.

Approach

Port the three completion generators from clap_complete (fish.rs, bash.rs, zsh.rs), stripping features that cmdline does not support. The stripping logic follows the pattern already established in our existing fish generator: only primary short/long names, bare file completion for value-taking options, global re-offer with dedup under subcommands.

A completions::Command struct captures a snapshot of the App tree (names, descriptions, options, args, subcommands) including auto-added --help and --version. All three generators receive (const Command&, std::ostream&).

Implementation

File What changed Why
src/completions.cppm Shell enum, Command struct, shell_from_string/to_string/shell_supported helpers Core types for shell-agnostic completion API
src/completions/bash.cppm Single-function complete -F generator with compgen -W word-list matching and a case "${cmd}" dispatch switch Port of clap_complete::bash.rs
src/completions/fish.cppm Declarative complete -c generator with -n conditions and helper functions Already existed; backslash escaping fix
src/completions/zsh.cppm #compdef generator with _arguments specs, recursive case $state dispatch, _commands() helpers Port of clap_complete::zsh.rs
src/cmdline.cppm snapshot(), four generate_completions() overloads, App::completions() Wiring: AppCommand → shell output
tests/completions_test.cpp 8 tests across all three shells: basic shape, subcommand dispatch, global propagation, no-subcommand edge case Regression prevention
docs/api.md Shell completion section with type table, API reference, usage example Documentation
examples/completions/ Full example app with a completions subcommand Runnable proof-of-concept

Architecture comparison (clap → cmdline)

clap cmdline adaptation
Fish generator gen_fish_inner recursive walk gen_inner with std::span<Option> for root globals
Bash generator Single format! template with placeholders generate() with out << streaming, 7 helpers in anonymous namespace
Zsh generator get_args_of / get_subcommands_of / subcommand_details Same decomposition; dual raw/escaped path tracking for walk_command vs zsh identifiers
Aliases get_short_and_visible_aliases(), subcommand aliases Stripped — only primary short_ and long_name
possible_values / ValueHint compgen -W / _files / _command_names etc. Stripped — all use _default (zsh) or compgen -f (bash)
Conflict annotations (-x --y) prefix on specs Stripped
Global options under subcommands Propagated by clap's Command::build() Generator-level root_globals span + dedup

Trade-offs

  • completions::Command is built eagerly via snapshot(), copying vectors. For command trees with hundreds of subcommands this could be noticeable, but the typical case is negligible.
  • walk_command returns a raw const Command*. Callers must not hold it past the lifetime of the root Command.
  • Bash and zsh generators duplicate walk_command / flatten_subcommands / has_option rather than sharing a utility module. This keeps each .cppm self-contained, matching fish.cppm.

Validation

$ cd /home/johan/github/cmdline && xmake run cmdline_test
[==========] Running 45 tests from 11 test suites.
...
[  PASSED  ] 45 tests.

All three generators produce valid output:

$ cd examples/completions && mcpp build
$ ./target/*/bin/completions completions --shell bash | head -5
_myapp() {
    local i cur prev opts cmd
    COMPREPLY=()
    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then

$ ./target/*/bin/completions completions --shell fish | head -5
function __fish_completions_global_optspecs
    string join \n y/yes v/verbose q/quiet agent h/help version
end

$ ./target/*/bin/completions completions --shell zsh | head -5
#compdef completions

autoload -U is-at-least
...

How to test

  1. Build: mcpp build (or xmake)
  2. Run tests: xmake run cmdline_test
  3. Generate completions for a sample app:
    cd examples/completions && mcpp build && ./target/*/bin/completions completions --shell bash
  4. (Manual) Source the output in a shell session and verify tab-completion prompts options and subcommands.

@johanvx johanvx marked this pull request as draft July 11, 2026 08:26
@johanvx johanvx changed the title feat: add shell completion generation (bash, fish, zsh), ported from clap_complete feat: add shell completion generation (bash, fish, zsh) Jul 11, 2026
@johanvx johanvx marked this pull request as ready for review July 11, 2026 08:37
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