Preexec hooks are finally trivial in bash 5.3
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:
- The entire command line is printed to stdout (only if the 
verbosesetting is enabled withset -vorbash -v) PS0is expanded, including parameters and command substitutions. Commands are unaffected by theDEBUGtrap, since it’s a prompt string- The 
DEBUGtrap is evaluated (because of the command) - The command is evaluated
 - The 
DEBUGtrap is evaluated (because ofPROMPT_COMMAND) PROMPT_COMMANDis evaluatedPS1is expanded, including parameters and command substitutions. Commands are unaffected by theDEBUGtrap, 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.