firestarter 20161219.523(in MELPA)
Execute (shell) commands on save

概要

ファイル保存後に特定のアクションをさせたいことはよくあります。

昨日紹介したauto-shell-command.el(レビュー) もそのひとつです。

auto-shell-command.elはinit.elにまとめて書く必要がある上に、
シェルコマンドの実行しかサポートされていません。

今回紹介する firestarter.el は、ファイルローカル変数や
ディレクトリローカル変数に保存後の処理を記述する方式です。

使い方は簡単で、ファイル・ディレクトリローカル変数 firestarter
値を設定するだけです。

  • 文字列: 実行させるシェルコマンド
  • シンボル: 実行させるEmacsコマンド
  • S式: 実行させるS式

シェルコマンドには以下の%記法が使えます。

;;; firestarter-formatより
(let* ((buffer (buffer-name))           ;%b バッファ名
       (path (or (buffer-file-name) "")) ;%p フルパス
       (file (file-name-nondirectory (or path ""))) ;%f ファイル名(ディレクトリ除外)
       (stem (file-name-sans-extension file)) ;%s 拡張子を抜いたフルパス
       (extension (file-name-extension file t))) ;%e 拡張子
  (format-spec string (format-spec-make ?b buffer ?p path ?f file
                                        ?s stem ?e extension)))

シンボル…たとえば「firestarter: gomoku」とすれば
保存時に五目並べをします(意味ねぇw)

S式は「firestarter: (message "ok")」などと指定します。

インストール

パッケージシステムを初めて使う人は
以下の設定を ~/.emacs.d/init.el の
先頭に加えてください。

(package-initialize)
(setq package-archives
      '(("gnu" . "http://elpa.gnu.org/packages/")
        ("melpa" . "http://melpa.org/packages/")
        ("org" . "http://orgmode.org/elpa/")))

初めてfirestarterを使う方は
以下のコマンドを実行します。

M-x package-install firestarter

アップグレードする方は、
以下のコマンドでアップグレードしてください。
そのためにはpackage-utilsパッケージが必要です。

M-x package-install package-utils (初めてアップグレードする場合のみ)
M-x package-utils-upgrade-by-name firestarter

セキュリティと利便性の兼ね合い

ファイル・ディレクトリローカル変数というのは本来危険と隣り合わせです。

悪意のある人がシステムを壊滅させる設定をさせることができるからです。

そのためEmacs側にセーフティガードが設けられていて、
変数を有効にするか訊いてきます。

ここで!を押せば安全な値として恒久的に有効にできますし、
nを押せば拒否できます。

20150413073345.png
Fig1: _

とはいえ毎回それを訊かれるのはうざったいです。

安全性と利便性というのはしばしばトレードオフになります。

今回、両者をうまくとりまとめるため、以下の方針にしています。

  • firestarterに任意の値を受け付けるようにする
  • ファイルを開いたときにfirestarterの値を示す
  • ファイルに関連付けられていないバッファ保存時に警告

任意の値を受け付けるのは本当に危険なので、
毎回訊かれる代わりにfirestarterを意識させるようにします。

20150413080907.png
Fig2: ファイルを開いたらfirestarterの設定をお知らせ

20150413080914.png
Fig3: 実行したことをお知らせ

設定 150413064753.firestarter.1.el(以下のコードと同一)

(firestarter-mode)
;;; 失敗時に結果をお知らせ
(setq firestarter-default-type 'failure)
;;; コマンド実行後に実行したコマンドを知らせる
(defun firestarter-sentinel--show-command (&rest ignore)
  (run-at-time 1 nil 'message (format "firestarted: %s" (firestarter-format firestarter))))
(advice-add 'firestarter-sentinel :before 'firestarter-sentinel--show-command)

;;; セキュリティと利便性を両立
;; firestarterに任意の値を受け取れるようにする
(put 'firestarter 'safe-local-variable 'identity)
;; firestarterが設定されているときはmessageでお知らせ
(defun find-file-hook--firestarter-notify ()
  (when (and (bound-and-true-p firestarter-mode) firestarter)
    (run-at-time 1 nil 'message "firestarter = %S" firestarter)))
(add-hook 'find-file-hook 'find-file-hook--firestarter-notify)
;; ファイルに関連付けられていないバッファの保存時にfirestarterが設定されているときに警告
(defun warn-firestarter-before-saving-nonfile-buffer (&rest ignore)
  (let (it)
    (when (and (not buffer-file-name)
               (save-excursion
                 (save-restriction
                   (widen)
                   (goto-char (point-min))
                   (and (re-search-forward "firestarter *:" nil t)
                        (setq it (buffer-substring (point-at-bol) (point-at-eol))))))
               (not (yes-or-no-p (concat "Save buffer with firestarter\n" it))))
      (error "Quit saving because of dangerous firestarter setting."))))
(advice-add 'basic-save-buffer :before 'warn-firestarter-before-saving-nonfile-buffer)
(advice-add 'write-file :before 'warn-firestarter-before-saving-nonfile-buffer)

本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。