Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Save Zsh history to ~/.persistent_history

Tags:

bash

shell

zsh

Recently I want to try Z shell in Mac. But I'd like to continue also saving the command history to ~/.persistent_history, which was what I did in Bash (ref).

However, the script in the ref link doesn't 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 working? Many thanks!

like image 629
astroboylrx Avatar asked May 15 '15 00:05

astroboylrx


3 Answers

After so much Googling, I finally found out the way to do this. First, in ~/.zshrc, add the following options for history manipulation:

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 record every input_time+command to ~/.zsh_history immediately. Then, put this function into ~/.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 use both bash and zsh, so I want a file that can save all their history commands. In this case, I can easily search all of them using "grep".

like image 133
astroboylrx Avatar answered Nov 01 '22 00:11

astroboylrx


Can't comment yet (and this went beyond a simple correction), so I'll add this as an answer.

This correction to the accepted answer doesn't quite work when, for example, the last command took quite a bit of time to execute - you'll get stray numbers and ; in your command, like this:

2017-07-22 19:02:42 | 3;micro ~/.zshrc && . ~/.zshrc

This can be fixed by replacing the sed -re '1s/.{15}//' in command_part with a slightly longer gawk, which also avoids us a 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 multiline commands where one of the lines begin with :. This can be (mostly) fixed by replacing grep -ane '^:' ~/.zsh_history in line_num_last with grep -anE '^: [0-9]{10}:[0-9]*?;' ~/.zsh_history - I say mostly because a command could conceivably contain a string matching that expression. Say,

% naughty "multiline
> command
> : 0123456789:123;but a command I'm not
> "

Which will result in a clobbered record in ~/.persistent_history.

To fix this we need, in turn, to check whether the previous redord ends with \ (there might be other conditions but I'm not familiar yet with this history format), 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's 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
}
like image 43
mftrhu Avatar answered Oct 31 '22 23:10

mftrhu


The original answer is mostly good, but to handle multi-line commands that also contain the 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}//')"
like image 22
Daniel Landau Avatar answered Nov 01 '22 00:11

Daniel Landau