Personal tools
You are here: Home 原稿・資料 Software Design 2008年2月号 「Emacsマスターへの道」 原稿 Emacs Lispでスクリプト処理
Document Actions

Emacs Lispでスクリプト処理

著者 松山智大

今回はこのバッチモードでのEmacs Lispに焦点を当てて,テキストを処理するスクリプトやサーバーデーモンを書くためのテクニックを紹介したいと思います.

■■ バッチモードでの標準入出力
バッチモードでは一部の関数が標準入出力を扱うための関数に変化します.早速おきまりのスクリプトを書いてみましょう(リスト1).

------------------
<リスト1> hello.el
(princ "Hello, world.\n")
------------------

princ関数は第一引数を標準出力に出力する関数です.このスクリプトを実行するにはターミナル上でEmacsを起動します(リスト2).

------------------
<リスト2>
% emacs --script hello.el
Loading subst-ksc...
oading subst-gb2312...
Loading subst-big5...
Loading subst-jis...
Hello, world.
------------------

--scriptオプションは指定されたファイルをバッチモードで実行します(脚注1).Loading...と出ているのはとりあえず無視してください.

------------------
<脚注1>
--scriptオプションはEmacs 22で入ったオプションです.この記事自体Emacs 22を前提にして書いていますが,Emacs 21を使いたい場合は--scriptオプションのかわりに-batchオプションと-lオプションを使用してください.
------------------

標準入力から文字列を取得するにはread-string関数を使います.この関数は第一引数を標準出力に出力して(脚注2),標準入力から一行取得して返します.標準入力から一行取得してそのまま標準出力に出力するスクリプトを書いてみましょう(リスト3).

------------------
<脚注2>
ユーザーにどのような入力を求めるかの説明文として使います
------------------

------------------
<リスト3> ask.el
(princ-list "Input string is: " (read-string "Input some string: "))
vv------------------

princ-list関数は全ての引数を標準出力に出力し,最後に改行を出力する関数です.実行するとリスト4のようになります.

------------------
<リスト4>
% emacs --script ask.el
Input some string: Hello, Emacs Lisp!
Input string is: Hello, Emacs Lisp!
------------------

単純に標準入力から一行取得するにはread-string関数の第一引数に空文字を指定します.関数として定義しておくと良いでしょう(リスト5).

------------------
<リスト5> read-line関数
(defun read-line () (read-string ""))
------------------

message関数やerror関数は標準エラーに出力する関数です.先ほどLoading...と出ていたのは,Emacs Lispファイルをロードするload関数がmessage関数を使ってロード中であることを知らせていたからです.気になるようでしたら実行時に標準エラーを表示されないようにしてください(リスト6).

------------------
<リスト6>
% emacs --script hello.el 2> /dev/null
Hello, world.
------------------

■■ コマンドライン引数
コマンドライン引数を取得するにはcommand-line-args-left変数を使います(リスト7).この変数にはEmacsが処理していないコマンドライン引数のリストが入っています.

------------------
<リスト7> args.el
(princ-list "First argument is " (nth 0 command-line-args-left))
(princ-list "Second argument is " (nth 1 command-line-args-left))
------------------

nth関数はリストのN番目の要素を取得する関数です.これを実行するとリスト8のようになります.

------------------
<リスト8>
% emacs --script args.el foo bar
First argument is foo
Second argument is bar
------------------

■■ ソースコード整形スクリプトの作成
基本的なことは説明したので,いくつか実用的なツールを作ってみたいと思います.

作るのはソースコード整形スクリプトです.これはsample.cのようなインデントの整っていないソースコードをきれいに整形するスクリプトです.

------------------
<リスト9> sample.c
int main(int argc, char *argv[]) {
char *s = argv[1];
while (*s)
putchar(((*s++ - 'A' + 13) % 26) + 'A');
return 0;
}
------------------

