今日はなんと、僕そして
Emacs界に大きなプレゼントがあります!
Emacsの魔境ともいえるシンタックステーブル、
あなたはきちんと理解していますか?

正直、とてもやないけど僕もわかっていませんでした。
僕のEmacs Lispプログラミングで
そこまで要求されたことがなかったからです。

人間、必要ないものは覚えようとしませんし、
仮に覚えたとしてもあっさり記憶から離れていってしまいます。

シンタックステーブルは僕の弱点のひとつでもあります。
正直、理解しようとすると難解すぎて頭がおかしくなるほどです。
その難解きわまりないシンタックステーブルについて
とても分かりやすい文章を寄稿してくださった方がいます。

有限会社時代工房 」の「柴田宣史」さんです。

彼はるびきち塾 の塾生で、何度もメールを交換しています。
そして、ありがたいことに学んだことを
僕に文章の形でアウトプットしてくれました。
かなりの長文でまとまっていたので、
これをそのまま埋もれさせるのは勿体ないと思いました。
そこで僕が文章を実際に読者が手を動かして理解できるように
丁寧に添削しました。
その過程で僕も大きな学びが得られました。

その結果がこの文章です。

Emacs力を高めたいあなた、
Emacsを通じてQOLを高めたいあなた、
まとまった文章を発表したいあなた、
ぜひともるびきち塾 に入塾してください。
あなたの書いたコードや文章を喜んで添削いたします。
無制限で僕にメール相談できます。
特典もりだくさんで初月は無料、月々527円です。

そして、御希望であれば僕のサイトに掲載します。

僕のサイトはEmacs関係で検索すると
必ず上位に表示されるようSEOしていますので、
より多くの人の目に触れることになります。

ぜひとも入塾をご検討ください。

実行方法

この文章はEmacs内でそのまま実行することを念頭に置いています。

http://rubikitch.com/f/syntax-table-beginner.txt
からダウンロードできますが、EWWで見ることを推奨します。

$ emacs -Q -eval '(eww "http://rubikitch.com/f/syntax-table-beginner.html")'

で開き、M-x text-modeにして、C-x C-+ + ... で文字の大きさを調整して実際に手を動かしてみてください。

ここからが本文です。

シンタックステーブル入門

Emacsは文字一つひとつに シンタックステーブルsyntax-table, 構文テーブル )があてられています。
シンタックステーブルはメジャーモードごとに異なり、各文字がどのように扱われるかを決定しています。
この文書を読むことであなたは、シンタックステーブルについて理解が深まり、 対応する括弧コメントアウト が実装できるようになります。

キャラクタコードの取得

シンタックステーブル理解のために、?を伴った記法を確認します。
Emacsでは、文字の前に?を置いて評価すると、文字の キャラクタコード 等を返してくれます。

?a

"?a"のすぐあとにかーソルを移動し、評価(C-x C-e)してください。

