[Shell] sedコマンドで改行も含めた正規表現パターンをキャプチャする方法の考察


2020/04/29

今回は
sedコマンドで、複数行にまたがるテキストの正規表現パターンをどう扱うかに注目してみます。


はじめに

sedコマンドはSteam EDiterの略で、その名の通りテキスト・ストリームを操作するコマンドです。

広義のテキスト・ストリーム処理では、1つのデータを読み出すための、
データの終わりを示すなんらかの識別子が存在します。

これには特に規定のフォーマットないようですが、
sedでは通常1行ずつ読み込んで操作を行うため、データの終わりを示すEOL(End of Line)と呼ばれるシステムで定義された改行文字に依存しています。

ちなみに
主要なOSごとのEOL文字を示すエスケープシーケンスは、

            
            UNIX系/Linux OS/現行のMac OS:
    \n
    LF(Line Feed)と記される。
    単に"改行"と呼ばれればだいたいコッチ。
昔のMac OS(バージョン9以前):
    \r
    CR(Carriage Return)と記される。
    コンソールから見て一番左端の文字入力位置に戻すので"復帰"と呼ばれる。
Windows系:
    \r\n
    CR+LFの意味。
        
が採用されている(いた)ようです。

よって、基本的な
sedの操作は一行一行をさばくテキスト処理を行うがゆえに、複数の行をまたぐ正規表現パターンを適用する時には相応のテクニックが必要です。

今回の記事では、複数行にまたがる正規表現をどう取り扱うのかを深堀して理解してみようという内容です。

ちなみに、
EOLは各OSごとやsedコマンドの種類ごと(BSD sedやGNU sed)によっても扱いが違うので、利用環境をまず確認することも重要です。

EOLの文字コード

本題に入る前に少し余談ですが、脳裏に留めておきたい知識として、上記の改行文字(エスケープシーケンス)の16進数文字(ASCII エンコーディングのバイト値)および8進法文字です。

それぞれ、16進数のプレフィックスは
0xまたは0X、8進数の数字にプレフィックス0を付けます。

プレフィックスなしは10進数を意味します。

ということで、ASCII文字コードに基づいたシステムにおいては、

            
            CR:
    0x0D = 015 = 13

LF:
    0x0A = 012 = 10

CRLF:
    0x0D 0x0A = 015 012 = 13 10
        
と解釈されます。

ちなみに、8進数表記をほとんど見かけるケースが少ない(...という個人の見解)ですが、通信制御分野では
厳密に8ビットを表す単位オクッテト)を表現しやすいように、より合理的に8進法を好んで使うようです。

...進数の違いによる数字の取扱は混乱しやすいので気を払う必要があります。


動作環境

今回利用したsedDebian内でのGNU sedになります。

よく比較される
BSD sedでやると、少々挙動が違いますのでご注意ください。

            
            $ sed --version
sed (GNU sed) 4.7
パッケージ作成者: Debian
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

作者 Jay Fenlason、 Tom Lord、 Ken Pizzini、
Paolo Bonzini、 Jim Meyering、および Assaf Gordon。
GNU sed home page: <https://www.gnu.org/software/sed/>.
General help using GNU software: <https://www.gnu.org/gethelp/>.
E-mail bug reports to: <bug-sed@gnu.org>.
        

今回のサンプルテキスト

では実際の例を挙げて、sedコマンドの挙動とともに実験していきます。

ひとまず以下のような改行を含むHTMLテキストファイルを
catさせてみます。

            
            $ cat test.html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8">
        <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=Edge">
        <base href="/">
    </head>
    <body>
        <div>
            <header>
                <span>蛸壺のプロブラミング・ブログ</span>
                <a href="/home"><span>HOME</span></a>
            </header>
            <footer>
                <ul>
                    <li><a href="/home">Home</a> |</li>
                    <li><a href="/home#blog">Blog</a> |</li>
                    <li><a href="/home#about">About</a> |</li>
                    <li><a href="/home#contactus">Contact Us</a></li>
                </ul>
            </footer>
        </div>
    </body>
</html>
        
たとえば、<head>~</head>に囲まれた要素のパターンマッチしてこの要素ごと消したいなどをsedで行うにはどうするか?というのを、今回の課題としてやってみます。

            
            $ str_show=$(cat test.html)
