Monday, November 10, 2008

Timestamped Shell History

Occasionally I save myself a lot of head-scratching by being able to refer to a timestamped history of my work in a particular shell session. You know the feeling, when you say either "I thought I already did that" or "Didn't I do X before Y?" Of course, usually you can piece together a timeline by looking at file modification times or revision control logs. Sometimes you absolutely cannot figure out the history -- a deleted file has no modification time -- and other times it would just be handy to have a transcript telling you when you did what.

Here are the ingredients to keeping a timestamped history:
  1. Emacs shell buffer: M-x shell.
  2. Timestamp script running in the background.
  3. Lisp code to remove shell output, leaving prompts and history.
Once you get used to running your shell within emacs, you'll never go back. It keeps a complete history for you, which you can search or navigate just like any emacs buffer. History keystrokes -- M-p, M-n, and M-r as opposed to most shells' C-p, C-n, C-r -- survive past subshells, su, ssh, and even things like ftp. If you need multiple shell buffers (say, on different hosts), use M-x rename-buffer.

The hitch is that after a few weeks in your emacs session, a shell buffer can grow very large. Here is a Lisp function to put in your .emacs which knocks it back down to size (make sure the emacs shell-prompt-pattern variable matches your shell prompt):


(defun engisneering-clean-shell-buffer ()
(interactive)
(let ((beg (point-min)))
(goto-char (point-min))
(if (> 10000 (buffer-size)) (buffer-disable-undo))
(while (re-search-forward shell-prompt-pattern nil t)
(if (not (= (point) (line-end-position)))
(progn
(forward-line 0) ;; beginning of line, ignore prompt boundary
(if (not (= (point) beg))
(delete-region beg (point)))
(forward-line 1)
(setq beg (point)))))
(buffer-enable-undo)
)
)


That will also be useful when you're ready to save your timestamped history. The timestamping is easy -- put this shell script into a file called "timestamp", and give it execute permission:


#!/bin/bash

while ((1)); do
echo -n "timestamp> "
date
echo -n "prompt> "
sleep 3600
done


There are two things to note about the script. First, when it outputs a timestamp, it will match the prompt pattern, so that it won't get deleted by our Lisp cleaning function. Second, it ends with "prompt> " so that the next time you hit return, emacs doesn't think the output from the timestamp is part of your next command.

So, whenever you start a shell buffer, run "timestamp&", to get the date and time printed out once an hour while you do other things.

Finally, here is a little script to help you save your shell histories. Create a directory to hold your histories; name the history files after the host the shell was on, and date them (including the year). Save them periodically, just like any of your work.


(defun engisneering-save-shell-buffer ()
(interactive)
(let ((name (buffer-name)))
(save-buffer 0)
(setq buffer-file-name nil)
(rename-buffer name)
(buffer-disable-undo)
(delete-region (point-min) (point-max))
(buffer-enable-undo)
)
)