使う仕組みはfind-file関数とindent-region関数のbuffer-string関数の三つです.find-file関数は引数に指定されたファイルを開いて,そのファイルのバッファをカレントバッファに設定します.indent-region関数はカレントバッファの指定された範囲のインデントを整えます.buffer-string関数はカレントバッファの全内容を文字列で返します.これを順番通りに書いてやれば完成です(リスト10).

------------------
<リスト10> indent.el
(find-file (nth 0 command-line-args-left)) ; コマンドラインの第一引数に指定されたファイルを開いて
(indent-region (point-min) (point-max)) ; 最初から最後まで整形
(princ (buffer-string))
------------------

整形したいファイルを第一引数に指定して実行します(リスト11).

------------------
<リスト11>
% emacs --script indent.el sample.c
int main(int argc, char *argv[]) {
char *s = argv[1];
while (*s)
putchar(((*s++ - 'A' + 13) % 26) + 'A');
return 0;
}
------------------

このスクリプトはEmacsが認識できる全ての言語で正しく動作します(脚注3).これはindent-region関数がそれぞれのメジャーモードに適した動作をするからなのですが,ここでは詳細は割愛させていただきます.

------------------
<脚注3>
ちなみにPythonは動きません.なぜならインデントの崩れたPythonスクリプトは人間でも修復できないからです.
------------------

■■ grepの作成
実用的かと言われると大きな疑問符が付きますが,Emacs Lispのパワフルさを知っていただくためにgrepも作ってみたいと思います.grepは指定されたパターンで入力に対してマッチを行い,マッチした行を出力するプログラムです.

早速作りたいのですが,その前に一つ重要な準備をしておく必要があります.先ほど定義したread-line関数は,Perlなどではよく使う一行読み出し関数ですが,バッファをベースにしてテキストを処理するEmacs Lispではかなり使いづらい関数です(脚注4).そこで標準入力の内容を文字列ではなくバッファとして読み出せるようにget-stdin-bufferという関数を定義しておきましょう(リスト12).

------------------
<脚注4>
正確にはバッファが使い易すぎるのかもしれません.
------------------

------------------
<リスト12> get-stdin-buffer関数
(defun get-stdin-buffer ()
(let ((buf-name "*stdin*"))
(or (get-buffer buf-name) ; バッファがすでに存在する場合はそのまま返す
(let ((buffer (generate-new-buffer buf-name))) ; なければバッファを作成して
(with-current-buffer buffer ; そのバッファを一時的にカレントバッファにする
(condition-case nil ; EOFに達したときに発生するエラーを抑制するためのおまじない
(let (line)
(while (setq line (read-line)) ; 一行読み出して
(insert line "\n"))) ; その一行をカレントバッファに挿入する
(error nil)))
buffer)))) ; 作成したバッファを返す
------------------

この関数は標準入力の内容を全て読み込んでバッファとして返す関数です.標準入力の内容を単純に標準出力に出力するスクリプトを書いてみましょう(リスト13).

------------------
<リスト13>
(set-buffer (get-stdin-buffer)) ; 標準入力バッファをカレントバッファにして
(princ (buffer-string)) ; カレントバッファの内容を標準出力に出力する
------------------

バッファを用いることによりEmacs Lispに搭載されているさまざまな便利な機能を使うことができます.Emacs Lispの真髄はバッファにあると言っても過言ではありません.

少し横道にそれましたがこれで準備ができました.

今回使う仕組みはget-stdin-buffer関数とoccur関数の二つです.

occur関数はカレントバッファの全ての行に対して引数で指定されたパターンでマッチを行って,マッチした行を*Occur*バッファに表示する関数です.試しにEmacsでsample.cを開いて,M-x occur RET char RETとしてみてください.リスト14のような内容が*Occur*バッファに表示されると思います.