$ echo $str_show
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8"> <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=Edge"> <base href="/"> </head> <body> <div> <header> <span>蛸壺のプロブラミング・ブログ</span> <a href="/home"><span>HOME</span></a> </header> <footer> <ul> <li><a href="/home">Home</a> |</li> <li><a href="/home#blog">Blog</a> |</li> <li><a href="/home#about">About</a> |</li> <li><a href="/home#contactus">Contact Us</a></li> </ul> </footer> </div> </body> </html>

#複数行にまたがる<head>要素の中身をキャプチャして消去したい
$ echo $str_show | sed -e "s/<head>[\s\S]*<\/head>//g"
#消えてない(複数行ではキャプチャされない)
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8"> <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=Edge"> <base href="/"> </head> <body> <div> <header> <span>蛸壺のプロブラミング・ブログ</span> <a href="/home"><span>HOME</span></a> </header> <footer> <ul> <li><a href="/home">Home</a> |</li> <li><a href="/home#blog">Blog</a> |</li> <li><a href="/home#about">About</a> |</li> <li><a href="/home#contactus">Contact Us</a></li> </ul> </footer> </div> </body> </html>

#複数行にまたがる<head>要素の中身をキャプチャして消去したい(シングルクォートで囲う)
$ echo $str_show | sed -e 's/<head>[\s\S]*<\/head>//g'
#消えてない(複数行ではキャプチャされない)
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8"> <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=Edge"> <base href="/"> </head> <body> <div> <header> <span>蛸壺のプロブラミング・ブログ</span> <a href="/home"><span>HOME</span></a> </header> <footer> <ul> <li><a href="/home">Home</a> |</li> <li><a href="/home#blog">Blog</a> |</li> <li><a href="/home#about">About</a> |</li> <li><a href="/home#contactus">Contact Us</a></li> </ul> </footer> </div> </body> </html>
        
このように通常通りにsedを適用させてみただけでは、複数行にまたがる文字パターンは抜き出すことが出来ません。


手順1 〜 改行文字を一時的に別の文字に置換させる

まずは、シングルクォートで囲ってエスケープシーケンス+文字リテラル置換を利用する方法から、改行文字をたとえば\nに置換してsedしてみます。

コンソールからLFを入力するには、ダブルクォート囲いなしの
$'\n'を使うことができます。

            
            #改行文字LFを出力
$ echo $'\n'

#16進法でLF(上のコマンドと等価)
$ echo $'\x0a'

#8進法でLF(上のコマンドと等価)
$ echo $'\012'
        
これを利用すると、改行文字を\nに置き換えることができます。

上記でも解説したように、
\x0a\012もLFを表すエスケープシーケンスになります。

