この記事は記憶が正しければEmacs Advent Calendar 2015の何日めかのつもりでしたが、あれ、目から水が…
さて、気をとりなほして。
教育者, 将軍, 栄養士, 心理学者, 親はプログラムする. 軍隊, 学生, 一部の社会はプログラムされる.
(計算機プログラムの構造と解釈 第二版 序文1より引用)
みなさんはEmacsをプログラムする。
以前、Emacs - 起動直後になんかする。recentfとかねって記事を書いたときに調べた流れから、ちょこちょこピックアップして紹介します。まとまりはありません。
筆者の環境の都合上、現在の開発版(25)ではなく、安定版(24.5)のファイルを参照します。
Emacs Lispのコードを読む前に
find-function
/find-variable
定義位置のソースコードを開いてジャンプすることができます。
which-func
モードラインに現在編集中の函数名を表示します。これから読むファイルにはちょっと長めの函数もあるので、現在位置を見失ったりしないためにべんりです。
hook
Emacsの初期化処理にはフックを設定することができます。
起動したときに何がおこるのか
この処理自体はCから呼び出されるはずですが、今回はLispのコードを呼んで把握する範囲のみを対象とします。
トップレベル
Emacs Lispの函数定義は、こんな具合です。
(defun normal-top-level ()
"Emacs calls this function when it first starts up.
It sets `command-line-processed', processes the command-line,
reads the initialization files, etc.
It is the default value of the variable `top-level'."
わざわざ翻訳するまでもないですが、「Emacsは最初の起動時にこの函数を呼び出す。command-line-processed
をセットし、コマンドラインを処理や初期化ファイルの読み込みなどをおこなふ」といったところですね。
startup.el
は基本的に説明的なコメントが多くて助かります。Lispには説明的なコードが多く付属し、Emacsは自己説明的であるとWikipediaにも言及があり、実際JavadocやPythonのdocstringに引き継がれてます。
このあとしばらく、ファイルをいくつか読み込んだり、OSなどの環境ごとに変数をセットしたりします。
次の世界への扉
(unwind-protect
(command-line)
ここでしれっと大物のサブルーチンを呼んでるので、うっかり読み飛ばしてると見逃します。
コマンドライン
(defun command-line ()
"A subroutine of `normal-top-level'.
Amongst another things, it parses the command-line arguments."
(setq before-init-time (current-time)
after-init-time nil
command-line-default-directory default-directory)
ここでは、初期化処理の中でもコマンドライン引数のパースを担当する部分ですね。
私にとって意外だったのは、最初に解釈するのがVERSION_CONTROL
環境変数だったことです。この環境変数はGNU Coreutils: Backup optionsで説明されて居ります。
Emacsの自動保存ファイル(##)やバックアップファイル(~)の作成先変更まとめ / マスタカの ChangeLog メモあたりの話とも絡みそうですが、ちゃんと追ってません。あと、vc.el
とは関係ないです。
次にsimple.elが読み込まれます。謎ファイル名ですが、ここではテキストエディタとしての基本的な機能が定義されます。それがこのタイミングで読み込まれるのは環境変数をチェックしてる流れですね。
オプション
((member argi '("-d" "-display"))
(setq display-arg (list argi (pop args))))
((member argi '("-Q" "-quick"))
(setq init-file-user nil
site-run-file nil
inhibit-x-resources t))
((member argi '("-D" "-basic-display"))
(setq no-blinking-cursor t
emacs-basic-display t)
(push '(vertical-scroll-bars . nil) initial-frame-alist))
((member argi '("-q" "-no-init-file"))
(setq init-file-user nil))
((member argi '("-u" "-user"))
(setq init-file-user (or argval (pop args))
argval nil))
((equal argi "-no-site-file")
(setq site-run-file nil))
((equal argi "-debug-init")
(setq init-file-debug t))
((equal argi "-iconic")
(push '(visibility . icon) initial-frame-alist))
((member argi '("-nbc" "-no-blinking-cursor"))
(setq no-blinking-cursor t))
このあたりを読めば、ロングオプションとショートオプションを指定したときに、どのような変数が対応するかわかりますね。
Emacsを活用する上で利用頻度が多そうなのは-Q
ですね。emacs -Q
で起動すると初期化ファイルを読み込まないので爆速で起動できます。
あと、-u
は複数人で共有してるサーバーでこっそり他人の.emacs
を動かしてみるときに便利です。
はじめてのhook
(run-hooks 'before-init-hook)
はい、はじめてのフック起動が登場しました。とはいっても、まだinit.el
は呼んでないので、ふつうはユーザーのカスタマイズとしてこのフックに何かを登録することはありません。
初期化ファイルを探せ
(let ((user-init-file-1
(cond
((eq system-type 'ms-dos)
(concat "~" init-file-user "/_emacs"))
((not (eq system-type 'windows-nt))
(concat "~" init-file-user "/.emacs"))
;; Else deal with the Windows situation
((directory-files "~" nil "^\\.emacs\\(\\.elc?\\)?$")
;; Prefer .emacs on Windows.
"~/.emacs")
((directory-files "~" nil "^_emacs\\(\\.elc?\\)?$")
;; Also support _emacs for compatibility, but warn about it.
(push '(initialization
"`_emacs' init file is deprecated, please use `.emacs'")
delayed-warnings-list)
"~/_emacs")
(t ;; But default to .emacs if _emacs does not exist.
"~/.emacs"))))
MS-DOSでは.
から始まるファイルが作れないので、みたいな事情で_emacs
ってファイルから読み込むようになってます。
よく見てみると(concat "~" init-file-user "/.emacs")
って感じで初期化ファイル名を作ってますね。この変数にはさきほど紹介した-u
でユーザー名を指定したときには~tadsan/.emacs.d
のように、ユーザーのホームディレクトリが指定されてますね。
初期化ファイルをもいっこ探せ
(when (eq user-init-file t)
;; If we did not find ~/.emacs, try
;; ~/.emacs.d/init.el.
(let ((otherfile
(expand-file-name
"init"
(file-name-as-directory
(concat "~" init-file-user "/.emacs.d")))))
(load otherfile t t)
;; If we did not find the user's init file,
;; set user-init-file conclusively.
;; Don't let it be set from default.el.
(when (eq user-init-file t)
(setq user-init-file user-init-file-1))))
今度は.emacs
または.emacs.el
が存在しなかったときには.emacs.d/init.el
を読み込もうとします。
実際のところ、GNU Emacs Lisp Reference Manual: Init Fileなどを読んでも、.emacs
.emacs.el
.emacs.d/init.d
のどれが推奨・非推奨だといったことは書かれてません。が、.emacs
は古い形式であり.el
と.elc
の関係に対応しない問題があります。
一つに決まってない以上は個人の好みなのですが、.emacs.d/init.el
の方がdotfilesとして管理しやすくて良いかなー、と思ってます。閑話休題。
ともあれ、この段階であなたがカスタマイズしたEmacsの初期化ファイルが読み込まれるわけです。
パッケージ初期化
package.elの初期化チェックをしてたりします。今回は割愛しますが、Emacs 25 (次期バージョン)ではinit.el
に(package-initialize)
って書いておくことが推奨されてます。
hook、ふたたび
(setq after-init-time (current-time))
(run-hooks 'after-init-hook)
ここで起動時間の記録と、初期化後のhookが呼ばれます。
また引数のターン
;; Process the remaining args.
(command-line-1 (cdr command-line-args))
ここでcommand-line-1
函数を呼んで、残りの引数を解釈します。
(defun command-line-1 (args-left)
"A subroutine of `command-line'."
ファイルを開く
このあとはもろもろの引数が処理されるのですが、Emacsはエディタなので、特にここに着目してみます。
((member argi '("-find-file" "-file" "-visit"))
(setq inhibit-startup-screen t)
;; An explicit option to specify visiting a file.
(setq tem (or argval (pop command-line-args-left)))
(unless (stringp tem)
(error "File name omitted from `%s' option" argi))
(setq file-count (1+ file-count))
(let ((file (expand-file-name
(command-line-normalize-file-name tem)
dir)))
(if (= file-count 1)
(setq first-file-buffer (find-file file))
(find-file-other-window file)))
(unless (zerop line)
(goto-char (point-min))
(forward-line (1- line)))
(setq line 0)
(unless (< column 1)
(move-to-column (1- column)))
(setq column 0))
inhibit-startup-screen
はEmacsの起動時の画面のことです。筆者の画面ではこんな感じ。
で、Emacsの起動時にファイルを開く場合は邪魔なので表示しないようになってます。
file-count
はこの函数の頭の方でlet
で作られたローカルな変数です。emacs
コマンドには複数のファイル名を指定することができるので、最初に指定されたファイルが最初の画面になるように区別されてます。
ここで気に留めておきたいのが、コマンドラインオプションの解釈と同時にfind-file
(またはfind-file-other-window
)で 逐次的に実ファイルも開いてる ことですね。
あと、実はこのあとに同じ処理がもう一度書かれてて、DRYじゃないです。
そしてまたhook実行
(if (or inhibit-startup-screen
initial-buffer-choice
noninteractive
(daemonp)
inhibit-x-resources)
;; Not displaying a startup screen. If 3 or more files
;; visited, and not all visible, show user what they all are.
(and (> file-count 2)
(not noninteractive)
(not inhibit-startup-buffer-menu)
(or (get-buffer-window first-file-buffer)
(list-buffers)))
;; Display a startup screen, after some preparations.
;; If there are no switches to process, we might as well
;; run this hook now, and there may be some need to do it
;; before doing any output.
(run-hooks 'emacs-startup-hook)
(and term-setup-hook
(run-hooks 'term-setup-hook))
(setq inhibit-startup-hooks t)
;; It's important to notice the user settings before we
;; display the startup message; otherwise, the settings
;; won't take effect until the user gives the first
;; keystroke, and that's distracting.
(when (fboundp 'frame-notice-user-settings)
(frame-notice-user-settings))
;; If there are no switches to process, we might as well
;; run this hook now, and there may be some need to do it
;; before doing any output.
(when window-setup-hook
(run-hooks 'window-setup-hook)
;; Don't let the hook be run twice.
(setq window-setup-hook nil))
まだcommand-line-1
の中なのですが、ここでいくつかのhookが実行されます。ここだけ見ると一定の条件下ではemacs-start-hook
とかterm-setup-hook
が実行されなさそうですが、実際はcommand-line
とcommand-line-1
を抜けた後で、きちんと実行されます。
スプラッシュスクリーン
(display-startup-screen (> file-count 0)))))
上記コードの直下にあります。いちおうfile-count
を参照して、複数のファイルが開かれてたら全面に出ないようにはなってます。
command-line
を抜けて
ここまでくると残りは消化試合で、load-path
のチェックをしたり、emacs-serverとして起動されてたら必要な処理をしたりします。
そして、normal-top-level
が終了。ありがとうございました。
振り返る
そのとき、何がしたかったのか
Emacsの起動時に処理を挟み込んで、「コマンドライン経由でファイルが開かれたときは何もしない」「ファイルが何も開かれてないときはrecentf
からファイルを開く」といった分岐をしたかったのです。
↑ この処理を実現したい型はMELPAから init-open-recentf
パッケージをインストールして、init.el
に (init-open-recentf)
って書いてね
何に悩んだのか
上記の処理をinit.el
だけで完結させて書けないかと試行錯誤したのですがうまくいかず、この記事に書いたスタートアップ処理を一から辿っていって把握するはめになったわけです。
開いたファイルの数
command-line-1
の中ではfile-count
変数を使ってうまく処理してるのですが、残念ながらこれはローカル変数なので別のところで参照することはできません。ので、全バッファを開いてみてbuffer-file-name
をリストアップしてるのですが、もっとうまい方法はないものか…
(defun init-open-recentf-buffer-files ()
"Return list of opened file names."
(let ((found-files '()))
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when buffer-file-name
(cl-pushnew buffer-file-name found-files))))
found-files))
ちなみにEmacs組み込みのlist-buffers
の中でも似たようなことをやってます。
まとめ
まとめることは特にないんですけど、Emacsデフォルトの機能だけでスタートアップ処理を辿っていったり、(やりたくないけど)改変すらできてしまふのがおもしろいですね。最近のElectron系のエディタではJavaScriptのコードを読めたりするのかな?
-
計算機プログラムの構造と解釈 第二版 序文 2000年 Gerald Jay Sussman, Harold Abelson, Julie Sussman 著 / 和田英一 訳 ↩