Save Zsh history to ~ / .persistent_history
I recently want to try the Z shell on a Mac. But I would like to continue storing the command history in ~ / .persistent_history as well, which I did in Bash ( ref ).
However, the script in the ref ref does not work under Zsh:
log_bash_persistent_history()
{
[[
$(history 1) =~ ^\ *[0-9]+\ +([^\ ]+\ [^\ ]+)\ +(.*)$
]]
local date_part="${BASH_REMATCH[1]}"
local command_part="${BASH_REMATCH[2]}"
if [ "$command_part" != "$PERSISTENT_HISTORY_LAST" ]
then
echo $date_part "|" "$command_part" >> ~/.persistent_history
export PERSISTENT_HISTORY_LAST="$command_part"
fi
}
run_on_prompt_command()
{
log_bash_persistent_history
}
PROMPT_COMMAND="run_on_prompt_command"
Is there anyone who can help me get it to work? Many thanks!
source to share
After so much Googling, I finally figured out how to do this. First, in ~ / .zshrc add the following parameters to handle history:
setopt append_history # append rather then overwrite
setopt extended_history # save timestamp
setopt inc_append_history # add history immediately after typing a command
In short, these three options will log every input_time + command to ~ / .zsh_history immediately. Then put this function in ~ / .zshrc:
precmd() { # This is a function that will be executed before every prompt
local date_part="$(tail -1 ~/.zsh_history | cut -c 3-12)"
local fmt_date="$(date -d @${date_part} +'%Y-%m-%d %H:%M:%S')"
# For older version of command "date", comment the last line and uncomment the next line
#local fmt_date="$(date -j -f '%s' ${date_part} +'%Y-%m-%d %H:%M:%S')"
local command_part="$(tail -1 ~/.zsh_history | cut -c 16-)"
if [ "$command_part" != "$PERSISTENT_HISTORY_LAST" ]
then
echo "${fmt_date} | ${command_part}" >> ~/.persistent_history
export PERSISTENT_HISTORY_LAST="$command_part"
fi
}
Since I am using both bash and zsh, so I need a file that can save all my history commands. In this case, I can easily find them all using "grep".
source to share
The original answer is mostly good, but for handling multi-line commands that also contain a ":" character, for example, this works:
local line_num_last=$(grep -ane '^:' ~/.zsh_history | tail -1 | cut -d':' -f1 | tr -d '\n')
local date_part="$(gawk "NR == $line_num_last {print;}" ~/.zsh_history | cut -c 3-12)"
local fmt_date="$(date -d @${date_part} +'%Y-%m-%d %H:%M:%S')"
local command_part="$(gawk "NR >= $line_num_last {print;}" ~/.zsh_history | sed -re '1s/.{15}//')"
source to share
Can't comment (and this is beyond a simple correction), so I'll add this as an answer.
This correction to the accepted answer doesn't quite work if, for example, the last command took quite a long time to execute - in your command, you will get stray numbers and ;
for example:
2017-07-22 19:02:42 | 3;micro ~/.zshrc && . ~/.zshrc
This can be eliminated by replacing sed -re '1s/.{15}//'
in command_part
with a slightly longer one gawk
, which also avoids the pipeline:
local command_part="$(gawk "
NR == $line_num_last {
pivot = match(\$0, \";\");
print substr(\$0, pivot+1);
}
NR > $line_num_last {
print;
}" ~/.zsh_history)"
It also has problems when dealing with multi-line commands where one of the lines starts with :
. This can be (mostly) fixed by replacing grep -ane '^:' ~/.zsh_history
with line_num_last
with grep -anE '^: [0-9]{10}:[0-9]*?;' ~/.zsh_history
- I'm talking mainly because a command can contain a string that matches that expression. Let's say
% naughty "multiline
> command
> : 0123456789:123;but a command I'm not
> "
This will result in an entry with clobbered in ~/.persistent_history
.
To fix this, we need to in turn check if the previous redord ends with \
(there may be other conditions, but I'm not familiar with this historical format yet), and if so, try the previous match.
_get_line_num_last () {
local attempts=0
local line=0
while true; do
# Greps the last two lines that can be considered history records
local lines="$(grep -anE '^: [0-9]{10}:[0-9]*?;' ~/.zsh_history | \
tail -n $((2 + attempts)) | head -2)"
local previous_line="$(echo "$lines" | head -1)"
# Gets the line number of the line being tested
local line_attempt=$(echo "$lines" | tail -1 | cut -d':' -f1 | tr -d '\n')
# If the previous (possible) history records ends with `\`, then the
# _current_ one is part of a multiline command; try again.
# Probably. Unless it was in turn in the middle of a multi-line
# command. And that why the last line should be saved.
if [[ $line_attempt -ne $HISTORY_LAST_LINE ]] && \
[[ $previous_line == *"\\" ]] && [[ $attempts -eq 0 ]];
then
((attempts+=1))
else
line=$line_attempt
break
fi
done
echo "$line"
}
precmd() {
local line_num_last="$(_get_line_num_last)"
local date_part="$(gawk "NR == $line_num_last {print;}" ~/.zsh_history | cut -c 3-12)"
local fmt_date="$(date -d @${date_part} +'%Y-%m-%d %H:%M:%S')"
# I use awk itself to split the _first_ line only at the first `;`
local command_part="$(gawk "
NR == $line_num_last {
pivot = match(\$0, \";\");
print substr(\$0, pivot+1);
}
NR > $line_num_last {
print;
}" ~/.zsh_history)"
if [ "$command_part" != "$PERSISTENT_HISTORY_LAST" ]
then
echo "${fmt_date} | ${command_part}" >> ~/.persistent_history
export PERSISTENT_HISTORY_LAST="$command_part"
export HISTORY_LAST_LINE=$((1 + $(wc -l < ~/.zsh_history)))
fi
}
source to share