将 Zsh 历史记录保存到 ~/.persistent_history

11

最近我想在Mac上尝试使用Z shell。但我也想继续将命令历史保存到~/.persistent_history中,这就是我在Bash中所做的(参考文献)。

但是,在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"

有没有人能帮我让它工作起来?非常感谢!


1
这个链接应该有助于替换PROMPT_COMMAND。使用grep -ocut或类似工具替换[[的用法是可行的,但取决于zsh中history的确切输出。 - Etan Reisner
@EtanReisner 非常感谢!对于 PROMPT_COMMAND,这个链接应该会有帮助。关于 [[ 的部分,我刚刚发现使用 history 命令,Bash 会在最后一行给出最新的命令(在这种情况下是 history)。但在 Zsh 下,history 命令不会返回最新的命令,而是返回在 history 命令之前使用的命令。有什么想法吗? :-) - astroboylrx
好的,我认为没有必要重复造轮子。只需设置HISTFILE并将HISTSIZESAVEHIST设置为一些荒谬的大尺寸(我的是100,000,我认为没有理由使它们更大,因为我在iTerm2中记录了所有终端会话 - 所有命令+输出,我的提示符中的时间精确到秒)。默认历史记录格式具有关联的POSIX时间戳,这比您的更准确,因为您的没有tzinfo。 - 4ae1e1
1
如果你想要合并zsh和bash的历史记录,那么这可能会带来麻烦。两个shell的语法在许多方面都是不兼容的,特别是如果你已经对zsh进行了一定程度的自定义。 - 4ae1e1
@4ae1e1 是的,你提出了一个很好的观点。我会考虑一下的。非常感谢! - astroboylrx
4个回答

6

在经过多次谷歌搜索后,我终于找到了实现这个的方法。首先,在 ~/.zshrc 文件中,添加以下选项以进行历史记录操作:

setopt append_history # append rather then overwrite
setopt extended_history # save timestamp
setopt inc_append_history # add history immediately after typing a command

简而言之,这三个选项将立即记录每个input_time+command到~/.zsh_history中。 然后,将此函数放入~/.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
}

由于我同时使用bash和zsh,因此我想要一个可以保存它们所有历史命令的文件。这样,我就可以使用“grep”轻松搜索所有命令。


这在多行命令中会失败。以下代码可以正常运行:local date_part="$(gawk 'substr($0, 0, 1) == ":" {print;}' ~/.zsh_history | tail -1 | cut -c 3-12)" - Daniel Landau
1
append_historyinc_append_history 是互斥的。同时指定它们与仅设置 inc_append_history 没有任何区别。根据我对 Zsh 选项 的阅读,append_historyinc_append_historyinc_append_history_timeshare_history 都是互斥的。 - Keith Devens
抱歉,@KeithDevens,请问您在文档中确切找到了append_historyinc_append_history互斥的地方吗? - astroboylrx
1
根据我链接的文档中的描述,@astroboylrx。例如,文档中说inc_append_history“……与APPEND_HISTORY类似,不同之处在于新的历史记录行是逐步添加到$HISTFILE中的”。因此,无需同时指定append_historyinc_append_historyinc_append_history_time更加明确:“仅当关闭INC_APPEND_HISTORYSHARE_HISTORY时,此选项才有用。这三个选项应被视为互斥的。” - Keith Devens

2

目前还无法评论(这已经超出了简单的更正),所以我会将这个作为回答。

这个更正被接受的答案并不完全适用,例如最后一个命令执行了相当长的时间 - 你的命令中会有杂乱的数字和;,就像这样:

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

这可以通过将command_part中的sed -re '1s/.{15}//'换成稍微长一点的gawk来解决,这也避免了使用管道。
local command_part="$(gawk "
  NR == $line_num_last {
    pivot = match(\$0, \";\");
    print substr(\$0, pivot+1);
  }
  NR > $line_num_last {
    print;
  }" ~/.zsh_history)"

它在处理多行命令中的问题时也存在一些问题,其中一行以:开头。这可以通过将line_num_last中的grep -ane '^:' ~/.zsh_history替换为grep -anE '^: [0-9]{10}:[0-9]*?;' ~/.zsh_history(主要是)解决-我说“主要”是因为命令可能包含匹配该表达式的字符串。例如:
% naughty "multiline
> command
> : 0123456789:123;but a command I'm not
> "

这将导致~/.persistent_history中的记录被覆盖。
为了解决这个问题,我们需要检查前一个记录是否以\结尾(可能存在其他条件,但我还不熟悉这种历史格式),如果是,则尝试匹配前一个记录。
_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
}

抱歉晚批准了。你关于以 : 开头的命令是正确的。感谢你深入挖掘并提供有见地的建议。 :-) - astroboylrx

2

原始答案大部分是正确的,但是要处理同时包含字符“:”的多行命令,例如这样:

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

太酷了!我从没想过多行命令。这非常有帮助。非常感谢你! :-) - astroboylrx
我添加了另一行代码,将多行命令存储为单行命令:local one_line_command=${(Q)command_part//\\$'\n'/}。现在看起来非常棒。 :-) - astroboylrx

0
如果您想要为bash和zsh添加持久历史记录,可以尝试以下方法:
# You should source this file from both .zshrc and .bashrc

if [ -n "${ZSH_VERSION}" ]; then
    setopt append_history # append rather then overwrite
    setopt extended_history # save timestamp
    setopt inc_append_history # add history immediately after typing a command

    _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="$(awk "NR == $line_num_last {print;}" ~/.zsh_history | cut -c 3-12)"
      # Try to get date with non-mac date function.
      local fmt_date="$(date -d @${date_part} +'%Y-%m-%d %H:%M:%S')" >& /dev/null
      # Try again with mac date function if that failed.
      if [ -z "$fmt_date" ]; then
          local fmt_date="$(date -r 1623959079 +'%Y-%m-%d %H:%M:%S')" >& /dev/null
      fi
      # I use awk itself to split the _first_ line only at the first `;`
      local command_part="$(awk "
        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
    }
elif [ -n "${BASH_VERSION}" ]; then
    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
    }
    export PROMPT_COMMAND="log_bash_persistent_history"
fi

export HISTSIZE=1000000
export HISTFILESIZE=-1
export HISTCONTROL=ignoredups:erasedups
export HISTTIMEFORMAT="%F %T  "

alias persistent_history='cat ~/.persistent_history'
alias ph='cat ~/.persistent_history'
alias phgrep='ph | grep'
alias phg='ph | grep'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接