sedのパターンスペース・ホールドスペースの動作を図で学ぶ

  • 53
    いいね
  • 0
    コメント

概要

sedは、入力ストリームに対して様々なテキスト変換をおこなう、ストリームエディタです。
cut, grep, trといった基本的なフィルタコマンドと比較して、柔軟なテキスト処理が可能です。
このsedの機能の1つとして、パターンスペース・ホールドスペースがあります。
高度なテキスト処理が可能になる反面、パターンスペース・ホールドスペースは、動作が理解し辛いという難点があります。
ですが、sedのパターンスペース・ホールドスペースの動作を丁寧に解説した記事は、私が探した限りでは見つかりませんでした。
そこで、sedを深く学ぶ方への助けとして、また私自身の復習として、sedのパターンスペース・ホールドスペースの動作を、記事としてまとめました。

本記事では、sedのパターンスペース・ホールドスペースの動作を、図示して解説します。

実行環境

  • Arch Linux 4.8.8-2-ARCH
  • GNU bash 4.4.5
  • GNU coreutils 8.25-2
  • sed (GNU sed) 4.2.2


シェル芸でのみ使用

  • GNU findutils 4.6.0-2
  • grep (GNU grep) 2.26
  • gawk 4.1.4
  • nkf 2.1.4
  • mecab 0.996

解説

前提知識の解説

まず、本記事を読むにあたり、前提知識となるsedの動作を、ホールドスペース抜きで解説します。

sedは、各レコードをパターンスペースに取り込み、パターンスペース(またはホールドスペース)に対して各コマンドを順次実行します。
この際、後に実行するコマンドは、先に実行したコマンドの処理結果の影響を受けます。
正確には、前述の通り、 パターン(ホールド)スペースの内容に対して、各コマンドの処理が順次実行 されます。
以下、一例を示します。

※本記事は、sedの基本的な使い方を知っている方を、対象としています。
したがって、sedの各コマンドの意味や、ホールド・パターンスペースとは何かについては、本記事では解説しません。
必要に応じて、man sedや、書籍『sed & awkプログラミング 改訂版』を参照してください。

$ cat poem
Hey Siri, show me the magic!
I'm afraid I cannot do it.
$ cat poem | sed 's/Siri/Ketu/; /not/s/.*/Sure./; /Sure/aただし魔法は尻から出る'
Hey Ketu, show me the magic!
Sure.
ただし魔法は尻から出る

上記の処理を、各レコード・各コマンドについて、それぞれ説明します。

まず、1レコード目の入力、Hey Siri, show me the magic!を、sedはパターンスペースに取り込みます。

fig1-1-1.jpg

レコードをパターンスペースに取り込んだ後、パターンスペースに対してコマンドを順次実行していきます。
初めに、1番目のsコマンドを実行します。
その結果、パターンスペースのSiriという文字列が、Ketuに置換されます。

fig1-1-2.jpg

次に、2番目のsコマンド、最後のaコマンドが適用されます。
ただし、それぞれアドレスに正規表現/not//Sure/が指定されているため、現在のパターンスペースにはマッチせず、今回はどちらも実行されません。

fig1-1-3.jpg

全コマンドの適用が終わった後は、sedのデフォルトの動作として、パターンスペースの内容が自動的に出力されます。
今回の場合、s/Siri/Ketu/が実行された結果が、出力されます。

fig1-1-4.jpg


次に、2レコード目の入力、I'm afraid I cannot do it.を、パターンスペースに取り込みます。

fig1-2-1.jpg

レコードをパターンスペースに取り込んだ後は、1レコード目と同様に、コマンドを順次実行していきます。
まず、1番目のsコマンドを実行します。
ただし、現在のパターンスペースは正規表現/Siri/にマッチしないため、結果的に何も処理がおこなわれません。

fig1-2-2.jpg

次に、2番目のsコマンドを実行します。
今回、パターンスペースに文字列notが含まれているため、2番目のsコマンドは実行され、パターンスペースの内容はSure.に置換されます。

fig1-2-3.jpg

そして、3番目のコマンドaのアドレス、正規表現/Sure/にマッチするかどうかを判定します。
ここで、判別対象は、 2番目のsコマンドが実行された後のパターンスペース が用いられます。
したがって、パターンスペースSure.に正規表現/Sure/にマッチし、3番目のaコマンドが実行されます。
その結果、パターンスペースにただし魔法は尻から出るが追記されます。

fig1-2-4.jpg

そして最後に、1レコード目と同様に、全コマンド実行後のパターンスペースが出力されます。

fig1-2-5.jpg


