ㅈㅅㄹ

이맥스는 기본적으로 버퍼의 변경점을 자동으로 저장하는 기능이 있다. 물론 강제로 M-x do-auto-save RET 명령어를 사용해서 자동 저장을 시키는 방법도 있지만 보통은 주기적으로 현재 파일의 경로에 # 접두사와 접미사를 붙여서, 즉 test.txt 파일이라면 #test.txt#와 같은 파일명으로 파일을 저장한다.


그러나 사실 로컬에서야 그다지 문제가 되지 않지만, tramp를 커넥션이 느리거나 네트워크 상황이 좋지 않은 곳에서 사용할 때 가끔 자동 저장 파일을 저장하겠다고 이맥스가 미쳐 날 뛰는 상황을 경험하게 된다. 그래서 자동 저장 파일을 파일의 원래 위치에 만드는 것 보다는 특정 디렉토리 한 곳에서 생성하도록 하는 것이 유리한 측면이 많다.

;; store backup files in .emacs.d/backups
(defconst emacs-backup-directory "~/.emacs.d/backups/")
(setq backup-directory-alist `((".*" . ,emacs-backup-directory))
      auto-save-file-name-transforms `((".*" ,emacs-backup-directory t)))

이렇게 설정하게 되면, 모든 자동 저장 파일은 $HOME/.emacs.d/backups 아래에 저장되게 된다. 물론 추가적으로 이렇게 자동 저장 파일을 한 곳으로 몰아 두게 되면 파일 시스템 이곳 저곳에 남아 있는 자동 저장 파일의 흔적들을 굳이 찾아서 지울 필요 없이, 그냥 $HOME/.emacs.d/backups 아래에 있는 파일들만 알아서 지워 주면 되니 그런 측면에서도 꽤 유용하다.

  /home/ysjang/.emacs.d/backups:
  total used in directory 28 available 403893348
  drwx------  2 ysjang ysjang 4096  4월  4 11:48 .
  drwxrwxr-x 15 ysjang ysjang 4096  4월  4 11:28 ..
  -rw-rw-r--  1 ysjang ysjang 3073  4월  4 12:52 #!home!ysjang!blog!review-recover-file.html#
  -rw-rw-r--  1 ysjang ysjang 2009  4월  1 14:57 #!home!ysjang!Downloads!shBrushLisp.js#
  -rw-rw-r--  1 ysjang ysjang   12  3월 31 15:22 #!home!ysjang!.emacs.d!test.html#
  -rw-rw-r--  1 ysjang ysjang   30  4월  4 09:32 #!home!ysjang!.emacs.d!test.org#

자동 저장을 했으면 이 내용을 바탕으로 복구를 할 수 있어야 할것이다. 자동 저장 기능을 켜 둔 채로 이맥스를 쓰다 보면, 파일을 새로 열 때 shBrushLisp.js has auto save data; consider M-x recover-this-file와 같이 복구 가능하다는 메시지가 표시 되는 것을 본 적이 있을 것이다. 이 것은 자동 저장된 파일이 존재할 경우 나타나는 메시지로써 해당 버퍼에서 M-x recover-this-file RET이나 M-x recover-file RET 파일명 RET을 입력함으로써 auto save된 파일의 내용으로 현재 버퍼를 복구할 수 있다.


recover-file의 원래 실행 결과


다만 위의 그림처럼 보듯이, 파일의 저장시간을 표시하는 것만으로는 그다지 유용하다는 느낌을 받을 수 없다. 이전에 kill-buffersave-some-buffers에서 변경점을 보여 주듯이 recove-file을 할 때에도 현재 버퍼와 자동 저장된 파일을 비교해서 표시해준다면 확실히 그 자리에서 복구 여부를 판단하기 쉬울 것이다. recover-file의 경우 advice function으로는 정의하기 난감해서 이전에 save-some-buffers때 처럼, 이맥스 기본 lisp library의 files.el을 가져와서 고쳐서 정의하도록 했다.

;; to override recover-file (from 24.5.5 files.el)
(defun my:recover-file (file)
  "Visit file FILE, but get contents from its last auto-save file."
  ;; Actually putting the file name in the minibuffer should be used
  ;; only rarely.
  ;; Not just because users often use the default.
  (interactive "FRecover file: ")
  (setq file (expand-file-name file))
  (if (auto-save-file-name-p (file-name-nondirectory file))
      (error "%s is an auto-save file" (abbreviate-file-name file)))
  (let ((file-name (let ((buffer-file-name file))
                     (make-auto-save-file-name))))
    (cond ((if (file-exists-p file)
               (not (file-newer-than-file-p file-name file))
             (not (file-exists-p file-name)))
           (error "Auto-save file %s not current"
                  (abbreviate-file-name file-name)))
          ((save-window-excursion
             (with-temp-buffer-window
              "*Directory*" nil
              #'(lambda (window _value)
                  (with-selected-window window
                    (unwind-protect
                        (yes-or-no-p (format "Recover auto save file %s? " file-name))
                      (when (window-live-p window)
                        (quit-restore-window window 'kill)))))
              (let* ((file-buffer (find-buffer-visiting file))
                     (diff-switches "-urN"))
                (display-buffer file-buffer '(display-buffer-same-window))
                (delete-other-windows)
                (diff file-buffer file-name nil 'noasync)
                (with-current-buffer standard-output
                  (let ((switches dired-listing-switches))
                    (if (file-symlink-p file)
                        (setq switches (concat switches " -L")))
                    ;; Use insert-directory-safely, not insert-directory,
                    ;; because these files might not exist.  In particular,
                    ;; FILE might not exist if the auto-save file was for
                    ;; a buffer that didn't visit a file, such as "*mail*".
                    ;; The code in v20.x called `ls' directly, so we need
                    ;; to emulate what `ls' did in that case.
                    (insert-directory-safely file switches)
                    (insert-directory-safely file-name switches)))
                (fit-window-to-buffer
                 (select-window (display-buffer standard-output '(display-buffer-at-bottom)))))))
           (switch-to-buffer (find-file-noselect file t))
           (let ((inhibit-read-only t)
                 ;; Keep the current buffer-file-coding-system.
                 (coding-system buffer-file-coding-system)
                 ;; Auto-saved file should be read with special coding.
                 (coding-system-for-read 'auto-save-coding))
             (erase-buffer)
             (insert-file-contents file-name nil)
             (set-buffer-file-coding-system coding-system))
           (after-find-file nil nil t))
          (t (user-error "Recover-file canceled")))))

(eval-after-load 'files
  `(progn
     (defalias 'save-some-buffers 'my:save-some-buffers)
     (defalias 'recover-file 'my:recover-file)))

recover-file의 경우는 각 파일의 저장 시간을 보는 것도 중요하므로 파일의 변경점을 보여 주는 것과 동시에 원래 recover-file의 표시 내용처럼 파일의 directory listing 결과도 함께 보여 줄 필요가 있다. 따라서 alias를 정의 한 이후, 자동 저장이 존재하는 버퍼에서 M-x recover-this-file RET를 실행하면 아래의 이미지와 같이, 왼쪽에는 해당 버퍼, 오른쪽에는 변경점, 그리고 아래쪽에는 원래 보여주던 directory listing 결과를 보여준다.


수정된 recover-file의 실행 결과


이제 복구 할 건지 말 건지를 파일을 열어서 일일히 확인할 필요 없이 복구 프롬프트에서 간단하게 결정할 수 있다.