2021-09-06

シェルのコマンド履歴をインクリメンタルサーチで検索して素早く再利用する方法

この記事では、シェルのコマンド履歴をインクリメンタルサーチで検索して素早く再利用する方法についてステップバイステップで紹介します。

最終的に、次のgifアニメのような操作ができるようになります。 search-command-history-with-incremental-search

はじめに

僕は、これから紹介するコマンド履歴探索のワークフローを3年くらい使っていますが(※ コミットログから推測)、ターミナルに触れる日はほぼ間違いなくこの機能を使っています。もはやこの機能なしでターミナル操作をすることは考えられないほどに手に馴染んでいます。

実際に使うようになって感じることは、開発作業中に行うコマンドのほとんどはこれまでに実行したコマンドの繰り返しです。全く新しいコマンドを実行することは滅多にありません。
例えば、自分はRailsアプリケーションを開発することが多いですが、bin/rails db:migrate とか bin/rails g migration Xxx.. とか bundle exec rspecbundle exec rubocop といったコマンドを何度も実行しています。

故に、コマンド実行履歴から再利用したいコマンドを掘り起こして素早く実行できるようにすることで、作業効率がグンと上がってきます。

想定環境

以下の環境を前提とします。

  • macOS
  • zsh

STEP1 : Historyの設定を整える

コマンドの履歴データ無くしては使えない機能なので、最初にHistoryに関するzshの設定を整備します。

以下の設定を ~/.zshrc に書きます。

# ~/.zshrc

export HISTFILE=$HOME/.zsh_history
export HISTSIZE=100000        # メモリ上の履歴リストに保存されるイベントの最大数
export SAVEHIST=100000        # 履歴ファイルに保存されるイベントの最大数

setopt hist_expire_dups_first # 履歴を切り詰める際に、重複する最も古いイベントから消す
setopt hist_ignore_all_dups   # 履歴が重複した場合に古い履歴を削除する
setopt hist_ignore_dups       # 前回のイベントと重複する場合、履歴に保存しない
setopt hist_save_no_dups      # 履歴ファイルに書き出す際、新しいコマンドと重複する古いコマンドは切り捨てる
setopt share_history          # 全てのセッションで履歴を共有する

環境変数 HISTSIZESAVEHIST の値は大きめの値にしておきます。この値が小さいと履歴ファイルに少ししか保存できないためです。

setopt で有効化しているのはzshのオプションです。ここで特にやりたいことは、重複した履歴データを保存しないようにすることです。重複した履歴データが存在すると、後続のステップで履歴をインクリメンタルサーチするときに同じデータがヒットしてしまい邪魔になるためです。

share_history の設定は、個人の好みで無効化しても良いと思います。

※ 参考: man zshparam , man zshoptions

STEP2 : fzfをインストールする

履歴データをインクリメンタルサーチで絞り込む際に fzf というライブラリを使います。

macOSの場合、Homebrewでインストールできます。以下は fzfのREADME からの抜粋です。

$ brew install fzf

# To install useful key bindings and fuzzy completion:
$ $(brew --prefix)/opt/fzf/install

インストールしたら、PATHが通っていることだけ確認しておきます。

$ which fzf
/usr/local/bin/fzf

STEP3 : コマンド履歴をインクリメンタルサーチで検索するシェルの関数を定義する

履歴データをインクリメンタルサーチで絞り込む関数を実装します。
といっても、fzfのおかげもあり2行で終わるような非常にシンプルな関数です。

# ~/.zshrc

function select-history() {
  BUFFER=$(history -n -r 1 | fzf --exact --reverse --query="$LBUFFER" --prompt="History > ")
  CURSOR=${#BUFFER}
}

短い関数ではあるものの、初見だとよくわからない部分も多いと思うのでかんたんに説明します。

history -n -r 1

  • -n は履歴番号を表示しないオプションです。検索を行うときに行番号はノイズになるため非表示にしています
  • -r は履歴の表示順序を逆にするオプションです。新しい履歴ほど上に表示されます
  • 1 は表示する履歴の範囲として始点だけ指定している状態です。始点が1で終点は未指定のため、全履歴が表示されます

fzf --exact --reverse --query="$LBUFFER" --prompt="History > "

  • --exact は完全一致を有効化するオプションです。fzfはデフォルトでは曖昧な検索となり、それがfzfの特徴の1つでもあるようですが、ここでは有効化しています。個人の好みによって無効化しても良いと思います
  • --reverse は、fzfのUIをターミナル画面の上を基準に表示するオプションです。デフォルトでは画面の下を基準に表示されます
  • --query="$LBUFFER" で、入力した検索クエリをfzfに渡して履歴を絞り込んでいます。LBUFFER 変数には、コマンドプロンプト上でカーソル位置より左側にある文字列が入っています
  • --prompt="History > " はfzfのUIにおけるプロンプトの表示です

BUFFERCURSOR=${#BUFFER}

  • BUFFER 変数に値をセットすると、コマンドライン上の表示が変わります。つまり、fzfで絞り込んだ履歴の1つをコマンドライン上に表示するようにしています
  • CURSOR 変数に値(数値)をセットすると、コマンドライン上でのカーソル位置が変わります。${#BUFFER}BUFFER の文字列サイズを取得しているので、コマンドライン上の最も右側にカーソルを配置することになります

※ 参考 : man zshzle

STEP4 : ZLEのウィジェットとして登録し、Ctrl+r に割り当てる

最後のステップです。STEP3で実装した関数を、コマンドラインから素早く呼び出せるようにします。

# ~/.zshrc

zle -N select-history       # ZLEのウィジェットとして関数を登録
bindkey '^r' select-history # `Ctrl+r` で登録したselect-historyウィジェットを呼び出す

ZLEの詳細は省略しますが、上記の設定により Ctrl + r で履歴探索のUIが立ち上がるようになります。

なお、選択した履歴を即座に実行したい人は次のように関数に1行追加することで実現可能です。

function select-history() {
  BUFFER=$(history -n -r 1 | fzf --exact --reverse --query="$LBUFFER" --prompt="History > ")
  CURSOR=${#BUFFER}
+ zle accept-line
}

※ 参考 : man zshzle

おわりに

日常的に利用しているコマンド履歴探索のワークフローの紹介でした。

完全一致のオプションだったり、履歴をすぐ実行するか否かあたりは個人の好みが分かれるところかなと思うので、使ってみて自分にあったオプションに変えていくのが良いかなと思います。

参考 として各項目の情報源を載せているので、より詳細に知りたい方はそちらをご参照ください(ほとんど zsh の man が情報源です)。

以上、誰かの参考になれば嬉しいです。