When ricing their prompts, bash powerusers often face a difficult problem: It is not trivial to run something before the execution of a command and also have it run in the current shell. Commonly, you would want to do such a thing to automatically measure the time it took for your command to run. The most well-known solution is to use bash-preexec, which uses bash’s built-in DEBUG trap. However, it’s a bit convoluted for the task it’s trying to achieve, since the DEBUG trap is not only triggered before your command runs, but before any simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function, and it will also be active inside your PROMPT_COMMAND (not in prompt expansion tho). Therefore, it has to be very careful in it’s implementation. And of course, you cannot use the DEBUG trap for your own sake when you use this.

Since the release of bash 5.3 however, there’s finally a new, easy way to achieve this:

1PS0='${ __cmd_start=$BASH_MONOSECONDS;}'
2PS1="[ \$((BASH_MONOSECONDS - __cmd_start))s ] \u@\h:\w\$ "

The prompt parameter PS0 was always pretty powerful, but unfortunately, the only way to hook into into was via command substitution. Before bash 5.3, command substitution always ran in a subshell, making it impossible to communicate state in a clean way (you could always use temporary files or other IPC mechanisms, of course). Using the new alternative command substitution syntax however, and in the specific case of measuring execution time using the new special shell variable BASH_MONOSECONDS, it’s now trivial, since you can set variables in the current shell context even inside PS0.

To somewhat properly expose a preexec and precmd function, you can trivially do something like this:

 1preexec () {
 2    echo "Running right before the command and with pid $BASHPID"
 3}
 4
 5precmd () {
 6    echo "Running right after the command and with pid $BASHPID"
 7}
 8
 9PS0='${ preexec;}'
10PROMPT_COMMAND="precmd"

With this setup, running echo $BASHPID will execute the preexec function, showing a certain pid, then execute your command, showing the same pid, and then execute the precmd function, showing the same pid yet again.

Also interesting: The order in which stuff gets expanded and evaluated is as follows, at least in bash 5.2.21 and 5.3.0:

  1. The entire command line is printed to stdout (only if the verbose setting is enabled with set -v or bash -v)
  2. PS0 is expanded, including parameters and command substitutions. Commands are unaffected by the DEBUG trap, since it’s a prompt string
  3. The DEBUG trap is evaluated (because of the command)
  4. The command is evaluated
  5. The DEBUG trap is evaluated (because of PROMPT_COMMAND)
  6. PROMPT_COMMAND is evaluated
  7. PS1 is expanded, including parameters and command substitutions. Commands are unaffected by the DEBUG trap, since it’s a prompt string

In other words, if you really want to measure the time from the point where you lose control to bash/readline and reaquire that control, you should be using PS0 (earlier than DEBUG trap) and PS1 (later than PROMPT_COMMAND) because they are completely unaffected by the DEBUG trap.