A few years ago I was using ZSH with Sindre Sorhus’s Pure prompt and generally enjoying the experience. The big, dumb, obvious caveat of using ZSH is that it’s not Bash. As a result, when you SSH into production machines that only have bash installed, you feel a little off-balance. Off-balance is not a feeling you want during an emergency on a live application server. As a result, I switched back to using bash everywhere.

How I learned to stop worrying and love bash.

I never really missed fancy syntax highlighting, or the nice globbing features, or most of ZSH really. The one thing I missed immensely was not so much a feature of ZSH as it was a feature of the Pure prompt I had been using: execution time for long-running commands shown in the prompt.

The time(1) command is a command that I never think to run until it’s too late. The Pure prompt is fancy. By default, if a particular command takes longer than 5 seconds to run, Pure calculates the running time of that command, and renders a human-readable version just above your current prompt.

~sleep 5

~ 5ssleep 10

~ 10s

Bash it until it works

Command execution time in Pure uses the preexec and precmd feature of ZSH, which doesn’t exist in Bash. Instead Bash has the less intuitive trap command and the PROMPT_COMMAND shell variable.

PROMPT_COMMAND is a variable that can be set in your bash config file that, “is executed as a command prior to issuing each primary prompt.” [bash(1)]

trap [-lp] [arg] [sigspec] can be used to listen for the DEBUG signal. “If a sigspec is DEBUG, the command arg is executed before every simple command”

Using these two tools in conjunction, you can approximate preexec and precmd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
debug() {
    # do nothing if completing
    [ -n "$COMP_LINE" ] && return

    # don't cause a preexec for $PROMPT_COMMAND
    [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return

    echo 'debug'
}

prompt() {
    echo 'prompt'
}

trap 'debug' DEBUG
PROMPT_COMMAND=prompt

So now debug is executed before each “simple command” and prompt is executed before each issuing the primary prompt.

$ echo 'hi'
debug
hi
prompt

Timing each command is then pretty trivial:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
debug() {
    # do nothing if completing
    [ -n "$COMP_LINE" ] && return

    # don't cause a preexec for $PROMPT_COMMAND
    [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return

    start_time=$(date +'%s')
}

prompt() {
    end_time=$(date +'%s')

    echo "$(( end_time - start_time )) seconds"
}

trap 'debug' DEBUG
PROMPT_COMMAND=prompt
$ sleep 2
2 seconds
$

Integrating that into the prompt and making it look pretty is just a little-bit of code away:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Human readable time output
# e.g., 5d 6h 3m 2s
format_time() {
  local _time=$1

  # Don't show anything if time is less than 5 seconds
  (( $_time < 5 )) && return

  local _out
  local days=$(( $_time / 60 / 60 / 24 ))
  local hours=$(( $_time / 60 / 60 % 24 ))
  local minutes=$(( $_time / 60 % 60 ))
  local seconds=$(( $_time % 60 ))
  (( $days > 0 )) && _out="${days}d"
  (( $hours > 0 )) && _out="$_out ${hours}h"
  (( $minutes > 0 )) && _out="$_out ${minutes}m"
  _out="$_out ${seconds}s"
  printf "$_out"
}

debug() {
    # do nothing if completing
    [ -n "$COMP_LINE" ] && return

    # don't cause a preexec for $PROMPT_COMMAND
    [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return

    start_time=$(date +'%s')
}

prompt() {
    end_time=$(date +'%s')
    time_f=$(format_time $(( end_time - start_time )))

    PS1="${time_f} (•◡•)❥"
}

trap 'debug' DEBUG
PROMPT_COMMAND=prompt
 (•◡•)sleep 5
5s (•◡•)❥

Motherfucking magic.