By default most shells don't log your history in a detailed durable way, which gives up one of the big advantages of working on the command line. Good history lets you look back at things you did months or years ago, in a searchable and skimmable fashion, so you can answer questions like "how did I generate this number?", "what was that trick I used?", or "if I'm doing something similar now what should I recall from last time?"

I use bash as my shell and have long used:

.bashrc:
  promptFunc() {
    echo "$(date +%Y-%m-%d--%H-%M-%S) $(hostname) $PWD $(history 1)" \
      >> ~/.full_history
  }
  PROMPT_COMMAND=promptFunc

I was recommending this to folks at work, but they use zsh. In zsh you can get a lot of the way here by setting INC_APPEND_HISTORY (append to history immediately instead of waiting for the shell to exit), SAVEHIST=1000000000 (effectively don't limit the history size on disk), and EXTENDED_HISTORY (to store timestamps with history entries). But you risk losing most of your history if you ever accidentally invoke zsh without these set, and it doesn't write the directory you ran the command in (which is metadata I reference a lot).

Mike tweaked my snippet to run in zsh:

.zshrc:
  precmd() {
    echo "$(date +%Y-%m-%d--%H-%M-%S) $(hostname) $PWD $(history -1)" \
      >> ~/.full_history
  }

The two changes are that zsh uses precmd instead of PROMPT_COMMAND, and that you need history -1 instead of history 1.

You can still use the same histgrep command on both:

function histgrep {
  local n_lines=10
  if [[ "$1" =~ ^[0-9]*$ ]]; then
    n_lines="$1"
    shift
  fi
  grep "$@" ~/.full_history | tail -n "$n_lines"
}

Note that this doesn't replace your shell's built-in history tooling, and I wouldn't recommend turning that off. This just a very cheap additional layer of logging with additional metadata and less risk of accidental deletion.

New to LessWrong?

New Comment
2 comments, sorted by Click to highlight new comments since: Today at 4:26 PM

I've finally gotten around to setting this up, and I tweaked it a bit more:

function precmd() {
    previous_status=$?

    if [[ -n "$preexec_waiting" ]]; then
        echo -E "$preexec_time $(date +%Y-%m-%dT%H:%M:%S) : $previous_status : $$ : $preexec_pwd : $(history -n -1)" \
            >> ~/.full_history
        preexec_waiting=''
    fi
}

function preexec() {
    # $1, $2 and $3 all have variants of the command to be executed, but none of
    # them is what I want. $1 and $3 can contain newlines. $2 and $3 are
    # alias-expanded. `history -1` in precmd is better.
    preexec_time=$(date +%Y-%m-%dT%H:%M:%S)
    preexec_pwd=$(print -Pn %~)
    preexec_waiting=1
}

Differences from Mike's version:

  • You get both time of start and time of finish for every command (I'd prefer time of start and duration, but oh well)
  • You also get exit status
  • Correct pwd in case the command is cd or similar, and pwd uses ~ for concision
  • The pid makes it easier to separate commands from two different shell instances
  • If you type a command with a newline, it gets saved as \n instead of getting split over multiple lines in the file
  • I think the : separators make it a bit easier to read
  • Doesn't include the number of the history entry (e.g. history -1 for me is 5785 cat ~/.full_history with a space before 5785 and two spaces after, where history -n -1 would be just cat ~/.full_history)

I think I'd rather have this in a sqlite db than a text file, but that would be more effort.

(edit: made slight improvements. https://github.com/larkery/zsh-histdb is probably worth looking into for sqlite history.)

Worth noting that (unless I'm missing something) you don't get "duration of command" from this, which you do get from zsh's extended history. You do get "time between previous command finishing and this one finishing", which might be good enough in a lot of circumstances.

So it's not strictly "additional" metadata for zsh (it might be for bash), but since you don't recommend disabling the built-in history that's not really a problem.