SyntaxHighlighter

2022年2月7日月曜日

Bash 向けの tcsh 同様な history-search

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 に置いてあります。

おおまかな流れとしては、以下のようになっています。

  1. fc コマンドでコマンド履歴の一覧を取ってくる。
  2. 一行ずつ read コマンドで読み込む。
  3. 特定の文字列で始まるコマンドを抜き出し、それを READLINE_LINE, READLINE_POINT に設定することで、ターミナルの文字列を置換する。

coproc の使い方

fc コマンドを実行し、その結果を read コマンドで処理する際に、 coproc コマンドを使うことにしました。
Bash の while でパイプから読み込んだテキストを一行ずつ処理をする場合、 Qiita の記事にあるように、 Process Substitution などを使えばよいようですが、 Windows 版の Git に含まれている Bash などでは実行することができません。
そのため、今回はこの方法は諦め、 coproc を使って読み込むことにしました。

概要

coproc に関連する部分をシンプルにして抜き出すと以下のようになっています。

coprocfc を実行するプロセスを作り、その出力を whileread で一行ずつ処理しています。

ちなみに、よく言われるように下記では while 内がサブシェルで実行されるので、 HISTORY_ARRAY に期待した通りに値が保存されません。

また、上述したように、下記のように Process Substitution を使う方法では、私の手元の Windows の bash.exe では動作しませんでした。

ジョブ情報の出力の抑制

そのまま使ってしまうと、[1] 1859[1]+ Done のような、ジョブの情報が出力されてしまうので、それらを抑制する必要がありました。

開始時は以下のようにします。
coproc の標準エラー出力を捨てればよいのですが、波括弧で囲ってリダイレクトすると指定しやすいです。

終了時の出力は、 coproc 呼び出し後に以下の 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 のプロセスの出力をバッファリングせずに出力しきるために、以下のようにします。

このように、 coproc の中で read -s で入力を待ち、 exec 1>&- で出力のファイルディスクリプタを閉じることで出力を確実に処理させ、 coproc での出力をすべて処理できるようにします。

というような感じで、 Bash の組み込みコマンドのみで tcsh のような history-search-backward を実装することができました。
私はこれまでにほとんどシェルスクリプトを書く機会がなかったのですが、 Bash 4 以降は coproc や連想配列もあり、それなりにいろいろなことができるようです。