履歴の乱れは心の乱れ 〜ヒストリファイルも掃除しよう〜


心の乱れ、とは

部屋の乱れは心の乱れ」という格言があるらしいです。部屋を綺麗にしておくことで心の平穏を得よう、といったことのようです。

ヒストリファイル(.history, .bash_history, .zsh_history など)も使ううちにごみが溜まってきます。打ち間違いをしたコマンド、山ほどの cdls コマンド、なぜか紛れ込む dc コマンド、などなど。

部屋と違ってこのごみはそのままでも困りませんが、後で話すようにヒストリは重要な道具です。たとえば包丁は、時折手入れをする(研ぐ)ことで切れ味を保てるわけですが、ではヒストリという道具は、どう手入れをするのがいいんでしょうか?

そんなことを書いていきたいと思います。

その前に、ヒストリ検索は使ってますか?

ヒストリは道具です。history コマンドのお世話になっていない人はいないでしょう。過去に入力したコマンドを再利用できるヒストリは、大きく効率をあげてくれます。

ですがヒストリ検索はどうでしょうか。使っていますか?
これを活用してこそ、ヒストリは重要な道具といえると私は思います。

最近のほとんどのシェルでは、標準で Ctrl+R を押すとヒストリを検索できるようになっています。これのおかげで繰り返し作業は楽になり、alias を書くこともなくなり、引数を調べるのに man を見る回数も減ります。使ったことのない人は、ぜひ使ってみてください。

その検索のときに、前にあげたようにごみがヒットすると効率が落ちてしまいます。特に、構文間違いで実行に失敗したものがヒットして、失敗したことを忘れてうっかり再実行したときは、自分のアホさを再認識してつらいです。

そういったことを避けるためにも、ヒストリのメンテナンスはごみを消していくのが基本になります。

0. 消すにあたって

ヒストリから何を消すといいのかは、人それぞれです。ただ「心がときめくものだけ残す」というこんまり流のように、主眼においた方がいいものはあります。

それは、「この先、使えそうなものだけ残す」です。

ヒストリファイルは作業記録として、残しておいた方がいいかもしれませんし、そう求められる場合も多いでしょう。ただそれは別ファイルにコピーしておき、シェルが扱うヒストリファイルはあくまで「作業効率を上げる道具」として使う、と考えると、これは消していいなーと思うのは見つけやすいと思います。

眺めていると「あー、懐かしいなー」と思うものもあるかもしれません。しかしそれは消す消さないの基準にはしない方がいい、と私は思います。

ここから先、私が使っている実例を挙げていきます。あくまで私の例なので、自分がどうヒストリ検索を使っているかを思い描いて、参考にしてもらえれば幸いです。

作業用にコピーする

ヒストリファイルをそのままいじるのは精神衛生上よくないので、テンポラリディレクトリなどにコピーします。

途方にくれるほどヒストリファイルが大きいときは、ソートしてコマンド別にまとめるとやりやすくなります。元の順序に戻すため、先頭に番号を振ってソートします。

pre_proc
cat histfile_origin | \
perl -ne 's/[\s\r\n]+$//; printf("%04d: %s\n", $i++, $_)' | LC_ALL=C sort -u -k 2

元の順序に戻すには、

post_proc
sort histfile_processed | LC_ALL=C sed -E 's/^[0-9]+: //'

こんな感じでしょうか。

1. まず機械的に削る

まず、何も考えずに sed などで一気に消してしまっていいものを消します。

下であげたものは、私自身がかつて何度か検討した結果、全部消してもいいな、と感じたものです。

全員に当てはまるかどうかはわからないので、最初の1回はじっくり眺めて検討してもいいもかしれません。そうすると2回目からの掃除は早くできるようになるでしょう。

1.1. 相対パスを指定したもの

コマンドを実行するときに引数で相対パスのファイルやディレクトリを指定しているものは、コマンドを実行した場所にいないと意味がなく、ディレクトリ名で検索しようとするときに邪魔になるので消します。

