Emacs
emacs-lisp

[Emacs] メッセージを表示せずにファイルを保存する方法

More than 1 year has passed since last update.

問題

Emacs のもっとも基本的なファイル出力関数である write-region は、保存時に Wrote <filename>. というメッセージを表示してしまうので、 Emacs アプリやスクリプト内部でファイル出力を扱う場合にはうるさくなってしまうことがあります。

(write-region (point-min) (point-max) "file")

他のファイル保存関数も、内部で write-region を利用しているので同様です。

だめだったアイデア

1. with-temp-message

ミニバッファの表示を固定しながらフォームを評価する、 with-temp-message というマクロがあるので、基本的にはこれで足ります。

(with-temp-message ""
  (let ((message-log-max nil))
    (write-region (point-min) (point-max) "file")))

ただこれはたんに表示を固定するだけで、メッセージを表示するルーチン自体は呼ばれているので、たとえば emacs --scriptemacs --batch でターミナルから Emacs が実行されている場合は、普通に stderr に Wrote <filename>. が出力されてしまいます。

2. message 関数を再定義

write-region が内部で message 関数を呼んでいるなら、 message 関数を一時的に再定義してしまえばメッセージを表示させなくすることができそうです

(flet ((message (&rest _) nil))
  (write-region (point-min) (point-max) "file"))

flet が obsolete な最近のバージョンでは

(let ((orig-message-fn (symbol-function 'message)))
  (unwind-protect
      (progn (fset 'message (lambda (&rest _) nil))
             (write-region (point-min) (point-max) "file"))
    (fset 'message orig-message-fn)))

結果は、だめでした。なぜなら、 write-region は C のコアレベルで定義されている関数なので、そもそも Emacs Lisp の世界の message を呼んでいないからです。


余談:

関数再定義で挙動を変えられるのは動的束縛の強みです。個人的に Emacs Lisp は動的束縛であり続けて欲しいですし、 flet を削除するのは賛成できないです。

3. シェルコマンドを呼ぶ

emacs には、指定した範囲の文字列を標準入力としてシェルコマンドを呼び出す shell-command-on-region という関数があります。これを使ってファイルを保存すれば Emacs のメッセージは表示されません。

(shell-command-on-region (point-min) (point-max) (format "cat > file"))

Unix の場合これでうまくいくのですが、この方法だとバッファの coding-system が反映されないので、 windows や mac を使っているとたとえば CRLF で保存されてしまったりします。

うまくいった方法

どうにもうまくいかなかったので、 Emacs のソースを見てみました。

ファイル IO 関連の関数は src/fileio.c にあります。 write_region の実装を読んでみると、一番下にメッセージを表示している箇所が見つかります。

Lisp_Object
write_region (Lisp_Object start, Lisp_Object end, Lisp_Object filename,
          Lisp_Object append, Lisp_Object visit, Lisp_Object lockname,
          Lisp_Object mustbenew, int desc)
{

  ...

  ...

  ...

  if (!auto_saving && !noninteractive)
    message_with_string ((NUMBERP (append)
              ? "Updated %s"
              : ! NILP (append)
              ? "Added to %s"
              : "Wrote %s"),
             visit_file, 1);

  return Qnil;
}

よく見てみると、メッセージを表示する条件に if (!auto_saving && !noninteractive) とあります。その手があった!

というわけで、 auto-save 機構を悪用してファイルを保存するこのコードに行き着きました。これはうまくいきます。

(let ((buffer-auto-save-file-name "file"))
  (do-auto-save t t))

自動保存ファイルの向き先を一時的に目当てのパスに let で上書きして、 do-auto-save で自動保存をトリガーします。 do-auto-save の第二引数に non-nil な値を渡すとカレントバッファだけが対象になるので、それを利用すれば任意のバッファを任意のパスに、「自動保存機構を経由して」保存できます。

追記

write-region の第5引数に nil でも t でもない値を明示するとメッセージ出ないらしい。がびーん。

(write-region (point-min) (point-max) "file" nil 'nomsg)