When did I run that command?
Update your Bash prompt with the command start time.
· 7 min read
By Sky · @countrmeasure
I often ask “What time did I run that command?”
For a long time I couldn’t find out quickly and accurately.
It seems easy to do with other shells, but I couldn’t find a way to do it with Bash, so I cooked one up. Here it is.
How is this an improvement?
For a long time people have included the time in the Bash prompt at the time the prompt is written.
The problem with this is that a prompt can sit for any length of time before being used to run a command; the time in the prompt and command start time are uncoupled.
Here’s an example.
The example shows me working in the terminal at 20:45, going to to do something else for five minutes, then coming back to the terminal at 20:50. The prompt for the command I ran when I came back at 20:50 continues to show 20:45 though, and it’s only the prompt printed after that final command which shows the time as 20:50.
Sure, I can get into the habit of looking at the prompt which appears after a command to show me when it ran, but that’s clunky and unintuitive. Also, that workaround breaks for long-running commands, because the next prompt shows the time the command finished, which might be a long time after it was executed.
My solution modifies the prompt to show the time when the command is executed, not the time the prompt was written, removing that confusion.
How can I do this too?
First, make a backup of your .bashrc
file because we’re about to change it.
In .bashrc
, delete or comment out the existing definitions of PS0
, PS1
and PS2
(some of which may not be present) and put this snippet in their
place.
DIRECTORY="\w"
DOUBLE_SPACE=" "
NEWLINE="\n"
NO_COLOUR="\e[00m"
PRINTING_OFF="\["
PRINTING_ON="\]"
PROMPT_COLOUR="\e[0;33m"
PS1_PROMPT="\$"
PS2_PROMPT=">"
RESTORE_CURSOR_POSITION="\e[u"
SAVE_CURSOR_POSITION="\e[s"
SINGLE_SPACE=" "
TIMESTAMP="\A"
TIMESTAMP_PLACEHOLDER="--:--"
move_cursor_to_start_of_ps1() {
command_rows=$(history 1 | wc -l)
if [ "$command_rows" -gt 1 ]; then
let vertical_movement=$command_rows+1
else
command=$(history 1 | sed 's/^\s*[0-9]*\s*//')
command_length=${#command}
ps1_prompt_length=${#PS1_PROMPT}
let total_length=$command_length+$ps1_prompt_length
let lines=$total_length/${COLUMNS}+1
let vertical_movement=$lines+1
fi
tput cuu $vertical_movement
}
PS0_ELEMENTS=(
"$SAVE_CURSOR_POSITION" "\$(move_cursor_to_start_of_ps1)"
"$PROMPT_COLOUR" "$TIMESTAMP" "$NO_COLOUR" "$RESTORE_CURSOR_POSITION"
)
PS0=$(IFS=; echo "${PS0_ELEMENTS[*]}")
PS1_ELEMENTS=(
# Empty line after last command.
"$NEWLINE"
# First line of prompt.
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON"
"$TIMESTAMP_PLACEHOLDER" "$DOUBLE_SPACE" "$DIRECTORY" "$PRINTING_OFF"
"$NO_COLOUR" "$PRINTING_ON" "$NEWLINE"
# Second line of prompt.
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON" "$PS1_PROMPT"
"$SINGLE_SPACE" "$PRINTING_OFF" "$NO_COLOUR" "$PRINTING_ON"
)
PS1=$(IFS=; echo "${PS1_ELEMENTS[*]}")
PS2_ELEMENTS=(
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON" "$PS2_PROMPT"
"$SINGLE_SPACE" "$PRINTING_OFF" "$NO_COLOUR" "$PRINTING_ON"
)
PS2=$(IFS=; echo "${PS2_ELEMENTS[*]}")
shopt -s histverify
Now open a new shell and try it out.
You can change the digits 33
in the PROMPT_COLOUR
variable for a different
prompt colour. See the FG Code column in this colour table in Wikpedia's ANSI escape code article for colour
codes.
How does it work?
The overview
First the PS1
prompt is printed in the terminal and it waits for a command.
Once a command is typed and Enter is pressed, PS0
is expanded. This runs
the move_cursor_to_start_of_ps1
function. This function moves the cursor to
the beginning of the first line of the PS1
prompt, overwrites the string
--:--
with the current time, then returns the cursor to its earlier position.
Then the command executes.
Now let’s look at each portion of the code in more detail.
The constants
DIRECTORY="\w"
DOUBLE_SPACE=" "
NEWLINE="\n"
NO_COLOUR="\e[00m"
PRINTING_OFF="\["
PRINTING_ON="\]"
PROMPT_COLOUR="\e[0;33m"
PS1_PROMPT="\$"
PS2_PROMPT=">"
RESTORE_CURSOR_POSITION="\e[u"
SAVE_CURSOR_POSITION="\e[s"
SINGLE_SPACE=" "
TIMESTAMP="\A"
TIMESTAMP_PLACEHOLDER="--:--"
Strictly speaking, these constants don’t need to be created, but I find that
they make building, reading and debugging PS0
, PS1
and PS2
much easier.
The characters which start with a \
are interpreted in the prompt as
explained in the Bash Reference Manual section on controlling the prompt.
Constant values which don’t start with a \
are strings which will be printed
in the prompt.
The SAVE_CURSOR_POSITION
and RESTORE_CURSOR_POSITION
values are escape
codes which save and restore the cursor position but aren’t printed in the
prompt.
The PROMPT_COLOUR
and NO_COLOUR
values are escape codes which set and unset
a colour but aren’t printed in the prompt.
The cursor repositioning function
move_cursor_to_start_of_ps1() {
command_rows=$(history 1 | wc -l)
if [ "$command_rows" -gt 1 ]; then
let vertical_movement=$command_rows+1
else
command=$(history 1 | sed 's/^\s*[0-9]*\s*//')
command_length=${#command}
ps1_prompt_length=${#PS1_PROMPT}
let total_length=$command_length+$ps1_prompt_length
let lines=$total_length/${COLUMNS}+1
let vertical_movement=$lines+1
fi
tput cuu $vertical_movement
}
This is where the magic happens.
No matter how many lines the command spans and where the cursor ends up as a
result, the cursor has to be repositioned to the beginning of the prompt to
overwrite the --:--
placeholder with the time the command was run.
This function works out how many lines are in the command, and repositions the cursor accordingly.
Here are examples of commands of varying lengths which must be handled.
In move_cursor_to_start_of_ps1
, the command_rows
variable gets the number
of lines the command is split over.
If it’s a multi-line command, we take the first path through the if
statement. In this case, the number of lines below the top line of the PS1
prompt that the cursor will be is the number of lines in the command plus one,
so that’s what we’ll set vertical_movement
to. The tput
command moves the
cursor up by that number of lines and places it at the start of the top line of
the PS1
prompt.
If it’s a single-line command, we take the second path through the if
statement. We get the length of the command and add add it to the length of the
second line of the PS1
prompt, then divide that by the number of columns in
the terminal window (the COLUMNS
variable) to get the number of lines the
command takes up. This allows for long commands which wrap over multiple lines.
Then we use the tput
command to place the cursor at the start of the top line
of the PS1
prompt.
The PS variables
PS0_ELEMENTS=(
"$SAVE_CURSOR_POSITION" "\$(move_cursor_to_start_of_ps1)"
"$PROMPT_COLOUR" "$TIMESTAMP" "$NO_COLOUR" "$RESTORE_CURSOR_POSITION"
)
PS0=$(IFS=; echo "${PS0_ELEMENTS[*]}")
PS1_ELEMENTS=(
# Empty line after last command.
"$NEWLINE"
# First line of prompt.
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON"
"$TIMESTAMP_PLACEHOLDER" "$DOUBLE_SPACE" "$DIRECTORY" "$PRINTING_OFF"
"$NO_COLOUR" "$PRINTING_ON" "$NEWLINE"
# Second line of prompt.
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON" "$PS1_PROMPT"
"$SINGLE_SPACE" "$PRINTING_OFF" "$NO_COLOUR" "$PRINTING_ON"
)
PS1=$(IFS=; echo "${PS1_ELEMENTS[*]}")
PS2_ELEMENTS=(
"$PRINTING_OFF" "$PROMPT_COLOUR" "$PRINTING_ON" "$PS2_PROMPT"
"$SINGLE_SPACE" "$PRINTING_OFF" "$NO_COLOUR" "$PRINTING_ON"
)
PS2=$(IFS=; echo "${PS2_ELEMENTS[*]}")
The point of declaring all those constants earlier was to make the prompt declarations more semantic, and not just the usual one-liner soup of backslashes and square brackets. I think it works.
For each of PS0
, PS1
and PS2
, we make a list of their elements, then join
the elements together.
For comparison, here’s the same code written in the more traditional fashion.
PS0="\e[s\$(move_cursor_to_start_of_ps1)\e[0;33m\A\e[00m\e[u"
PS1="\n\[\e[0;33m\]--:-- \w\[\e[00m\]\n\[\e[0;33m\]$ \[\e[00m\]"
PS2="\[\e[0;33m\]> \[\e[00m\]"
This is far more terse, but it’s also much more difficult to read, modify and debug.
But let’s move away from style and back to functionality.
You’re probably already familiar with the PS1
prompt. All I’ll say about this
particular PS1
is that we have to switch the prompt colour on then off for
each of the two lines, otherwise the colourisation of the second line with the
$
prompt doesn’t work properly.
The PS0
variable is comparatively recent, apparently having been introduced
to Bash in 2016. It is expanded after the Enter key is pressed and before the
command is run. You can see that this particular PS0
is saving the position
of the cursor once Enter is pressed, moving the cursor to the start of the
prompt, writing the current time there which overwrites the --:--
placeholder, then returning the cursor to the saved position ready for the
command to execute.
You’ll notice that the PS2
variable is also declared. Once you’ve coloured
PS1
, you might as well colour PS2
to match it.
The shopt command
shopt -s histverify
Last but not least, this covers a corner case by showing the result of a
history substitution command in a subsequent prompt for verification rather
than executing it immediately. Without it, history substitution commands like
!!
and !274
cause the time to be written one line too low when the command
is run.
I won’t spend time explaining why. If you’re interested, comment it out
and see how your prompt behaves when you give it !!
. It has to do with the
position of the cursor when PS0
is expanded.
One less niggle
The command line is simple and satisfying, and if you spend hours in it every day it’s also full of little niggles.
This tweak removes one of those niggles for me. If you’re also a Bash user, I hope it can do the same for you.