Software Design連載記事を掲載します。

株式会社技術評論社の許可を得て掲載しています。
草稿なので細かい部分は実際の記事とは異なることがあります。

他の記事は左下にある「■雑誌連載中(全文公開)」から見られます。

検索と置換

 ども、ドメイン取得以降さらにEmacs愛が加熱したるびきちです。前回は日常的にEmacsを使う上で便利な機能を紹介しました。カーソル移動と入力支援はテキストエディタの両輪となる機能なので、しっかりと押えておきましょう。

 今回取り上げるのは、検索と置換です。前回でもisearch、正規表現isearchを取り上げましたが、それらはたった1箇所が対象です。今回は、一度に複数箇所を編集する方法です。これも知っておくと楽しくなってきます。

 この前、興味本意でメモ帳を触ってみましたが、編集機能があまりに貧弱すぎて発狂しそうでした(笑)

置換

 順番が逆になってしまいますが、まずは置換についてお話していきます。というのは、今回は検索といっても全検索であり、検索結果を編集することで実際のファイルに反映させるのが目的だからです。検索結果を編集する多くの場合は置換を使うので、まず最初に置換を知っておくべきなのです。

 置換は多くの場面で使われます。プログラミングにおけるリファクタリングはもちろんのこと、文書作成においても用語を統一したり、データを見やすいレイアウトに整えたりなど、あらゆる場面で活躍します。もし、置換を知らなかった場合は1箇所1箇所isearchで移動をして、元の文字列を消して新しい文字列に置き換えるという単純作業を何度も繰り返すハメになってしまいます。数ヶ所ならともかく、5箇所以上あったらうんざりですよね。

 置換は前方検索、すなわちカーソル位置よりも後で行われます。よって、置換に入る前準備として置換対象の文字列よりも前にカーソルを持って行く必要があります。特にバッファ全体を置換対象にするときは、M-<でバッファ先頭に移動しておきます。

 単純な文字列置換はM-%を使います。たとえば、aをAに置き換えるときは、M-% a RET A RETと操作します。実行すると、「a」が見付かった場所にカーソルが移動し、置換するか聞いてきます。

Query replacing a with A: (? for help)

 このようにミニバッファに出てくるので、まだ使ったことのない方は「?」を押してみましょう。*Help*に操作方法が出てきますね。特に「y(カーソル位置を置換する)」、「n(カーソル位置を置換しない)」、「!(すべて置換する)」、「q(置換をやめる)」を覚えていれば困りません。

 これは置換するか毎回尋ねてくるのでquery-replaceというコマンド名です。すべて置換することが分かっている場合は尋ねない方が嬉しいですね。M-%で置換前後の文字列を入力してから「!」を押せば一気に置換してくれます。尋ねないバージョンM-x replace-stringも存在しますが筆者は使っていません。M-%は尋ねて欲しいときと欲しくないときの両方をまかなえるからです。

 正規表現は置換にも使えます。M-%の正規表現版C-M-% (query-replace-regexp)があるのですが、慣れるまで難しいです。C-M-%というイカれたキーバインドもそれを暗示しています。正規表現を知っていても確実にマッチさせるのは難しいものがあります。実は筆者も複雑な正規表現置換は使いきれていません。ですが、御安心ください。初級者にでもそれと同等のことができる方法があるのです。

バッファ内を全検索する

 isearchはカーソル位置から見て次の位置を検索しますが、バッファ全体を検索したいときがありますよね。端的に言えばバッファ内grepです。特定のキーワードが含まれる行をリストアップするときにものすごい役立ちます。プログラミングでいえば関数定義のリストが欲しいときがありますよね。

 この場合に手軽に使える標準コマンドがM-s o(occur)です。実行すると、表示する行の正規表現を訊いてきます。たとえば、Fig1の上の画面に表示されているテキストにおいてM-s oの後に「-」を押すと、「-」が含まれている行が下に表示されます。

