bashに自作のホスト名補完機能を追加してsshを楽にする


# はじめに

bash-completionをインストールすることで、sshやpingなどのコマンドで、タブキーによるホスト名補完が可能になります。
これを『自作のホスト名補完に **置き換える**』例は様々ありますが、『自作のホスト名補完を **追加する** 』手順の例がなかなか見つからないので、私の実装例をTipsとしてまとめました。

以下は、bash-completionのversion 1.3を検証環境として記載しています。
また、私自身bash-completion設定を初めて書いたので、お作法を無視しているかもしれません。ご承知ください。
(2016/01/19追記)
本手順に従うと、補完機能はシステム共通設定として設定されます。個人設定を作成する際には別の手順を参照してください。

## bash-completion導入方法

以下が参考になります。
ぶっちゃけ、`yum install bash-completion`のようなことをすればOKです。

- [Bash-Completion で SSH のホスト名を自動補完](http://qiita.com/kotarella1110/items/34ba059d076f4148bebc)
- [sshのhost名を自動補完させる](http://qiita.com/soramugi/items/846e6eac0ce1d3dc1e42)

## 自作ホスト名補完機能の実装例
自作ホスト名補完機能の実装については、Qiita内に幾つか良記事があります。

- [bash-completionを使用したsshのホスト名を自動補完](http://qiita.com/takemotto/items/c83807bacc72cf801225)
- [consul membersの結果でbash補完する](http://qiita.com/sfujiwara/items/f4fa907ead53ed104e1a)

これらの実装例の欠点については、以下で述べていきます。


# ホスト名補完はどう実現しているのか
実装例を説明する前に、bash-completionのホスト名補完機能を解読します。

## ホスト名一覧を返す関数
Linux系では`/etc/bash_completion`ファイルにて、
Macのbrewインストールでは`/usr/local/etc/bash_completion`ファイルにて、
以下の2つの関数が実装されています。
※ 本文以下では、Macのパスについては省略します。/usr/localのプレフィクスが必要と考えてください。

* `_known_hosts()`
* `_known_hosts_real()`

前者はdeprecatedな関数であり`_known_hosts_real()`を呼び出しているだけです。後者の`_known_hosts_real()`がホスト名補完の実装となっています。

この`_known_hosts_real()`関数の中では以下のファイルやコマンド(詳細は割愛)を参照して「known_hosts」を抽出しています。

* `ssh_config`ファイル
* `ssh_known_hosts`ファイル
* `avihi-browse`コマンド

このように生成されたknown_hostsは、ホスト名やIPアドレスを引数に必要とするコマンドに渡されて利用されます。

```bash:known_hostsの利用
complete -F _known_hosts traceroute traceroute6 tracepath tracepath6 ping \
    ping6 fping fping6 telnet host nslookup rsh rlogin ftp dig mtr \
    ssh-installkeys showmount
```

したがって、 **`_known_hosts_real()`を拡張すればホスト名補完に自作機能を追加できる** ということになります。

## 自作拡張機能の実装の問題
さて、問題となるのは、`_known_hosts_real()`の実装場所です。

上記したとおり、この関数は`/etc/bash_completion`ファイルに存在しています。
このファイルは、 **bash_completionの親ファイル** です。
つまり、yumやbrewで入れた場合、管理対象になっているファイルなわけです。

**`/etc/bash_completion`を改造してしまうと、アップデートが不便になってしまいます。**

ところで、bash_completionでは`/etc/bash_completion.d/`フォルダに設定ファイルを追加することで、ファイル単位で機能を読みこませることが可能です。
**機能を拡張するなら、このフォルダに設定ファイルを配置する方が望ましいですね。**

ではどうするか。


# ホスト名補完を拡張しよう

長々と書いた前節の通り、bash-completionのホスト名補完を拡張するためには、`_known_hosts_real()`関数を拡張する必要があります。とはいえ、`/etc/bash_completion`ファイルに変更を加えたくはありません。

まとめると、要件はこうです。

* `/etc/bash_completion`ファイルに変更を加えない
* `/etc/bash_completion.d/`以下にファイルを配置することでロードする
* 既存の`_known_hosts_real()`を潰さない


## 実装例
### コード
何はともあれコードから紹介します。
例としてファイルの名前は`my_known_hosts`として、以下のコードを`/etc/bash_completion.d/my_known_hosts`に配置します。
※ `自作コマンド`部分は任意のコマンド（後述）に置き換えましょう。

```bash:/etc/bash_completion.d/ex_known_hosts
have 自作コマンド &&
{

_save_function() {
    local ORIG_FUNC=$(declare -f $1)
    local NEWNAME_FUNC="$2${ORIG_FUNC#$1}"
    eval "$NEWNAME_FUNC"
}

_save_function _known_hosts_real _org_known_hosts_real
_known_hosts_real()
{
    _org_known_hosts_real "$@"

    local __cachedir __cachefile __expired
    __cachedir=${HOME}
    __cachefile=.knownhosts.cache
    __expired=$(find ${__cachedir} -name ${__cachefile} -maxdepth 1 -mmin -1 2>/dev/null)
    if [ ${#__expired} -le 0 ]; then
        COMPREPLY=( "${COMPREPLY[@]}" $( \
            compgen -W "$(自作コマンド 2>/dev/null | tee ${__cachedir}/${__cachefile} 2>/dev/null)" \
                ${COMP_WORDS[COMP_CWORD]} \
        ))
    else
        COMPREPLY=( "${COMPREPLY[@]}" $( \
            compgen -W "$(cat ${__cachedir}/${__cachefile} 2>/dev/null)" \
                ${COMP_WORDS[COMP_CWORD]} \
        ))
    fi
    return 0
}

}
```

### 解説(1): 自作コマンド
コード中の`自作コマンド`は、任意のスクリプトを実行することを想定しています。
この`自作コマンド`こそが、ホスト名補完機能の核です。

この`自作コマンド`は「ホスト名orIPアドレス一覧を出力すること」が必須要件です。
例えばこんな感じです。（役に立たない例だ・・・）

```bash:自作コマンド
#!/bin/bash -
grep '^[0-9]' /etc/hosts | awk '{print $2}'
```

この自作コマンドはこんな出力を得ます。

```自作コマンドの実行例
$ ./自作コマンド
localhost
host_A
host_B
```

出力する内容はconsulのメンバー一覧でも、DNSのホスト名一覧でも、ZabbixやMackerelのホスト名一覧でもいいでしょう。

### 解説(2): _save_function()
これが`_known_hosts_real()`に機能拡張するためのトリックです。
この関数は、既存の関数をリネームする事ができます。
なお、この実装は[mivok.netのBash function renaming and overriding](http://mivok.net/2009/09/20/bashfunctionoverrist.html)を真似たものです。

`_save_function()`関数により、以下のコマンドでオリジナルの`_known_hosts_real()`関数は`_org_known_hosts_real()`関数にリネームされます。

```bash:_save_function利用例
_save_function _known_hosts_real _org_known_hosts_real
```

リネームしてあれば、自作の`_known_hosts_real()`の中から明示的に呼び出すことができます。
**元の関数の実装を汚すことなく機能拡張できる** ようになるわけです。

```bash:_known_hosts_real()関数
_known_hosts_real()
{
    _org_known_hosts_real "$@"

    好きな実装をする
}
```

なお、上記の`_org_known_hosts_real "$@"`の実行結果は、`COMPREPLY`配列に格納されています。
そのため、後半の`COMPREPLY=`部分では、`COMPREPLY`配列を上書きしないように、データを追加するようなコードとしています。

### 解説(3): キャッシュ関連
if〜elseの直前で色々変なことをしている部分がありますが、それはキャッシュ関連の処理です。
この実装例は **自作コマンドの応答が遅いことを想定** しており、bash_completion内でキャッシュすることで高速化を図っています。 

キャッシュの破棄はこのfind文で判定しています。

```bash:キャッシュ破棄の判定
__expired=$(find ${__cachedir} -name ${__cachefile} -maxdepth 1 -mmin -1 2>/dev/null)
```

この例ではキャッシュ時間は1分間です。
なんでfindなんだろうと思うかもしれませんが、 **LinuxとMacでコマンドオプションが違うため** 同じオプションで使えるfindを採用したという経緯です。

if〜elseにおいて、キャッシュを再生成する際は`tee`と組み合わせた以下の実装へ。

```bash:キャッシュ再生成
        COMPREPLY=( "${COMPREPLY[@]}" $( \
            compgen -W "$(自作コマンド 2>/dev/null | tee ${__cachedir}/${__cachefile} 2>/dev/null)" \
                ${COMP_WORDS[COMP_CWORD]} \
        ))
```

キャッシュを利用する場合は`cat`文の実装へ進みます。

```bash:キャッシュを利用するとき
        COMPREPLY=( "${COMPREPLY[@]}" $( \
            compgen -W "$(cat ${__cachedir}/${__cachefile} 2>/dev/null)" \
                ${COMP_WORDS[COMP_CWORD]} \
        ))
```

我ながらDRY的にカッコ悪い実装だなと思いますが・・・

# まとめ
オリジナルのbash-completionを汚さずにホスト名補完機能を拡張する実装例を紹介しました。ついでに、キャッシュの実装例も紹介しました。

実装例を利用する場合は、`自作コマンド`部分を用意・置き換えれば応用が効くことと思います。キャッシュ時間も環境やコマンドに応じて変更してください。