How can the last command's wall time be put in the Bash prompt?

Viewed 13071

Is there a way to embed the last command's elapsed wall time in a Bash prompt? I'm hoping for something that would look like this:

[last: 0s][/my/dir]$ sleep 10
[last: 10s][/my/dir]$

Background

I often run long data-crunching jobs and it's useful to know how long they've taken so I can estimate how long it will take for future jobs. For very regular tasks, I go ahead and record this information rigorously using appropriate logging techniques. For less-formal tasks, I'll just prepend the command with time.

It would be nice to automatically time every single interactive command and have the timing information printed in a few characters rather than 3 lines.

14 Answers

Here's my take on Thomas'

uses date +%s%3N to get milliseconds as base unit, simplified following code (less zeros)

function t_now {
    date +%s%3N
}

function t_start {
    t_start=${t_start:-$(t_now)}
}

function t_stop {
    local d_ms=$(($(t_now) - $t_start))
    local d_s=$((d_ms / 1000))
    local ms=$((d_ms % 1000))
    local s=$((d_s % 60))
    local m=$(((d_s / 60) % 60))
    local h=$((d_s / 3600))
    if ((h > 0)); then t_show=${h}h${m}m
    elif ((m > 0)); then t_show=${m}m${s}s
    elif ((s >= 10)); then t_show=${s}.$((ms / 100))s
    elif ((s > 0)); then t_show=${s}.$((ms / 10))s
    else t_show=${ms}ms
    fi
    unset t_start
}
set_prompt () {
t_stop
}

trap 't_start' DEBUG
PROMPT_COMMAND='set_prompt' 

Then add $t_show to your PS1

Another approach for bash 4.x and above would be to use coproc with PS0 and PS1 like below:

cmd_timer()
{
    echo $(( SECONDS - $(head -n1 <&"${CMD_TIMER[0]}") ))
}

coproc CMD_TIMER ( while read; do echo $SECONDS; done )
echo '' >&"${CMD_TIMER[1]}" # For value to be ready on first PS1 expansion
export PS0="\$(echo '' >&${CMD_TIMER[1]})"
export PS1="[ \$(cmd_timer) ] \$"

This is a .bashrc ready snippet. It is especially useful for everyone that uses undistract-me which overwrites trap DEBUG for its own purposes.

If somone just wants to see the time of execution, add this line to bash_profile

trap 'printf "t=%s\n" $(date +%T.%3N)' DEBUG

Translated version for zsh.

Append to your ~/.zshrc file

function preexec() {
  timer=$(date +%s%3N)
}

function precmd() {
  if [ $timer ]; then
    local now=$(date +%s%3N)
    local d_ms=$(($now-$timer))
    local d_s=$((d_ms / 1000))
    local ms=$((d_ms % 1000))
    local s=$((d_s % 60))
    local m=$(((d_s / 60) % 60))
    local h=$((d_s / 3600))
    if ((h > 0)); then elapsed=${h}h${m}m
    elif ((m > 0)); then elapsed=${m}m${s}s
    elif ((s >= 10)); then elapsed=${s}.$((ms / 100))s
    elif ((s > 0)); then elapsed=${s}.$((ms / 10))s
    else elapsed=${ms}ms
    fi

    export RPROMPT="%F{cyan}${elapsed} %{$reset_color%}"
    unset timer
  fi
}

A version with split hours, minutes and seconds inspired by the zsh spaceship prompt, based on Ville's answer and this time conversion function by perreal.

I also added a threshold variable so that the timer only displays for long running commands.

prompt output

time_threshold=5;

function convert_secs {
    ((h=${1}/3600))
    ((m=(${1}%3600)/60))
    ((s=${1}%60))
    if [ $h -gt 0 ]; then printf "${h}h "; fi
    if [ $h -gt 0 ] || [ $m -gt 0 ]; then printf "${m}m "; fi
    if [ $s -gt 0 ]; then printf "${s}s "; fi
}

function timer_start {
    timer=${timer:-$SECONDS}
}

function timer_stop {
    timer_time=$(($SECONDS - $timer))
    
    if [ ! -z $timer_time ] && [ $timer_time -ge ${time_threshold} ]; then
        timer_show="took $(convert_secs $timer_time)"
    else
        timer_show=""
    fi

    unset timer
}

trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop

PS1='\n\w ${timer_show}\n\\$ '

For the coloured output in my screenshot:

bold=$(tput bold)
reset=$(tput sgr0)
yellow=$(tput setaf 3)
cyan=$(tput setaf 6)

PS1='\n${bold}${cyan}\w ${yellow}${timer_show}${reset}\n\\$ '

this is my version

  • use date to format time, only calc days
  • set terminal title
  • use \$ in PS1 for user $ + root #
  • show return code / exit code
  • use date -u to disable DST
  • use hidden names like _foo
_x_dt_min=1 # minimum running time to show delta T
function _x_before {
    _x_t1=${_x_t1:-$(date -u '+%s.%N')} # float seconds
}
function _x_after {
    _x_rc=$? # return code
    _x_dt=$(echo $(date -u '+%s.%N') $_x_t1 | awk '{printf "%f", $1 - $2}')
    unset _x_t1
    #_x_dt=$(echo $_x_dt | awk '{printf "%f", $1 + 86400 * 1001}') # test
    # only show dT for long-running commands
    # ${f%.*} = int(floor(f))
    (( ${_x_dt%.*} >= $_x_dt_min )) && {
        _x_dt_d=$((${_x_dt%.*} / 86400))
        _x_dt_s='' # init delta T string
        (( $_x_dt_d > 0 )) && \
            _x_dt_s="${_x_dt_s}${_x_dt_d} days + "
        # format time
        # %4N = four digits of nsec
        _x_dt_s="${_x_dt_s}$(date -u -d0+${_x_dt}sec '+%T.%4N')"
        PS1='rc = ${_x_rc}\ndT = ${_x_dt_s}\n\$ '
    } || {
        PS1='rc = ${_x_rc}\n\$ '
    }
    # set terminal title to terminal number
    printf "\033]0;%s\007" $(tty | sed 's|^/dev/\(pts/\)\?||')
}
trap '_x_before' DEBUG
PROMPT_COMMAND='_x_after'
PS1='\$ '

sample output:

$ sleep 0.5
rc = 0
$ sleep 1
rc = 0
dT = 00:00:01.0040
$ sleep 1001d
rc = 0
dT = 1001 days + 00:00:00.0713
$ false
rc = 1
$ 
Related