2013-12-20

タブ補完(その1) - Bash Advent Calendar - Day 12

いまは、ぼくのこころの中では 12 月 12 日の 234 時ぐらいです。当然「昨日」とは 12 月 11 日のことです(すべて US 太平洋時間)。

compgen関数

昨日は補完の仕様をさらっと説明しましたが、実際に補完をどう生成すればいいのかというのを説明していませんでした。COMPREPLYは配列変数なので、この Bash Advent Calendar を初日から読めば、自力でかなりのことができるはずです。

とは言え、Bashさんも鬼ではありません。ありがちなパターンに簡単に対応する関数 compgen 関数が予め用意されてます。、一般的には
COMPREPLY=($(compgen オプション... -- 補完したい単語))
という形で使います。

たとえば "-foo", "-bar", "-buzz" というオプション名を補完させたいとしましょう。そしてユーザーが "-" まで入力している場合には
COMPREPLY=($(compgen -W "-foo -bar -buzz" -- "-"))
となります。compgen だけを動作させると以下のようになります
$ compgen -W "-foo -bar -buzz" -- "-"
-foo
-bar
-baz
$ compgen -W "-foo -bar -buzz" -- "-b"
-bar
-baz
$ compgen -W "-foo -bar -buzz" -- "f"
$ echo $?
1
候補が何もなかった場合に、終了ステータスが1になります。

ではよく使うオプションについて説明します。

  • -W 文字列
    引数で与えられた文字列を「単語分割」($IFS のいずれかの文字で区切る)して、それを候補とみなします。 補完候補の中に空白が含まれる場合は、IFSを適宜変更しておく必要があります。何を言ってるかわからない人は諦めてくださいw
  • -P 文字列
    引数で与えられた文字列を、補完候補それぞれの先頭に挿入します。

    例えばディレクトリ名を ":" 区切りで入力するとします。既に "/bin:/usr/bin:/usr/l" まで入力されている状態であれば、まずは "/bin:/usr/bin:" と "/usr/l" に入力を分割、"/usr/l" に対して補完候補を生成、最後に compgen -W "生成結果" -P "/bin:/usr/bin:" -- "/usr/l" とすることで、処理を簡潔にすることができます。
  • -S 文字列
    引数で与えられた文字列を、補完候補それぞれの末尾に追加します。

    上記の -P の例であれば、-S ":" とすれば、ディレクトリ名の末尾に : が追加され、引き続きユーザーがディレクトリ名を入力できます。

以上です。ちなみに、compgen は重複削除をしてくれないので、複雑な補完をする場合には気をつける必要があります。

_get_comp_words_by_ref関数

これはBashビルトイン関数ではありません。昨日の記事を参照してください。どうしても自力で全部したい人は、以下
function ... () {
  local cur prev
  _get_comp_words_by_ref cur prev
  ...
}

function ... () {
  local cur="${COMP_WORDS[COMP_CWORD]}"
  local prev="${COMP_WORDS[COMP_CWORD-1]}"
  ...
}
と読み替えてください。違いは COMP_WORDBREAKS にある文字を正しくハンドリングできるかどうかです。

実例

自分の欲しいパスワードマネージャーがなかった時代、私はテキストファイルにパスワードを書き込みつつ GPG でそれを保護してました。それらは
pass_edit パスワード種別
pass_show パスワード種別
pass_export パスワード種別 メールアドレス [メールアドレス ...]
というコマンド形式です。 パスワード種別は単純に暗号化されたファイルのファイル名から .gpg を取り除いた部分、メールアドレスは gpg で信頼済みのユーザーのメールアドレスです。暗号化されたファイルは $HOME/secure 以下にあります。

以上の条件のとき、~/.bashrc には
complete -F _pass_tool -o bashdefault pass_show
complete -F _pass_tool -o bashdefault pass_edit
complete -F _pass_tool -o bashdefault pass_export
と書いておきます(~/.bashrc が読み込まれる条件は色々あるのですが、ここでは詳細は割愛。自分で調べてね)。これでこれらのコマンドが実行されると、_pass_tool 関数が実行されます

で _pass_tool 関数の中身は
_pass_tool () {
  local data_dir=/home/${USER}/secure
  local cur candidates
  COMPREPLY=()
  _get_comp_words_by_ref cur prev
  if [[ ! -d ${data_dir} ]] || \
     [[ ! -x ${data_dir} ]] || \
     [[ ! -r ${data_dir} ]]; then
    return 1
  fi
  if [[ "${COMP_WORDS[0]##*/}" == "pass_export" ]] && \
     [[ ${COMP_CWORD} -ge 2 ]]; then
    candidates=$( gpg --list-keys 2>/dev/null | \
                    sed -n 's:^uid.*<\([^>]*\)>.*:\1:p')
    COMPREPLY=($(compgen -W "${candidates}" -- "${cur}"))
  else
    candidates=$(
      /bin/ls -1 ${data_dir}/*.gpg | \
      sed -n -e 's:.*/::' -e 's:\.gpg$::p')
    COMPREPLY=($(compgen -W "${candidates}" -- "${cur}"))
  fi
}
これは実際に使ってたスクリプトをベースにしてるので、Bash Advent Calendarの俺ルール「sed, awk を使わない」に反してるし、もうちょっとキレイに、というかもっと Bash らしい書き方ができるところがありますが、もう力尽きたの許してください。あと、公開するとまずいところや改行位置を修正した過程でなんらかのミスがあるかもしれません。というわけで実際に動くかどうかはわかりません。が、雰囲気が伝わればそれでよしということでお願いします。

本来はもっと _get_comp_words_by_ref が生きる例を出す予定だったのですが、とにかくもう、力尽きたので許してください。

追記

スクリプトのフォーマットが崩れてたのを直しました。Bloggerはコードスニペットを書くのには向いてないですねorz

コメントを投稿

インフラエンジニアのスキルチェック

どっかのサイバーメガネさんが ネタ 提供してくれたので。ネタの妥当性には触れません。 DB設計 既にあるのからER図を作成せざるをえない状況に追い込まれたというか、そういう状況だから呼ばれたことはあるけど、実は一からやったことない。 要件からDB定義を作成できる 「...