Edited at

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

More than 3 years have passed since last update.


はじめに

bash-completionをインストールすることで、sshやpingなどのコマンドで、タブキーによるホスト名補完が可能になります。

これを『自作のホスト名補完に 置き換える』例は様々ありますが、『自作のホスト名補完を 追加する 』手順の例がなかなか見つからないので、私の実装例をTipsとしてまとめました。

以下は、bash-completionのversion 1.3を検証環境として記載しています。

また、私自身bash-completion設定を初めて書いたので、お作法を無視しているかもしれません。ご承知ください。

(2016/01/19追記)

本手順に従うと、補完機能はシステム共通設定として設定されます。個人設定を作成する際には別の手順を参照してください。


bash-completion導入方法

以下が参考になります。

ぶっちゃけ、yum install bash-completionのようなことをすればOKです。


自作ホスト名補完機能の実装例

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

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


ホスト名補完はどう実現しているのか

実装例を説明する前に、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アドレスを引数に必要とするコマンドに渡されて利用されます。


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に配置します。

自作コマンド部分は任意のコマンド(後述)に置き換えましょう。


/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アドレス一覧を出力すること」が必須要件です。

例えばこんな感じです。(役に立たない例だ・・・)


自作コマンド

#!/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を真似たものです。

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


_save_function利用例

_save_function _known_hosts_real _org_known_hosts_real


リネームしてあれば、自作の_known_hosts_real()の中から明示的に呼び出すことができます。

元の関数の実装を汚すことなく機能拡張できる ようになるわけです。


_known_hosts_real()関数

_known_hosts_real()

{
_org_known_hosts_real "$@"

好きな実装をする
}


なお、上記の_org_known_hosts_real "$@"の実行結果は、COMPREPLY配列に格納されています。

そのため、後半のCOMPREPLY=部分では、COMPREPLY配列を上書きしないように、データを追加するようなコードとしています。


解説(3): キャッシュ関連

if〜elseの直前で色々変なことをしている部分がありますが、それはキャッシュ関連の処理です。

この実装例は 自作コマンドの応答が遅いことを想定 しており、bash_completion内でキャッシュすることで高速化を図っています。

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


キャッシュ破棄の判定

__expired=$(find ${__cachedir} -name ${__cachefile} -maxdepth 1 -mmin -1 2>/dev/null)


この例ではキャッシュ時間は1分間です。

なんでfindなんだろうと思うかもしれませんが、 LinuxとMacでコマンドオプションが違うため 同じオプションで使えるfindを採用したという経緯です。

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


キャッシュ再生成

        COMPREPLY=( "${COMPREPLY[@]}" $( \

compgen -W "$(自作コマンド 2>/dev/null | tee ${__cachedir}/${__cachefile} 2>/dev/null)" \
${COMP_WORDS[COMP_CWORD]} \
))

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


キャッシュを利用するとき

        COMPREPLY=( "${COMPREPLY[@]}" $( \

compgen -W "$(cat ${__cachedir}/${__cachefile} 2>/dev/null)" \
${COMP_WORDS[COMP_CWORD]} \
))

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


まとめ

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

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