上記の処理過程の結果、次の内容が出力されます。

Hey Ketu, show me the magic!
Sure.
ただし魔法は尻から出る

以上が、前提知識となるsedの基本動作になります。


ここで、繰り返しになりますが、 sedの各コマンドは、パターンスペースに対して順次実行される 事に、留意してください。

次に、2つの実用的なsedスクリプトを用いて、ホールドスペースを含めた動作を解説します。

例題1

Linuxのtacや、BSDのtail -rのように、入力レコードを反転して出力するスクリプトを用いて、パターンスペース・ホールドスペースの動作を解説します。
以下、入力データとスクリプトの実行例を示します。

$ cat shells
シェル変態
シェル戦隊
シェル軟体
シェル倦怠
$ cat shells | sed '1!G; $!h; $!d'
シェル倦怠
シェル軟体
シェル戦隊
シェル変態

上記の処理を、各レコード・各コマンドについて、それぞれ説明します。

まず、前セクションの例と同様に、1行目の入力レコードシェル変態を、パターンスペースへと取り込みます。

fig2-1-1.jpg

次に、1番目のGコマンドを適用し、ホールドスペースの内容を、パターンスペースへと追記します。
ただし、Gコマンドのアドレスは1行目以外1!に指定されているため、今回は実行しません。
(アドレス1!を指定せずに実行した場合、パターンスペースに空行が追記されます。)

fig2-1-2.jpg

その後、2番目のhコマンドを実行し、現在のパターンスペースの内容シェル変態を、ホールドスペースへとコピーします。

fig2-1-3.jpg

そして、3番目のdコマンドを実行し、パターンスペースの内容を削除します。
ここで、ホールドスペースの内容は削除されない事に、着目してください。

fig2-1-4.jpg

最後に、全コマンドを実行した状態のパターンスペースが、sedの自動出力機能によって出力されます。
ただし、 3番目のdコマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません

fig2-1-5.jpg


次に、2行目の入力レコードシェル戦隊を、処理していきます。
1行目と同様に、まず入力レコードをパターンスペースへと取り込みます。

fig2-2-1.jpg

次に、1番目のGコマンドを実行し、ホールドスペースの内容シェル変態を、パターンスペースへと追記します。
今回は、2行目であるため、アドレス1!に該当し、Gコマンドは実行されます。
その結果、パターンスペースの内容は、シェル戦隊\nシェル変態になります。

fig2-2-2.jpg

その後、2番目のhコマンドを実行し、現在のパターンスペースの内容シェル戦隊\nシェル変態を、ホールドスペースへとコピーします。
ここで、 直前のGコマンドの処理によって変化したパターンスペースを用いる ことに、留意してください。
また、1行目の処理でホールドスペースにコピーした内容シェル変態は、上書きされます。
(hの代わりにHコマンドを使うと、上書きコピーではなく追記になります。)

fig2-2-3.jpg

そして、3番目のdコマンドを実行し、パターンスペースの内容を削除します。

fig2-2-4.jpg

最後に、全コマンドを実行した状態のパターンスペースが、sedの自動出力機能によって出力されます。
ただし、3番目のdコマンドによってパターンスペースの内容は削除されているため、今回も何も出力されません。

fig2-2-5.jpg


そして、3レコード目のシェル軟体の処理をおこないます。
この3レコード目の動作は、2レコード目の動作と同様であるため、詳細な解説は省きます。

fig2-3-1.jpg


最後に、末尾行である4レコード目を処理します。

まず、入力レコードシェル倦怠を、パターンスペースへと取り込みます。

fig2-4-1.jpg

次に、1番目のGコマンドを実行し、ホールドスペースの内容シェル倦怠\nシェル軟体\nシェル戦隊を、パターンスペースへと追記します。

fig2-4-2.jpg

そして、2・3番目のhdコマンドを適用します。
ただし、両方共アドレスが末尾行以外$!に指定されているため、今回は実行しません。

fig2-4-3.jpg

最後に、全コマンドを実行した状態のパターンスペースシェル倦怠\nシェル軟体\nシェル戦隊\nシェル変態が、sedの自動出力機能によって出力されます。

fig2-4-4.jpg

以上が、tactail -rと同等の動作、sedを用いた入力レコードの反転処理になります。

例題2

今度は、入力レコードを縦方向に対称に出力するスクリプトを用いて、パターンスペース・ホールドスペースの動作を再度解説します。
以下、入力データとスクリプトの実行例を示します。

$ cat saga
よいかジェラール
われわれはインペリアルクロス
という陣形で戦う
$ cat saga | sed '$!p; 1!G; $!h; $!d'
よいかジェラール
われわれはインペリアルクロス
という陣形で戦う
われわれはインペリアルクロス
よいかジェラール