occur-1.png
Fig1

 occurの実行後、マッチした行にジャンプできます。そのための方法は2つあります。

  1. *Occur*バッファから、該当行にジャンプする
  2. 元のバッファにいながら、マッチした行にジャンプする

 わかりやすいのは前者の方法です。ただoccurを実行した後、カレントウィンドウは元のバッファのままで、*Occur*バッファにはありません。そのため*Occur*のウィンドウを選択する必要があります。Fig1の*Occur*ウィンドウの4行目(目玉焼)にカーソルを移動してRETを押すと、occur-sample.txtの該当行に移動します。

 後者の方法はマッチした行に順番に移動する方法です。言ってみれば、isearchにマッチ行一覧が付属したようなものです。M-g M-n (next-error)で次のマッチ行、M-g M-p (previous-error)で前のマッチ行にカーソルを移動します。この2つのコマンドはoccur以外にも後述するM-x grepやコンパイラのエラー行に進むなどの用途があります。

 2つの方法は場合によって使い分けてください。特定の行にジャンプしたいのならば、前者の方法がよいです。そのときは*Occur*バッファでisearchをするなどして絞り込むことになるでしょう。対して、すべてのマッチ行を見たいのならばウィンドウ選択なしで使える後者の方法がよいです。

 前者の方法がわかりやすくて大好きという人がいると思います。かつての筆者もそうでした。それならば解決策はいくつかあります。

  1. ウィンドウ選択を楽にする
  2. マウスで*Occur*の行をクリックする
  3. ace-jump-modeで見えている*Occur*の行にジャンプする
  4. 元からoccurのウィンドウを選択させる

ウィンドウ選択をしやすくする

 ウィンドウ選択を楽にするというのは、C-x oを別なキーに割り当て直すことです。Emacsで複数のウィンドウを使っていると、本当に頻繁にC-x oを使います。それならば押しやすいキーに割り当て直すべきです。筆者は大昔からC-tに割り当てています。このたった1行の設定で、ウィンドウ選択が本当にやりやすくなり、ウィンドウ選択をする他のコマンドがいらなくなるくらいです。

