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
@johanvx

johanvx commented Jul 13, 2026

Copy link
Copy Markdown
Author

@Sunrisepeak Would you mind checking this PR?

@Sunrisepeak Sunrisepeak self-requested a review July 13, 2026 08:51
@Sunrisepeak

Copy link
Copy Markdown
Member

@Sunrisepeak Would you mind checking this PR?

需要适配一下gcc 15 musl 工具链

@Sunrisepeak Sunrisepeak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已做了本地验证,建议优先按以下方向处理(CI 已可复现):

  1. 结论:问题更像是代码层的模块化链接性设计问题,而非单纯某个编译器"偶发"行为。
  • src/completions/bash.cppmSubCmdEntry 在匿名 namespace(TU-local)
  • ScBranchsubcommand_details() 函数体内(function-local)
  • src/completions/zsh.cppm 中也有 SubCmdEntry 在匿名 namespace
  • std::vector<...> 及其相关 std::allocator 实例化会把这些局部类型暴露到模块接口可见语义,gcc 15.1.0-musl 报 exposes TU-local entity,该链路在 CI 上失败。
  1. 建议改法(最小改动):
  • 将以上容器元素类型统一移到具名命名空间(例如 namespace mcpplibs::cmdline::completions::detail)且保有 module 可见链接;
  • 保留业务函数可在匿名命名空间;
  • flatten_subcommands / subcommand_detection_cases / subcommand_details 等逻辑不变,仅调整类型定义位置与路径。

建议变更示例:

  • src/completions/bash.cppm
    • 移动 SubCmdEntry 出匿名 namespace;
    • 移动 ScBranchsubcommand_details 函数体到 file scope;
  • src/completions/zsh.cppm
    • 同样将 SubCmdEntry 移到 file scope 的具名命名空间。
  1. 验证(建议):
  • gh run view 29146418379 --log --job 86773636958 当前失败链路保留;
  • 本地请固定用:
    • mcpp toolchain default gcc 15.1.0-musl
    • mcpp build --workspace --no-cache

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