简体   繁体   中英

Save Zsh history to ~/.persistent_history

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!

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".

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}//')"

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 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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM