まんがでわかるLinux シス管系女子の74ページで、仮想端末(本編中ではtmux)の使用時に複数の画面間でコマンド履歴が共有されない問題の解決策として、~/.bashrc
にこういう記述を追加すると良いという話を書きました。
function share_history {
# 最後に実行したコマンドを履歴ファイルに追記
history -a
# メモリ上のコマンド履歴を消去
history -c
# 履歴ファイルからメモリへコマンド履歴を読み込む
history -r
}
# 上記の一連の処理を、プロンプト表示前に(=何かコマンドを実行することに)実行する
PROMPT_COMMAND='share_history'
# bashのプロセスを終了する時に、メモリ上の履歴を履歴ファイルに追記する、という動作を停止する
# (history -aによって代替されるため)
shopt -u histappend
ところが、これを使用しているとコマンド履歴に同じ項目がどんどん溜まっていくという問題が起こるようになります。自分の場合だと、git commit -p
してからgit push
するとか、その前にgit pull
するとかいう操作が多く、コミットの粒度も細かくするようにしているので、コマンド履歴があっという間にこれらのコマンド列で埋まってしまいます。
Ubuntuの場合だと~/.bashrc
には最初からHISTCONTROL=ignoreboth
という記述がありますが、この指定は「直前のコマンド列と同じコマンド列の実行時にはコマンド履歴を残さない」という物です1。似た指定でHISTCONTROL=erasedups
という指定もあり、こちらは直前のコマンド列に限らずコマンド履歴全体の中で重複を排除する物です。しかしどちらを指定しても、上記の設定と組み合わせると期待通りの結果を得られません。何故なのでしょうか。
これは、HISTCONTROL
の指定が一体何に対して作用するのかという事を考えると分かります。GNUのbashのバージョン4.4.18のソースコードを見てみると、ignoreboth
やerasedups
はどちらもメモリ上のコマンド履歴に対して、新しい項目を追加する時にメンテナンスを実行するようになっています。しかし、上記の設定を使用している場合、せっかくそのようにメンテナンスしたコマンド履歴はhistory -c
で消去されてしまって、その後、重複を含んだままの履歴ファイルからhistory -r
で履歴を読み込み直しています。これではignoreboth
もerasedups
も全く効果を得られなくて当たり前です。
ではどうすればよいかという話なのですが、とりあえずの解決策としてはこういう方法があります。
function share_history {
history -a
tac ~/.bash_history | awk '!a[$0]++' | tac > ~/.bash_history.tmp
[ -f ~/.bash_history.tmp ] &&
mv ~/.bash_history{.tmp,} &&
history -c &&
history -r
}
PROMPT_COMMAND='share_history'
shopt -u histappend
tac
コマンドというのは、cat
の逆で最後の行から最初の行に向かってファイルの内容を出力するコマンドです。これを使ってファイルを逆順出力してから、重複行の最初の1行目を残して残りを削除して、それをまた逆順にして出力し直した後、出力結果で~/.bash_history
を置き換えています(1行の中でそのままリダイレクトでファイルを置換しようとすると元ファイルが消えてしまうので要注意!)。[ -f ~/.bash_history.tmp ]
で一時ファイルがちゃんと作成されていることを確認して(何らかのエラーで一時ファイルが作成されなかった場合、そのまま続行するとコマンド履歴が消失して終わりになってしまいます!)、history -c
でメモリ上のコマンド履歴を消去してhistory -r
で読み込み直すようにするわけです。
これにより、同じコマンド列を何度も実行しても最新の1つだけが履歴に残るようになります。やりましたね!
もっとスマートなやり方もあるんだろうとは思いますが2、自分がパッと思いつく解決策はこういう物でした、ということで主にシス管系女子読者の方向けの3年越しのフォローアップ記事として公開しておきます。
余談:tac
の代用
tac
はGNU coreutilsのコマンドの一つなのですが、macOSなどのBSD系の環境だとコマンドが存在しません。そのためtail -r
で代替する必要があります。
WindowsでのFirefoxのビルドに使用するMozillaBuildだと、tac
もtail -r
もどちらも使えません。このような環境では、sed
での代替実装を使って以下のようにtac
コマンドの代わりの関数を定義しておくとよいでしょう。
function tac {
exec sed '1!G;h;$!d' ${@+"$@"}
}