LoginSignup
85
69

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-11-28

概要

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のあたりを使って、上手いこと出来ないのかな?
    • ご存知の方がいらっしゃいましたら、ご教授くださいますとありがたいです。


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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
69