I don't think any zsh option is going to save your precious .zsh_history
.
My .zsh_history
has been randomly truncated over the years, and I still don't know why. I've tried every option I could find on StackExchange, and obviously tried the config from oh-my-zsh.
Automated backups
In order to not care next time my history gets truncated, I added this line to crontab -e
:
30 14 * * * cp /home/my_user/.zsh_history /backup/folder/zsh_history_$(date +\%Y_\%m_\%d).bak
Feel free to use anacron
, rsync
or any other tool. The goal is to have a collection of .zsh_history
files somewhere safe, with at least some of them containing the desired information.
Restore history
When you need to restore one complete .zsh_history
from your possibly truncated backups, you can use this command:
cat zsh_history*.bak | awk -v date="WILL_NOT_APPEAR$(date +"%s")" '{if (sub(/\\$/,date)) printf "%s", $0; else print $0}' | LC_ALL=C sort -u | awk -v date="WILL_NOT_APPEAR$(date +"%s")" '{gsub('date',"\\\n"); print $0}' > .merged_zsh_history
Which comes from this excellent article ("Combining zsh history files").
It merges the history files, sorts the command, removes duplicates and doesn't break multiline commands.
A few weeks later
As planned, my .zsh_history
got truncated for no apparent reason.
My backups worked fine. Some of them still had duplicate commands in the same files. The awk code above only recognizes exact duplicates (time+duration+command) between files, but will leave them if, for example, ls
has been called at different times. So I wrote this small Ruby script:
#! /usr/bin/env ruby
# Ruby script to merge zsh histories. In case of duplicates, it removes the old timestamps.
# It should do fine with multi-line commands.
# Make backups of your backups before running this script!
#
# ./merge_zsh_histories.rb zsh_history_*.bak ~/.zsh_history > merged_zsh_history
MULTILINE_COMMAND = "TO_BE_REMOVED_#{Time.now.to_i}"
commands = Hash.new([0,0])
ARGV.sort.each do |hist|
$stderr.puts "Parsing '#{hist}'"
content = File.read(hist)
content.scrub!("#")
content.gsub!(/\\n(?!:\s*\d{10,})/, MULTILINE_COMMAND)
should_be_empty = content.each_line.grep_v(/^:/) + content.each_line.grep(/(?<!^): \d{10,}/)
raise "Problem with those lines : #{should_be_empty}" unless should_be_empty.empty?
content.each_line do |line|
description, command = line.split(';', 2)
_, time, duration = description.split(':').map(&:to_i)
old_time, _old_duration = commands[command]
if time > old_time
commands[command] = [time, duration]
end
end
end
commands.sort_by{|_, time_duration| time_duration}.each{|command, (time, duration)|
puts ':%11d:%d;%s' % [time, duration, command.gsub(MULTILINE_COMMAND, "\\n")]
}
It worked fine, and returned a valid zsh_history file, which contained all my commands, and wasn't much larger than the largest backup.