97 (#o141, #x61, ?a)

が返ってきます。答え合わせをすると、

(insert (char-to-string 97))

を評価したら、期待通り"a"が入力されます。エスケープ付きの文字だと、こんな感じ(バックスペースの例)。

?\b => 8 (#o10, #x8, ?\C-h)

パーレンなどは、Elispの一部として評価されないように、エスケープする必要があります。バックスラッシュそのものも同様です。

?\( => 40 (#o50, #x28, ?\()
?\\ => 92 (#o134, #x5c, ?\\)

このクエスチョンマークのおかげで、手軽に「任意の文字が、どのシンタックステーブルに属しているか」を調べることができます。

シンタックステーブルの取得

シンタックステーブルはメジャーモードごとに異なります。今回は、text-modeに軸足を置いていきたいと思います。

text-mode(M-x text-mode RET)で

(char-syntax ?a)

と書いてC-x C-eで評価すると、

119 (#o167, #x77, ?w)

が返ってきます。文字コード119。つまりtext-modeにおいて、"a"は"w"だ、といっているわけです。

(char-syntax 97)

でも同様です。マルチバイト文字も送ることができます。以下例でも"w"だと返ってきます。

(char-syntax ?あ) => 119 (#o167, #x77, ?w)

"w"って?

"a"は"w"だ、と言われると意味不明な感じですが、これがシンタックステーブルの「 構文クラス 」です。以下、構文クラスの一覧です。

M-: (info "(elisp) Syntax Class Table")

elisp.infoがインストールされていないとエラーになるのでその場合はWebから見てください。

日本語(古い)
http://www.geocities.co.jp/SiliconValley-Bay/9285/ELISP-JA/elisp_565.html#SEC566
英語(最新)
https://www.gnu.org/software/emacs/manual/html_node/elisp/Syntax-Class-Table.html

一例を挙げるとこんな感じです。

" " 白文字 (Whitespace characters)
"w" 単語構成文字 (Word constituents)
"_" シンボル構成文字 (Symbol constituents)
"." 句読点文字 (Punctuation characters)
"(" 開き括弧文字 (Open parenthesis characters)
")" 閉じ括弧文字 (Close parenthesis characters)
"\"" 文字列クォート (String quotes)
"\\" エスケープ (Escape-syntax characters)
"<" コメント開始 (Comment starters)
">" コメント終了 (Comment enders)

つまり"a"が"w"であるということは、"a"は「単語構成文字」だということです。

Emacsのバージョンが上がるごとに構文クラスも増えているので、上記以外にも、最近のEmacsであれば、「"@"が、親テーブルの継承(Inherit standard syntax)」や「TeX用のクォートの"/"」なども増えています。

シンタックステーブルの仕事

たとえば、以下のような文字列があります。

The quick brown fox jumps over the lazy dog.

"The"の先頭にカーソルを移動し、

M-: (skip-syntax-forward "w")

を実行します。カーソルは"the"の"e"のあとに移動します。 skip-syntax-forward は、指定したシンタックステーブルをスキップする関数なので、「単語構成文字」をスキップしたわけです。が、そのままのカーソルの位置("e"の後ろ)で、もう一回同じ式を評価しても、カーソルは移動しません。次が「白文字」だからです。そのままの位置で白文字をスキップする式は次のような式です。

M-: (skip-syntax-forward " ")

白文字と単語構成文字をスキップするには、以下の式です。

M-: (skip-syntax-forward " w")

ドットの手前まで一気に移動します。ドットは単語構成文字でも白文字でもなく「句読点文字」だからです。ちょっと面白いのが以下の評価。

(char-syntax ?。) => 95 (#o137, #x5f, ?_)

"。"は日本語の意味的には句点ですが、Emacsの扱いは"_"(シンボル構成文字)なんですね。いずれにせよ、日本語の文章の冒頭で、

M-: (skip-syntax-forward "w")

を評価すると、"。"が、単語構成文字でないために、そこでスキップが止まります。

syntax-entryの保存

これから使う modify-syntax-entry という関数は、既存のシンタックステーブルを破壊的に変更する関数です。
ちくちくと戻すこともできますが、もとの状態を一気に復元できるようにしておきましょう。以下の関数は、るびきちさんがシンタックステーブル勉強用に用意してくださった関数です。三つのS式があるので、それぞれの式の末尾でC-x C-eするか、コード全体をリージョンにして M-x eval-region を実行してください。

(setq text-mode-syntax-table-orig
      (copy-syntax-table text-mode-syntax-table))
(defun restore-text-mode-syntax-table ()
  (interactive)
  (setq text-mode-syntax-table (copy-syntax-table text-mode-syntax-table-orig))
  (set-syntax-table text-mode-syntax-table)
  (message "Restore text-mode-syntax-table"))
;;; syntax-tableの勉強のために暫定的にキーを割り当てる
(define-key text-mode-map (kbd "C-c C-z") 'restore-text-mode-syntax-table)

以降のmodify-syntax-entryでシンタックステーブルをいじってしまい、「もとはどうだっけ?」となったらC-c C-zとしてください。シンタックステーブルを復元できます。

対応する括弧

シンタックステーブルを改造してみましょう。実験のため、まず以下文字列をtext-modeに入力し、文字列の"`"の手前にカーソルを移動し、C-M-n(forward-list 対応する次の括弧へ移動)してみてください。カーソルは期待通りには動かないはずです。

`The quick brown fox jumps over the lazy dog'

それぞれの構文クラスを確認してみます。

(char-syntax ?`) => 46 (#o56, #x2e, ?.)
(char-syntax ?') => 119 (#o167, #x77, ?w)

次に以下式を評価してください。

(modify-syntax-entry ?` "\(")
(modify-syntax-entry ?' "\)")

同じくC-M-nすると、今度は"'"に向かって移動してくれます。つまり「括弧」として認識されたわけです。

`The quick brown fox jumps over the lazy dog'

構文クラスを確認すると、"`"に"\("、"'"に"\)"があたっています。それぞれ「開き括弧文字」と、「閉じ括弧文字」です。

(char-syntax ?`) => 40 (#o50, #x28, ?\()
(char-syntax ?') => 41 (#o51, #x29, ?\))

エスケープ文字

さて次の文字列はどうでしょう。

`The quick brown fox jumps over the lazy dog\'s head'

"'"が文章中にあるために、括弧の対応がおかしくなっています。プログラムをすこしかじったことがある人ならわかりますよね。エスケープが働いていないのです。さっそくやってみましょう。

さて、バックスラッシュはtext-modeにおいて以下の通り、"."(区切り文字)にあたっています。

(char-syntax ?\\) => 46 (#o56, #x2e, ?.)

変更してみましょう。

(modify-syntax-entry ?\\ "\\")

ふたたびC-M-nを試してみてください。text-modeでもエスケープが機能するようになりました。

`The quick brown fox jumps over the lazy dog\'s head'

*対応する*括弧

modify-syntax-entryの引数はこんな感じです。

<f1> f modify-syntax-entry
(modify-syntax-entry CHAR NEWENTRY &optional SYNTAX-TABLE)

CHARは、今回の「括弧」の場合、"?`"や"?'"です。2つめの引数はNEWENTRYといって、いわば「あたらしい設定」です。NEWENTRYの1文字目は構文クラスです(この場合は、"\("というようにエスケープされているので2文字分)。2文字目は、「対応する括弧(matching parenthesis)」です。さっそく設定してみましょう。

(modify-syntax-entry ?` "\('")
(modify-syntax-entry ?' "\)`")
`The `quick' brown fox jumps over the lazy dog\'s head'

これで"`'"のペアが明示されました。quickの前の`でC-M-nとC-M-pを押すと確認できます。

ただ、試したらわかるのですが、きちんとネストしていない(入れ子になっていない)括弧関係を書いてしまうと、対応を明示していても、挙動はおかしくなりますのでご注意ください。

`The `quick brown fox jumps over the lazy dog\'s head'

"#"をコメントアウトに

手始めにtext-modeで"#"をコメントアウトにしてみましょう。

(modify-syntax-entry ?# "<")

"#"以降がコメントになったかどうかを確認するにはいくつか方法がありますが、ひとつ目はカラーリングでいきます。以下の三つのS式を評価してください。

(font-lock-mode t)
(set-face-foreground 'font-lock-comment-face "blue")
(font-lock-fontify-buffer)

場合によると、画面が真っ青になりませんでしか? 真っ青の場合、気が散るようなら、C-c C-zして(font-lock-fontify-buffer)です。カラーリングで確認する際、(font-lock-fontify-buffer)は、頻用することになるので、キーバインドをあててしまいましょう。

(define-key text-mode-map (kbd "C-c C-f") 'font-lock-fontify-buffer)

もちろん、

(char-syntax ?#)

で、"<"が返ってくる、というのも確認の一つではあります。

今回の場合、NEWENRTYの1文字目(構文クラス)は"<"で、「コメント開始」です。

とはいえまだ望む結果を得るためには設定が足りません。謎はすぐに解明しますので、もう少し読み進めていただくと幸いです。

"//"をコメントアウトに

つづけてtext-modeで"//"をコメントアウトにしてみましょう。NEWENTRYにあたらしい記法がありますが、とりあえず以下を実行してみてください。

(modify-syntax-entry ?/ ". 12")

NEWENRTYの1文字目(構文クラス)は"."で、「区切り文字」です。2文字目(対応する括弧)は" "なので、ナシ。そのあとに数字が続きます。結論的に書くと、3文字目以降はコメントの設定なのです。この部分はmodify-syntax-entryのヘルプでは「フラグ」と呼ばれているので、以降、フラグと呼びます。フラグの設定は以下のようになっています。

"1"は、設定される文字が2文字からなるコメント開始のまとまりの1文字目であること
"2"は、設定される文字が2文字からなるコメント開始のまとまりの2文字目であること
"3"は、設定される文字が2文字からなるコメント終了のまとまりの1文字目であること
"4"は、設定される文字が2文字からなるコメント終了のまとまりの2文字目であること

NEWENTRYは、次のような読み解きになります。

"/"は、区切り文字で、コメント開始のまとまりの1文字目かつ2文字目

このように2文字で構成されるコメントアウトの場合は、NEWENRTYの3文字目以降に1〜4の役割を設定することで、コメントアウトかどうかを判定するようになっています。

ところで、構文クラスには"<"(コメント開始)があるのに、なぜ"/"でつかわないのでしょう。答えは割と単純で、"/"は、そもそも算術演算子であってコメントアウトがその主な仕事ではないからです。また、NEWENTRYの3文字目以降の数字は、1文字目の構文クラスと共存するようになっていて、"/"は、「区切り文字」という構文クラスの性質を保持したまま、コメントアウト用の文字としての仕事をしている、という設定になっています。

行末までのコメントアウト

ここまで見てきたとおり、

(modify-syntax-entry ?# "<")
(modify-syntax-entry ?/ ". 12")

を評価すると、"#"や"//"以降、すべての文字がコメントアウトだと判定されます。「行末までがコメントアウト」であるようにしましょう。以下の式を評価してからC-c C-fしてください。

(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")

 

// comment
# comment

構文クラスによれば">"は「コメント終了」です。改行文字に対して「コメント終了」をあてることで、「"#"、"//"から行末まで」がコメントアウトになりました。

本質的なコメントアウト

コメントアウトになっているかどうかの確認はいくつかある、と書きましたが、こちらは別解です。以下、二つのS式を評価してください。2つのS式があるので、それぞれの式の末尾でC-x C-eするか、コード全体をリージョンにしてM-x eval-regionを実行してください。

(defun my-comment-p ()
  (interactive)
  (message "%s" (nth 4 (syntax-ppss (point)))))
(define-key text-mode-map (kbd "C-c C-v") 'my-comment-p)

C-c C-vで、カーソル位置がコメントアウトならt、そうなければnilが返ってきます。以下の2行についてC-c C-vを試してください。コメント文字そのものはコメントアウトではありませんが、それ以降はコメントアウトになっていることが確認できます。

// comment
# comment

面白いのは、たとえばfont-lockの指定でなんからの文字列をfont-lock-comment-faceにあてていても、それは見栄えだけコメントアウトふうになっただけで、本質的にはコメントアウトでない、ということです。C-x C-zでシンタックステーブルを戻した後、以下の式を評価してC-c C-fを押してみてください。

(font-lock-add-keywords nil '(("^# .+" . font-lock-comment-face)))

 

# comment like face

行頭にある"#"以降が、コメントアウトと同じ見栄えになったと思います。しかし、C-c C-vは、nilを返すはずです。

この確認が終わったら、以降のために再度以下式を評価して、"#"と"//"をコメントアウトに戻してください。

(modify-syntax-entry ?# "<")
(modify-syntax-entry ?/ ". 12")
(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")

挟むコメントアウト

今度は、"/* */"をコメントアウトにしてみましょう。

(modify-syntax-entry ?/ ". 14")
(modify-syntax-entry ?* ". 23")

適当に"/* */"で、コメントアウトを書いて、C-c C-fしてみてください。"/* */"の間がコメントアウトになると思います。

NEWENTRYは、"/"がコメント開始のまとまりの1文字目かつコメント終了のまとまりの2文字目であること。"*"がコメント開始のまとまりの2文字目かつコメント終了のまとまりの1文字目であることを設定しています。

複数のコメントアウトの混在

挟むコメントアウトを作ったら、"//"のコメントアウトが無効になりましたよね。

// コメントアウトが有効でない状態

しかも、行末でのコメント終了が効いているため、"/* */"で、本来有効であるはずの「複数行にわたるコメントアウト」が効きません。

/*
ここは複数行のコメントアウトです。
multiline comment out sample
*/

つまりコメントアウトを混在させる工夫が必要だということです。先に解から示すので評価してください。

(modify-syntax-entry ?/ ". 124")
(modify-syntax-entry ?# "<")
(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")
(modify-syntax-entry ?* ". 23b")

"/"は、挟まないコメントアウトでも、挟むコメントアウトでも用いますので、すべての役割である"124"をあてます。

"#"は、挟まないコメントアウトでのみ用います。コメントの位置的な条件としては変更なしです。

""と""(いわゆるnewline)は、"//"と"#"のときには、コメントを終了させます。

"*"は、コメントの位置的な条件としては、変更はありません。

今回NEWENRTYに顔を出したのは、"b"です。この"b"は便宜的に「 コメントスタイル 」と呼びます。じつは無指定の状態だとコメントスタイル"a"が割り振られることになっています。"*"にあてられた「コメント終了」の役割は"a"のスタイルに対しては行わない、"b"のスタイルで使いますよ、という設定が"b"なのです。

コメントスタイル"a"や、コメントスタイル"b"、あとで"c"も出てきますが、これらはいったいなんでしょうか? modify-syntax-entryのヘルプによれば、コメントスタイル"a"についてはとくに説明がありませんが、"b"は、コメント開始句("/*")の2文字目とコメント終了句("*/")の1文字目──つまり今回の例だと"*"の位置が、コメントであるかどうかを決定する、と書いてあります。

すこし先取りして書きますが、コメントスタイル"c"は、コメント句("/*"や"*/"や"//"など、コメント領域を特定するまとまり)のそれぞれの文字がコメントかどうかを決定するそうです。

コメントスタイル

コメントスタイルは複雑なので、もう少し立ち止まりましょう。コメントスタイル"b"(コメント開始句の2文字目とコメント終了句の1文字目がコメントかどうかを決定するようなコメントアウト)の例は"/* */"です。では、似た設定のコメントアウト"/= =/"という架空のコメントアウトを書いてみましょう。

"/= =/"も、単純には以下のようになるはずです。

(modify-syntax-entry ?/ ". 124")
(modify-syntax-entry ?# "<")
(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")
(modify-syntax-entry ?* ". 23b")
(modify-syntax-entry ?= ". 23b") ; added

C-c C-fで確認してみると、以下のコメントアウトはおそらく機能しているでしょう。

/=
comment out
=/

しかし、ここでおかしいことが起こります。

/*
comment out?
=/

ペアでなくても、コメントアウトが成立してしまいます。これは、newline(""、"")が、"//"も"#"も終わらせるのと同じ理屈です。では、コメントスタイル"b"は混在できないのでしょうか? じつはこういったコメントアウトを混在させるために、"n"というフラグがあります。"n"は"nestable"の"n"で、入れ子になるようなコメントを許す、ということのようです。

(modify-syntax-entry ?/ ". 124")
(modify-syntax-entry ?# "<")
(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")
(modify-syntax-entry ?* ". 23b")
(modify-syntax-entry ?= ". 23bn") ; fixed

この設定で、"/* */"と"/= =/"が混在でき、ただしいペアを認識できるようになります。

簡単におさらいしておくと、フラグには"1〜4"の数字と"b"、"c"、"n"を組み合わせた文字列を指定できる、ということです(じつはあと、"p"というフラグがありますが、ここではおいておきます)。

"/= =/"は、あまり実用的なコメントアウトではないので、C-c C-zで消してしまいましょう。勉強を進めるため、C-c C-zのあと、以下式を評価しておいてください。

(modify-syntax-entry ?/ ". 124")
(modify-syntax-entry ?# "<")
(modify-syntax-entry ?\r ">")
(modify-syntax-entry ?\n ">")
(modify-syntax-entry ?* ". 23b")

comment-dwim

これでいくつかの文字が、シンタックステーブルとしてはコメントと定義されたのですが、すこしだけシンタックステーブルから外れて、コメントアウト設定について加筆してみます。Emacsには comment-dwim があるので、この設定をします。

(setq-local comment-start "//")

M-;で"//"がコメントアウトとして扱われます。これで、いろんな意味でコメントアウトらしくなりますね。
複数行のコメントアウトをどう扱うか、など、 newcomment.el を見ると参考になると思います。

文字列

HTMLでもphpでも、JavaScriptでも原則、引用符(""や'')に囲まれている箇所は文字列です。これらはせっかくなので、文字列として認識してもらいましょう。コメントアウトに比べるとこちらは随分とシンプルです。

(modify-syntax-entry ?\" "\"")
(modify-syntax-entry ?\' "\"")

文字列指定をすると"//"のようにコメントアウトを"で囲むことで、「コメントアウトでなく、文字列」というように設定できます。

ここまでのまとめ

これでシンタックステーブルについてすべて語り尽くしたとはとても言えませんが、シンタックステーブルがどういうものなのか、の入門にはなるのではないでしょうか。以上です。

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