69

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.

Mr Fooz
  • 109,094
  • 6
  • 73
  • 101
  • 3
    This is a good question for Unix/Linux Stackexchange. – Flimm Jul 03 '13 at 09:48
  • 1
    But I would have never found it there, nor been able to award a bounty to the answerer. :( – Theodore R. Smith Aug 08 '13 at 18:26
  • 1
    @Flimm It's been reasked there already: [Forcing an 'added' alias to every command](http://unix.stackexchange.com/questions/12059/forcing-an-added-alias-to-every-command) Anyway [this question is too old to be migrated](http://meta.stackexchange.com/questions/151890/disable-migration-for-questions-older-than-60-days). – Gilles 'SO- stop being evil' Nov 19 '13 at 20:36

14 Answers14

112

This is minimal stand-alone code to achieve what you want:

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

function timer_stop {
  timer_show=$(($SECONDS - $timer))
  unset timer
}

trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop

PS1='[last: ${timer_show}s][\w]$ '
Ville Laurikari
  • 28,380
  • 7
  • 60
  • 55
  • Thanks Martin. I updated my answer to give a greatly improved solution taking advantage of the DEBUG trap. – Ville Laurikari Dec 08 '09 at 10:58
  • Alternatively, you could use a `echo` command in the `timer_stop` function to get some `time` like output. – Martin Ueding Oct 07 '12 at 19:27
  • 11
    After implementing this on the 5th system and using literally tens of thousands of times over the last two years, I want to thank you again. It has revolutionized my life ;-) – Theodore R. Smith Aug 08 '13 at 18:37
  • 3
    Awesome. I wanted the time displayed only for "slow" commands, and I wanted a *bell* on those so I would be automatically alerted to completion of a slow command. So: `PS1='$([ $timer_show -gt 10 ] && echo -e "\a[${timer_show}s]")[\w]$ '` – retracile Feb 06 '14 at 15:37
  • how to translate this awesome to zsh? – slashdottir Jul 16 '14 at 16:21
  • 2
    This is an ugly hack. Command lines like `(date)` do not trigger anything. Command lines like `true | false` trigger it twice (once for each part of the pipe). All in all, it's not really what we need. The bash lacks this feature, period. – Alfe Apr 06 '17 at 09:00
  • This doesn't work while using Midnight Commander sub shell, as mc is modifying the PROMPT_COMMAND. The displayed time is not correct. – StackedUser May 17 '18 at 07:35
  • @Alfe The twice trigger can be fixed by having a flag set in `timer_start` and reset in `timer_stop`, to stop `timer_start` from running (or more precisely stop it from doing anything) more than once per `timer_stop`. The no trigger I don't know how to work around. – Arthur Nov 08 '21 at 09:55
  • If you want to retain your original prompt as well(and not replace), then replace the last line with: `TMP='[last: ${timer_show}s]'`. `PS1="${TMP} ${PS1}` – simplfuzz Dec 27 '21 at 09:39
  • 1
    This doesn't seem to work on MacOS. I get `syntax error: operand expected (error token is " ")` at the first line in `timer_stop` – Nicu Surdu Jul 08 '23 at 11:07
30

Using your replies and some other threads, I wrote this prompt which I want to share with you. I took a screenshot in wich you can see :

  • White : Last return code
  • Green and tick mark means success (return code was 0)
  • Red and cross mark means error (return code was >0)
  • (Green or Red) : Last command execution time in parenthesis
  • (Green or Red) : Current date time (\t)
  • (Green if not root, Red if root) : the logged username
  • (Green) : the server name
  • (Blue) : the pwd directory and the usual $

Custom prompt

Here is the code to put in your ~/.bashrc file :

function timer_now {
    date +%s%N
}

function timer_start {
    timer_start=${timer_start:-$(timer_now)}
}

function timer_stop {
    local delta_us=$((($(timer_now) - $timer_start) / 1000))
    local us=$((delta_us % 1000))
    local ms=$(((delta_us / 1000) % 1000))
    local s=$(((delta_us / 1000000) % 60))
    local m=$(((delta_us / 60000000) % 60))
    local h=$((delta_us / 3600000000))
    # Goal: always show around 3 digits of accuracy
    if ((h > 0)); then timer_show=${h}h${m}m
    elif ((m > 0)); then timer_show=${m}m${s}s
    elif ((s >= 10)); then timer_show=${s}.$((ms / 100))s
    elif ((s > 0)); then timer_show=${s}.$(printf %03d $ms)s
    elif ((ms >= 100)); then timer_show=${ms}ms
    elif ((ms > 0)); then timer_show=${ms}.$((us / 100))ms
    else timer_show=${us}us
    fi
    unset timer_start
}


