PS1 prompt to show elapsed time

I currently use this to display the current time in my bash prompt:

PS1=[e[0;32m]t W>[e[1;37m]

20:42:23 ~>

Is it possible to display the elapsed time since the previous prompt?
Such as:

00:00:00 ~> sleep 10
00:00:10 ~> sleep 20
00:00:20 ~>

This has nothing in common with Is it possible to change the PS1 periodically by a script in the background?

Asked By: TeasingDart

||

One way to do it would be to use the PROMPT_COMMAND feature of bash to execute code that modifies PS1. The function below is an updated version of my original submission; this one uses two fewer environment variables and prefixes them with “_PS1_” to try to avoid clobbering existing variables.

prompt_command() {
  _PS1_now=$(date +%s)
  PS1=$( printf "[e[0;32m]%02d:%02d:%02d W>[e[1;37m] " 
           $((  ( _PS1_now - _PS1_lastcmd ) / 3600))         
           $(( (( _PS1_now - _PS1_lastcmd ) % 3600) / 60 )) 
           $((  ( _PS1_now - _PS1_lastcmd ) % 60))           
       )
  _PS1_lastcmd=$_PS1_now
}
PROMPT_COMMAND='prompt_command'
_PS1_lastcmd=$(date +%s)

Put that into your .bash_profile to get things started up.

Note that you have to type pretty quickly to get the sleep parameter to match the prompt parameter — the time really is the difference between prompts, including the time it takes you to type the command.

00:00:02 ~> sleep 5   ## here I typed really quickly
00:00:05 ~> sleep 3   ## here I took about 2 seconds to enter the command
00:00:10 ~> sleep 30 ## more slow typing
00:01:35 ~>

Late addition:

Based on @Cyrus’ now-deleted answer, here is a version that does not clutter the environment with extra variables:

PROMPT_COMMAND='
    _prompt(){
        PROMPT_COMMAND="${PROMPT_COMMAND%-*}-$SECONDS))""
        printf -v PS1 "[e[0;32m]%02d:%02d:%02d W>[e[1;37m] " 
                      "$(($1/3600))" "$((($1%3600)/60))" "$(($1%60))"
    }; _prompt "$((SECONDS'"-$SECONDS))""

Extra late addition:

Starting in bash version 4.2 (echo $BASH_VERSION), you can avoid the external date calls with a new printf format string; replace the $(date +%s) pieces with $(printf '%(%s)T' -1). Starting in version 4.3, you can omit the -1 parameter to rely on the “no argument means now” behavior.

Answered By: Jeff Schaller
PS1[3]=$SECONDS
PS1='${PS1[!(PS1[1]=!1&(PS1[3]=(PS1[2]=$SECONDS-${PS1[3]})/3600))
   ]#${PS1[3]%%*??}0}$((PS1[3]=(PS1[2]/60%60),  ${PS1[3]})):${PS1[1
   ]#${PS1[3]%%*??}0}$((PS1[3]=(PS1[2]%60),     ${PS1[3]})):${PS1[1
   ]#${PS1[3]%%*??}0}$((PS1[3]=(SECONDS),       ${PS1[3]})):'$PS1

This handles the formatting by calculation – so, while it does expand several times, it doesn’t do any subshells or pipes.

It just treats $PS1 as an array and uses the higher indices to store/calculate any/all necessary state between prompts. No other shell state is affected.

00:00:46:[mikeserv@desktop tmp]$
00:00:01:[mikeserv@desktop tmp]$
00:00:00:[mikeserv@desktop tmp]$
00:00:01:[mikeserv@desktop tmp]$
00:00:43:[mikeserv@desktop tmp]$ sleep 10
00:00:33:[mikeserv@desktop tmp]$ sleep 10
00:00:15:[mikeserv@desktop tmp]$
00:00:15:[mikeserv@desktop tmp]$
00:00:02:[mikeserv@desktop tmp]$
00:02:27:[mikeserv@desktop tmp]$

I can break it down a little maybe…

First, save the current value of $SECONDS:

PS1[3]=$SECONDS

Next, define $PS1[0] to be self-recursive in a way that will always set the right values to $PS1[1-3] while simultaneously self-referencing. To get this part you have to consider the order in which shell-math expressions are evaluated. Most importantly, shell-math is always the last order of business for shell-math. Before all else, the shell expands values. In this way you can reference an old-value for a shell-variable in a math expression after assigning it by using $.

Here is a simple example first:

x=10; echo "$(((x+=5)+$x+x))" "$x"

40 15

The shell will evaluate that statement by first substituting the value of $x wherever the $ dollar-sign reference is used, and so the expression becomes:

(x+=5)+10+x

…then the shell adds 5 to the value of $x and afterward expands the whole expression to x+10+x, while retaining only the actually assigned value in the reference variable. And so the math expression’s expanded value is 40, but the ultimate value of $x is 15.

That is largely how the $PS1 equation works as well, except that there is a further level of math expansion/evaluation exploited in the array indices.

PS1='${PS1[!(PS1[1]=!1&(...))]#...}...'

I’m not really sure why I chose to use PS1[1]=!1 there – I guess it was probably just silly aesthetics – but this assigns 0 to $PS1[1] while expanding it for parameter substitution. The value of a bitwise AND for 0 and anything else will always be 0, but it doesn’t short-circuit as a boolean && does when the left-most primary is 0 and so the parenthetical expression still gets evaluated every time. That is important, of course, because that first elipsis is where the initial values for $PS1[2,3] are set.

Anyway, $PS1[1] is here assured to be 0 even if it is tampered w/ between prompt draws. Within the parentheses there…

PS1[3]=(PS1[2]=$SECONDS-${PS1[3]})/3600