しかし、相対パスで実行していても出力結果を pipe で渡しているものは、渡した先のコマンド次第なので、いったん保留します。

例として、以下のものは結果を pipe で渡すことは(ほとんど)ないので消します:

  • cd, vi, less, chmod, source

pipe を使っていない以下のものも消します:

  • ls, cat, find, grep, ps

pipe を使っているものは、あとでざっと眺めながら選んで消すため、いったん保留にします。

1.2. よく使う破壊的コマンド

実行すると対象が存在しなくなるコマンドのうち、よく使うのでオプションもほとんど把握できているものを消します。

例としては以下のようなものです:

  • rm, rmdir, mv, kill, tar

(tar は実行しても消えるわけではないですが、tar x (伸長)の場合、tar ファイルを消すことがほとんどだろう、という関係からです)

1.3. 対象をtab補完できる

上で消したもの以外で、オプションがなく、対象がファイルやディレクトリなどでtab補完できるものを消します。

例としては以下のようなものです:

  • file, type, which

これ以外にも、tab補完を拡張しているものは対象にしていいかもしれません。

1.4. 特定のコマンド

  • man

コマンド名で検索をかけて引っかかると邪魔なので消します。

  • dc, sl

dc は逆ポーランド記法を使った計算をするコマンドですが、その意味で使ったことのある人はいますか?
Qiita - ヘイトが溜まりがちなdcコマンドの活用方法を探る

sl というのは、昔嫌がらせに使われていました。
sl(UNIX) - Wikipedoa

このような、コマンドの有名な typo は機械的に消します。

1.5. 機械的に生成されたもの

たとえば VSCode で Python のスクリプトを書いていると、デバッグ実行をするときにコマンドがそのまま履歴に残るので、これも消します。

2. ちょっとだけ眺めてみる

次は、機械的に削るのは難しいですが、ほとんど考えることなく手動で消せるものです。

2.1. pipe しているもの

機械的に消さなかった pipe で渡しているヒストリについて見ていきます。

単純に grep しているだけとか、less でページ送りしているだけなら、pipe はないものとして消していいと思います。

sort, grep, awk など多段に繋いで最後は sed と、ワンライナーに繋げるてある場合は、もっとじっくり考えるために保留にします。

2.2. 引数が違うだけで同じもの

たとえば docker でコンテナを作ったり消したりしていると、引数に意味の無いコマンドの履歴が重複して残っているでしょう。コンテナIDが違うだけとか。

そういうものは、引数の順序とか、普段使うすべてのオプションのサンプルとか、検索で役に立ちそうな特徴的なものを一つ二つ残して、あとは消してしまいます。

たとえば docker だと run の引数をど忘れすることに備えて一つ残します。この場合、

docker run -it --name d5 -p 8080:80 -v local-path:container-path keioni/httpd-devel:5

みたいな感じのものが1つあれば十分だと思います。

3. 目をこらす

ここから先は、もう完全に人それぞれで、普段どういう作業をしているかによって判断が変わってきます。

私のケースをいくつか挙げてみます。

3.1. 固有名詞はよく検討する

sshcurl, ping, ip などで使っているホスト名、IPアドレス、URIなどで、もう二度と使わないと確実に思うものを消します。コマンドで検索したとき、不要なものがヒットすると Ctrl+R を押す回数が増えますし。

ただ、これは悩みどころです。万が一必要になったとき、ヒストリから消したことを後悔しながら別の資料にあたらないといけなくなります。

3.2. 試行錯誤は結論だけ残す

何かをするためいろいろコマンドを試行錯誤してみた、ということもあると思います。その場合、上手くいったもの、しっくりしたものだけ残します。

sedperl を使ったワンライナーの正規表現で、過不足のない適切なパターンを見つけるのにいろいろ試した場合、などです。