(global-set-key (kbd "C-t") 'other-window)

 元のC-tはカーソル直前の2文字を入れ替えるtranspose-charsです。このコマンドが好きという人もいますが、筆者は使っていません。タイプミスでよくあるのが2つのキーを入れ違いです。たとえばlsをslとタイプしてしまうとかです。それに気付いたら即座にC-tを押せば直ります。元からなじめなかったのもありますが、なによりローマ字入力での日本語入力で使えないのが痛いです。

 「ください」はローマ字入力だとkudasaiですが、iとaを入れ違いにしてkudasiaになると「くだしあ」になってしまいます。そこでC-tを押しても「くだあし」になってしまいます。C-tを押すのがクセになっている人にとって、日本語入力でそれが使えないのはものすごい苦痛です。ローマ字入力に対応したtranspose-charsが待ち望まれます。それまではC-tはいらない子というのが僕の結論です。それならば頻繁に使うコマンドにC-tを譲ってあげるべきだと考えています。

マウスの代用としてace-jump-modeが大活躍

 ace-jump-modeは先月紹介しましたが、近距離のカーソル移動手段として超強力な方法です。isearchは画面外にも検索範囲が伸びますが、ace-jump-modeは画面内移動に特化しています。それもカレントウィンドウに限らず、他のウィンドウにも3ストローク以内で移動できるのです。

 マウスも画面内に特化した移動方法で、*Occur*ウィンドウ内をワンクリックで該当行に移動できます。occurの局面においてマウスも強力な方法ですが、筆者の場合、ace-jump-modeを使ってからEmacsでマウスに手を伸ばすことがほとんどなくなりました。

 Fig1ではスペースの関係上横幅を取っていないので上下にウィンドウが分割されていますが、最近主流となっているワイド画面でoccurを実行すれば、左右に分割されます。左右分割されれば40〜60行も画面に表示されます。よって、*Occur*が画面内に収まるのであれば、ace-jump-modeで*Occur*のカーソルを移動してRETを押せば、元のバッファの該当行にジャンプできます。

最初から*Occur*を選択させる

 3番目の方法である最初から*Occur*を選択する方法もあります。このアプローチが好きならば、M-x occur-and-selectを定義して使ってください。ついでにoccurに割り当てられているキーも置き換えておきます。

(defun occur-and-select (regexp &optional nlines)
  (interactive (occur-read-primary-args))
  (occur regexp nlines)
  (select-window (get-buffer-window "*Occur*"))
  (forward-line 1))
(global-set-key (kbd "M-s o") 'occur-and-select)

 それをさらに推し進めた外部Emacs Lispにcolor-moccurというのがあります。occurを超強化したものでかつての筆者も使っていました。でもcolor-moccurの機能は別な便利な方法で実現できてしまう今はもう使っていません。お好みで。

occurの結果を編集する

 occurが提供する機能はこれだけではありません。なんと、*Occur*バッファを編集して元のバッファに反映させられるのです!この機能を実現している外部Emacs Lispプログラムが昔からありますが、今や標準機能であるのです。

 これを使えば正規表現置換に躊躇する人でも、単純な文字列置換でそれと同等の処理が行えます。正規表現置換は本誌に書ききれないほどの機能がありますが、高度な正規表現置換が使われることはめったにありません。ならば、直観的でわかりやすいoccur編集で間に合います。

 今、Fig1の状態にあるとします。つまり、occur-sample.txtでM-x occur -を実行した直後です。「-」を「**」に置き換えてみましょう。以下の手順で操作します。

  1. *Occur*バッファを選択する(C-x o)
  2. eを押してoccur-edit-modeに入る
  3. M-% - RET ** RETで置換する

 すると、置換しただけで元のバッファが変更されます(Fig2)。M-x occur-and-selectを使えば1は省略できます。

occur-edit-1.png
Fig2

カスタマイズはほどほどに

 なお、いきなりoccur-edit-modeに入るコマンドM-x occur-and-editを定義することはできますが、筆者はやりすぎだと思います。なぜなら、割に合わないからです。M-x occurの目的は普通にバッファ内grepとして使うことが多く、occur-edit-modeを使う頻度は多くありません。それにたった1ストロークでoccur-edit-modeに入れます。たかが1ストロークを節約するために頻度の低いコマンドを定義するのはコストに見合わないのです。M-x occur-and-editで起動するとなると、M-s o→C-x o→eでoccur-and-editを起動する場合よりもかえってストロークが多くなってしまいます。

 このように、カスタマイズはやりすぎないで一定のところで止めることが大事です。この線引きについては経験がものを言います。そして常に「このカスタマイズをすることによるメリットとコストは何なのか」と自問してください。カスタマイズをしすぎると管理コストが発生します。なにより使用頻度の低いコマンドはそのうち忘れてしまいます。

 筆者もかつて猛烈にカスタマイズしまくった時期がありました。世界有数レベルでカスタマイズに没頭していました。間違いなく日本一Emacsをいじくり回していました。今はかなり落ち着いていますが、その時期があったからこそ、今こうしてあなたにレッスンをお伝えすることができるのです。

grepの結果にジャンプする

 ここまでは、単一のバッファに対して検索・編集を行うものでした。M-x grepはEmacsの中でgrepプログラムを動かして、その検索結果にジャンプするものです。これにより、複数のファイルやディレクトリにまたがる検索もできるようになります。しかもelispよりもはるかに高速に。

 M-x multi-occurは複数のバッファに対してoccurしますが、予め検索対象のバッファを指定する必要があります。仮にすべてのバッファを検索対象にしたら、遅すぎて日が暮れます。なぜなら、Emacs Lispでgrepの真似事をしても、所詮は子供の遊びレベルです。Emacs Lispはユーザーインターフェースを記述するのが得意ですが、大量のデータを扱うのが大の苦手です。おまけにマルチスレッドやマルチコアに対応していないので、現在の高性能なコンピュータの性能を活かせません。

 対してgrepプログラムは検索のプロです。特にGNU grepは爆速で、数GB程度のデータなら数秒あれば結果を出力してくれます。フルEmacs Lispで検索するより何千何万倍も速いです。適材適所、餅は餅屋です。

 そこで、検索はgrepプログラムに任せて、表示および検索結果へのジャンプはEmacs Lispで書くという役割分担をすることにしました。それならば速度と利便性を両立できます。おまけにEmacs Lisp部分の行数も削減できます。M-x grep以外にもこの方式を採っているEmacsコマンドはたくさんあります。

 M-x grepを実行するとミニバッファに「Run grep (like this): 」というプロンプトと「grep -nH -e 」などの初期入力が出てきます。grepのオプションは環境によって異なるのですが、この調整はM-x grep側がやってくれます。

 大事なのは「-n」「-H」オプションでそれぞれ検索結果の行番号、ファイル名を出力することです。検索結果にジャンプするためにはこれらの情報が必要です。「-H」オプションが使えない場合は、検索ファイルの指定の後に/dev/nullなどのヌルデバイスを付加し、強制的にファイル名を出力させるようにします。

 あとはいつも通りgrepプログラムを実行するコマンドラインを入力するだけです。つまり正規表現とファイル名を入力します。もちろん他のオプションを入力しても構いません。

 M-x grepを実行したら、別ウィンドウの*grep*バッファに検索結果が出てきます(Fig3)。grepを実行するのは時間がかかることがあるので、grepプログラム実行中でもEmacsの操作ができます。grepプログラムが出力するたびに*grep*バッファが更新されます。実行中であっても、現時点での検索結果にアクセスできます。

grep-1.png
Fig3 Ricty-35.8:bold

 M-x grepの検索結果にアクセスする方法は、M-x occurとまったく同じです。*grep*バッファでRETを押すか、M-g M-n/M-g M-pを実行するかです。

 M-x grepには、もうひとつ大事な特徴があります。それは他のプログラムも実行できるということです。たとえば、grepの代わりにgzipされたファイルも検索するzgrepを実行できます。オプションを設定する必要がありますが、ソースコード検索に特化した高速grepのack/ag/ptを実行させることも可能です。grepそのものではなくてソースコード検索ツールmilkode(gmilk)をも実行できてしまいます。grep -nH形式、すなわち「ファイル名:行番号:」を出力してくれるプログラムであればなんでもよいわけです。M-x grepのように外部プログラム丸投げ方式のEmacsコマンドは高速性だけでなく柔軟性をも獲得したのです。

grepの結果を編集してファイルに反映させる奥義

 M-x occurではoccur-edit-modeで検索結果を編集することができますが、M-x grepでも同じようなことができないでしょうか?それを実現するパッケージがwgrepです。wgrepとはWritable GREPのことで、*grep*を編集することで元のファイルにも反映させていくものです。

 wgrepはMELPAというパッケージ登録所に登録されているので、パッケージの設定さえしてしまえばEmacsの中でインストールできます。

M-x package-refresh-contents
M-x package-install wgrep

(package-initialize)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)

 そして、wgrepの設定もしておきます。*grep*バッファでeを押すことで編集可能になります。eを選んだのは、occur-edit-modeと揃えるためです。似たコンセプトのコマンドのキー割り当てを揃えておくことで、ストレスなく使うことができます。

==
(require 'wgrep)
(setq wgrep-change-readonly-file t)
(setq wgrep-enable-key "e")
==

 Fig1の状態でwgrepを使うには、*grep*バッファにウィンドウを切り替え、eを押します。すると、
「Press C-x C-s when finished or C-c C-k to abort changes.」
とメッセージが出てきます。つまり、*grep*バッファの変更をファイルに反映させたければC-x C-sを、取り止めたければC-c C-kを押せということです。

 Fig4は最初のregexpをREGEXPに置き換えたところです。変更された部分はこのように色がつきます。ここでC-x C-sを押すと実際のファイルに反映されますが、その時点ではファイルは保存されていません。occur-select.elが修正済状態になっている(モードラインの左に「**」と表示されている)ことに注意してください。

 wgrepで複数のファイルを変更し、すべてのファイルを保存するにはC-x sの後に!を押してください。

wgrep-1.png
Fig4

wgrep-2.png
Fig5

 このようにwgrepは大きな編集をこなせる超強力なコマンドです。言うまでもなくM-x grepの強味は生きており、任意のgrep -nH形式のプログラムの出力結果を編集してファイルに反映できます。wgrepはさほど使用頻度が高いわけではありませんが、飛び道具として覚えておいてください。

終わりに

 今回は置換と全検索を取り上げました。ここまでの段階で、かなり複雑な編集ができるようになったはずです。とくに検索結果を編集する機能には驚かれたと思います。

 筆者のサイトrubikitch.comではEmacsの情報発信基地をめざすべく、定番情報や最新情報を日々更新しています。さらにステップアップしたい方はメルマガ登録お願いします。http://rubikitch.com/juku/ Happy Emacsing!

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