set_prompt () {
    Last_Command=$? # Must come first!
    Blue='\[\e[01;34m\]'
    White='\[\e[01;37m\]'
    Red='\[\e[01;31m\]'
    Green='\[\e[01;32m\]'
    Reset='\[\e[00m\]'
    FancyX='\342\234\227'
    Checkmark='\342\234\223'


    # Add a bright white exit status for the last command
    PS1="$White\$? "
    # If it was successful, print a green check mark. Otherwise, print
    # a red X.
    if [[ $Last_Command == 0 ]]; then
        PS1+="$Green$Checkmark "
    else
        PS1+="$Red$FancyX "
    fi

    # Add the ellapsed time and current date
    timer_stop
    PS1+="($timer_show) \t "

    # If root, just print the host in red. Otherwise, print the current user
    # and host in green.
    if [[ $EUID == 0 ]]; then
        PS1+="$Red\\u$Green@\\h "
    else
        PS1+="$Green\\u@\\h "
    fi
    # Print the working directory and prompt marker in blue, and reset
    # the text color to the default.
    PS1+="$Blue\\w \\\$$Reset "
}

trap 'timer_start' DEBUG
PROMPT_COMMAND='set_prompt'
Nicolas Thery
  • 2,319
  • 4
  • 26
  • 36
  • There is an adjustment on MACOSX : Remove the %N after the date command – Nicolas Thery Feb 29 '16 at 09:45
  • 4
    Wowwweee! This is like the best thing ever! – William Hilton Oct 16 '16 at 23:38
  • 2
    @NicolasThery for me, I had to `brew install coreutils` and replace `date +%s%N` with `/usr/local/opt/coreutils/libexec/gnubin/date +%s%N` to make this work on macOS Sierra. – fredrik Jan 29 '17 at 19:03
  • 2
    Please note that this way of using `FancyX` and `Checkmark` will destroy multi-line (symptoms the same as here https://unix.stackexchange.com/questions/105958/terminal-prompt-not-wrapping-correctly). Simple workaround will be to escape two out of three parts of the symbols: `\[\342\234\]\227` like this, for example. – The Godfather Sep 14 '18 at 09:11
  • I've been looking for this for ages! – Javier PR Nov 22 '19 at 11:46
11

Another very minimal approach is:

trap 'SECONDS=0' DEBUG
export PS1='your_normal_prompt_here ($SECONDS) # '

This shows the number of seconds since the last simple command was started. The counter is not reset if you simply hit Enter without entering a command -- which can be handy when you just want to see how long the terminal has been up since you last did anything in it. It works fine for me in Red Hat and Ubuntu. It did NOT work for me under Cygwin, but I'm not sure if that's a bug or just a limitation of trying to run Bash under Windows.

One possible drawback to this approach is that you keep resetting SECONDS, but if you truly need to preserve SECONDS as the number of seconds since initial shell invocation, you can create your own variable for the PS1 counter instead of using SECONDS directly. Another possible drawback is that a large seconds value such as "999999" might be be better displayed as days+hours+minutes+seconds, but it's easy to add a simple filter such as:

seconds2days() { # convert integer seconds to Ddays,HH:MM:SS
  printf "%ddays,%02d:%02d:%02d" $(((($1/60)/60)/24)) \
  $(((($1/60)/60)%24)) $((($1/60)%60)) $(($1%60)) |
  sed 's/^1days/1day/;s/^0days,\(00:\)*//;s/^0//' ; }
trap 'SECONDS=0' DEBUG
PS1='other_prompt_stuff_here ($(seconds2days $SECONDS)) # '

This translates "999999" into "11days,13:46:39". The sed at the end changes "1days" to "1day", and trims off empty leading values such as "0days,00:". Adjust to taste.

willdye
  • 795
  • 9
  • 10
  • 1
    I like how this answer looks, but unfortunately it doesn't seem to work for me, at least not with 4.2.25(1)-release on Ubuntu 12.04. Once you set SECONDS to 0, it's always 0. – pioto May 02 '13 at 16:30
  • 1
    The problem I have with this is that there is a `PROMPT_COMMAND` that gets fired after every command, and that sends the `DEBUG` signal as well, which resets the timer - so every time the prompt comes up, the timer was started 0 seconds ago. – PJSCopeland Apr 12 '16 at 22:50
10

You could utilize this zsh-borrowed hook for bash: http://www.twistedmatrix.com/users/glyph/preexec.bash.txt

Timing done with this hook (Mac OS X): Use Growl to monitor long-running shell commands

Hanxue
  • 12,243
  • 18
  • 88
  • 130
miku
  • 181,842
  • 47
  • 306
  • 310
  • 1
    Here's another look at using Bash's debug hook in a similar manner: http://www.davidpashley.com/articles/xterm-titles-with-bash.html from fellow member David Pashley: http://serverfault.com/users/7342/david-pashley – Dennis Williamson Dec 07 '09 at 21:19
  • The hook was just what I was looking for (and the growl link is a nice quick usage example). Thanks! – Mr Fooz Dec 07 '09 at 21:31
8

If you hadn't set up any of the other answers before you kicked off your long-running job and you just want to know how long the job took, you can do the simple

$ HISTTIMEFORMAT="%s " history 2

and it will reply with something like

  654  1278611022 gvn up
  655  1278611714 HISTTIMEFORMAT="%s " history 2

and you can then just visually subtract the two timestamps (anybody know how to capture the output of the shell builtin history command?)

philsnow
  • 89
  • 1
  • 1
  • 3
    What do you mean, "how to capture the output"? You can just pipe it, use command substitution, `cut` it, `grep` it, `awk` it, `perl` it, `sed` it... [Technologic](http://www.youtube.com/watch?v=UoPplpBPQxQ). – Camilo Martin Jun 28 '14 at 14:40
  • This won't work. It will only show you how much time has passed after the first before the second command was issued — not how long the first command took. The user could have been idling in between. – PDK Aug 30 '22 at 14:28
6

I took the answer from Ville Laurikari and improved it using the time command to show sub-second accuracy:

function timer_now {
  date +%s%N
}

function timer_start {
  timer_start=${timer_start:-$(timer_now)}
}

function timer_stop {
  local delta_us=$((($(timer_now) - $timer_start) / 1000))
  local us=$((delta_us % 1000))
  local ms=$(((delta_us / 1000) % 1000))
  local s=$(((delta_us / 1000000) % 60))
  local m=$(((delta_us / 60000000) % 60))
  local h=$((delta_us / 3600000000))
  # Goal: always show around 3 digits of accuracy
  if ((h > 0)); then timer_show=${h}h${m}m
  elif ((m > 0)); then timer_show=${m}m${s}s
  elif ((s >= 10)); then timer_show=${s}.$((ms / 100))s
  elif ((s > 0)); then timer_show=${s}.$(printf %03d $ms)s
  elif ((ms >= 100)); then timer_show=${ms}ms
  elif ((ms > 0)); then timer_show=${ms}.$((us / 100))ms
  else timer_show=${us}us
  fi
  unset timer_start
}

trap 'timer_start' DEBUG
PROMPT_COMMAND=timer_stop

PS1='[last: ${timer_show}][\w]$ '

Of course this requires a process to be started, so it's less efficient, but still fast enough that you wouldn't notice.

Thomas
  • 174,939
  • 50
  • 355
  • 478
4

I found that trap ... DEBUG was running every time $PROMPT_COMMAND was called, resetting the timer, and therefore always returning 0.

However, I found that history records times, and I tapped into these to get my answer:

HISTTIMEFORMAT='%s '
PROMPT_COMMAND="
  START=\$(history 1 | cut -f5 -d' ');
  NOW=\$(date +%s);
  ELAPSED=\$[NOW-START];
  $PROMPT_COMMAND"
PS1="\$ELAPSED $PS1"

It's not perfect though:

  • If history doesn't register the command (e.g. repeated or ignored commands), the start time will be wrong.
  • Multi-line commands don't get the date extracted properly from history.
PJSCopeland
  • 2,818
  • 1
  • 26
  • 40
4

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

Ju Tutt
  • 201
  • 1
  • 11
  • I like this clean minimalistic version most because it integrates well with already existing PS1 strings. Feels like the good old Linux modular spirit :-) – ChristophK Nov 05 '19 at 16:56
1

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.

kamren
  • 37
  • 4
1

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
Mendi Barel
  • 3,350
  • 1
  • 23
  • 24
  • This is nice and simple. You can also `trap 'export START=$(date +%s.%3N)' DEBUG` followed by `export PS1='($(printf "$(date +%s.%3N) - $START\\n" | bc))\n\$ '` if you want a simple way to see execution time. – SO_fix_the_vote_sorting_bug May 18 '22 at 17:08
1

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
}
Ju Tutt
  • 201
  • 1
  • 11
