---------- SED 教室 第八回 「有象無象」 ---------- SED 教室もおかげさまで八回目を迎えました。前回までの講義で SED の最も よく使われる (と私が勝手に思っている) 命令の説明が一応終わりましたので、 今回は趣向を変えて色々なスクリプトを紹介して、みなさんがスクリプトを書く ときの参考にしていただく事にしましょう。 今回からスクリプトに行番号を付ける事にしました。SED で実行するときは行 頭の行番号を ( SED で :-) 削除してから使ってください。 いままで紹介してきたスクリプトは比較的(?) 実用度が高いものでした。実用 一点張りでは面白くないということでちょっと遊んでみます。 <<< REV.SED >>> --------------------------------------- 1 s/$/\ 2 / 3 :loop 4 /..*\n/{ 5 s/\(.\)\(.*\)\n/\2\ 6 \1/ 7 b loop 8 } 9 s/\n// --------------------------------------- 一~二行目の置換命令でパターンスペース内にセグメントを二つ (二番目のセ グメントはヌルストリング) 作り、五~六行目の置換命令で前のセグメントの先 頭文字を二番目のセグメントに一文字づつ移していきます。前のセグメントに文 字がなくなれば、ループを抜けて最後の置換命令でセグメントの区切りを削除し て、パターンスペースを標準出力に吐き出す、というわけです。 たとえばパターンスペースの内容が「abc」だとすると、一~二行目の置換に より「abc<改行>」になって、四行目の条件「/..*\n/」が成立するから、次の行 の置換で「bc<改行>a」になります。「:loop」にジャンプして再び置換、今度は 「c<改行>ba」になります。同様にして「<改行>cba」になると四行目の条件が成 立しなくなりますから、九行目の置換によって「cba」になるわけです。面白い でしょう? :-) ところで、四行目の実行条件の正規表現「..*\n」と、五行目の置換命令の正 規表現「\(.\)\(.*\)\n」が、「\(」「\)」を除けば同じであることに気づきま したでしょうか。同じことを二度書くのは無駄ということで、<<< REV.SED >>> は次のように書き換えることができます。 <<< REV2.SED >>> --------------------------------------- 1 s/$/\ 2 / 3 :loop 4 s/\(.\)\(.*\)\n/\2\ 5 \1/ 6 t loop 7 s/\n// --------------------------------------- 命令「b」の代わりに新しい命令を使っています。 t ラベル 直前の置換命令の実行で、実際に置換が行われたならば 「:ラベル」の行にジャンプする。 命令「t」でジャンプするのは置換が行われたときですが、ではどんな時に置 換が行われるのでしょう。もちろん置換命令の正規表現がパターンスペースにマ ッチするときですね。したがって、 s/正規表現/置換文字列/ t ラベル と /正規表現/{ s/正規表現/置換文字列/ b ラベル } は全く同じ動作をします。このように命令「t」を使ったスクリプトは常に命 令「b」を使ったスクリプトで書き換え可能なので、命令「t」は本質的に不要な のですが、命令「t」を使った方が同じ「正規表現」を二度書かなくて済むので、 スクリプトが短くなりますし、実行も速くなります。 次はちょっと実用的なものです。標準入力から読み込んだテキストの最後の 5 行だけ標準出力に吐き出します。 <<< TAIL.SED >>> --------------------------------------- 1 :loop 2 $q 3 N 4 2,5b loop 5 D --------------------------------------- まず三行目の「N」でパターンスペース内に標準入力の 2 行分が記憶されます。 そして標準入力の 2 行目から 5 行目までは、スクリプト四行目の条件「2,5」 が成立しますから、一行目にジャンプして三行目の「N」が繰り返し実行されま す。結局標準入力の 1 行目から 6 行目まで記憶された後「2,5」の条件が成立 しなくなって五行目の「D」が実行されます。つまりパターンスペースの最初の セグメントが削除され、スクリプトの先頭へジャンプします。このときパターン スペース内には、標準入力の 2 行目から 6 行目までの計 5 行分が記憶されて います。 後は「N」と「D」が交互に実行されますので、標準入力から 1 行づつ読み込ん でパターンスペースの最後尾にセグメントを追加しつつ、先頭からセグメントを 一つづつ削除していくことになります。従ってスクリプトの二行目では常にパタ ーンスペース内に 5 行分が記憶されているわけです。そして標準入力の最終行 を読んだとき「$」の条件が成立して、「q」を実行。つまり、パターンスペース の内容を標準出力に吐き出して終了します。 では次のスクリプトは何をするでしょうか。 <<< BOTTOMUP.SED >>> --------------------------------------- 1 :loop 2 N 3 s/\(.*\)\n\(.*\)/\2\ 4 \1/ 5 $!b loop --------------------------------------- このスクリプトの面白いところは、一度も「d」あるいは「D」が実行されない という点にあります。つまり標準入力を全部パターンスペースに記憶してしまう のです。当然 SED が記憶できる容量には限度がありますから、標準入力の行数 が多いと実行できません。 スクリプトの五行目に「$!b loop」とありますから、標準入力の最後の行を読 むまでスクリプトの一行目~五行目が繰り返し実行されます。つまり「N」と「s」 が交互に実行されるわけです。「N」は標準入力から 1 行読んで、パターンスペ ースの*最後尾に*セグメントとして追加されるのでした。では次の「s」は何 をしているのでしょう? 実は最後尾のセグメントを一番先頭に持ってくるので す。結局新たに標準入力から読み込んだ行を、パターンスペースの*先頭に*セ グメントとして追加することになります。標準入力から読み込んだ行をどんどん パターンスペースの先頭に追加していくとどうなるか、... もうおわかりですね。 三~四行目の置換命令がちょっと難しいかも知れません。たとえば次のファイ ル INPUT.TXT を標準入力から読んだ場合を考えます。 <<< INPUT.TXT >>> --------------------------------------- これは一行目だ。 こっちは二行目。 そして三行目。 つづいて四行目。 これが五行目。 --------------------------------------- スクリプトの二行目の「N」で標準入力の 3 行目を読んだ時点で、パターンス ペースの内容は次のようになります。(命令「p」を「N」の次の行に挿入する事 により確認できるでしょう。) --------------------------------------- こっちは二行目。<改行> これは一行目だ。<改行> そして三行目。 --------------------------------------- 次の置換命令「s」でパターンスペースはどう変化するでしょうか。正規表現 「\(.*\)\n\(.*\)」には、改行コードを含むすべての文字列がマッチするから大 変です。たとえば、「二行目。<改行>これは一」とか、「。<改行>そして三行」 とか、何でもありです。でも実際に置換される文字列は、SED 教室 第五回で説 明したように、 マッチする文字列の中で、文字列の左端が一番左にあるもの。 もし左端が一致する文字列が複数個ある時は、その中で一番長いもの。 ですから、「こっちは二行目。<改行>これは一行目だ。<改行>そして三行目。」 です。この文字列はパターンスペース全部だから、左端が一番左で一番長く、文 句無しです。 ところが、この正規表現には「\(」と「\)」が使ってあります。単に置換され る文字列が分かっただけでは不十分なのです。すなわち、「\1」と「\2」に記憶 された文字列が分からなければ、置換文字列が分かりません。「\1」は前半の 「.*」がマッチした文字列が記憶され、「\2」は後半の「.*」がマッチした文字 列が記憶されます。前半/後半の「.*」がマッチした文字列はどれでしょう? 二つの「.*」の間に改行コードである事が必要なので、次の二つのうちのどち らかだと思われます。 候補その 1 前半の「.*」(\1) 「こっちは二行目。」 後半の「.*」(\2) 「これは一行目だ。<改行>そして三行目。」 候補その 2 前半の「.*」(\1) 「こっちは二行目。<改行>これは一行目だ。」 後半の「.*」(\2) 「そして三行目。」 確か、SED は一番長い文字列を置換するのだから、マッチする文字列の長い方 だろうと思うのですが、前半の「.*」は候補その 2 の方が長く、後半の「.*」 は候補その 1 の方が長いので困ってしまいます。あちらたてればこちらがたた ず、というわけでどちらを優先するか規則を決めておかないと、前半の正規表現 「.*」と後半の正規表現「.*」の間で喧嘩になってしまいます。(ならないって) そこで次のような規則が決められています。 左にある正規表現がマッチする文字列が一番長くなるものを選ぶ。 この場合でしたら、前半の「.*」が優先されるわけです。つまり候補その 2 です。従って置換文字列「\2<改行>\1」は --------------------------------------- そして三行目。<改行> こっちは二行目。<改行> これは一行目だ。 --------------------------------------- になり、パターンスペース全体がこの文字列で置き換えられます。結局、最後 尾のセグメント「そして三行目。」が一番先頭に移動しました。 正規表現の置き換えは、このように結構複雑ですから慣れるまでは難しいと思 います。最初のうちは思わぬ部分が置換され、スクリプトが思った通りに動かな い事が多いと思いますが、try and error で頑張ってください。 --- GCD03723 (Greatest Common Divisor:最大公約数)