はじめに

行単位に処理するのであれば、sedで簡潔に正規表現を使用してマッチングができる。しかし、複数行にわたって正規表現のマッチングをするのであれば、Rubyを使った方が簡単。複数行であっても、正規表現内に行数指定がないのであれば、Rubyでなくてもいいのだが。

以下検証はCentOS 6とRuby 2.4をもとに実施するが、CentOS 7に入っていたsed (sed (GNU sed) 4.2.2) を使うと-zオプションが使えてRubyに近い使い勝手ができたので、-zオプションの時のみCentOS 7で実施する。

複数行の正規表現マッチング

まずは複数行であっても正規表現内に行数の指定がない単純なものを考える。題材として、Tomcatのserver.xmlからコメントアウトされた部分(<!--,-->)を省いて標準出力に表示してみることにする。

sedで複数行の置換

sedを使うと次のようになる。

cat server.xml | sed -r '/<!--.*-->/d' | sed -r '/<!--/,/-->/d'  | sed -r '/^$/d'

1つ目のsedで1行内にコメントアウトがある部分を消し、2つ目のsedでコメントアウトの開始タグと終了タグが複数行に別れている場合のその範囲を消している。最後のsedでは空行になった行を消して、見やすくしている。

Rubyで複数行の置換

Rubyで何通りかに書き換えてみる。

mオプションを使う場合

ruby -e 'puts File.read("server.xml").gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")'

この書き方は、File.readでserver.xmlファイルの全行を読み込んで、その文字列をgsubで正規表現による置換にかけている。正規表現にmオプションをつけており、.が改行にもマッチするようになっている。.が改行にマッチするので、<!--.*?-->はコメントアウトタグに囲まれており、その囲いの中は改行を含む任意の文字の繰り返しの最短マッチとなる。ちなみに?の部分が最短マッチを示している。正規表現のデフォルトは最長マッチになっているので注意が必要。

正規表現自体には関係ないが、Rubyのワンライナーの練習のために、ファイルの読み込み方を変えて書いてみる。

ruby -e 'puts File.read("server.xml").gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")'
ruby -e 'puts File.read(ARGV[0]).gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")' server.xml
ruby -e 'puts ARGF.read.gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")' server.xml
cat server.xml | ruby -e 'puts ARGF.read.gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")'
cat server.xml | ruby -e 'puts STDIN.read.gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")'
cat server.xml | ruby -e 'puts readlines.join.gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")'

先ほどの書き方も再掲して、計6通りの書き方をしている。

  1. File.readでファイルを全行読み込み。
  2. File.readで読み込む点は同じであるものの、Rubyワンライナー自体の引数としてファイルを渡している。ワンライナーではなくスクリプトファイルにRubyを書く場合にはこちらの方が引数としてファイル名を受け取れるので便利。
  3. ARGF(スクリプトに指定した引数をファイル名とみなして、それらのファイルを連結した 1 つの仮想ファイルを表すオブジェクトで、ARGV が空なら標準入力を対象とする)という仮想ファイルオブジェクトのreadで読み込んでいる。
  4. catをパイプでつないで標準入力をRubyで使用している。引数がないので、ARGFは標準入力が対象になる。
  5. 4つ目とほぼ同じだが、ARGFの代わりに標準入力STDINを使っているため、より素直な書き方になっている。
  6. catをパイプでつないで標準入力をRubyで使用している。readlinesで標準入力全行を各行の配列として取得できるので、joinで配列を1つの文字列につなぎ合わせている。

3つ目のARGFか5つ目のSTDINを使う書き方が素直な書き方だと思う。

\nを使う場合

正規表現自体を別の書き方に変えてみる。先ほどはmオプションを使い、.が改行にマッチするようにして複数行に渡る正規表現のマッチングを行ったが、今度はmオプションを使わずに実現する。

cat server.xml | ruby -e 'puts readlines.join.gsub(/ *<!--(.*?|\n)*-->/, "").gsub(/\n+/, "\n")'

