Today I learned to make Bash print start and end timestamps for tracking execution times.

To be honest the first result looks ugly, but it’s an achievement. Read on to learn how I did it.

$ lsb_release -a; sleep 3
----------------------------------------------------------------------------------------------------------------------
2024-03-31T06:07:20+02:00
----------------------------------------------------------------------------------------------------------------------
LSB Version:	core-11.1.0ubuntu2-noarch:printing-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.6 LTS
Release:	20.04
Codename:	focal
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2024-03-31T06:07:23+02:00
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
$

It’s bugged me a for while that Bash doesn’t display the times a command starts and ends.

It bugs me most when I run a command and later realize I need to track its duration for performance measurements or the execution timestamps for auditing.

I knew the variable PS1 sets the prompt string. And I knew how to embed a command so Bash would rerun it time to display the latest output. I could use that to display the end time of the previous command, but without the start time it’s useless.

While searching for something completely different in the Bash manual, I stumbled across the missing piece PS0. Its value is expanded like PS1 and displayed by interactive shells after reading a command and before the command is executed.

Now PS0 can print the start time and PS1 can print the end time. That provides automatic execution timestamp auditing and allows computing the duration.

Its discovery carried me away. I wanted to know how to draw a horizontal rule to make a clear saparation between the injected output and the command’s normal output. I googled bash draw a horizontal line for good results. Patrick DeYoreo shows how to draw a line between Bash commands. Vivek Gite shows how to repeat a character ‘n’ times in Bash, which amounts to the same thing. Below I show how to adapt both solutions as a Bash function.

Patrick’s insight is to use the COLUMNS variable to know the terminal width, or how many characters (columns) fit in one line of text, and so draw exactly enough characters. It uses a loop to print each line segment and increments a line counter to stop after printing a segment for each column.

function horizontal_line {
    local line=0
    while ((line++ < COLUMNS)); do
      printf "${1:--}"
    done
    printf '\n'
}

Vivek’s insight is to trick the printf builtin into drawing a line. The format specifier contains the line segment. The trick is %.0s which I think means “print 0 characters of the argument” and to pass a sequence of arguments as long as the terminal width. It draws the same line.

function horizontal_line {
    printf -- "${1:--}"'%.0s' $(seq $COLUMNS)
    printf '\n'
}

I prefer Patrick’s solution because it’s easier for me to understand.

My own contribution is ${1:--}. It’s the first parameter, the character to draw the line with. The default character is -. It needs braces to use the syntax :- that separates the parameter name 1 from the default.

After defining the function here’s how I set PS0and PS1:

PS0='$(horizontal_line; date --iso=seconds; horizontal_line)\n'
PS1='$(horizontal_line "!"; date --iso=seconds; horizontal_line "!")\n$ '

(Probable bug: For some reason the second horizontal_line call doesn’t put a baked-in newline and so first line of the command output fused to the prompt. The workaround is to put another literal newline at the end of the prompt.)

The next step is to write the timestamp and rule in one horizontal line to save space. Maybe the horizontal_line function needs a parameter to set the length, or a new function takes a string and a horizontal position to overwrite the line.