コードアシストを利用したeclipseプラグインを作りたい人へ

eclipse で便利な機能といえばコードアシスト(コンテンツアシスト)。Ctrl + Space でコードの補完ができるアレだ。これを自作プラグインで拡張したいと思う人も多いだろうが、情報も少なく私自身もかなり苦戦したのでここに説明しておく。

拡張ポイント

そもそも eclipse のプラグイン作成は自分が拡張したい機能の拡張ポイントを探すことから始まる。拡張ポイントを選択し、その拡張ポイントについて正しく設定を行い(正しく plugin.xml を記述し)、その拡張ポイントに必要なクラスを書く、これが eclipse プラグインの作り方の簡単な流れだ。

コードアシストを拡張するための拡張ポイントはズバリ「org.eclipse.jdt.ui.javaCompletionProposalComputer」である。

まずは適当なプラグインプロジェクトを作り(そのときテンプレートは使用しなくてよい)、拡張タブの[追加]でこの拡張ポイントを追加してほしい。

次にこの拡張ポイントの設定だ。plugin.xml を直接書き換えることもできるが、拡張タブからGUIで行えるのでそこで行うことにする。

先ほど追加した拡張ポイントを右クリックし[新規]-[javaCompletionProposalComputer]を選択し、この新しい項目の拡張要素詳細の activate を true に変更する。次に、新しい項目を右クリックし[新規]-[partition]を選択し、type を「__dtfl_partition_content_type」にする。

これで一通り拡張ポイントの設定は完了だ。なお、最後の type は「どこで拡張したコードアシストを有効にするか」の設定である。他の設定の詳細は「拡張ポイント記述の表示」を読んでもらいたい(英語だが)。

クラス・メソッド作成

作成しなければならないクラスは、先ほど追加した項目「(javaCompletionProposalComputer)」のclassに記述された名前のクラスである。「class*:」をクリックすると、必要な名前やインターフェイスが入力された状態でクラス作成画面が開くので、そのまま完了しよう。
コードアシストを拡張するのに最低限必要なのは、computeCompletionProposals メソッドを実装することだ。このメソッドで返すべきインスタンスを返せば、その内容をコードアシストに表示することができる。

クラスを作った時点で、クラス名の部分にエラーが発生していると思う。そこをマウスオーバーして「実装されていないメソッドの追加」を選択すると、エラーは増えるが computeCompletionProposals メソッドが記述されるだろう。

増えたエラーは全てインポートクラスを発見できていないことによるものである。eclipse プラグインでは、インポートしたいクラスのパッケージを全て依存関係に追加しなければならない。「org.eclipse.jdt.ui」は拡張ポイントを追加したときに依存関係に追加されたのだが、他のパッケージは依存関係に存在しない。そこで MANIFEST.MF の依存関係タブを開き「org.eclipse.core.runtime」「org.eclipse.jface.text」の2つを依存関係に追加すればエラーは解消される。

メソッド実装

では最後に computeCompletionProposals メソッドを実装しよう。

このメソッドでは List (実際は ICompletionProposal の実装である CompletionProposal のリスト)を返すことになっている。1つの CompletionProposal インスタンスがコードアシスト1項目に対応する。

とりあえず私が作ったサンプルの computeCompletionProposals メソッドを見てもらおう。

public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
    List<ICompletionProposal> propList = new ArrayList<ICompletionProposal>();
    int offset = context.getInvocationOffset();
    String str;
    CompletionProposal proposal;

    // 文字列"akisute"をコードアシストリストに登録する
    str = "akisute";
    proposal  =  new CompletionProposal(str, offset, 10, str.length());
    propList.add(proposal);
    // 文字列"kashitsune"をコードアシストリストに登録する
    str = "kashitsune";
    proposal =  new CompletionProposal(str, offset, 0, str.length());
    propList.add(proposal);

    return propList;
}

これを記入してプラグインを実行し、適当なファイルで Ctrl + Space を押すと「akisute」と「kashitsune」が表示され、項目を選択することでファイルに文字列を挿入できる。

このコードで説明した方がいいのは offset と proposal くらいだろう。