------------------
<リスト14> *Occur*
3 matches for "char" in buffer: sample.c
1:int main(int argc, char *argv[]) {
2:char *s = argv[1];
4:putchar(((*s++ - 'A' + 13) % 26) + 'A');
------------------

見てのとおりoccur関数がやっていることはgrepがやっていることそのものです.なので後は*Occur*バッファの内容を出力するコードを書いてやればやれば完成です(リスト15).

------------------
<リスト15> *Occur*
; grep.el
(set-buffer (get-stdin-buffer)) ; 標準入力バッファをカレントバッファにして
(occur (nth 0 command-line-args-left)) ; occur関数を実行する
(set-buffer "*Occur*") ; *Occur*バッファをカレントバッファにして
(forward-line 1)
(princ (buffer-substring (point) (point-max))) ; 標準出力に出力する
------------------

forward-line関数でポイントを一行下に移動してから,buffer-substring関数でそのポイントからバッファの最後までの文字列を取得することにより,*Occur*バッファの最初の行を出力しないようにしています.Emacs Lispならではという処理方法です.

これを実行するとリスト16のようになります.

------------------
<リスト16>
% emacs --script grep.el char sample.c
1:int main(int argc, char *argv[]) {
2:char *s = argv[1];
4:putchar(((*s++ - 'A' + 13) % 26) + 'A');
------------------

■■ 非同期処理
Emacsは一見マルチスレッドで動作しているように見えますが,実はシングルスレッドで動作しています.それはEmacs Lispも同様です.例えばrun-at-timeというコールバックを定期的に呼び出すようにする関数がありますが,このコールバックもシングルスレッドで呼び出されます.また,コールバックを用いて実行したプロセスの出力を逐次的に処理することができますが,このコールバックもシングルスレッドで呼び出されます.

このようにシングルスレッドで非同期処理を実行しつつユーザーへのレスポンス性も疎かにしないということを実現しているのがコマンドループです.コマンドループはEmacsが起動したときに実行される永久ループですが,ユーザーによる入力がないなどのアイドル時間を利用して非同期処理を実行しています.

明確な終了があることを前提としているバッチモードでは当然ながらコマンドループは実行されません.つまり非同期処理は使えないということになります.テキスト処理スクリプトを実行する分には問題ないのですが,コネクションが張られたことやデータを受信したことを知らせるのにコールバックを使うサーバーデーモンを実行する場合は少々困ったことになります.

対策としてはスクリプト内で明示的にコマンドループを実行する方法が考えられます.先ほどのrun-at-time関数を使って検証してみましょう(リスト17).

------------------
<リスト17> timer1.el
(run-at-time 1 1 (lambda () (princ-list "Hello"))) ; 1秒ごとにHelloを出力する
(recursive-edit) ; コマンドループを実行する
------------------

これを実行すれば1秒ごとにHelloが出力されると期待したのですが,いつまで待っても出力されません(リスト18).

------------------
<リスト18>
% emacs --script timer1.el
何もでない
------------------

原因はコマンドループ内のgetchar関数がユーザーの入力を待っているからで,試しに適当な文字を打ってエンターを押してやるとエラーメッセージとともにいくつかのHelloが表示されます.

コマンドループを実行するのはうまくいかないようなので,自分で永久ループを実行しつつ適当にアイドル時間を作る方法を検証してみます(リスト18).

------------------
<リスト18> timer2.el
(run-at-time 1 1 (lambda () (princ-list "Hello")))
(while t
(sleep-for 0 100)) ; 100ミリ秒のアイドル時間を作る
------------------

これを実行すると期待通りに1秒ごとにHelloが出力されます(リスト19).

------------------
<リスト19>
% emacs --script timer2.el
Hello
Hello
Hello
...
------------------

これでバッチモードでも非同期処理が使えるようになりました.サーバーデーモンを書く際はこの永久ループを必ずどこかに書く必要があります.

■■ HTTPサーバーの作成
サーバーデーモンの書き方がわかったので,HTTPサーバーを作ってみたいと思います.このHTTPサーバーはあるファイルをクライアントに送信するだけのごく簡単なものです.

■■■ ポートを開いて待機
まずポートを開いて待機する部分を作りましょう.ポートを開くにはmake-network-process関数(脚注5)を使います.この関数のfilterキーワードにコールバックを設定するとクライアントの送信したデータを逐次的に処理することができます.make-network-process関数だけだとすぐに終了してしまうので先ほどの永久ループを最後に書いて雛形の完成です(リスト20).

------------------
<脚注5>
make-network-process関数はEmacs 22で入った関数です.
------------------

------------------
<リスト20> httpd.el
(defun httpd-filter (process string)
; ここにファイルを送信するコードを書く
(delete-process process)) ; コネクションを切断する

(defun httpd-start ()
(interactive)
(make-network-process
:name "HTTPD"
:server t
:host 'local
:service 4000
:filter 'httpd-filter))

(when noninteractive ; バッチモードなら
(httpd-start) ; サーバーを開始して

(while t ; 待機
(sleep-for 0 100)))
------------------

noninteractive変数はEmacsがバッチモードで起動したかどうかが入っています.noninteractive変数で分岐することにより,Emacsでこのスクリプトをロードしても勝手にサーバーが開始されたり永久ループが実行されたりしないようにしています.

■■■ ファイルの送信
次にhttpd-filter関数を実装します.クライアントがリクエストを送信してくるとhttpd-filter関数がコールバックされます.コールバック時に,コネクションのプロセスとクライアントから送られたデータ(今回は使いません)が渡されるので,process-send-string関数とprocess-send-region関数を使ってクライアントにファイルを送信します(リスト21).

------------------
<リスト21> httpd.el
(defvar httpd-file "~/.emacs")

(defun httpd-filter (process string)
(with-current-buffer (find-file-noselect httpd-file) ; ファイルのバッファを一時的にカレントバッファに設定して
(process-send-string process ; レスポンスヘッダを送信する
(concat "HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n\r\n"))
(process-send-string process "<html><body><pre>")
(process-send-region process (point-min) (point-max)) ; カレントバッファの内容をクライアントに送信する
(process-send-string process "</pre></body></body>"))
(delete-process process))

...
------------------

find-file-noselect関数は開いたファイルのバッファをカレントバッファに設定しないという点を除けばfind-file関数と同じです.

このスクリプトを実行してから,ブラウザでhttp://localhost:4000/を開くと~/.emacsの内容が表示されると思います.

■■■ もう少し工夫
一応当初の目的は達成できたのですが,これだとあまり芸がないので,単純にファイルを送信するのではなく,予約語やコメントに色を付けて送信するようにしてみたいと思います.

まずhtmlizeというパッケージをインストールします(リスト22).

------------------
<リスト21>
; install-elisp.elが入ってない場合は
; URLから直接ダウンロードしてインストールしてください.
(install-elisp "http://fly.srk.fer.hr/~hniksic/emacs/htmlize.el")
------------------

このパッケージのhtmlizer-buffer関数を使えば色づけされたバッファをHTMLに変換することができます(リスト22).

------------------
<リスト22> httpd.el
(require 'htmlize) ; 追加

...

(defun httpd-filter (process string)
(with-current-buffer
(with-current-buffer (find-file-noselect httpd-file) ; ファイルのバッファを一時的にカレントバッファに設定して
(font-lock-fontify-buffer) ; 色づけして
(htmlize-buffer)) ; HTML変換してそのバッファを返す
(process-send-string process ; レスポンスヘッダを送信する
(concat "HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n\r\n"))
(process-send-region process (point-min) (point-max))) ; カレントバッファの内容をクライアントに送信する
(delete-process process))

...
------------------

これをバッチモードで実行してブラウザで開くと図1のように真っ黒になると思います.これはバッチモードで起動したEmacsは表示関連の初期化を完全にスキップするからです.リスト23のようにRGBで色を指定してやれば図2のようにちゃんと色づけされます.

------------------
<図1> httpd1.png
------------------

------------------
<図2> httpd2.png
------------------

------------------
<リスト23> httpd.el
(when noninteractive
; ここから
(set-foreground-color "black")
(set-background-color "#ffffe0")

(set-face-attribute 'font-lock-builtin-face nil :foreground "#da70d6")
(set-face-attribute 'font-lock-comment-face nil :foreground "#b22222")
(set-face-attribute 'font-lock-constant-face nil :foreground "#5f9ea0")
(set-face-attribute 'font-lock-function-name-face nil :foreground "#0000ff")
(set-face-attribute 'font-lock-keyword-face nil :foreground "#a020f0")
(set-face-attribute 'font-lock-string-face nil :foreground "#ffc1c1")
(set-face-attribute 'font-lock-type-face nil :foreground "#bc8f8f")
(set-face-attribute 'font-lock-variable-name-face nil :foreground "#ffb90f")
(set-face-attribute 'font-lock-warning-face nil :bold t :foreground "#ff0000")
; ここまで追加

...
------------------

最終的にリスト24のようになります.

------------------
<リスト24> httpd.el
(require 'htmlize)

(defvar httpd-file "~/.emacs")

(defun httpd-filter (process string)
(with-current-buffer
(with-current-buffer (find-file-noselect httpd-file)
(font-lock-fontify-buffer)
(htmlize-buffer))
(process-send-string process
(concat "HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n\r\n"))
(process-send-region process (point-min) (point-max)))
(delete-process process))

(defun httpd-start ()
(interactive)
(make-network-process
:name "HTTPD"
:server t
:host 'local
:service 4000
:filter 'httpd-filter))

(when noninteractive
(set-foreground-color "black")
(set-background-color "#ffffe0")

(set-face-attribute 'font-lock-builtin-face nil :foreground "#da70d6")
(set-face-attribute 'font-lock-comment-face nil :foreground "#b22222")
(set-face-attribute 'font-lock-constant-face nil :foreground "#5f9ea0")
(set-face-attribute 'font-lock-function-name-face nil :foreground "#0000ff")
(set-face-attribute 'font-lock-keyword-face nil :foreground "#a020f0")
(set-face-attribute 'font-lock-string-face nil :foreground "#ffc1c1")
(set-face-attribute 'font-lock-type-face nil :foreground "#bc8f8f")
(set-face-attribute 'font-lock-variable-name-face nil :foreground "#ffb90f")
(set-face-attribute 'font-lock-warning-face nil :bold t :foreground "#ff0000")

(httpd-start)

(while t
(sleep-for 0 100)))
------------------

■■■ emacsclientの技
emacsclientを使って動的にサーバーの状態を変更するテクニックを紹介します.まずhttpd.elの適当な場所にserver-start関数を追加します(リスト25).

------------------
<リスト25> httpd.el
(when noninteractive
(server-start) ; 追加

...
------------------

これをバッチモードで実行して,クライアントに送信したいファイルを変えたい場合はemacsclientを使って動的にhttpd-file変数の値を変更します(リスト26).

------------------
<リスト26>
% emacsclient -e '(setq httpd-file "~/sample.c")'
------------------

これでブラウザをリロードすると~/sample.cの内容が色つきで表示されます.Emacs Lispでスクリプトを書けばこのようなこともできるのです.

■■ 最後に
今回触れたスクリプト言語としてのEmacs Lispは実はそれほど新しいものではありません.やろうと思えばかなり古いEmacsでもスクリプトを実行することができると思います.しかし,Emacs Lispが遅いからなのか,その手の関数郡が貧弱だからなのか,あるいはEmacsの起動が遅いからなのか,よくわかりませんが,Emacs Lispをスクリプト言語として使っているのはこれまで見たことがありません.最近ではEmacsの起動の遅さが気にならないぐらいにマシンスペックが向上し,先ほど使ったmake-network-process関数のように新たな使い方を展開するような関数も徐々に増えてきているので,今後Emacs Lispをスクリプト言語として使う機会がもしかしたらあるかもしれません.今回紹介したのはEmacs Lispの一面でしかありませんが,この記事を機会にEmacs Lispでスクリプトを書く人が一人でも増えればうれしい限りです.

■■ TODO Appendix よく使う関数

Attachments

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.