これも悩みがあって、ワンライナーは一度きりしか使わないものも多いですが、何度も使うものも多いのです。ヒストリ検索に頼るのでスクリプト化したり alias に設定することもないので、ワンライナーは積もっていくばかりです。

といって検索して引っかかりすぎるのも困りものです。いい塩梅で判断するしかありません。

3.3. 実行ミスを消す

一番最初に書いたように、構文の間違いで実行に失敗したものがヒットして、うっかり実行して同じミスをしてしまう……というのはかっこ悪く、自分が嫌になること請け合いです。
掃除をする一番の目的は、これかもしれません。

私は AWS CLI を使うことが多いのですが、実行ミスもかなりあります。単純な typo もあれば、記憶違いによるものもあります。

しかし中には、覚えたはずの構文を忘れてしまっていたのを意地になって「成功するまで繰り返してやる!」と、失敗を続けた結果、

aws ec2 describe-instances --query "Reservations[].Instances[].InstanceId.[?Tag==`Name"]
aws ec2 describe-instances --query "Reservations[].Instances[].InstanceId.[?Tag==`Name`]"
aws ec2 describe-instances --query "Reservations[].Instances[].InstanceId[?Tag==`Name`]"
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId[?Tag==`Name`]'
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId[?Tags[Key]==`Name`]'
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId[?Tags["Key"]==`Name`]'
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId[Tags[?"Key"==`Name`]'
aws ec2 describe-instances --query 'Reservations[].Instances[].[Tags[?"Key"==`Name`]'
aws ec2 describe-instances --query 'Reservations[].Instances[].[Tags[?"Key"==`Name`].InstanceId'
aws ec2 describe-instances --query 'Reservations[].Instances[].[Tags[?"Key"==`Name`]].InstanceId'

こんな感じで出てくるものもあるわけです。頭痛がする。
これだけあると、どれが正しく動作するものか分かりません1

失敗したのがどれか分かるなら、サクッと消します。またまた忘れてしまった場合は、破壊的ではないコマンドなら全部試すしか……!

さいごに

ヒストリ検索は強力な武器です。
それを活かせるかどうかは、ヒストリの中身が使えるものかどうかにかかっています。

最初は多めに残しておくといいと思います。そのうち、なんだか Ctrl+R を押す回数が多いな、と感じることがあれば、それはよく手入れをした方がいい、といったことが分かってくると思います。

ヒストリファイル Tips

ヒストリをより活用するための Tips です。あとで付け足すかも。

環境を壊すときは必ずヒストリファイルを拾う

ヒストリファイルは、自分の経験の「目に見える蓄積」です。
最近はクラウドやコンテナなどで環境を使い捨てにすることも少なくありませんが、環境を壊すときには必ずバックアップをとることをお勧めします。

すぐ使うことはなくても、また同じ作業をすることがあるかもしれません。そのときには、バックアップした中身を今使っているヒストリファイルの先頭に加えるだけで作業効率は格段にあがるはずです。

せっかくの経験の記録なのです。活かさないのはもったいないですよね。

バックアップをとったものは、掃除のときなどに必要なものを取り出してメインのヒストリファイルに付け足すのもありだと思います。

ヒストリファイルは自由に書き換えできる

シェルが実行結果を記録するもので人間が書き足するものではない、となんとなく感じている人は多いかもしれません。ですが書き足すのも書き換えるのも自由です。
先にあげましたが、他の環境から取ってきたものを加えるのはもちろん、検索してコマンドラインに出てくると便利そうだと思うものを手で書き込むのも自由です。

それほど書き換えるシーンはないのですが、書き換えてもいいということは覚えておくと役に立つ……かもしれません。

コマンドを実行すれば自動で書き足されるので、加えたいものは実行すればいいだけですが、そうするとと検索で最初にひっかかってしまいます。優先度を低くしたいときはヒストリファイルの先頭に加えると上手く、とかですね。


  1. いやこれだけ繰り返したんだから記録じゃなくて記憶に残せよ、という話もあるとかないとか……