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
verbose
setting is enabled withset -v
orbash -v
) PS0
is expanded, including parameters and command substitutions. Commands are unaffected by theDEBUG
trap, since it’s a prompt string- The
DEBUG
trap is evaluated (because of the command) - The command is evaluated
- The
DEBUG
trap is evaluated (because ofPROMPT_COMMAND
) PROMPT_COMMAND
is evaluatedPS1
is expanded, including parameters and command substitutions. Commands are unaffected by theDEBUG
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.