Bashでタブ補完を自作する(同じオプションを2回サジェストしない実装付き)

Bashのタブ補完って便利ですよね。
私の一押しはgitのコマンドを補完してくれるgit-completion.bashです。
めっちゃ便利なので使ってない人は是非使いましょう。

さて、Bashの補完が作れるということなのですが、実際どう作るのか気になったので調べながら実装しました。

基本的には分かりやすい解説があるので下記のブログを読みましょう。下記のブログの方が分かりやすいし親切です。「同じオプションが2回サジェストされない」実装をしたくなったら戻ってきてください。 - Bashタブ補完自作入門 - Cybozu Inside Out | サイボウズエンジニアのブログ

作ったもの

今回の題材command.py。
command.py --number one --animal catとかすると1 catと返してくれるコマンド。
英単語をアラビア数字に変換して、animalはあれば続けて出力する。

#!/usr/bin/python3
"""
コマンドラインからnumberとanimalを受け取って出力する

実行権限の付与を忘れずに
"""

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--number", help="input one word from zero to nine")
parser.add_argument("--animal", help="input one word from cat, dog or deer")
args = parser.parse_args()

w2n = {
    'zero': 0,
    'one': 1,
    'two': 2,
    'three': 3,
    'four': 4,
    'five': 5,
    'six': 6,
    'seven': 7,
    'eight': 8,
    'nine': 9,
}

if args.number:
    print(w2n[args.number], end=' ')

if args.animal:
    print(args.animal, end='')

print()

下記の内容を~/.bash_completionに追記すると補完が効くようになる。

_except() {
  # 文字列を集合と見たときの差集合`a - b`を返す
  local a b intersection difference
  a=$1
  b=$2

  intersection=($(for item in ${a} ${b}; do echo "$item"; done | sort | uniq -d))
  difference=($(for item in ${a} ${intersection[@]}; do echo "$item"; done | sort | uniq -u))
  echo "${difference[@]}"
}


_command() {
  local cur prev cword opts numbers animals unused_opts
  _get_comp_words_by_ref -n : cur prev cword
  opts="--number --animal"
  unused_opts="$(_except "$opts" "${COMP_WORDS[*]}")"
  numbers="zero one two three four five six seven eight nine"
  animals="cat dog deer"


  COMPREPLY=( $(compgen -W "${unused_opts[@]}" -- "${cur}") )
  if [ "${prev}" = "--number" ]; then
    COMPREPLY=( $(compgen -W "${numbers}" -- "${cur}") )
  elif [ "${prev}" = "--animal" ]; then
    COMPREPLY=( $(compgen -W "${animals}" -- "${cur}") )
  fi
}

complete -F _command command.py

解説

  • completeコマンドで補完時に使う関数を補完したいコマンドに紐つけておくと、関数内でCOMPREPLYに代入した単語群を使って補完してくれる
  • ターミナルで入力している内容がスペース区切りで配列としてCOMP_WORDSに渡されている
  • optsCOMP_WORDSの差集合を取ることで、既に使っているオプションサジェストしない実装になっている
  • ${prev}(一つ前の文字列)がオプション名だった場合は、それに対応するanimalsnumbersから補完させる