mオプションをつけることで、.が改行を「含む」任意の文字になったので、mオプションをつけないのならば、改行を「含む」という点をor(|)で論理的に実現すればいい。(.*?|\n)*は任意の文字列の最短マッチもしくは改行となり、コメントアウトの閉じタグが出現するまでどんな文字が来てもいいし何度改行してもよくなる。

(.*?|\n)*を少し書き直してみる。

cat server.xml | ruby -e 'puts readlines.join.gsub(/ *<!--(.*\n)*?.*-->/, "").gsub(/\n+/, "\n")'

(.*\n)*?.*は任意の文字が0個以上あり改行がその次に来るというパターン((.*\n))が最短マッチで0個以上あり(*?)、その後コメントアウトの閉じタグに出会うまで任意の文字が0個以上続くという正規表現になる。

置換結果

sedとRubyを使ってserver.xmlからコメントアウト部分を除外したが、実際にどのようになっているのかdiffで確認してみる。

$ cat server.xml | ruby -e 'puts readlines.join.gsub(/ *<!--.*?-->/m, "").gsub(/\n+/, "\n")' | diff - server.xml -y -W 150 | head -30
<?xml version='1.0' encoding='utf-8'?>                                          <?xml version='1.0' encoding='utf-8'?>
                                                                          >     <!--
                                                                          >       Licensed to the Apache Software Foundation (ASF) under one or more
(中略)
                                                                          >          Documentation at /docs/config/server.html
                                                                          >      -->
<Server port="8005" shutdown="SHUTDOWN">                                        <Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListen            <Listener className="org.apache.catalina.startup.VersionLoggerListen
                                                                          >       <!-- Security listener. Documentation at /docs/config/listeners.html
                                                                          >       <Listener className="org.apache.catalina.security.SecurityListener"
                                                                          >       -->
                                                                          >       <!--APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener"             <Listener className="org.apache.catalina.core.AprLifecycleListener"

複数行の正規表現マッチングと行数指定

ここまでみると、sedの方がRubyより書き方が簡潔に思えるし、実際、今回のようなお題であれば、Linux系であればどこでも使えるsedの方を使うべきだろう。しかし、単なる複数行への正規表現マッチングではなく、行数の概念が出てくるとRubyの方が簡単に書ける。

sedのNコマンドで次行を読み込む

例えばある文字列を含む行の2行下に別のある文字列が登場した時だけマッチングさせたい場合、sedでもNコマンドを使ってできなくはないがわかりづらくなってしまう。これはsedが行単位で処理を行う行指向の言語で、改行を扱うことをあまり考慮していないからである。

sedのNコマンドを使うと以下のようになる。

cat server.xml | sed -r -n '/<\?xml/N;N;  /^([^\n]+\n){2}[^\n]+Licensed/p'

「<?xml」から始まる行でマッチさせて、その次の2行分(N;N;)をパターンスペースに読み込んでいる。この3行分を次で検査している。

[^\n]+\nは任意の一行を表していてる。それが2つ続いた後、[^\n]+で改行以外の文字列がきてLicensedに続く場合、正規表現に一致することになる。

<?xml version='1.0' encoding='utf-8'?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more

Rubyで改行を扱う

行指向ではない汎用のプログラミング言語であるRubyであれば、改行を普通に扱うことができる。

cat server.xml | ruby -e 'puts readlines.join.match(/<\?xml(.*\n){2}.*Licensed.*/)'

このワンライナーでは、「<?xml」の後に任意の文字列が0個以上先行する改行が2つあり、その後行中に「Licensed」という文字列がくるという正規表現をマッチングさせている。2行下というように行数の概念が出て来るとRubyの方が簡単。

sedの-zオプションで改行を区切り文字としない

CentOS 7のsedをmanで見てみると、-zオプションがある。

-z, --null-data
separate lines by NUL characters

NUL文字での分割となるため、server.xmlは一行の文字列として扱える。

cat server.xml | sed -zr 's/(<\?xml([^\n]*\n){2}[^\n]*Licensed[^\n]*).*/\1\n/'

正規表現部分の考え方はRubyと全く同じになったが、Rubyと異なり\n.でマッチしてしまうため、.*\n[^\n]*\nとしなければならない。