上記の処理を、各レコード・各コマンドについて、それぞれ説明します。

まず、これまでの例と同様に、1行目の入力レコードよいかジェラールを、パターンスペースへと取り込みます。

fig3-1-1.jpg

次に、1番目のpコマンドを実行し、現在のパターンスペースの内容よいかジェラールを出力します。

fig3-1-2.jpg

その後、2番目のGコマンドを適用します。
ただし、アドレスに1行目以外1!が指定されているため、今回はGコマンドは実行されません。

fig3-1-3.jpg

そして、3番目のhコマンドを実行し、パターンスペースの内容よいかジェラールを、ホールドスペースへとコピーします。

fig3-1-4.jpg

また、4番目のdコマンドを実行し、パターンスペースの内容を削除します。

fig3-1-5.jpg

最後に、全コマンドを実行した状態のパターンスペースが、sedの自動出力機能によって出力されます。
ただし、直前のdコマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません。

fig3-1-6.jpg


次に、2行目の入力レコードわれわれはインペリアルクロスを、処理していきます。
まず、1行目と同様に、入力レコードをパターンスペースへと取り込みます。

fig3-2-1.jpg

次に、1番目のpコマンドを実行し、現在のパターンスペースの内容われわれはインペリアルクロスを出力します。

fig3-2-2.jpg

その後、2番目のGコマンドを実行します。
1行目では実行されませんでしたが、今回はアドレス1!にマッチするため、ホールドスペースの内容よいかジェラールが、パターンスペースに追記されます。
その結果、パターンスペースの内容は、われわれはインペリアルクロス\nよいかジェラールになります。

fig3-2-3.jpg

そして、3番目のhコマンドを実行し、現在のパターンスペースの内容われわれはインペリアルクロス\nよいかジェラールを、ホールドスペースへとコピーします。

fig3-2-4.jpg

また、4番目のdコマンドを実行し、パターンスペースの内容を削除します。

fig3-2-5.jpg

最後に、全コマンドを実行した状態のパターンスペースが、sedの自動出力機能によって出力されます。
ただし、1行目と同様に、直前のdコマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません。

fig3-2-6.jpg


最後に、末尾行の入力レコードという陣形で戦うを処理します。
まず、これまでと同じく、入力レコードをパターンスペースへと取り込みます。

fig3-3-1.jpg

次に、1番目のpコマンドを適用します。
ただし、今回は末尾行であるため、アドレス$!にマッチせず、pコマンドは実行されません。

fig3-3-2.jpg

その後、2番目のGコマンドを実行します。
現在のホールドスペースの内容われわれはインペリアルクロス\nよいかジェラールを、パターンスペースという陣形で戦うに追記します。
その結果、パターンスペースの内容は、という陣形で戦う\nわれわれはインペリアルクロス\nよいかジェラールになります。

fig3-3-3.jpg

そして、3・4番目のhdコマンドを適用します。
しかし、1番目pコマンドと同様に、アドレス$!にマッチしないため、コマンドはどちらも実行されません。

fig3-3-4.jpg

最後に、全コマンドを実行した状態のパターンスペースが、sedの自動出力機能によって出力されます。
今回は、直前のdコマンドが実行されなかったため、パターンスペースの内容は空ではありません。
その為、現在のパターンスペースの内容、という陣形で戦う\nわれわれはインペリアルクロス\nよいかジェラールが、出力されます。

fig3-3-5.jpg

以上が、入力レコードを縦方向に対称に出力する動作になります。
1文で簡単に書き表すと、「入力nレコードに対し、n-1レコード目までをpコマンドで出力しつつ、入力を行単位で反転しながらホールドスペースにて保持、n行目でホールドスペースを出力する」という処理になります。

まとめ

以下、例示で強調したポイントをまとめます。

  • sedの各コマンドは、先頭から順次実行される。
  • sedの各コマンドは、前に実行したコマンドの処理結果の影響を受ける。
    • 正確には、前コマンドの処理が適用されたパターン・ホールドスペースを用いて、処理をおこなう。
    • sedのデフォルトの動作であるパターンスペースの自動出力も、同様に影響を受ける。

これらの点に留意すれば、パターン・ホールドスペースを用いた複雑なsedスクリプトでも、理解しやすくなると思います。

補足事項

(2017-02-13追記)

sed は、入力をレコード単位で処理しますが、パターン・ホールドスペース内では、レコードの概念はありません。
言い換えると、改行文字は、出力するまで文字 \n として扱われます。
つまり、パターンスペース内に含まれる改行文字を、 s コマンドで置換する事が可能です。
以下に例を示します。

