Bash
shell
シス管系女子

tmuxとかの仮想端末で複数の画面間でBashのコマンド履歴を共有すると同じ履歴が何度も記録されてしまう問題を解決する

この記事は自サイトとのクロスポストです

まんがでわかる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のソースコードを見てみると、ignorebotherasedupsはどちらもメモリ上のコマンド履歴に対して、新しい項目を追加する時にメンテナンスを実行するようになっています。しかし、上記の設定を使用している場合、せっかくそのようにメンテナンスしたコマンド履歴はhistory -cで消去されてしまって、その後、重複を含んだままの履歴ファイルからhistory -rで履歴を読み込み直しています。これではignorebotherasedupsも全く効果を得られなくて当たり前です。

ではどうすればよいかという話なのですが、とりあえずの解決策としてはこういう方法があります。

function share_history {
  history -a
  # ここから追記
  tac ~/.bash_history | awk '!a[$0]++' | tac > ~/.bash_history.tmp
  mv ~/.bash_history{.tmp,}
  # ここまで追記
  history -c
  history -r
}
PROMPT_COMMAND='share_history'
shopt -u histappend

tacコマンドというのは、catの逆で最後の行から最初の行に向かってファイルの内容を出力するコマンドです。これを使ってファイルを逆順出力してから、重複行の最初の1行目を残して残りを削除して、それをまた逆順にして出力し直した後、出力結果で~/.bash_historyを置き換えています(1行の中でそのままリダイレクトでファイルを置換しようとすると元ファイルが消えてしまうので要注意!)。その後で、history -cでメモリ上のコマンド履歴を消去してhistory -rで読み込み直すようにするわけです。これにより、同じコマンド列を何度も実行しても最新の1つだけが履歴に残るようになります。やりましたね!

もっとスマートなやり方もあるんだろうとは思いますが2、自分がパッと思いつく解決策はこういう物でした、ということで主にシス管系女子読者の方向けの3年越しのフォローアップ記事として公開しておきます。

余談:tacの代用

tacはGNU coreutilsのコマンドの一つなのですが、macOSなどのBSD系の環境だとコマンドが存在しません。そのためtail -rで代替する必要があります

WindowsでのFirefoxのビルドに使用するMozillaBuildだと、tactail -rもどちらも使えません。このような環境では、sedでの代替実装を使って以下のようにtacコマンドの代わりの関数を定義しておくとよいでしょう。

function tac {
  exec sed '1!G;h;$!d' ${@+"$@"}
}

  1. 正確には、これはHISTCONTROL=ignorespace:ignoredupsの省略形で、重複を記録しないという動作はignoredupsの効果です。 

  2. 例えば、BashではなくZshを使えば最初からこういう挙動になっているそうです。