概要
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
はパターンスペースに取り込みます。
レコードをパターンスペースに取り込んだ後、パターンスペースに対してコマンドを順次実行していきます。
初めに、1番目のs
コマンドを実行します。
その結果、パターンスペースのSiri
という文字列が、Ketu
に置換されます。
次に、2番目のs
コマンド、最後のa
コマンドが適用されます。
ただし、それぞれアドレスに正規表現/not/
、/Sure/
が指定されているため、現在のパターンスペースにはマッチせず、今回はどちらも実行されません。
全コマンドの適用が終わった後は、sed
のデフォルトの動作として、パターンスペースの内容が自動的に出力されます。
今回の場合、s/Siri/Ketu/
が実行された結果が、出力されます。
次に、2レコード目の入力、I'm afraid I cannot do it.
を、パターンスペースに取り込みます。
レコードをパターンスペースに取り込んだ後は、1レコード目と同様に、コマンドを順次実行していきます。
まず、1番目のs
コマンドを実行します。
ただし、現在のパターンスペースは正規表現/Siri/
にマッチしないため、結果的に何も処理がおこなわれません。
次に、2番目のs
コマンドを実行します。
今回、パターンスペースに文字列not
が含まれているため、2番目のs
コマンドは実行され、パターンスペースの内容はSure.
に置換されます。
そして、3番目のコマンドa
のアドレス、正規表現/Sure/
にマッチするかどうかを判定します。
ここで、判別対象は、 2番目のs
コマンドが実行された後のパターンスペース が用いられます。
したがって、パターンスペースSure.
に正規表現/Sure/
にマッチし、3番目のa
コマンドが実行されます。
その結果、パターンスペースにただし魔法は尻から出る
が追記されます。
そして最後に、1レコード目と同様に、全コマンド実行後のパターンスペースが出力されます。
上記の処理過程の結果、次の内容が出力されます。
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行目の入力レコードシェル変態
を、パターンスペースへと取り込みます。
次に、1番目のG
コマンドを適用し、ホールドスペースの内容を、パターンスペースへと追記します。
ただし、G
コマンドのアドレスは1行目以外1!
に指定されているため、今回は実行しません。
(アドレス1!
を指定せずに実行した場合、パターンスペースに空行が追記されます。)
その後、2番目のh
コマンドを実行し、現在のパターンスペースの内容シェル変態
を、ホールドスペースへとコピーします。
そして、3番目のd
コマンドを実行し、パターンスペースの内容を削除します。
ここで、ホールドスペースの内容は削除されない事に、着目してください。
最後に、全コマンドを実行した状態のパターンスペースが、sed
の自動出力機能によって出力されます。
ただし、 3番目のd
コマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません 。
次に、2行目の入力レコードシェル戦隊
を、処理していきます。
1行目と同様に、まず入力レコードをパターンスペースへと取り込みます。
次に、1番目のG
コマンドを実行し、ホールドスペースの内容シェル変態
を、パターンスペースへと追記します。
今回は、2行目であるため、アドレス1!
に該当し、G
コマンドは実行されます。
その結果、パターンスペースの内容は、シェル戦隊\nシェル変態
になります。
その後、2番目のh
コマンドを実行し、現在のパターンスペースの内容シェル戦隊\nシェル変態
を、ホールドスペースへとコピーします。
ここで、 直前のG
コマンドの処理によって変化したパターンスペースを用いる ことに、留意してください。
また、1行目の処理でホールドスペースにコピーした内容シェル変態
は、上書きされます。
(h
の代わりにH
コマンドを使うと、上書きコピーではなく追記になります。)
そして、3番目のd
コマンドを実行し、パターンスペースの内容を削除します。
最後に、全コマンドを実行した状態のパターンスペースが、sed
の自動出力機能によって出力されます。
ただし、3番目のd
コマンドによってパターンスペースの内容は削除されているため、今回も何も出力されません。
そして、3レコード目のシェル軟体
の処理をおこないます。
この3レコード目の動作は、2レコード目の動作と同様であるため、詳細な解説は省きます。
最後に、末尾行である4レコード目を処理します。
まず、入力レコードシェル倦怠
を、パターンスペースへと取り込みます。
次に、1番目のG
コマンドを実行し、ホールドスペースの内容シェル倦怠\nシェル軟体\nシェル戦隊
を、パターンスペースへと追記します。
そして、2・3番目のh
とd
コマンドを適用します。
ただし、両方共アドレスが末尾行以外$!
に指定されているため、今回は実行しません。
最後に、全コマンドを実行した状態のパターンスペースシェル倦怠\nシェル軟体\nシェル戦隊\nシェル変態
が、sed
の自動出力機能によって出力されます。
以上が、tac
やtail -r
と同等の動作、sed
を用いた入力レコードの反転処理になります。
例題2
今度は、入力レコードを縦方向に対称に出力するスクリプトを用いて、パターンスペース・ホールドスペースの動作を再度解説します。
以下、入力データとスクリプトの実行例を示します。
$ cat saga
よいかジェラール
われわれはインペリアルクロス
という陣形で戦う
$ cat saga | sed '$!p; 1!G; $!h; $!d'
よいかジェラール
われわれはインペリアルクロス
という陣形で戦う
われわれはインペリアルクロス
よいかジェラール
上記の処理を、各レコード・各コマンドについて、それぞれ説明します。
まず、これまでの例と同様に、1行目の入力レコードよいかジェラール
を、パターンスペースへと取り込みます。
次に、1番目のp
コマンドを実行し、現在のパターンスペースの内容よいかジェラール
を出力します。
その後、2番目のG
コマンドを適用します。
ただし、アドレスに1行目以外1!
が指定されているため、今回はG
コマンドは実行されません。
そして、3番目のh
コマンドを実行し、パターンスペースの内容よいかジェラール
を、ホールドスペースへとコピーします。
また、4番目のd
コマンドを実行し、パターンスペースの内容を削除します。
最後に、全コマンドを実行した状態のパターンスペースが、sed
の自動出力機能によって出力されます。
ただし、直前のd
コマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません。
次に、2行目の入力レコードわれわれはインペリアルクロス
を、処理していきます。
まず、1行目と同様に、入力レコードをパターンスペースへと取り込みます。
次に、1番目のp
コマンドを実行し、現在のパターンスペースの内容われわれはインペリアルクロス
を出力します。
その後、2番目のG
コマンドを実行します。
1行目では実行されませんでしたが、今回はアドレス1!
にマッチするため、ホールドスペースの内容よいかジェラール
が、パターンスペースに追記されます。
その結果、パターンスペースの内容は、われわれはインペリアルクロス\nよいかジェラール
になります。
そして、3番目のh
コマンドを実行し、現在のパターンスペースの内容われわれはインペリアルクロス\nよいかジェラール
を、ホールドスペースへとコピーします。
また、4番目のd
コマンドを実行し、パターンスペースの内容を削除します。
最後に、全コマンドを実行した状態のパターンスペースが、sed
の自動出力機能によって出力されます。
ただし、1行目と同様に、直前のd
コマンドによってパターンスペースの内容は削除されているため、ここでは何も出力されません。
最後に、末尾行の入力レコードという陣形で戦う
を処理します。
まず、これまでと同じく、入力レコードをパターンスペースへと取り込みます。
次に、1番目のp
コマンドを適用します。
ただし、今回は末尾行であるため、アドレス$!
にマッチせず、p
コマンドは実行されません。
その後、2番目のG
コマンドを実行します。
現在のホールドスペースの内容われわれはインペリアルクロス\nよいかジェラール
を、パターンスペースという陣形で戦う
に追記します。
その結果、パターンスペースの内容は、という陣形で戦う\nわれわれはインペリアルクロス\nよいかジェラール
になります。
そして、3・4番目のh
とd
コマンドを適用します。
しかし、1番目p
コマンドと同様に、アドレス$!
にマッチしないため、コマンドはどちらも実行されません。
最後に、全コマンドを実行した状態のパターンスペースが、sed
の自動出力機能によって出力されます。
今回は、直前のd
コマンドが実行されなかったため、パターンスペースの内容は空ではありません。
その為、現在のパターンスペースの内容、という陣形で戦う\nわれわれはインペリアルクロス\nよいかジェラール
が、出力されます。
以上が、入力レコードを縦方向に対称に出力する動作になります。
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
やGNUsed
のソースを読んで何とか把握した。 -
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
のあたりを使って、上手いこと出来ないのかな? - ご存知の方がいらっしゃいましたら、ご教授くださいますとありがたいです。
-
- スッゲーどうでもいい話。
- 尻だのケツだの書いていたら、半ケツ状態になりました。
うっ... 電源ケーブルをコンセントに挿そうとしゃがんだら、スーツの股間のあたりから、ブチッっていう音がした...... もしかしたら、また半ケツ状態かも(`;ω;´)
— ginjiro (@gin_135) 2016年11月28日
駅のホームでやたらと尻がスースーすると思ったら、案の定半ケツになってた(T_T) やっぱり、朝のあれが原因だよなぁ... てか、客先にいる間はずっと、パンツ見えている状態だったのかorz このスーツもうダメだ...
— ginjiro (@gin_135) 2016年11月28日