shell-mode のログを自動保存

久し振りのはてな
何か本当にブログとか日記を継続的に書くってことができないんですよね。
色々と書き溜めておきたいことってのはあるんですが、面倒になってしまって。
そういう意味で twitter とかの簡単に post できるアイデアってのは凄いと思います。

ところで、こんな記事を見掛けました。

ターミナルのログを自動保存したい - まちゅダイアリー(2011-05-27)

私も、同じ様なことを以前に考えていましたことがあります。
私の場合は、仕事で初心者に毛の生えた程度の同僚たちに、ターミナル上での作業ログを記録させるためでした。
そのため、本来は制御文字が含まれない形態が良いのですが、ページャで表示すればまあ使えるという割り切りで script コマンドを利用させていました。実際、何かあったときに保存されている内容を解析しようとすると、結構面倒だったりはするのですが。

screen に関しては、自動保存させる方法が判らなかったので、ログの記録という面では利用しませんでした。手動だと必要なときほど忘れていたりするものですよね。但し、screen は、これとは別に、端末入出力を切り離して detach/attach ができるということでは重宝しましたけど。

で、私自身は、端末操作はほぼ全て Emacs の shell-mode か ssh-mode を利用しているので、Emacs さんに自動で保存させる様にしています。

(defun process-buffer-visit-file ()
  (interactive)

  (save-excursion

    ;; bash が起動してバッファに反応があるまで待ち合わせる。
    (while (if (= (point-max) 1) (sit-for 0.5)))

    (let* ((date-alist '(("Jan"   1) ("Feb"  2) ("Mar"  3)
                         ("Apr"   4) ("May"  5) ("Jun"  6)
                         ("Jul"   7) ("Aug"  8) ("Sep"  9)
                         ("Oct"  10) ("Nov" 11) ("Dec" 12)))
           (date (current-time-string))
           (date-time-str
            (format "%02d%02d%02d-%02d%02d"
                    (string-to-number (substring date 22 24))
                    (car (cdr (assoc (substring date 4 7) date-alist)))
                    (string-to-number (substring date 8 10))
                    (string-to-number (substring date 11 13))
                    (string-to-number (substring date 14 16))))
           (buffer-name (buffer-name))
           (buffer-mame-string
            (let ((regexp "[*<>]")
                  (replace "")
                  (string buffer-name))
              (while (string-match regexp string)
                (set 'string (replace-match replace nil nil string)))
              string))
           (pid (number-to-string (emacs-pid)))
           (tty (save-excursion
                  (when (string-match "ssh" buffer-name)
                    (process-buffer-get-tty))
                  (goto-char (point-min))
                  (if (re-search-forward "/dev/" nil t)
                      (let ((start (point)))
                        (end-of-line)
                        (buffer-substring-no-properties start (point)))
                    "ttyX")))
           (tty-string
            (let ((regexp "/") (replace "") (string tty))
              (while (string-match regexp string)
                (set 'string (replace-match replace nil nil string)))
              string))
           (base-dir (expand-file-name "~/tmp/buffers/")))

      ;; バッファの内容を保存。(保存したファイルにバッファが結び付けられる)
      (write-file (concat base-dir date-time-str "-"
                          pid "-" tty-string "-" buffer-mame-string ".log"))

      ;; 時間調整。
      (sit-for 0.5)

      (dirs)
      (rename-buffer buffer-name)))
  (goto-char (point-max)))

(defun process-buffer-get-tty (&optional arg)
  (comint-send-string
   (get-buffer-process (current-buffer)) "tty\n"))

色々と面倒なことをしているのは、ユニークかつ後に判別し易いファイル名を生成するために工夫している部分ですね。

保存するファイル名は、shell-mode の場合、

"日付 (YYMMDD)"-"時刻 (HHMM)"-"プロセスID"-"キャラクタデバイスファイル名"-shell.log

  ex. ~/tmp/buffers/110528-2314-21558-pts12-shell.log

の様になります。
ssh-mode の場合は、base name の末尾の "shell" が "ssh-ホスト名" となります。

これ以外の部分の作りは単純で、

  1. write-file でバッファ内容を保存し、バッファにファイルを貼り付ける。
  2. バッファ名がファイル名に置き換えられてしまうので、元に戻す。

と、これだけです。
私は Emacs を通常、

  • 自動保存する
  • バックアップファイルを作成する

の設定で利用しているので、あとは Emacs が勝手に保存してくれますし、Emacs が落ちてしまったときでも、`recover-session' や `recover-this-file' することでほぼ最新状態に復元することができます。