【问题标题】:Use PS0 and PS1 to display execution time of each bash command使用 PS0 和 PS1 显示每个 bash 命令的执行时间
【发布时间】:2017-08-29 07:15:05
【问题描述】:

似乎通过在 PS0 和 PS1 变量中执行代码(据我了解,在运行提示命令之前和之后评估)应该可以记录每个运行命令的时间并将其显示在提示中.类似的东西:

user@machine ~/tmp
$ sleep 1

user@machine ~/tmp 1.01s
$

但是,我很快就被 PS0 中的录制时间卡住了,因为这样的东西不起作用:

PS0='$(START=$(date +%s.%N))'

据我了解,START 分配发生在子外壳中,因此在外壳中不可见。您将如何处理?

【问题讨论】:

    标签: bash shell ps1


    【解决方案1】:

    我把这个当作谜题,想展示我的困惑结果:

    首先我摆弄了时间测量。 date +%s.%N(我之前没有意识到)是我开始的地方。不幸的是,bashs 算术评估似乎不支持浮点数。因此,我选择了其他东西:

    $ START=$(date +%s.%N)
    
    $ awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$START') }' /dev/null
    8.059526s
    
    $
    

    这足以计算时间差。

    接下来,我确认了您已经描述的内容:子 shell 调用防止使用 shell 变量。因此,我考虑了我可以在哪里存储开始时间,它对于子 shell 来说是全局的,但足够本地以同时在多个交互式 shell 中使用。我的解决方案是临时的。文件(在/tmp)。为了提供一个唯一的名称,我想出了这个模式:/tmp/$USER.START.$BASHPID

    $ date +%s.%N >/tmp/$USER.START.$BASHPID ; \
    > awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$BASHPID)') }' /dev/null
    cat: /tmp/ds32737.START.11756: No such file or directory
    awk: cmd. line:1: BEGIN { printf("%fs", 1491297723.111219300 - ) }
    awk: cmd. line:1:                                              ^ syntax error
    
    $
    

    该死!我再次陷入子外壳问题。为了解决这个问题,我定义了另一个变量:

    $ INTERACTIVE_BASHPID=$BASHPID
    
    $ date +%s.%N >/tmp/$USER.START.$INTERACTIVE_BASHPID ; \
    > awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)') }' /dev/null
    0.075319s
    
    $
    

    下一步:与PS0PS1 一起解决这个问题。在一个类似的谜题(SO: How to change bash prompt color based on exit code of last command?)中,我已经掌握了“引用地狱”。因此,我应该可以再做一次:

    $ PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'
    
    $ PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"
    0.118550s
    $
    

    啊。它开始工作。因此,只有一个问题 - 为INTERACTIVE_BASHPID 的初始化找到正确的启动脚本。我找到了~/.bashrc,这似乎是正确的,并且我过去已经将其用于其他一些个人定制。

    所以,把它们放在一起 - 这些是我添加到我的 ~/.bashrc 的行:

    # command duration puzzle
    INTERACTIVE_BASHPID=$BASHPID
    date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}"
    PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'
    PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"
    

    添加了第 3 行(date 命令)以解决另一个问题。将其注释掉并启动一个新的交互式 bash 以找出原因。

    我的 cygwin xterm 与 bash 的快照,我将以上行添加到 ./~bashrc

    注意事项:

    1. 我认为这是解决难题的方法,而不是“非常有成效”的解决方案。我敢肯定,这种时间测量会消耗大量时间。 time 命令可能会提供更好的解决方案:SE: How to get execution time of a script effectively?。但是,这是练习 bash 的好讲座……

    2. 不要忘记,此代码污染了您的/tmp 目录,其中包含越来越多的小文件。不时清理/tmp,或添加适当的清理命令(例如~/.bash_logout)。

    【讨论】:

      【解决方案2】:

      我正在寻找一个不同问题的解决方案并遇到了这个问题,并认为这听起来是一个很酷的功能。使用@Scheff 的出色答案作为基础,除了我为其他问题开发的解决方案外,我还想出了一个更优雅、功能更全面的解决方案。

      首先,我创建了一些函数来读取/写入内存的时间。写入共享内存文件夹会阻止磁盘访问,并且如果由于某种原因未清理文件,则不会在重新启动时持续存在

      function roundseconds (){
        # rounds a number to 3 decimal places
        echo m=$1";h=0.5;scale=4;t=1000;if(m<0) h=-0.5;a=m*t+h;scale=3;a/t;" | bc
      }
      
      function bash_getstarttime (){
        # places the epoch time in ns into shared memory
        date +%s.%N >"/dev/shm/${USER}.bashtime.${1}"
      }
      
      function bash_getstoptime (){
        # reads stored epoch time and subtracts from current
        local endtime=$(date +%s.%N)
        local starttime=$(cat /dev/shm/${USER}.bashtime.${1})
        roundseconds $(echo $(eval echo "$endtime - $starttime") | bc)
      }
      

      bash_ 函数的输入是 bash PID

      这些函数和以下被添加到 ~/.bashrc 文件中

      ROOTPID=$BASHPID
      bash_getstarttime $ROOTPID
      

      这些创建初始时间值并将 bash PID 存储为可以传递给函数的不同变量。然后将函数添加到 PS0 和 PS1

      PS0='$(bash_getstarttime $ROOTPID) etc..'
      PS1='\[\033[36m\] Execution time $(bash_getstoptime $ROOTPID)s\n'
      PS1="$PS1"'and your normal PS1 here'
      

      现在它将在处理终端输入之前在PS0中生成时间,在处理终端输入后在PS1中再次生成时间,然后计算差值并添加到PS1。最后,这段代码会在终端退出时清理存储的时间:

      function runonexit (){
        rm /dev/shm/${USER}.bashtime.${ROOTPID}
      }
      
      trap runonexit EXIT
      

      把它们放在一起,再加上一些正在测试的额外代码,它看起来像这样:

      重要的部分是以毫秒为单位的执行时间,以及存储在共享内存中的所有活动终端 PID 的 user.bashtime 文件。 PID 也显示在终端输入之后,因为我在 PS0 中添加了它的显示,您可以看到添加和删除的 bashtime 文件。

      PS0='$(bash_getstarttime $ROOTPID) $ROOTPID experiments \[\033[00m\]\n'
      

      【讨论】:

        【解决方案3】:

        算术展开在当前进程中运行,可以赋值给变量。它还产生输出,您可以使用\e[$((...,0))m(输出\e[0m)或${t:0:$((...,0))}(不输出任何内容,这可能更好)之类的东西来使用它。 Bash 支持中的 64 位整数支持将计算 POSIX 纳秒,直到 2262 年。

        $ PS0='${t:0:$((t=$(date +%s%N),0))}'
        $ PS1='$((( t )) && printf %d.%09ds $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)))${t:0:$((t=0))}\n$ '
        0.053282161s
        $ sleep 1
        1.064178281s
        $ 
        
        $ 
        

        PS0 不会针对空命令进行评估,它会留下一个空行(我不确定您是否可以有条件地打印 \n 而不会破坏事物)。你可以通过切换到 PROMPT_COMMAND 来解决这个问题(这也节省了一个 fork):

        $ PS0='${t:0:$((t=$(date +%s%N),0))}'
        $ PROMPT_COMMAND='(( t )) && printf %d.%09ds\\n $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)); t=0'
        0.041584565s
        $ sleep 1
        1.077152833s
        $ 
        $ 
        

        也就是说,如果您不需要亚秒级的精度,我建议您改用$SECONDS(如果设置了时间,这也更有可能返回合理的答案)。

        【讨论】:

          【解决方案4】:

          正如问题中正确说明的那样,PS0 在子 shell 中运行,因此无法用于设置开始时间。

          相反,可以使用history 命令与纪元秒数%s 和内置变量$EPOCHSECONDS 仅利用$PROMPT_COMMAND 来计算命令何时完成。

          # Save start time before executing command (does not work due to PS0 sub-shell)
          # preexec() {
          #   STARTTIME=$EPOCHSECONDS
          # }
          # PS0=preexec
          
          # Save end time, without duplicating commands when pressing Enter on an empty line
          precmd() {
              local st=$(HISTTIMEFORMAT='%s ' history 1 | awk '{print $2}');
              if [[ -z "$STARTTIME" || (-n "$STARTTIME" && "$STARTTIME" -ne "$st") ]]; then
                  ENDTIME=$EPOCHSECONDS
                  STARTTIME=$st
              else
                  ENDTIME=0
              fi
          }
          
          __timeit() {
              precmd;
              if ((ENDTIME - STARTTIME >= 0)); then
                  printf 'Command took %d seconds.\n' "$((ENDTIME - STARTTIME))";
              fi
          
              # Do not forget your:
              #     - OSC 0 (set title)
              #     - OSC 777 (notification in gnome-terminal, urxvt; note, this one has preexec and precmd as OSC 777 features)
              #     - OSC 99 (notification in kitty)
              #     - OSC 7 (set url) - out of scope for this question
          }
          
          export PROMPT_COMMAND=__timeit
          

          注意:如果您的 $HISTCONTROL 中有 ignoredups,则不会报告重新运行的命令。

          【讨论】:

            【解决方案5】:

            作为@tc said,使用算术扩展允许您在PS0PS1 的扩展期间分配变量。较新的 bash 版本还允许 PS* 样式扩展,因此您甚至不需要子外壳来获取当前时间。使用 bash 4.4:

            # PS0 extracts a substring of length 0 from PS1; as a side-effect it causes
            # the current time as epoch seconds to PS0time (no visible output in this case)
            PS0='\[${PS1:$((PS0time=\D{%s}, PS1calc=1, 0)):0}\]'
            # PS1 uses the same trick to calculate the time elapsed since PS0 was output.
            # It also expands the previous command's exit status ($?), the current time
            # and directory ($PWD rather than \w, which shortens your home directory path
            # prefix to "~") on the next line, and finally the actual prompt: 'user@host> '
            PS1='\nSeconds: $((PS1calc ? \D{%s}-$PS0time : 0)) Status: $?\n\D{%T} ${PWD:PS1calc=0}\n\u@\h> '
            

            %N 日期指令似乎没有作为\D{...} 扩展的一部分与 bash 4.4 一起实现。很遗憾,因为我们只有一秒单位的分辨率。)

            由于PS0 仅在有要执行的命令时才被评估和打印,所以PS1calc 标志设置为1 以在PS1 扩展或不(@ 987654332@ 是0 意味着PS0 之前没有扩展,因此没有重新评估PS1time)。 PS1 然后将 PS1calc 重置为 0。这样一来,空行(只需按回车键)就不会在按回车键之间累积秒数。

            这个方法的一个好处是当你激活set -x 时没有输出。看不到子外壳或临时文件:一切都在 bash 进程本身内完成。

            【讨论】:

              猜你喜欢
              • 2010-11-13
              • 2017-12-20
              • 2020-10-09
              • 2016-07-25
              • 1970-01-01
              • 2013-08-05
              • 2019-03-12
              • 1970-01-01
              相关资源
              最近更新 更多