How to have command history with timestamps output to the terminal continuously?
With GNU awk
:
tail -fn+1 ~/.bash_history | awk '
/^#/{printf "%-4d [%s] ", ++n, strftime("%F %T", substr($0, 2)); next}; 1'
Here's the final product in action on a split-screen xterm from basically shell defaults to working in just a couple of commands:
A rougher way to do this than is demonstrated in the screenshot might look like this:
PS1='$( { date ; fc -l -0 ; } >${TGT_PTY} )'$PS1
Where ${TGT_PTY}
would be whatever you get out of the tty
command when actually running an interactive shell on the screen where you want your output. Or, really, you could use any writable file at all as it's essentially just a target for file-redirection.
I use the pty syntax for pseudo-terminal because I'm assuming it's an xterm of some kind, but you might as easily dedicate a vt - and your streamed history is always only a CTRL-ALT-Fn
key combo away. If it were me I might combine the two notions and make it a screen
or tmux
session on a dedicated vt... But I digress.
On a freshly booted machine I'm greeted with the typical /bin/login
prompt on a typical Linux getty
console. I press CTRL-ALT-F2
to access a less typical kmscon
console which behaves a lot more like an xterm
than a tty
. I enter the command tty
and receive in response /dev/pts/0
.
Generally xterms multiplex a single terminal device into multiple using pseudo-terminals - so if you were to do a similar thing in X11 by switching between terminal tabs or windows you'd likely receive output like /dev/pts/[0-9]*
as well. But the the virtual terminal consoles accessed with CTRL-ALT-Fn
key combinations are true(er) terminal devices and so receive their own /dev/tty[0-9]*
designation.
This is why after logging into console 2 when I type tty
at the prompt the response is /dev/pts/0
but when I do the same on console 1 the output is /dev/tty1
. In any case, back on console 2 I then do:
bash
PS1='$( { date ; fc -l -0 ; } >/dev/tty1 )'$PS1
There is no discernible effect. I carry on typing a few more commands out and then I switch to console 1 by pressing CTRL-ALT-F1
again. And there I find repeated entries that look like <date_time>\n<hist#>\t<hist_cmd_string>
for every command I typed on console 2.
Barring directly writing to a terminal device though, another option could look something like:
TGT_PTY=
mkfifo ${TGT_PTY:=/tmp/shell.history.pipe}
{ echo 'OPENED ON:'
date
} >${TGT_PTY}
And then maybe...
less +F ${TGT_PTY}
The rough prompt command doesn't meet your specs - no format string for date
and no formatting options for fc
either - but its mechanism doesn't require much: every time your prompt renders the last history command and the current date and time are written out to the ${TGT_PTY}
file you specify. It's as simple as that.
Watching and printing shell history is fc
's primary purpose. It's a shell built-in, even if date
is not. In zsh
fc
can provide all kinds of fancy formatting options, several of which apply to time-stamps. And of course, as you note above, bash
's history
can do the same.
In the interests of cleaner output, you can use a technique I've explained better here to set a persistent tracking variable in the current shell despite having to track it and process it in subshells within the prompt sequence.
Here's a portable means of formatting to your specifications:
_HIST() { [ -z ${_LH#$1} ] ||
{ date "+${1}%t[%F %T]"
fc -nl -0
} >${TGT_PTY}
printf "(_LH=$1)-$1"
}
: "${_LH=0}"
PS1='${_LH##*[$(($(_HIST \!)))-9]}'$PS1
I implement the last_history counter $_LH
that just tracks the latest updates so you don't write the same history command out twice - for instance just for pressing enter. There's a little wrangling necessary to get the variable incremented in the current shell so it retains its value even though the function is called in a subshell - which is, again, better explained in the link.
Its output looks like <hist#>\t[%F %T]\t<hist_cmd>\n
But that's just the fully portable version. With bash
it can be done with less and by implementing only shell builtins - which is likely desirable when you consider that this is a command that will run every time you press [ENTER]
. Here are two ways:
_HIST() { [ -z ${_LH#$1} ] || {
printf "${1}\t[%(%F %T)T]"
fc -nl -0
} >${TGT_PTY}
printf "(_LH=$1)-$1"
}
PROMPT_COMMAND=': ${_LH=0};'$PROMPT_COMMAND
PS1='${_LH##*[$(($(_HIST \!)))-9]}'$PS1
Alternatively, using bash
's history
command, you can define the _HIST
function in this way:
_HIST() { [ -z ${_LH#$1} ] ||
HISTTIMEFORMAT="[%F %T]<tab>" \
history 1 >${TGT_PTY}
printf "(_LH=$1)-$1"
}
The output for either method also looks like: <hist#>\t[%F %T]\t<hist_cmd>\n
though the history
method includes some leading whitespace. Still, I believe the history
method's timestamps will be more accurate as I don't think they'd need to wait for the referenced command to complete before acquiring their stamp.
You can avoid tracking any state at all in both cases if only you somehow filter the stream with uniq
- as you might do with mkfifo
as I mentioned before.
But doing it in the prompt like this means its always updated only as soon as need be just by the mere action of updating the prompt. It's simple.
You might also do something similar to what you're doing with tail
but rather set
HISTFILE=${TGT_PTY}
Feel free to play w/ the formatting, but this (I believe) does what you're asking for ... save to somewhere in your PATH, make executable and enjoy:
#!/bin/bash
count=$( echo "scale=0 ; $(cat ~/.bash_history | wc -l ) / 2" | bc -l )
tail -f ~/.bash_history | awk -v c=$count '{if($1 ~/^#/){gsub(/#/, "", $1);printf "%s\t", c; "date \"+%F %T\" --date @" $1 | getline stamp; printf "[%s]\t",stamp;c++}else{print $0}}'
I'm sure it can be optimised, but you get the idea.
Brief explanation: as the ~/.bash_history doesn't keep track of the count we first determine the number of entries. Then a bit of awk magic to get the formatting right, and keep track of the number of entries.