まず、このメソッドの引数 context というのはコードアシストを実行したファイルの情報が含まれていて各メソッドで取得できる。その1つが getInvocationOffset() であり、これによってオフセット、すなわちカーソルの位置を得ることができる。

proposal は CompletionProposal のインスタンスである。CompletionProposal のコンストラクタは「CompletionProposal(String replacementString, int replacementOffset, int replacementLength, int cursorPosition) 」といった4つの引数を取り、それぞれの役割は以下の通り。

replacementString
置換後の文字列
replacementOffset
置換を開始するカーソル位置
replacementLength
置換前の(削除する)文字列の長さ
cursorPosition
置換後のカーソル位置(replacementOffsetからの距離)

「挿入」ではなく「置換」となってるのは、例えば「aki」と書いてる状態でコードアシストを使用して「akisute」としたいとき、これは「aki」を「akisute」に置換したと考えるためである。この例であれば、

replacementString
"akisute"
replacementOffset
context.getInvocationOffset() - "aki".length()
replacementLength
"aki".length()
cursorPosition
(置換後にakisuteの後にカーソルを置きたいなら)"akisute".length()

となる。上手く引数を書けば、任意の文字数の記述途中の文字列を補完することももちろん可能だ。

まとめ

長々と説明してきたが、言いたかったことは「コードアシストを利用したプラグインを作成するための拡張ポイントは "org.eclipse.jdt.ui.javaCompletionProposalComputer" である」ということだ。

これさえわかれば、拡張ポイント説明文書を読んだりクラスやメソッドをググることで自分の作りたいものに近づくことができるのだが、ここにたどり着くのに多くの人はかなり苦戦すると思う。

この記事が私のように悩んでいる人の助けになれば幸いである。

JavaでCamelCaseを分割する

CamelCaseを分割する必要が出てきたのでググってみたけど、大文字続きを一つの単語として認識するものが見つからなかったので書いた。

キャメルケースを単語に分割する - うなの日記 を参考。ここの正規表現

String[] strs = str.split( "(?<=[a-z])(?=[A-Z])" );

だと、「JavaDBOption」が [Java, DBOption] に分割される。これを [Java, DB, Option] にしたいので、正規表現を書き加える。

String[] strs = str.split("(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])");

キャメルケースを単語に分割する - うなの日記 の説明によると、「(?<=[a-z])」が小文字の後、「(?=[A-Z])」が大文字の前に一致する。そこで、大文字の前である「(?<=[A-Z]+)」と直後に小文字が来る大文字の前「(?=[A-Z][a-z])」も区切りとしてやれば、先ほどの [Java, DBOption] の「DBOption」が [DB, Option] のように分割されるというわけだ。

とりあえず適当なCamelCaseを食わせてみた。

String str = "JavaDBOptionMAINGoogleProjectSTRING";
String[] strs = str.split("(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])");
for (String s : strs) {
    System.out.println(s);
}

実行結果。

Java
DB
Option
MAIN
Google
Project
STRING


一応、自分が期待する結果になった。完璧だと断言する自信はない。

WindowsでEmacsをセットアップ

WindowsのEmacsと言えばMeadowを使ってる人も多いかもしれないけど、MeadowはEmacs22系だから古いんだよね。今はEmacs23.3だから新しい機能を使いたい人には向いてない。そこで今日はWindowsでEmacsを使う方法を紹介。

インストール

gnupack Users Guide の右の「ダウンロード」の「emacs for gnupack」をクリック、そこから一番新しいemacsのexeファイルをダウンロードする。記事書いてる時点では「emacs-23.3a-20110815.exe」。

これを実行すると解凍されてディレクトリができる。これを好きにリネームして好きなところにおいてインストール完了。

レジストリ弄ったりとかは無い。解凍したディレクトリの bin/runemacs.exe を実行するだけでOK。

ところで、Windows7を使ってる人は、タスクバーに常に表示して、タスクバーのアイコンをクリックすることで起動したいと思うかもしれないが、それをすると不必要な窓が開いてしまい、しかもそれを閉じるとemacsまで閉じてしまう。流石Windows、残念である。

これはAppIDというものが問題らしい。AppIDについては アプリケーション ユーザー モデル ID (AppID) に説明があるがよくわからない。これを「GNU.Emacs」にすることでタスクバーのアイコンをクリックしたときの問題は解決するようだ。