基本どれを利用されてもOKです。

            
            $ str_show_2=${str_show//$'\n'/\\n}
$ echo $str_show_2
<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8">\n <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8">\n <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8">\n <meta http-equiv="X-UA-Compatible" content="IE=Edge">\n <base href="/">\n </head>\n <body>\n <div>\n <header>\n <span>蛸壺のプロブラミング・ブログ</span>\n <a href="/home"><span>HOME</span></a>\n </header>\n <footer>\n <ul>\n <li><a href="/home">Home</a> |</li>\n <li><a href="/home#blog">Blog</a> |</li>\n <li><a href="/home#about">About</a> |</li>\n <li><a href="/home#contactus">Contact Us</a></li>\n </ul>\n </footer>\n </div>\n </body>\n</html>
        
これで一行扱いになったのでsedを利用します。

            
            $ echo $str_show_2 | sed -e 's/<head>.*<\/head>//g'
<!doctype html>\n<html lang="en">\n \n <body>\n <div>\n <header>\n <span>蛸壺のプロブラミング・ブログ</span>\n <a href="/home"><span>HOME</span></a>\n </header>\n <footer>\n <ul>\n <li><a href="/home">Home</a> |</li>\n <li><a href="/home#blog">Blog</a> |</li>\n <li><a href="/home#about">About</a> |</li>\n <li><a href="/home#contactus">Contact Us</a></li>\n </ul>\n </footer>\n </div>\n </body>\n</html>
        
これで通常通りのsedコマンドが利用でき、<head>要素が削除されました。

あとは、改行文字として置換した
\nにした仮文字をLFにするように逆の操作で戻せばとにかく正規表現的な操作が可能になります。

余談1 〜 BSD sedで文字置換する場合

GNU sedでは、上記の文字リテラルでの置換${str_show//$'\n'/\\n}が上手く行きましたが、Mac OS標準で使えるBSD sedではこれは上手く行きません。

BSD sedでこれと等価なことを行おうとすると、${str_show//$'\n'/\\\\n}というように置換後の文字のバックスラッシュ\を4つにします。

            
            % str_show_2=${str_show//$'\n'/\\\\n}
% echo $str_show_2
<!doctype html>\n<html lang="en">\n    <head>\n        <meta charset="utf-8">\n        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n        <meta http-equiv="Content-Style-Type" content="text/css; charset=UTF-8">\n        <meta http-equiv="Content-Script-Type" content="text/javascript; charset=UTF-8">\n        <meta http-equiv="X-UA-Compatible" content="IE=Edge">\n        <base href="/">\n    </head>\n    <body>\n        <div>\n            <header>\n                <span>蛸壺のプロブラミング・ブログ</span>\n                <a href="/home"><span>HOME</span></a>\n            </header>\n            <footer>\n                <ul>\n                    <li><a href="/home">Home</a> |</li>\n                    <li><a href="/home#blog">Blog</a> |</li>\n                    <li><a href="/home#about">About</a> |</li>\n                    <li><a href="/home#contactus">Contact Us</a></li>\n                </ul>\n            </footer>\n        </div>\n    </body>\n</html>
        
これでGNU sedと同様のことが可能になります。


手順2 〜 改行文字を一時的に別の文字に置換させる

前項目では改行文字(LF) --> \nに一時的に変換したので、今度は戻す作業の方、\n --> 改行文字(LF)の手法を考えてみます。

まず、当然ながら先程の文字リテラルの置換の逆操作で、以下のように可能です。

            
            $ echo "${str_show_2//\\n/$'\n'}"
<!doctype html>
<html lang="en">
 
 <body>
 <div>
 <header>
 <span>蛸壺のプロブラミング・ブログ</span>
 <a href="/home"><span>HOME</span></a>
 </header>
 <footer>
 <ul>
 <li><a href="/home">Home</a> |</li>
 <li><a href="/home#blog">Blog</a> |</li>
 <li><a href="/home#about">About</a> |</li>
 <li><a href="/home#contactus">Contact Us</a></li>
 </ul>
 </footer>
 </div>
 </body>
</html>
        
ちなみに、echoで改行表示させるには、ダブルクォート(")で文字リテラル部分を囲うことがミソです。

これだけだと
sedコマンドの見せ場が足りません。

そこで同様の操作を
sedでもやらせてみると、以下のようにできます。

            
            $ echo $str_show_2 | sed -e s/'\\n'/\\$'\n'/g
<!doctype html>
<html lang="en">
 
 <body>
 <div>
 <header>
 <span>蛸壺のプロブラミング・ブログ</span>
 <a href="/home"><span>HOME</span></a>
 </header>
 <footer>
 <ul>
 <li><a href="/home">Home</a> |</li>
 <li><a href="/home#blog">Blog</a> |</li>
 <li><a href="/home#about">About</a> |</li>
 <li><a href="/home#contactus">Contact Us</a></li>
 </ul>
 </footer>
 </div>
 </body>
</html>
        
このときsedで改行文字の置換を行うポイントとしては、置換後の文字に\\$'\n'を指定していることです。

ここの部分で
$だけだと、単なる$という文字だとsedコマンドが認識してしまいます。

そこでコマンド入力だと示すために
\\$と与えてあげるようなテクニックだそうです。


まとめ

以上の手順から、sedコマンドで複数行にまたがる文章パターンを正規表現で操作する際には、改行文字を上手く扱う必要があることが分かります。

今回やったことをパイプライン処理でつなげ、更に元のファイルに上書き保存まで加えると、以下のようになります。

            
            $ str_show=$(cat test.html)
$ echo ${str_show//$'\n'/\\n} | sed -e s/'<head>.*<\/head>'//g -e s/'\\n'/\\$'\n'/g > test.html
<!doctype html>
<html lang="en">
 
 <body>
 <div>
 <header>
 <span>蛸壺のプロブラミング・ブログ</span>
 <a href="/home"><span>HOME</span></a>
 </header>
 <footer>
 <ul>
 <li><a href="/home">Home</a> |</li>
 <li><a href="/home#blog">Blog</a> |</li>
 <li><a href="/home#about">About</a> |</li>
 <li><a href="/home#contactus">Contact Us</a></li>
 </ul>
 </footer>
 </div>
 </body>
</html>
        
シンプルとはいかないまでも、中々見通しのよいスクリプトにすることができました。

参考サイト

sedで改行を出力する

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。