$PS1[2] is assigned the difference of $PS1[3] and $SECONDS, and $PS1[3] is assigned the quotient of that value and 3600. All values are here initialized. And so:

${PS1[1]#${PS1[3]%%*??}0}

…if there are at least two digits in $PS1[3] then the inner expansion there is null, and because we know $PS1[1] is 0 then if $PS1[3] can be substituted away to nothing, so also is $PS1[1] else it is expanded to its value. In this way only single digit values for each iteration of $PS1[3] assignments will expand a leading zero, and $PS1[3] is itself expanded modulo 60 immediately thereafter while being concurrently assigned the next successively smaller value for each of hours, minutes, seconds.

Rinse and repeat, until the last iteration when $PS1[3] is overwritten w/ the current value of $SECONDS so that it may be compared to $SECONDS once more when the prompt is next drawn.

Answered By: mikeserv

The best solution I found so far is this: https://github.com/jichu4n/bash-command-timer

Which prints [ 1s011 | May 25 15:33:44 BST ] aka the elapsed time on the right hand side after the executed command, so it doesn’t clutter you PS1.

The whole string and time format is configurable. Even the color and the precision is configurable. I know it might be a bit much for some minimalist out there, but it’s pretty cool.

Answered By: sebs

I’ve been trying to solve this problem off-and-on for years, always frustrated by the variability of when PS1 is evaluated. :-/

But today I found PS0, which is run immediately after you press ‘enter’ to run a command!

# Reformat seconds into days/hours/minutes/seconds:
function seconds2days() { awk -vtime="$1" 'BEGIN {days=int(time/86400);time-=days*86400;hours=int(time/3600);time-=hours*3600;minutes=int(time/60);time-=minutes*60;printf "%dd%02d:%02d:%02dn",days,hours,minutes,time}'; }

# Find the age of a file in seconds (MacOS+Linux stat compatible):
function file_rel_age() {
  local now filetime
  now=$(date +%s)
  if ! filetime=$(stat -f%m "$1" 2>/dev/null || stat -c%Y "$1" 2>/dev/null ); then return 1; fi
  echo "($(seconds2days $(( now - filetime ))))"
}

# Passing the start time as the modification time of ~/.cmd.tty###:
_LAST_CMD_TIMESTAMP=~/.cmd.$(basename $(tty))
PS0="$(date +%s > $_LAST_CMD_TIMESTAMP)"
PS1="$(file_rel_age $_LAST_CMD_TIMESTAMP; rm -f $_LAST_CMD_TIMESTAMP )$ "
unset _LAST_CMD_TIMESTAMP

Nice properties: if you didn’t run a command, it doesn’t print a time, and the timestamp file is absent:

$ sleep 10
(0d00:00:10)$ vi /tmp/a
(0d00:00:03)$ ls -l .cmd*
-rw-rw-r--. 1 pfudd pfudd 11 Aug 12 17:07 .cmd.0
(0d00:00:00)$
Answered By: Pfudd

Eminently doable since Bash 4.4, which introduced the PS0 env var:

export PS0='$(date +%s%03N > ~/.execStart.$$)'

printElapsedCommandTime () {
    local execStart execEnd execDur elaStr
    if [ -e ~/.execStart.$$ ]; then
        read execStart < ~/.execStart.$$ 2>/dev/null
        rm -f ~/.execStart.$$
        if [ -n "$execStart" ]; then
            execEnd=`date +%s%03N`
            execDur=$((execEnd-execStart))
            printf -v elaStr "Ela %u.%03u" $((execDur / 1000)) $((execDur % 1000))
            printf "%*se[32;7m%se[0mn" $((COLUMNS-${#elaStr})) "" "$elaStr"
        fi
    fi
}
export PROMPT_COMMAND='printElapsedCommandTime'

In PS0 (displayed after Bash has read a command but before Bash has begun executing it) we save in a tempfile the output of date +%s%03N, which is Epoch seconds and the first 3 digits of nanoseconds: overall effect is to save Epoch microseconds.

In PROMPT_COMMAND (evaluated after Bash has finished executing a command but before Bash has displayed the primary prompt PS1) we find Epoch microseconds again, and from it we subtract our saved-in-tempfile measurement.

We show elapsed time on its own line, over on the right. The lower printf is slightly tricky:

  • Format specifier %*s consumes two arguments, an integer and a string; the former says how many columns to use up when printing the latter. The two arguments we provide are N = $((COLUMNS-${#elaStr})) and the empty string ""; effect is to print N empty spaces.
  • e[32;7m requests inverse-video (“7”) text with green (“32”) background.
  • %s is what consumes our $elaStr string.
  • e[0m resets font color to your normal.

Bash 5.0 introduced the EPOCHREALTIME env var, which obviates the two date(1) invocations. EPOCHREALTIME is fractional seconds with microsecond granularity; with . removed it is same as output of date +%s%03N, and so we obtain an improved solution with just two small changes:

export PS0='echo ${EPOCHREALTIME/./} > ~/.execStart.$$)'   ### change here...

printElapsedCommandTime () {
    local execStart execEnd execDur elaStr
    if [ -e ~/.execStart.$$ ]; then
        read execStart < ~/.execStart.$$ 2>/dev/null
        rm -f ~/.execStart.$$
        if [ -n "$execStart" ]; then
            execEnd=`${EPOCHREALTIME/./}                   ### ...and here
            execDur=$((execEnd-execStart))
            printf -v elaStr "Ela %u.%03u" $((execDur / 1000)) $((execDur % 1000))
            printf "%*se[32;7m%se[0mn" $((COLUMNS-${#elaStr})) "" "$elaStr"
        fi
    fi
}
export PROMPT_COMMAND='printElapsedCommandTime'
Answered By: Vainstein K
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.