Bash 向けの tcsh 同様な history-search
Bash のシェルスクリプトの勉強がてら、 tcsh 同様な history-search-backward
のようなものを実装してみました。
Bash の history-search-backward
Bash には、元々 history-search-backward
というコマンドが用意されています。
しかし、このコマンドは tcsh のコマンドとは異なり、カーソル位置が行の末尾に移動しません。
私は Solaris の tcsh で Unix 系の環境に初めて触れたこともあり、 tcsh のようにカーソルが末尾に移動するという挙動の方が馴染みがあるので、そのようなコマンドを作ってみようと思いました。
なお、あくまで私が Bash のシェルスクリプトの練習をするというのも重要な目的の一つなので、例えば zsh をなど他の解は、今回は気にしていません。
ポイント
今回は、 Bash のシェルスクリプトの勉強を兼ねていたので、以下を守ることにしました。
- Bash 以外での動作は気にしない。
- Bash の組み込みコマンド以外は使わない。
- Windows 版の Git に含まれている Bash でも動作させる。
コードの概要
コードは GitHub に置いてあります。
おおまかな流れとしては、以下のようになっています。
fc
コマンドでコマンド履歴の一覧を取ってくる。- 一行ずつ
read
コマンドで読み込む。 - 特定の文字列で始まるコマンドを抜き出し、それを
READLINE_LINE
,READLINE_POINT
に設定することで、ターミナルの文字列を置換する。
coproc
の使い方
fc
コマンドを実行し、その結果を read
コマンドで処理する際に、 coproc
コマンドを使うことにしました。
Bash の while
でパイプから読み込んだテキストを一行ずつ処理をする場合、 Qiita の記事にあるように、 Process Substitution などを使えばよいようですが、 Windows 版の Git に含まれている Bash などでは実行することができません。
そのため、今回はこの方法は諦め、 coproc
を使って読み込むことにしました。
概要
coproc
に関連する部分をシンプルにして抜き出すと以下のようになっています。
1 2 3 4 5 6 7 | { coproc FC_FD { fc -lnr ; exec 1>&- ; read -s ; } ; } 2> /dev/null disown declare -a HISTORY_ARRAY=() while read -r -u ${FC_FD[0]} LINE; do HISTORY_ARRAY+=( "$LINE" ) done echo END >&${FC_FD[1]} |
coproc
で fc
を実行するプロセスを作り、その出力を while
と read
で一行ずつ処理しています。
ちなみに、よく言われるように下記では while
内がサブシェルで実行されるので、 HISTORY_ARRAY
に期待した通りに値が保存されません。
1 2 3 4 5 6 7 | # 下記ではうまく動作しない declare -a HISTORY_ARRAY=() fc -lnr | while read -r LINE; do HISTORY_ARRAY+=( "$LINE" ) done echo ${HISTORY_ARRAY[@]} # => 何も保存されていない。 |
また、上述したように、下記のように Process Substitution を使う方法では、私の手元の Windows の bash.exe では動作しませんでした。
1 2 3 4 5 6 7 | # 下記では Windows の bash.exe では動作しなかった。 declare -a HISTORY_ARRAY=() while read -r LINE; do HISTORY_ARRAY+=( "$LINE" ) done < <(fc -lnr) # 以下が表示 # bash: /dev/fd/XX: No such file or directory |
ジョブ情報の出力の抑制
そのまま使ってしまうと、[1] 1859
や [1]+ Done
のような、ジョブの情報が出力されてしまうので、それらを抑制する必要がありました。
開始時は以下のようにします。coproc
の標準エラー出力を捨てればよいのですが、波括弧で囲ってリダイレクトすると指定しやすいです。
1 | { coproc FC_FD { fc -lnr -${HISTSIZE} ; } ; } 2> /dev/null |
終了時の出力は、 coproc
呼び出し後に以下の disown
を呼んでおくことで、抑制することができます。
1 2 | { coproc FC_FD { fc -lnr -${HISTSIZE} ; } ; } 2> /dev/null disown |
coproc
からの読み込み
coproc
で返されるファイルディスクリプタから read
コマンドでテキストを読み込むには、 -u
オプションを使うことで実現できます。
この辺りは Web 上で情報がたくさん見つかるので省略します。
一方で、ただ coproc FC_FD { fc -lnr -${HISTSIZE} ; }
として実行した coproc を read -u ${FC_FD[0]}
のように読むだけでは、場合によってはうまくいきません。
Stack Overflow に書かれているように、 Bash の coproc では、 coproc で実行したプロセスが先に完了してしまうと、プロセスからの出力をすべて処理する前に出力が読めなくなります。
そのため、 coproc のプロセスの出力を読み終わるまで、プロセスが終了しないように工夫する必要があります。
また、 coproc のプロセスの出力をバッファリングせずに出力しきるために、以下のようにします。
1 2 3 4 | { coproc FC_FD { fc -lnr -${HISTSIZE} ; exec 1>&- ; read -s ; } ; } 2> /dev/null disown # ここで fc の出力の処理を行う echo END >&${FC_FD[1]} # read -s に文字列を送る |
このように、 coproc の中で read -s
で入力を待ち、 exec 1>&-
で出力のファイルディスクリプタを閉じることで出力を確実に処理させ、 coproc での出力をすべて処理できるようにします。
というような感じで、 Bash の組み込みコマンドのみで tcsh のような history-search-backward
を実装することができました。
私はこれまでにほとんどシェルスクリプトを書く機会がなかったのですが、 Bash 4 以降は coproc
や連想配列もあり、それなりにいろいろなことができるようです。