$ seq 1 4
1
2
3
4
$ seq 1 4 | sed '1!G; $!h; s/\n/ /g; $!d' # 入力を逆順にしつつ、各行をスペース区切りの1行として出力する
4 3 2 1

(図解部分では、可読性を優先したため、改行した状態で記載しました。)


参考にしたもの

  • $ man sed
  • $ info sed
  • 『sed & awkプログラミング 改訂版』、Dale Dougherty, Arnold Robbins 著、福崎 俊博 訳、オライリー・ジャパン

雑記

  • sedのパターンスペース・ホールドスペースの概念は、動作を把握しづらくて、なかなか難しい。
    • とはいえ、本記事のように、1レコード毎、1コマンド毎に動作を書いてみると、理解しやすいと思う。
    • 紙やホワイトボードに書いてみて、実際に手を動かしてみる事が大切
  • ホールドスペースは、Windowsのクリップボードの仕組みに似ているかも?
    • Windows環境にてメモ帳を起動して、メモ帳をパターンスペース、クリップボードをホールドスペースに見立てて、動作を確認してみるのも、良い勉強法かもしれない。
    • Hコマンドのような、ホールドスペースに追記する処理は、残念ながら再現できないけれど...
  • 実は、例1と例2のスクリプトは、コード自体は殆ど同じだったりする。
    • 少しコマンドを弄っただけで、動作が大きく変化する所が、sedの面白い部分であり、難しい所でもあると思う。


  • 今回、sedの動作を、表形式にして書き表してみたけれど...
    • ホールドスペースの初期値は何か、パターンスペースの内容はどの段階で上書きされるのかといった詳細な動作は、実はきちんと理解していなかったり。
    • パターンスペースが自動出力される処理に関しては、今回記事を書くにあたり必要になったので、info sedやGNU sedのソースを読んで何とか把握した。
    • sedの内部動作に詳しい方がいらっしゃいましたら、本記事の些細な間違い等を指摘していただけますと、幸いですm(_ _)m

  • 例題2のsedスクリプトを使えば、シェル上で回文を作れます。
01 #!/bin/sh
02 
03 echo たけやぶ                | # 原文の出力
04 grep -o .                    | # 縦にする
05 sed '$!p; 1!G; $!h; $!d'     | # 入力レコードを鏡合わせで出力
06 xargs                        | # 横にする
07 tr -d ' '                      # 余分なスペースの除去

以下、コピペ用のワンライナー版。

$ echo たけやぶ | grep -o . | sed '$!p; 1!G; $!h; $!d' | xargs | tr -d ' '
  • 漢字が含まれていても、いい感じに出来ます♥
01 #!/bin/sh
02 
03 echo 酢で漬けた                | # 原文の出力
04 mecab                          | # 入力を単語単位に分解
05 awk -F, '!/EOS/ && $0=$NF'     | # 余分なレコード・フィールドの除去
06 grep -o .                      | # 1文字1レコードに変換
07 sed '$!p; 1!G; $!h; $!d'       | # 入力レコードを鏡合わせで出力
08 xargs                          | # 1レコードn文字に変換
09 tr -d ' '                      | # 余分なスペースの除去
10 nkf --hiragana                   # カタカナをひらがなに変換

以下、コピペ用のワンライナー版と、その出力結果。

$ echo 酢で漬けた | mecab | awk -F, '!/EOS/ && $0=$NF' | grep -o . | sed '$!p; 1!G; $!h; $!d' | xargs | tr -d ' ' | nkf --hiragana
すでつけたけつです
$ !! | mecab
echo 酢で漬けた | mecab | awk -F, '!/EOS/ && $0=$NF' | grep -o . | sed '$!p; 1!G; $!h; $!d' | xargs | tr -d ' ' | nkf --hiragana | mecab
す 名詞,一般,*,*,*,*,す,ス,ス
で 助詞,格助詞,一般,*,*,*,で,デ,デ
つけ  動詞,自立,*,*,一段,連用形,つける,ツケ,ツケ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
けつ  名詞,一般,*,*,*,*,けつ,ケツ,ケツ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
  • (CLI(パイプライン)上でかな漢字変換をする方法は、さすがに分からなかった...)
    • uim-fepのあたりを使って、上手いこと出来ないのかな?
    • ご存知の方がいらっしゃいましたら、ご教授くださいますとありがたいです。


  • スッゲーどうでもいい話。
    • 尻だのケツだの書いていたら、半ケツ状態になりました。