12 ポイント 投稿者 GN⁺ 2025-08-13 | 1件のコメント | WhatsAppで共有
  • BashZshで、すでに補完済みの単語にも説明を表示するタブ補完機能を実装する方法を紹介
  • BashとZshは異なるタブ補完APIを使用しており、Zshのみがデフォルトで補完候補に説明を表示する機能を提供
  • _generate_foo_completionsで候補を生成し、BashではCOMPREPLY、Zshではcompaddで返す構成を実装
  • Zshの説明表示機能をBashでも実現するため、候補文字列に説明を含め、単一候補のときだけ説明を取り除く方式で処理
  • 単一候補の場合でも意図的に曖昧さを追加し、<TAB>入力時に説明が表示されるよう改善
  • 最終スクリプトは両方のシェルで同一のユーザー体験を提供し、部分補完・完全補完・候補説明の表示をすべてサポート

問題の背景

  • タブ補完(tab-completion) はコマンドやフラグを探す際に便利で、とくにAPIやCLIツールを初めて使うときに役立つ
  • Zshはデフォルトで複数候補があるときだけ説明を表示し、Bashは別途設定した場合にのみ可能
  • しかしすでに補完済みの単語については、どちらのシェルも説明を表示しない
  • そのため、説明を見るにはユーザーが次のような面倒な手順を踏む必要がある
    • 一部の文字を削除して複数候補が一致する状態にする
    • <TAB>キーを押して候補一覧を確認
    • 目的の説明を目で探す
    • 削除した文字を再入力してコマンドを実行

解決方法の概要

  • 単一候補の場合でもダミー候補(dummy completion) を追加して候補を曖昧にする
  • これによりBashとZshの両方で候補一覧とともに説明を出力できる
  • ただし、実際のコマンドに説明テキストが挿入されないよう注意が必要

基本概念

  • タブ補完は<TAB>入力時に現在の単語とカーソル位置を受け取り、可能な候補一覧を返す仕組みで動作する
  • Bash: COMPREPLY配列に候補を代入
  • Zsh: compaddコマンドで候補を登録
  • _generate_foo_completions関数は候補文字列を出力し、実運用ではCLIの状態に基づいて動的生成も可能

BashとZshを同時にサポートする

  • _complete_foo_bash_complete_foo_zsh関数で、それぞれのシェルに合わせて実装
  • if [ -n "${ZSH_VERSION:-}" ]; then ... elif [ -n "${BASH_VERSION:-}" ]; then ... fiで分岐
  • ユーザーはスクリプトを.bashrcまたは.zshrcに登録して適用する

Zshでの説明表示

  • 候補文字列に名前: 説明形式を使用
  • Zsh: compadd -d raw -- $trimmedで名前と説明を並列配列として渡す
  • Bash: 説明部分を取り除いた候補だけをCOMPREPLYに渡す(デフォルトでは説明をサポートしないため)

Bashで説明を実装する

  • 複数候補の場合は説明を含む文字列をそのまま表示
  • 単一候補の場合にだけ説明を削除
  • Bashの補完が共通接頭辞だけを挿入する動作を利用し、説明が実際の入力に含まれないようにする

単一候補でも説明を表示

  • 補完済みの単語に<TAB>を入力したときでも説明を見せるため、ダミー候補を追加して曖昧さを誘発
  • Zsh: 2つの並列配列(raw, trimmed)の両方にダミー候補を追加
  • Bash: 単一候補の場合、trimmedに名前だけを追加

最終結果

  • 複数候補時は名前+説明の両方を表示
  • 単一候補時でも<TAB>で説明を確認可能
  • BashとZshの両方で同一の体験を提供
  • 適用例:
    $ foo <TAB>  
    apple: a common fruit banana: starchy and high in potassium  
    apricot: sour fruit... cherry: small and sweet...  
    