1

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\\$ '
JShorthouse
  • 1,450
  • 13
  • 28
0

Will putting a \t in PS1 work for you?

It does not give the elapsed time but it should be easy enough to subtract the times when necessary.

$ export PS1='[\t] [\w]\$ '
[14:22:30] [/bin]$ sleep 10
[14:22:42] [/bin]$

Following the OP's comment that he is already using \t. If you can use tcsh instead of bash, you can set the time variable.

/bin 1 > set time = 0
/bin 2 > sleep 10
0.015u 0.046s 0:10.09 0.4%      0+0k 0+0io 2570pf+0w
/bin 3 >

You can change the format of the printing to be less ugly (se the tcsh man page).

/bin 4 > set time = ( 0 "last: %E" )
/bin 5 > sleep 10
last: 0:10.09
/bin 6 >

I do not know of a similar facility in bash

Andrew Stein
  • 12,880
  • 5
  • 35
  • 43
  • 2
    At the moment, I am in fact using \t, and it's often sufficient. Unfortunately, when I'm running long commands non-interactively there are gaps of time from when a previous command completes and I interactively start a new one. If I press just before running a new command it works, but I don't always remember to do so, so I'd like to automatically capture elapsed time. – Mr Fooz Dec 07 '09 at 20:28
0

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
$ 
Zoe
  • 27,060
  • 21
  • 118
  • 148
milahu
  • 2,447
  • 1
  • 18
  • 25
  • 1
    Markdown can be a little annoying. If you make a list (numbered or bullets), it will also consider code as a continuation because of the indentation. You need 8 spaces to format code within a list. I personally prefer breaking the list formatting with an invisible HTML comment (``), which allows you to use regular formatting instead – Zoe Dec 09 '18 at 16:26