世界を動かす技術を、日本語で。

BashとZshのためのシンプルなタブ補完の作成

2025年8月10日原文(mill-build.org)

概要

  • BashとZshでの タブ補完 の違いと統一的な実装方法を解説
  • Mill build tool での実践例を紹介
  • ZshだけでなくBashでも 補完候補の説明文 を表示する手法
  • コマンドが1つだけの時にも説明を見せる 工夫
  • CLIツールにおける ユーザー体験向上 のヒント

BashとZshでのタブ補完の基本

  • BashZsh ではタブ補完APIが異なるため、両対応の実装が必要

  • タブ補完は、ユーザーが<TAB>を押した時に ハンドラ関数 を呼び出し、入力中の単語やカーソル位置を受け取る仕組み

  • 補完候補は 配列 で管理し、入力のプレフィックスに一致するものをリストアップ

  • Bashでは COMPREPLY、Zshでは compadd を使って補完候補を返却

  • 設定は ~/.bashrc~/.zshrc などに記述し、シェル起動時に読み込ませる

    • 例: Mill build toolの./mill mill.tabcomplete/installコマンドは自動で設定ファイルを更新

補完候補の説明文表示

  • Zshは 補完候補の説明文 を標準サポート、Bashは未対応
  • 候補生成関数で「単語:説明文」の形式で配列を作成
  • Zshではcompadd -d説明文付き候補 を表示
  • Bashでは説明文を カットして COMPREPLYに入れることで、単語のみ補完

Bashで説明文を見せるハック

  • Bashで説明文を表示するには、 複数候補 がある時だけ説明文付きで候補を返す

  • 候補が1つだけなら説明文をカットし、複数ならそのまま表示

  • Bashは共通プレフィックスしか自動補完しないため、説明文がコマンドラインに挿入されることはない

    • 例:
      • foo <TAB> → 候補+説明文一覧を表示
      • foo a<TAB> → プレフィックス一致の候補+説明文のみ表示

完全一致時にも説明文を表示する工夫

  • コマンドが 1つだけ一致 した場合でも説明文を表示したい場合は、 ダミー候補 を追加

  • 1つの候補+説明文と、説明文なしの同じ単語を2つ返すことで、シェルが候補一覧を表示

  • 実際に補完されるのは説明文なしの単語のみ

    • Bash・Zshともにこの方法で 完全一致時の説明文表示 が可能

実装例のまとめ

  • 補完ロジックは _generate_foo_completions 関数に集約

  • BashとZshで 補完API の違いを吸収しつつ、UXを向上

  • 補完候補の 説明文 はCLIツールの学習コストを下げる重要な機能

    • Mill, Maven, Gradleなど ラッパースクリプト 型のツールにも有効

CLIツール開発者へのアドバイス

  • Bash/Zsh両対応 の補完スクリプトを用意することで、幅広いユーザーに快適な体験を提供
  • 補完候補の 説明文 を積極的に活用し、ユーザーが迷わずコマンドを選択できるようにする
  • シェルの仕様差異を吸収しつつ、 一貫した補完体験 を目指す設計が重要

Hackerたちの意見

これを書いたんだけど、みんなが読んで面白いと思ってくれたらいいな!初めてこれを理解したとき、めっちゃ楽しかったから。

面白い記事をありがとう!

zshの補完についての良い初めの一歩だね。全体的にかなり大きなシステムで、まだちょっと苦労してるけど。仕事では、ansibleのラッパースクリプトに自動補完を少しずつ追加してるんだ。どのプレイブックをいつ使うかの説明とか、選択されたプレイブックに基づいたスマートな-l補完(例えば、プレイブックがpostgres.ymlのときはmariadbグループを提案しない)とか、タグの自動補完(ちょっとハードコーディングされた説明があるけど、これらのタグの使い方)とかね。金曜日の午後の苦労プロジェクトみたいなもんだけど、大きなansibleプロジェクトを使いやすくしてるよ。

共有してくれてありがとう!君のbash補完のアイデアを自分のCLIに取り入れたいな(もうzshの補完はあるけど)。zshの補完スクリプトを毎回起動時に読み込む代わりに、$fpathのどこかにインストールすれば、zshが補完を「コンパイル」してキャッシュしてくれるんだ。これでシェルの起動時間がかなり速くなるけど、もちろん設定はちょっと難しい。ユーザーは補完をそこに置くために$fpathを理解する必要があるからね。自分のCLIはHomebrew経由で配布してて、補完を自動でインストールできるよ。

プログラムがこのbashスクリプトを書くのを避けるための標準的なフラグってないの?理想的には、argparseみたいなライブラリの一部になってるべきだよね。

zshでは、--helpフラグを使ったコマンドの簡単な補完に、_gnu_generic関数を使えるよ。スタートアップファイルのどこかに、こんな感じの行を入れておけばOK:compdef _gnu_generic

これについても考えたことがある。標準の--completionみたいなものがあれば、共通の引数解析ライブラリが自動的に実装してくれるといいのに(自動的なヘルプテキストを実装するのと同じように)。

Rustには最も人気のある引数解析ライブラリのためのclap_completeパッケージがあるよ:https://crates.io/crates/clap_complete。ripgrepは独自のシェル補完とmanページ生成を--generateオプションを通じて公開してる:rg --generate=man、rg --generate=complete-bash、などなど。xh(clapベース)でも同じことを提供してるけど、AFAIK、あのインターフェースをコピーしてるのはうちだけだと思う。Symfony(PHP用)は何らかのランタイム補完生成を提供してるけど、詳細は知らないな。

こちらは、組み込み関数を使ってzshの補完を作成するための別のチュートリアルだよ: https://github.com/vapniks/zsh-completions/blob/master/zsh-c...

fishの場合、興味のあるプログラムが古くからのmanページを提供してくれてるなら、fish_update_completionsを実行するだけで簡単にできるよ。システム上のすべてのmanページを解析して、補完ファイルを生成してくれる。デフォルトでは、~/.cache/fish/generated_completions/*に入るよ。もしmanページがうまく書かれてなかったり、欠けてたりしたら、自分で補完を書くこともできるし(できれば上流に送ってね)。fishはすごくシンプルなフォーマットを使ってるから、公式ドキュメント以外のチュートリアルは必要ないと思うよ: https://fishshell.com/docs/current/completions.html 例えば、curlの補完の一部を紹介すると、complete --command curl --short-option 'L' --long-option 'location' --description 'リダイレクトを追う' complete --command curl --short-option 'O' --long-option 'remote-name' --description 'リモートファイル名で出力をファイルに書き込む'

スクリーンシェアしてると、みんながzshやたくさんのプラグインを使ってないことに気づかないんだよね。実際にはfishだけで、初めからめっちゃ美しいんだ。

コメントありがとう。https://github.com/umlx5h/zsh-manpage-completion-generatorはこれをZSHに適応させるみたいだね。まだ試してないけど。

Hacker Newsで議論の続きを見る