使用PS0和PS1显示每个bash命令的执行时间

时间:2017-04-04 07:48:56

标签: bash shell ps1

似乎通过在PS0和PS1变量中执行代码(根据我的理解,在运行提示命令之前和之后进行评估),应该可以记录每个运行命令的时间并在提示中显示它。这样的事情:

user@machine ~/tmp
$ sleep 1

user@machine ~/tmp 1.01s
$

然而,我很快就陷入PS0的录音时间,因为这样的事情不起作用:

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

据我所知,START赋值发生在子shell中,因此在外壳中不可见。你会怎么做到这一点?

5 个答案:

答案 0 :(得分:1)

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

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

$ 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的快照,其中我将上面的行添加到./~bashrcSnapshot of my cygwin xterm with the "tuned" bash prompt

注意:

  1. 我认为这是一个谜题的解决方案,而不是一个严肃的生产方式#34;解。我确信这种时间测量会消耗很多时间。 time命令可能提供更好的解决方案:SE: How to get execution time of a script effectively?。然而,这是练习bash的一个很好的讲座......

  2. 不要忘记此代码会使用越来越多的小文件污染您的/tmp目录。可以不时清理/tmp或添加适当的清理命令(例如~/.bash_logout)。

答案 1 :(得分:1)

作为@tc said,使用算术展开允许您在PS0PS1的展开期间分配变量。较新的 bash 版本还允许 PS* 样式扩展,因此您甚至不需要子 shell 来获取当前时间。使用 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 日期指令似乎没有作为 bash 4.4 的 \D{...} 扩展的一部分实现。这很遗憾,因为我们只有以秒为单位的分辨率。)

由于 PS0 仅在有命令执行时才计算和打印,PS1calc 标志设置为 1 以在 {{ 1}} 扩展与否(PS1PS1calc 意味着 0 之前没有扩展,因此没有重新评估 PS0)。 PS1time 然后将 PS1 重置为 PS1calc。通过这种方式,空行(只是按回车键)不会在两次回车键之间累积秒数。

这种方法的一个好处是当您激活 0 时没有输出。看不到子shell或临时文件:一切都在bash进程本身内完成。

答案 2 :(得分:0)

我一直在寻找其他问题的解决方案,并提出了这个问题,并决定听起来像是一个很棒的功能。除了为其他问题开发的解决方案之外,还以@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

将它们放在一起,加上一些正在测试的其他代码,看起来像这样:

Terminal Output

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

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

答案 3 :(得分:0)

算术扩展在当前进程中运行,并且可以分配给变量。它还会产生输出,您可以使用\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(也可以节省派生)来解决此问题:

$ 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 :(得分:0)

正如问题中所述,PS0 在子 shell 中运行,这使得它无法用于设置开始时间。

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

# 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

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