以前はショートカットのAppIDを簡単に変更するShortcutAppIDChangerというプログラムを配布してくれてた人がいたのだが、今はそのサイトがなくなってしまっている。バックアップも取ってなかった。

ということで、どうしてもタスクバーから1つ窓で起動したい人はAppID周辺を調べてみてほしい。

設定

Emacs を使うならまずは .emacs の編集だろう。そこで、私のWindows用の設定をいくつか紹介しておく。

.emacs がわからないような人はこんなブログより丁寧に説明してるところを見た方がいいと思うので、環境変数HOMEを設定方法とか.emacsの置き場所とかcygwinの話とかは省略。

Meadowを使ってるころは PC でプログラミング にお世話になった。cygwinemacsも使ったことない人は、まずはここを参考にCygwinMeadowを設定してみて、ある程度理解したらgnupackを使ってみたらどうだろう。基本的な.emacsもあるし、初心者には重宝すると思う。

話が逸れてしまった。設定の紹介に移ろう。まずは「言語・文字コード関連の設定」から。

(when (equal emacs-major-version 21) (require 'un-define))
(set-language-environment "Japanese")
(set-terminal-coding-system 'utf-8-unix)
(set-keyboard-coding-system 'utf-8-unix)
(set-buffer-file-coding-system 'utf-8-unix)
(setq default-buffer-file-coding-system 'utf-8-unix)
(prefer-coding-system 'utf-8-unix)
(set-default-coding-systems 'utf-8-unix)
(setq file-name-coding-system 'shift_jis)

UTF-8でファイルを編集する設定がずらずら。「utf-8-unix」の「utf-8」が文字コード、「unix」が改行コードである。ほとんどutf-8-unixだけどWindowsのファイル名はSJISなのでそこだけshift_jisって書いてる。

次は「IMEの設定」。

;;;** 標準IMEの設定
(setq default-input-method "W32-IME")

;;;** IMEの初期化
(w32-ime-initialize)

;;;** IME状態のモードライン表示
(setq-default w32-ime-mode-line-state-indicator "[--]")
(setq w32-ime-mode-line-state-indicator-list '("[--]" "[あ]" "[--]"))

;;;** IME OFF時の初期カーソルカラー
(set-cursor-color "red")

;;;** IME ON/OFF時のカーソルカラー
(add-hook 'input-method-activate-hook
          (lambda() (set-cursor-color "green")))
(add-hook 'input-method-inactivate-hook
          (lambda() (set-cursor-color "red")))

;;;** バッファ切り替え時にIME状態を引き継ぐ
(setq w32-ime-buffer-switch-p nil)

私はATOKを使ってるが、MS-IMEでもGoogle-IMEでも変わらないと思う。多分。

IME ON/OFF時でカーソルの色を変えるのはけっこう便利。ミニバッファで自動でIMEをOFFにする設定とか書くとよくバグるんだけど。

次は「フォントの設定」。

;;;** Consolas + MSゴシック
(set-default-font "Consolas 10")
(set-fontset-font (frame-parameter nil 'font)
                  'japanese-jisx0208
                  '("MS ゴシック" . "unicode-bmp")
                  )
(set-fontset-font (frame-parameter nil 'font)
                  'katakana-jisx0201
                  '("MS ゴシック" . "unicode-bmp")
                  )

プログラミングするなら等幅じゃないとね。Consolasはお気に入り。

ラスト、「フレームサイズ・位置・色の設定」。

;;;** 黒背景に灰色の文字
(setq default-frame-alist
      (append (list '(foreground-color . "gray")
                    '(background-color . "black")
                    '(border-color . "black")
                    '(mouse-color . "white")
                    '(width . 120)
                    '(height . 30)
                    '(top . 100)
                    '(left . 200))
              default-frame-alist))

各項目のピリオドの右("gray"とか120とか)は自分の好みに合わせてね。Windowsだとwidthとheightを画面ピッタリに設定しても最大化ボタン押したときみたいに外枠が消えないから、Win+↑ で毎回自分で最大化してる。



以上。Windowsでも是非新しいEmacsを使ってあげてください。まあLinux常用することに問題がなければ絶対そっちの方がいいけど。

WindowsでEmacsをセットアップ

WindowsのEmacsと言えばMeadowを使ってる人も多いかもしれないけど、MeadowはEmacs22系だから古いんだよね。今はEmacs23.3だから新しい機能を使いたい人には向いてない。そこで今日はWindowsでEmacsを使う方法を紹介。

インストール

gnupack Users Guide の右の「ダウンロード」の「emacs for gnupack」をクリック、そこから一番新しいemacsのexeファイルをダウンロードする。記事書いてる時点では「emacs-23.3a-20110815.exe」。

これを実行すると解凍されてディレクトリができる。これを好きにリネームして好きなところにおいてインストール完了。

レジストリ弄ったりとかは無い。解凍したディレクトリの bin/runemacs.exe を実行するだけでOK。

ところで、Windows7を使ってる人は、タスクバーに常に表示して、タスクバーのアイコンをクリックすることで起動したいと思うかもしれないが、それをすると不必要な窓が開いてしまい、しかもそれを閉じるとemacsまで閉じてしまう。流石Windows、残念である。

これはAppIDというものが問題らしい。AppIDについては アプリケーション ユーザー モデル ID (AppID) に説明があるがよくわからない。これを「GNU.Emacs」にすることでタスクバーのアイコンをクリックしたときの問題は解決するようだ。

以前はショートカットのAppIDを簡単に変更するShortcutAppIDChangerというプログラムを配布してくれてた人がいたのだが、今はそのサイトがなくなってしまっている。バックアップも取ってなかった。

ということで、どうしてもタスクバーから1つ窓で起動したい人はAppID周辺を調べてみてほしい。

設定

Emacs を使うならまずは .emacs の編集だろう。そこで、私のWindows用の設定をいくつか紹介しておく。

.emacs がわからないような人はこんなブログより丁寧に説明してるところを見た方がいいと思うので、環境変数HOMEを設定方法とか.emacsの置き場所とかcygwinの話とかは省略。

Meadowを使ってるころは PC でプログラミング にお世話になった。cygwinemacsも使ったことない人は、まずはここを参考にCygwinMeadowを設定してみて、ある程度理解したらgnupackを使ってみたらどうだろう。基本的な.emacsもあるし、初心者には重宝すると思う。

話が逸れてしまった。設定の紹介に移ろう。まずは「言語・文字コード関連の設定」から。

(when (equal emacs-major-version 21) (require 'un-define))
(set-language-environment "Japanese")
(set-terminal-coding-system 'utf-8-unix)
(set-keyboard-coding-system 'utf-8-unix)
(set-buffer-file-coding-system 'utf-8-unix)
(setq default-buffer-file-coding-system 'utf-8-unix)
(prefer-coding-system 'utf-8-unix)
(set-default-coding-systems 'utf-8-unix)
(setq file-name-coding-system 'shift_jis)

UTF-8でファイルを編集する設定がずらずら。「utf-8-unix」の「utf-8」が文字コード、「unix」が改行コードである。ほとんどutf-8-unixだけどWindowsのファイル名はSJISなのでそこだけshift_jisって書いてる。

次は「IMEの設定」。

;;;** 標準IMEの設定
(setq default-input-method "W32-IME")

;;;** IMEの初期化
(w32-ime-initialize)

;;;** IME状態のモードライン表示
(setq-default w32-ime-mode-line-state-indicator "[--]")
(setq w32-ime-mode-line-state-indicator-list '("[--]" "[あ]" "[--]"))

;;;** IME OFF時の初期カーソルカラー
(set-cursor-color "red")

;;;** IME ON/OFF時のカーソルカラー
(add-hook 'input-method-activate-hook
          (lambda() (set-cursor-color "green")))
(add-hook 'input-method-inactivate-hook
          (lambda() (set-cursor-color "red")))

;;;** バッファ切り替え時にIME状態を引き継ぐ
(setq w32-ime-buffer-switch-p nil)

私はATOKを使ってるが、MS-IMEでもGoogle-IMEでも変わらないと思う。多分。

IME ON/OFF時でカーソルの色を変えるのはけっこう便利。ミニバッファで自動でIMEをOFFにする設定とか書くとよくバグるんだけど。

次は「フォントの設定」。

;;;** Consolas + MSゴシック
(set-default-font "Consolas 10")
(set-fontset-font (frame-parameter nil 'font)
                  'japanese-jisx0208
                  '("MS ゴシック" . "unicode-bmp")
                  )
(set-fontset-font (frame-parameter nil 'font)
                  'katakana-jisx0201
                  '("MS ゴシック" . "unicode-bmp")
                  )

プログラミングするなら等幅じゃないとね。Consolasはお気に入り。

ラスト、「フレームサイズ・位置・色の設定」。

;;;** 黒背景に灰色の文字
(setq default-frame-alist
      (append (list '(foreground-color . "gray")
                    '(background-color . "black")
                    '(border-color . "black")
                    '(mouse-color . "white")
                    '(width . 120)
                    '(height . 30)
                    '(top . 100)
                    '(left . 200))
              default-frame-alist))

各項目のピリオドの右("gray"とか120とか)は自分の好みに合わせてね。Windowsだとwidthとheightを画面ピッタリに設定しても最大化ボタン押したときみたいに外枠が消えないから、Win+↑ で毎回自分で最大化してる。



以上。Windowsでも是非新しいEmacsを使ってあげてください。まあLinux常用することに問題がなければ絶対そっちの方がいいけど。

emacsで前置引数を活用する

emacsには前置引数でコマンドに定義する機能の幅が広がるのだが以外に知られていない。そこでこの前置引数を利用したelispの関数定義について紹介しようと思う。


まずはemacs上で「C-u 5 a」と入力してみてほしい。「aaaaa」と入力されるはずだ。この5が前置引数となる。ここでの「C-u 5」は入力 a を5回繰り返すという意味である。

次に「C-u a」、「C-u C-u a」、「C-u C-u C-u a」と入力してほしい。繰り返しが「4」「16」「64」と増えただろう。このように、数字を入力しなければ前置引数の値は「C-uの回数 * 4」となる。
文字入力の場合は前置引数は繰り返し回数となるが、前置引数の意味はコマンドによって違う。この値を上手く利用すると便利なキー定義ができる。

まずは関数で前置引数を利用できることを確認しよう。以下の関数を評価してみてほしい。

(defun p-test (arg)
  (interactive "p")
  (message (format "%i" arg)))

「C-u M-x p-test」とするとミニバッファに「4」と表示される。このように、関数先頭に(interactive "p")と記述すると、引数argに前置引数の値が代入されるのだ。

これを利用すると、以下のような関数定義ができる。

(defun my-def (arg)
  (interactive "p")
  (case arg
    (4  (コマンド実行前にC-uを押した時の処理))
    (16 (コマンド実行前にC-u C-uを押した時の処理))
    (64 (コマンド実行前にC-u C-u C-uを押した時の処理))
    ...
    (t  (C-uを押してないときの処理))))

例えばこのmy-defをC-tに割り当てると、C-t, C-u C-t, C-u C-u C-t, ... に別の機能を割り当てることができる。

この前置引数を用いた関数定義は、似たような機能をどちらもキーに割り当てたいが、どのキーに割り当てるか困ったときに非常に重宝する。既に定義済みのコマンドを拡張したような機能を追加するときも、既に定義しているキーの前にC-uを叩くだけなので覚えやすいという利点もある。

例として私が定義している関数を挙げてみよう。

;;;** 画面の分割と移動(分割されていたら移動)
(defun other-window-or-split ()
  (interactive)
  (when (one-window-p) (split-window-horizontally))
  (other-window 1))

;;;** 上に加えてC-uが付いていたら画面を閉じる
(defun other-window-or-split-or-close (arg)
  "画面が1つなら分割、2つ以上なら移動。
C-uをつけるとウィンドウを閉じる。"
  (interactive "p")
  (case arg
    (4  (delete-other-windows))
    (16 (delete-window))
    (t  (other-window-or-split))))

これはEmacsテクニックバイブルで紹介されていた「C-tに画面分割と移動の両方を定義する」というelispに前置引数を用いて拡張したものである。

C-t で画面分割ができるなら、分割した画面を閉じるコマンドも C-t に割り当てたいということで、「C-u C-t」にカーソルが合っていない画面を閉じる機能を、「C-u C-u C-t」にカーソルが合っている機能を追加した。あとは other-window-or-split-or-close を C-t に割り当てればよい。


似たような機能をいろんなキーに定義していて覚えきれなくなっている人はぜひ前置引数を活用して欲しい。

subやgsubで括弧とマッチした部分の再利用、および式展開の方法

Rubyで文字列を置換するsub, gsubメソッドだが、元の文字列の一部を使用したいことはよくある。また、置換後の文字列に式展開を行いたいこともあるだろう。その方法が複数あるので紹介したい。

その前に sub, gsub の簡単な説明を

String#sub, String#gsub は文字列を置換するメソッドである。第1引数と一致する部分を第2引数に置換する。sub は最初に一致したものを置換し、gsubはマッチする全ての文字列を置換する。破壊的なメソッド sub!, gsub! も用意されている。

str = "a perl, a ruby, a python"
str.sub('a', 'the')             # => "the perl, a ruby, a python"
str.gsub('a', 'the')            # => "the perl, the ruby, the python"

第1引数には正規表現も利用できる。

str = "a perl, a ruby, a python"
str.sub(/\s/, '_')              # => "a_perl, a ruby, a python"
str.gsub(/\s/, '_')             # => "a_perl,_a_ruby,_a_python"

このように正規表現で置換元を指定する場合、括弧を使用すると置換後の文字列に利用することができる。今回紹介するのはその方法についてである。

シングルクォート内で \数字 を使う方法

シングルクォート内で「\数字」を使うと、n番目の括弧を置換後の文字列に使用できる。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, '<p>\1</p>') # => "<p>ruby</p> <p>python</p>"

このとき、「#{}」では式展開できない。それどころか、改行文字などの特殊文字も使用できない。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, '<p>#{1+2}</p>\n') # => "<p>\#{1+2}</p>\\n <p>\#{1+2}</p>\\n"

式展開したい場合、「String#%」を利用するのがいいだろう。いわゆるフォーマット文字列だ。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, '<p>%i</p>%s' % [1+2, "\n"]) # => "<p>3</p>\n <p>3</p>\n"

ダブルクォート内で \\数字 を使う方法

ダブルクォート内で「\\数字」を使うことでも、n番目の括弧を置換後の文字列に使用できる。

そもそも、置換後の文字列内で括弧にマッチした部分を表す文字列は「\数字」なのだが、ダブルクォート内では「\1」は特殊文字「\x01」と解釈されてしまう。そこで、バックスラッシュでエスケープしたバックスラッシュ(\\)と数字で「\数字」を表現しなければならないのだ。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, "<p>\\1</p>") # => "<p>ruby</p> <p>python</p>"

ダブルクォート内のため「#{}」での式展開も特殊文字も使用可能である。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, "<p>#{1+2}</p>\n") # => "<p>3</p>\n <p>3</p>\n"

ブロックを使う方法

第2引数を用いずにマッチした部分をブロックの評価結果で置換することもできる。この場合、括弧とマッチした文字列を示す特殊変数 $1~$9 が使用できる。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/) {"<p>#{$1}</p>"} # => "<p>ruby</p> <p>python</p>"

注意

ダブルクォート内で $1 を使用すればいいのでは?と思うかもしれないが、残念ながらそれはできない。

"<u>perl</u> <b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/, "<p>#{$1}</p>\n") # => "<p></p>\n <p></p>\n <p></p>\n"
$1                              # => "python"

$1~$9 はマッチ後の参照であり、正規表現内扱いのgsubの引数では使用できないためだ。

まとめ

個人的には、式展開がない場合はシングルクォート、ある場合はブロックを使うのがオススメだ。ちなみに、シングルクォート内での「\\」は「\」と解釈されるが、ややこしいので使うのはやめておいた方がいい。

正規表現の先読みを解説

正規表現の先読みを最近になって理解したので解説してみる。

正規表現の先読み

正規表現の先読みとは、簡単に言うと「それがあることを保証するがマッチはさせない文字列」である。

この、「あることを保証してマッチさせない文字列regexp」はRubyでは「(?=regexp)」と書く。

まずは先読みがない例を挙げる。

str = "hoge1huga2"
str.match(/hoge.+2/)            # => #<MatchData "hoge1huga2">
str.match(/hoge.+3/)            # => nil

これは問題ないと思う。次に先読みがある例だ。

str = "hoge1huga2"
str.match(/hoge(?=.+2)1/)         # => #<MatchData "hoge1">
str.match(/hoge(?=.+3)1/)         # => nil

この1つ目のmatchでは、「.+2」が「あることを保証してマッチさせない文字列」となる。

確かに、文字列strの後ろには「1huga2」という「.+2」にマッチする文字列が存在するが、MatchDataにはそれが反映されていない。

以下のように2段階に分けて考えると、もっとわかりやすいかもしれない。

  • (?=regexp) の後ろを除去した「hoge(?=.+2)」にマッチするかをまず調べる(これはつまり「hoge.+2」とマッチすることを確認する)
  • 上がマッチされることを確認したら (?=regexp) を除去した hoge1 と実際にマッチさせる(上がマッチしなければnilを返す)

いっそのことrubyのプログラムで書いてしまうとこんな感じ。

(str =~ /hoge.+2/) ? str.match(/hoge1/) : nil    # => #<MatchData "hoge1">

これだと、先読み例2つめのmatch(/hoge(?=.+3)1/)では、「hoge.+3」とマッチする文字列がstrに存在しないため結果がnilというのもわかるだろう。先ほどのようにプログラムで書いてもnilが返ってくる。

(str =~ /hoge.+3/) ? str.match(/hoge1/) : nil     # => nil

否定先読み

さて、実際にはこちらの方が使用頻度が多いかもしれない。

上と同じ言い方をすると、否定先読みは「それがないことを保証する文字列」である。

否定先読みはRubyで「(?!regexp)」と書く。

先ほどと同じ例を先読みではなく否定先読みで実行してみる。

str.match(/hoge(?!.+2)1/)         # => nil
str.match(/hoge(?!.+3)1/)         # => #<MatchData "hoge1">

先読みが理解できていれば、結果が当然逆になることもわかるだろう。2段階に分けた考え方はこのようになる。

  • (?=regexp) の後ろを除去した「hoge(?=.+3)」にマッチしないかをまず調べる(これはつまり「hoge.+3」とマッチしないことを確認する)
  • 上がマッチしないことを確認したら (?=regexp) を除去した hoge1 と実際にマッチさせる(上がマッチしたらnilを返す)

先読みの逆である。三項演算子を使用したプログラムの例についても、str.match(/hoge1/) と nil を入れ替えることになる。

使用例

例えば、以下のように記号で囲まれた文字列を取得したい場合。

:hoge:
:hoge:huga:piyo:

文字列が1つなら「:」で挟み、2つ以上ならその後ろに「文字列:」を書き足していくというものである。

これは2つ以上の場合「/:[^:]+:/」では取得できない。

s1 = ":hoge:"
s2 = ":hoge:huga:piyo:"
s1.scan(/:[^:]+:/)             # => [":hoge:"]
s2.scan(/:[^:]+:/)             # => [":hoge:", ":piyo:"]

なぜなら、s2の「:hoge:」とマッチが終わった時点での文字列が「huga:piyo:」のように、先頭の「:」が欠けてしまっているからだ。

s2.scan(/:([^:]+)/).flatten     # => ["hoge", "huga", "piyo"]
s2.scan(/([^:]+):/).flatten     # => ["hoge", "huga", "piyo"]

のようにしてもいいが、これは先頭や末尾に別の文字列があるときに上手くいかない。

そこで先読みを使ってみる。先読みを使って書くとこのように書ける。

s2.scan(/:(?=.+:)([^:]+)/).flatten      # => ["hoge", "huga", "piyo"]

後ろ「:」があることを保証して「:[^:]+」とマッチさせている。こうすれば、末尾に別の文字列があっても「:」が入っていなければマッチしない。

まとめ

先読みについて説明してきたが、絶対に必要かと言われたらそんなことはない。プログラム中では三項演算子の例のように分岐でどうにかなるからだ。

ただ、否定先読みは「[^ ]」の文字列版として便利なことが多い。正規表現に慣れてきたら、否定先読みを使えるところでは積極的に使ってみることをオススメする。