日々のターミナル作業で ~/.zsh_history には同じコマンドが何度も積み上がっていく。
peco / Ctrl-Rでの履歴検索がノイズだらけになるし、ファイルも肥大化しやすい。
普段使っているzshrcは、各種dotファイルをまとめてGitで管理しているdotfilesリポジトリの一部だ。
今回はこのzshrcに手を入れて、重複を減らす設定を追加することにした。
要件を先に決める
手を動かす前に、次の仕様を決めた。
- 重複を消すときは最新の1件を残す
- 入力時は連続重複のみ除去する(
hist_ignore_dups) - 定期整理では履歴全体をdedupする
- 実行タイミングはシェル起動時・1日1回
- あわせて
hist_ignore_spaceも有効化する
実行タイミングを終了時フックではなく起動時にしたのは、終了時フックは強制終了でスキップされやすく、確実に効かせたかったからだ。
頻度を1日1回に制限すれば、起動遅延もほぼ気にならないはずだと判断した。
既存の履歴設定を確認する
変更前の状態はこうだった。
HISTFILE=$HOME/.zsh_historyHISTSIZE=100000/SAVEHIST=1000000inc_append_history/share_historyは有効- 重複除外系の
setoptは未設定
複数ターミナルで履歴を共有している前提はそのまま維持する方針にした。
入力時の制御
まず、毎回の入力に効くsetoptを追加した。
setopt hist_ignore_dups
setopt hist_ignore_spacehist_ignore_dups: 直前と同じコマンドは履歴に追加しないhist_ignore_space: 先頭にスペースを付けたコマンドは履歴に残さない(一時的な秘密情報などに便利)
これだけでも日常の連続実行ノイズは減る。ただし、過去に溜まった重複や、時間をおいて再実行した同一コマンドまでは消えない。
全体dedup関数を書く
履歴ファイルを末尾から走査し、同じコマンドは最新だけ残して一時ファイル経由で置換する関数を書いた。
拡張履歴形式(: <time>:<elapsed>;<cmd>)にも対応させている。
zsh-history-dedup() {
local histfile="${HISTFILE:-$HOME/.zsh_history}"
local tmpfile
local saved_histsize
if [[ ! -f "$histfile" ]]; then
print -u2 "zsh-history-dedup: HISTFILE が存在しません: histfile=$histfile"
return 1
fi
tmpfile="$(mktemp "${histfile}.XXXXXX")" || return 1
awk '
function command_key(line, key) {
key = line
if (match(key, /^: [0-9]+:[0-9]+;/)) {
key = substr(key, RLENGTH + 1)
}
return key
}
{ lines[NR] = $0 }
END {
for (i = NR; i >= 1; i--) {
key = command_key(lines[i])
if (!(key in seen)) {
seen[key] = 1
kept[++kept_count] = lines[i]
}
}
for (i = kept_count; i >= 1; i--) print kept[i]
}
' "$histfile" > "$tmpfile" || {
rm -f "$tmpfile"
return 1
}
mv -f "$tmpfile" "$histfile" || {
rm -f "$tmpfile"
return 1
}
# メモリ上の履歴も再読込(終了時の書き戻しで重複が復活しないようにする)
saved_histsize=$HISTSIZE
HISTSIZE=0
HISTSIZE=$saved_histsize
fc -R "$histfile"
}ポイントは次の2つ。
- 一時ファイルへ書き出してから
mvで置換する。途中で失敗しても元ファイルを壊さない - ファイル整理後に
HISTSIZE=0→復元→fc -Rでメモリ上の履歴も同期する
ディスクだけ綺麗にしても、メモリ上に残った古い履歴がシェル終了時に書き戻されると重複が復活してしまう。
ここを見落とすと、dedupしたはずなのに次の日にはまた重複が増えている、ということになりかねない。
1日1回の起動トリガーを実装する
対話シェルのときだけ、スタンプファイルの日付を見て実行するようにした。
if [[ -o interactive ]]; then
typeset zsh_history_dedup_stamp_dir="${XDG_CACHE_HOME:-$HOME/.cache}/zsh"
typeset zsh_history_dedup_stamp_file="${zsh_history_dedup_stamp_dir}/history-dedup.stamp"
typeset zsh_history_dedup_today
zsh_history_dedup_today="$(date +%Y-%m-%d)"
mkdir -p "${zsh_history_dedup_stamp_dir}"
if [[ ! -f "${zsh_history_dedup_stamp_file}" ]] \
|| [[ "$(<"${zsh_history_dedup_stamp_file}")" != "${zsh_history_dedup_today}" ]]; then
if zsh-history-dedup; then
print -r -- "${zsh_history_dedup_today}" >| "${zsh_history_dedup_stamp_file}"
fi
fi
fiスタンプは~/.cache/zsh/history-dedup.stampに置いた。成功した日だけ日付を書き込むようにしているので、途中で失敗した場合は次回起動時にまた再試行される。
手動で整理したいときは、そのまま関数を叩けばいい。
zsh-history-dedup動作確認の目安
設定を入れたあと、次の手順で効いているか確認するつもりだ。
- 新しい対話シェルを開く
- その日初めての起動なら
~/.zsh_historyの行数が減っているか確認する ~/.cache/zsh/history-dedup.stampに今日の日付が入っているか確認する- peco / Ctrl-Rで同一コマンドが散らかっていないか確認する
まとめ
- 入力時は
hist_ignore_dups/hist_ignore_spaceで日常のノイズを抑制する - 起動時1日1回で履歴ファイル全体を「最新優先」に整理する
- ファイル置換後にメモリ上の履歴も再読込し、終了時の書き戻しで重複が復活する事故を防ぐ
履歴検索を快適にしたまま、zshrc側でメンテを自動化できた形になった。