Zsh のカスタマイズは日進月歩 − 失敗したコマンドを履歴に残さない
日々、かなりの頻度でシェルのコマンド履歴を検索します(よね?)。長かったりよく覚えていなかったりするコマンド(複数のコマンドをパイプでつないだり、コマンドに様々なオプションや引数を渡したり…)を、履歴の検索なしに再度実行するのはとてもつらいです。
ということでそんな便利なコマンド履歴の検索ですが、気になることもあるにはあります。その一つが、タイプミスをしたりオプションを間違えたりして失敗したコマンドを、うっかりまた履歴から検索して実行してしまうことです。これは失敗したコマンドを履歴から消去しない限り起こり得る問題なので、初めから失敗したコマンドは履歴に残さないように設定してみました。なお、タイトルにあるとおり対象のシェルは zsh になります。
こちら を参考にしました。zsh の フック関数(9.3.1 Hook Functions) という仕組みを使います。下記のような設定を .zshrc
などに記入すれば良いはずです。
__record_command() { typeset -g _LASTCMD=${1%%$'\n'} return 1 } zshaddhistory_functions+=(__record_command) __update_history() { local last_status="$?" # hist_ignore_space if [[ ! -n ${_LASTCMD%% *} ]]; then return fi # hist_reduce_blanks local cmd_reduce_blanks=$(echo ${_LASTCMD} | tr -s ' ') # Record the commands that have succeeded if [[ ${last_status} == 0 ]]; then print -sr -- "${cmd_reduce_blanks}" fi } precmd_functions+=(__update_history)
コマンド履歴の保存に関するフック関数 zshaddhistory
は、ドキュメントによるとコマンドの実行直前に呼ばれるようです。通常はこのタイミングで保存されるのですが、今回は実行結果の成否を見てから保存するかを決めたいのでここでは保存しません。そこで __record_command()
という関数内でコマンドをグローバルな変数 _LASTCMD
に保存しておき 1
を返します(0
を返せば通常どおり HISTFILE
に保存されます。2
を返すと履歴ファイルには保存されませんが、Ctrl-P/N
などでさかのぼれる内部ヒストリには保存されます)。この関数を zshaddhistory_functions
に追加します。
続いて実際に履歴を保存する関数が __update_history
になります。postcmd
というフック関数があるのかと期待したところ、ないのですが、どうやら precmd
関数がコマンドの実行直後に呼ばれるようなので(呼ばれるタイミングについては この記事 が詳しいです)、こちらを使います。まずはじめに、実行したコマンドの終了ステータスを last_status
変数に保存します。なお、自前でヒストリファイルを更新するため zsh の履歴保存に関するオプション相当の機能はここで実装する必要がある気がします。ということで、スペースで始まる行(やスペースだけの行)は保存しないようにチェックを行ない(hist_ignore_space
相当)、複数のスペースは tr
で一つにまとめます(hist_reduce_blanks
相当)。最後に、終了ステータスが 0
の場合は履歴に print
コマンドで保存します(参考:こちら)。この関数を precmd_functions
に追加します。
これでおおよそ所望の動作にはなったはずです。*1 ストレスが減って良いですね!
*1:直前の終了ステータスを見ているため失敗するコマンドと成功するコマンドを ; でつなげた場合などには記録されてしまいます。コマンドをつなげる場合には && を使うなどしましょう。
必要なときにはいつも忘れてしまっている (Neo)vim のコマンドたち(その1)
diffoff
差分モードを終了する。例えば :diffsplit
などして差分を見つつ作業をし、用が済んだ後に一方のファイルを閉じる。ところが sign(特定の行を強調するために、例えば +
、-
、>
などの印を表示する、左端に表示される一列)が残ってしまい、はてどうやって元に戻すんだっけかな?というときに :diffoff
する。
windo
あとに続くコマンドを(現タブ内の)全てのウィンドウに対して実行する。例えばスペースをタブに置換する場合は :windo %s/ /\t/ge
とする(最後の e
はパターンが見つからない場合にエラーの発生を抑制するオプション)。
g/pattern/d
ファイル内で pattern を含ファイル内で pattern を含む行を削除する。上記 windo
に書いたような置換のコマンドは忘れないのだけれど、この行の削除についてはどうも忘れてしまう。ちなみにこの g
コマンドは ここ にあるように色々と強力らしい。
fzf.vim による絞り込み対象からファイル名を除外する
先日の記事『重い腰を上げて Neovim ことはじめ ー ripgrep (rg) と fzf でソースコード検索編』で設定した :Rg
コマンドはとても便利ですが、使っていて不満な点が一つありました。それは、fzf.vim
で絞り込みを行うときにファイルの内容だけでなくファイル名自体も検索対象になってしまうことです。
例えばディレクトリ内のソースコードから foo()
という関数を検索するため :Rg
コマンドで fzf.vim
のインターフェースを立ち上げて foo
という文字列でマッチングを行うとします。この際、ディレクトリ内に foo.c
というファイルが存在した場合、foo.c
の全ての行がその内容に関わらずマッチング結果として表示されてしまいます。
これは rg
の出力がファイル名を含んでおり、fzf
がマッチングを行う際にファイル名なのかファイルの中身なのかを区別しないことに起因しています。少し調べたところ、同じことで悩んでいる人がおり、解決方法も提示されていました。
解決方法は fzf
に --delimiter : --nth 3..
というオプションを渡すことです。これは、:
をデリミタとして 3 つめのフィールド以降のみをマッチング対象にするというものです。Rg:
コマンドで設定している rg
の出力フォーマットは ファイル名:行番号:行の中身
となっているため、この設定によってファイル名は絞り込み対象から除外されます。
ということで、該当する箇所の設定はこうなります。
command! -bang -nargs=* Rg \ call fzf#vim#grep( \ 'rg --line-number --no-heading '.shellescape(<q-args>), 0, \ fzf#vim#with_preview({'options': '--exact --reverse --delimiter : --nth 3..'}, 'right:50%:wrap'))