9

I use a simple alias to enable the "tracking" of commands in one or many terminal window(s):

alias trackmi='export PROMPT_COMMAND="history -a; $PROMPT_COMMAND"'

Then I just tail -f my .bash_history file in another terminal on the workspace to get immediate feedback. I have just enabled unlimited history and updated my history format (export HISTTIMEFORMAT="[%F %T] ") in .bashrc. Of course the history command displays the timestamps. But the format the history file per se is:

#1401234303
alias
#1401234486
cat ../.bashrc 

How can I have the Unix time converted and the whole command displayed on a single line just like with the history command, including numbering:

578  [2014-05-27 19:45:03] alias
579  [2014-05-27 19:48:06] cat ../.bashrc 

...and follow that. Or find a way to continuously output the output of the history command to the terminal?

3 Answers 3

7
+100

With GNU awk:

tail -fn+1 ~/.bash_history | awk '
  /^#/{printf "%-4d [%s] ", ++n, strftime("%F %T", substr($0, 2)); next}; 1'
1
  • Works great an initially displays faster if I update the other solution with fn+1 to compare! Thanks!
    – user44370
    May 28, 2014 at 9:25
6

Here's the final product in action on a split-screen xterm from basically shell defaults to working in just a couple of commands:

enter image description here

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}
5
  • I'm actually editing it in another tab... More time please?
    – mikeserv
    May 29, 2014 at 8:07
  • Well, I'll hit save right now actually, @illuminÉ - but you'll see where I left off...
    – mikeserv
    May 29, 2014 at 8:08
  • @illuminÉ - By the way - and I hope I'm right about this - I'm assuming you entered the command as written ${TGT_PTY} and all? If so, it would explain the 'ambiguous redirect' because that would be an empty variable. You need a file. /dev/pts/[num] in all likelihood -
    – mikeserv
    May 29, 2014 at 8:47
  • It does work! The printscreen helped! I wish your first and only block of code were what you put in the printscreen and a clear reference to choosing which pts your want the output to go to and entering the tab into the function - nothing else is required. Using a variable to describe something I had to enter manually to try it quickly is not good practice in my opinion as you have to dig into the text to figure it all out, which obsfuscates the solution. Also all the best to you and your family.
    – user44370
    May 31, 2014 at 2:22
  • @illuminÉ - nevermind about cat I was paranoid. It works fine - even 12 hours later.
    – mikeserv
    May 31, 2014 at 13:36
4

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.

6
  • That doesn't work if there are multi-line entries. Also tail -f will read 10 lines initially which have already been included in your count. It assumes GNU date in a non-POSIX environment (POSIXLY_CORRECT unset). It runs one shell and one date command per timestamp. May 28, 2014 at 8:59
  • @StephaneChazelas For the multi-line entries, on my setup they seem to register with both solutions. Something in my setup maybe?
    – user44370
    May 28, 2014 at 9:23
  • 1
    @illuminÉ, tink's count counts half the line in .bash_history, and then increments for every line not starting with #, so the reported history count is likely to be wrong. Using count=$(grep -c '^#' ...) would probably be better, but in any case, those history numbers are likely to end up out of sync especially if you've got more than 2 bash running at the same time. May 28, 2014 at 9:48
  • @StephaneChazelas Sincerely as I can't fully appreciate either solution I'm very thankful for the explanation! Indeed the count is different, much higher consequently here... I can see you built your own solution around strftime, which is basically what the history command leverages.
    – user44370
    May 28, 2014 at 10:13
  • 1
    Thanks for the feed-back @StephaneChazelas, I'll see whether I can work around those :)
    – tink
    May 28, 2014 at 23:34

You must log in to answer this question.