【问题标题】:Save Zsh history to ~/.persistent_history将 Zsh 历史记录保存到 ~/.persistent_history
【发布时间】:2015-07-26 19:24:18
【问题描述】:

最近我想在 Mac 中尝试 Z shell。但我还想继续将命令历史记录保存到 ~/.persistent_history,这就是我在 Bash 中所做的 (ref)。

但是,参考链接中的脚本在 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"

有没有人可以帮助我让它工作?非常感谢!

【问题讨论】:

  • This 应该有助于替换 PROMPT_COMMAND。用grep -ocut 或类似方法替换[[ 用法应该是可行的,但取决于zsh 中history 的确切输出。
  • @EtanReisner 非常感谢!对于PROMPT_COMMAND,该链接应该会有所帮助。对于[[ 部分,我刚刚使用命令history 发现,bash 将在最后一行给出最新的(在本例中为history)。但是在 Zsh 下,history 命令不会返回最新的命令,它会在最后一行返回history 之前使用的命令。有任何想法吗? :-)
  • 好吧,我认为没有理由重新发明轮子。只需设置HISTFILE 并将HISTSIZESAVEHIST 设置为一些可笑的大尺寸(我的是100,000,我认为没有理由让它们变大,因为我在iTerm2中记录了我所有的终端会话——这就是所有命令+输出,在我的提示中时间减少到几秒钟)。默认的历史格式关联了 POSIX 时间戳,这比你的更准确,因为你的没有 tzinfo。
  • (如果你想结合 zsh 和 bash 历史——好吧,那是自找麻烦。两个 shell 的语法在很多方面都不兼容,特别是如果你在一定程度上自定义了 zsh。)
  • @4ae1e1 是的,你提出了一个很好的观点。我会考虑一下。非常感谢!

标签: bash shell zsh


【解决方案1】:

如果您希望能够为 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'

【讨论】:

    【解决方案2】:

    还不能发表评论(这超出了简单的更正范围),所以我会添加这个作为答案。

    This correctionthe accepted answer 不能很好地工作,例如,最后一个命令需要很长时间才能执行 - 你会在你的命令中得到杂散数字和 ;,如下所示:

    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 中的记录被破坏。

    为了解决这个问题,我们需要依次检查之前的redord是否以\结尾(可能还有其他条件,但我还不熟悉这种历史格式),如果是这样尝试 上一场比赛。

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

    【讨论】:

    • 很抱歉接受晚了。您对以: 开头的命令是正确的。感谢您富有洞察力的挖掘。 :-)
    【解决方案3】:

    最初的答案大多是好的,但要处理也包含字符“:”的多行命令,例如这有效:

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

    【讨论】:

    • 酷!我还没有考虑过多行命令。这很有帮助。太感谢了! :-)
    • 我添加了另一行以使多行命令存储为单行命令:local one_line_command=${(Q)command_part//\\$'\n'/}。现在看起来真的很棒。 :-)
    【解决方案4】:

    经过这么多的谷歌搜索,我终于找到了这样做的方法。 首先,在 ~/.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)"
    • append_historyinc_append_history 是互斥的顺便说一句。除了设置inc_append_history 之外,指定它们都没有任何作用。根据我对Zsh's optionsappend_historyinc_append_historyinc_append_history_timeshare_history 的阅读,它们都是相互排斥的。
    • @KeithDevens 抱歉,您在哪里发现 append_historyinc_append_history 在文档中是互斥的?
    • @astroboyrx 来自我链接的文档中的描述。例如,文档说inc_append_history“...工作方式与APPEND_HISTORY 类似,只是将新的历史记录行增量添加到$HISTFILE”。所以没有必要同时指定append_historyinc_append_historyinc_append_history_time 更明确:“这个选项只有在 INC_APPEND_HISTORYSHARE_HISTORY 被关闭时才有用。这三个选项应该被认为是互斥的。”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多