1件のコメント

 
GN⁺ 2025-08-13
Hacker Newsの意見
  • fishでは、対象のプログラムがmanページを提供していれば、fish_update_completionsを実行するだけでよい
    • このコマンドはシステム上のすべてのmanページを解析し、自動補完ファイルを~/.cache/fish/generated_completions/に生成する
    • manページの内容が不十分だったり存在しない場合は、自分で作成してupstreamに貢献できる
    • fishのフォーマットは単純なので、公式ドキュメントを見るだけで十分
    • 例: curl-L → 'Follow redirects'、-O → 'Write output to file named as remote file'
    • 画面共有をすると、みんな私がzshとプラグイン群を使っていると思うが、実際にはデフォルト設定のままでも見た目の良いfishを使っているだけ
    • ただし、car TABblkdiscardに展開される非接頭辞自動補完のせいで、cargoがPATHになくても誤動作する点は改善されてほしい
    • manページがなく--helpしか提供しないプログラムについては、fishにzshの_gnu_genericやbashのcomplete -F _longoptのような機能があるのか気になる
    • zshにもmanページベースの自動補完を生成するスクリプトがある → zsh-manpage-completion-generator
    • OpenSUSEでzypper search fish-completionを実行すると200個を超えるパッケージが出てきて、何か怪しいと感じた
  • bashの自動補完がだんだん「賢く」なるにつれ、現在のカーソル位置ではファイル名が不適切だと判断すると、ファイル/ディレクトリ補完を止めてしまうのが不便
    • むしろ常にファイル名補完へフォールバックする方がよいと思う
    • このせいで補完スクリプトを全部無効にしようかと考えたこともある
    • bashには、M-/にバインドされたcomplete-filenameのように、文脈を無視してファイル名だけを補完する関数がある
    • 特定のコマンドでファイル補完が完全に壊れていて、いったんlsで補完してからコマンドを打ち替えることで回避している
    • ファイル名は存在するが実行できないときは、“file foo.exe exists but it isn't executable”のようなメッセージを出してくれる方が混乱が少ない
    • complete -rを実行したあとで望みどおりに動くのを見ると、bash-completionスクリプト側に問題があるようだ
    • Webフォームでメールアドレス入力時に、最初の1文字を打っただけで“Invalid email!”エラーを出すフロントエンド検証のような、もどかしいUXに似ている
  • これは私が書いた記事で、他の人たちにも面白く読んでもらえたらうれしい
    • zsh補完スクリプトは毎回読み込む代わりに$fpathへインストールするとキャッシュされ、起動速度が速くなる
    • Homebrewで配布するときは、自動でcompletionsをインストールできる
    • zsh補完は規模が大きく習得しにくいが、会社でansibleラッパースクリプトにプレイブックごとのオプションやタグ補完などを追加しつつ、少しずつ磨いている
  • 最近CLI開発でusageライブラリを使い始めた → clapと統合されており、completions・argparse・manページの生成が可能
    • fishスクリプトのargparseブロックを置き換える価値があるかは考え中だが、optparseよりはずっと良い
    • 私も似たようなCLI仕様定義ツールを作っていて、ちょうどfish対応を追加したところ
  • bash/zshでJSONフィールド自動補完をサポートするfx.wtfの紹介
    • ijqより軽量だが、状況によっては便利かもしれない
  • zsh組み込み関数でcompleterを作るチュートリアル → zsh-completions-howto
  • プログラム側がbashスクリプトを書かなくても済む標準フラグがあるのか気になる
    • zshでは_gnu_generic--helpベースの簡単な補完が可能
    • Rustのclap_complete、ripgrepの--generateオプション、PHP Symfonyのランタイム補完生成などの事例がある
    • 共通のパーサーライブラリが自動実装してくれる--completion標準があるとよい
  • kshでは配列定義だけで簡単に補完を実装できる
    • 例: complete_kill_1配列にシグナル名を入れると、killコマンドの第1引数補完を提供できる
    • これがksh93の文法なのか、okshにバックポートされたものなのか気になる
  • 私が作った簡単なzsh補完スニペットの例: set-java-home関数で~/apps/java/*の一覧をバージョンとして補完するよう_describeを使用
    • ほとんどワンライナーに近い簡単な構造
  • LLMの主な機能が自動テキスト補完だからか、GitHub Copilotはvscodeターミナルでbashやzshの補完をかなりうまくやってくれる