HOME > 電算 > sed メモ
sed メモ
最終更新:2008-4-30
私はこの小さな働き者が大好きだ。
目次
sed と改行
(1) 入力にない改行の出力
入力にない改行の出力は、改行文字をバックスラッシュで隠せばよい。
s/xyz/&\ /
の如し。(例は xyz の後に改行を挿入する。& はマッチ全体を示すなり)
(2) 空行の削除
空行の削除は、改行だけ削除しようとせずに、行全体を d コマンドで削除せむと考えれば容易。
/^$/d
の如し。
(3) 改行にマッチさせる
改行にマッチさせるには、いくらか技が必要。sed は入力を一行ずつ読み込むくせに、その正規表現は行末にある改行にマッチしない仕様だからである。
sed は読み込んだ行の行末にある改行を削除してパターンスペースと呼ばれるバッファに格納したうえでテキスト処理をし、最後にパターンスペースの内容に改行をつけて出力する、とのことである
The \n symbol does not match the newline at an end-of-line because when sed reads each line into the pattern space for processing, it strips off the trailing newline, processes the line, and adds a newline back when printing the line to standard output. (http://www.student.northpark.edu/pemente/sed/sedfaq3.html)
N コマンドを使って、次行をパターンスペースの内容に追加すると、パターンスペースの途中にある改行が \n にマッチする。N コマンドについてのいくらかの説明はこちら。
試みに abcの後に改行が続く場合、この改行を削除しようとしてみる。
abc edf
なる入力に対し、
/abc$/N s/\n//
というコマンドを実行すれば
agcdef
と出力される。
しかし、もし入力が
abc abc def
の如くあれば、N コマンド s コマンドはそれぞれ一度しか実行されぬので、出力は
abcabc def
のようになり、不本意なもである。もし、
:a /abc$/N /abc$/b a s/\n//g
のようにすれば、所期の目的を達せられるであろう。これは、N コマンドにより行を連結した後、あらためてパターンスペースの最後に abc があるかどうかを調べ、もしそうであれば b コマンドでラベル a すなわちスクリプトファイルの一行目に戻して再び連結作業を行い、これ以上連結すべき行がなくなった場合に改行を削除する。
こうした例においては、行末に abc という目印あったので、それを目安に後続行をパターンスペースに追加するかどうかの選択することができた。
しかし、「行頭に abc がある場合にそれに先行する改行を削除したい」としたらどうであろうか。ある行を読み込んだ時点では、パターンスペースに次の行追加すればいいかどうかは分からない。
こういう方法がある。
:a N s/abc\n/abc/ t a P D
これは、とりあえず次行を N コマンドで連結してしまい、s コマンドの置換を利用して abc に続く改行文字の削除を試みる。t コマンドで置換の成否を判別し、置換が失敗した場合、P コマンドでパターンスペースの中から埋め込まれた改行以前をプリントし、D コマンドでその部分をパターンスペースから削除する(D コマンドについてのいくらかの説明はこちら)。そして、また先頭に戻って次行を読み込む。置換が成功した場合、ラベル a を目印に先頭に戻り、処理を続ける。
ようするに、1 行ぶんずつ入れ替えながら常に 2 行ぶんをパターンスペースに保持するようにしているのである。
(補足 1) もし t コマンドがないとどうなるか。s コマンドが成功すると改行が削除されてしまうので、D コマンドがパターンスペース内容を全部削除してしまう。これでは s コマンドに見逃される改行がでてきてしまう。
(補足 2) D コマンドはいささかトリッキーな働きをする。埋め込まれた改行までをパターンスペースから削除したのち、まだパターンスペースに何か残っていたならば、新たな入力行を読み込むことなく次のサイクルを始めるのである。
このやり方は、汎用性が高く、「abc に先行する改行の削除」のみならず「abc に後続する改行を削除」という最初の課題も難無くこなしてくれる。さらに、「abc と def の間の改行を削除する」というような変換も簡単だ。ただし、これは改行を削除するような置換をするのであり、s コマンドでマッチするパターンに改行が含まれていない場合や、置換の結果改行の数が増加するような置換では予期せぬ動きをする。sed が行を単位として働くものである以上、置換により行を増減させる場合には慎重にやらなくてはいけない。
次に、「abc でおわる行と def ではじまる行の間に、空行を挿入する」という課題が考えてみる。これは、置換によってパターンスペース中の行数が増加してしまうとういケースである。
/\n/{P
D}
N
s/abc\ndef/abc\
\
def/
P
D
のようにやるとうまくいく。最初の 2 行は、パターンスペースの中に埋め込まれた改行がある場合に、先頭からいちばん後ろにある改行までを出力し、それを(パターンスペースから)削除するものである。D コマンドが実行されると、制御が先頭に移るので、この 2 行はループとして働く。最終行とその前で P, D コマンドを実行しているのが一見無駄に見えるかもしれないが、これがないと、制御が先頭行に移る前に、パターンスペースの内容が吐き出されてしまう。
さらに汎用性が高くしようとすると、乱暴なことをしなくてはならないだろう。ただやみくもに入力ファイル中のすべての行を連結してパターンスペースに詰め込み、一気に置換してしまうのだ。
:a $!N $!b a s/\nabc/abc/g
はじめの 3 行によって、強引に入力ファイルの内容を一つにつなげてパターンスペースに押し込んでいる。(最終行だけは別あつかいしているが、これは最終行で N コマンドを実行すると、そこでパターンスペースの内容を出力して終了してしまい、以下の置換コマンドが実行されないからだ。)
このはじめの 3 行さえおまじないに書いておけば、多くの場合、「sed の正規表現は行末の改行にマッチしない」という制限が撤廃されたと同じような効果があるだろう(!)。ただし、場合によってはメモリが足りなくなる可能性がある。それが sed の制限なのか、sed が使えるメモリの制限なのかは、どういう sed をどういう環境で使っているかによるが、ともかくメモリが足りなくなるという危険は承知しておくべきである。
また、たとえメモリが十分使えるとしても、ごくつつましい環境で自在に sed を操っててきた強者に敬意を表するためには、もっと苦労してスクリプトを書くべきであろう :-)
ところで、sed にはパターンスペースのほかに、ホールドスペースというバッファがある。たんに文字列を一時的に退避できるだけのバッファであるが、パターンスペースの内容をこちらに入れたり引き出したりすることによって、さらに難しいケースに対応するスクリプトを書くことができるし、場合によってはそのほうがより自然な書き方なこともある。「段落内改行の削除」では、これを用いている。
2 行にわたるパターンにマッチさせる
改行を無視して探索し、2 行にまたがるパターンに対してもマッチさせたいという場合がある。たとえば、
____abc_____a bc__abc____ab c___abc____ab
という入力に対して、abc を xyz に変換して出力したい。
____xyz_____xyz __xyz____xyz ___xyz____ab
てなふうに。
やっかいなのは、abc の間に改行がはさまっている箇所がある点である。例によって sed は一行ずつの処理が原則だから、素朴にやると 2 行にわたるパターンにはマッチしないのだ。
(1) オシャレなやり方
これは、メモリをほんの少ししか使わない。
# 1 行におさまるパターンの置換 s/abc/xyz/g N # 2 行にわたるパターンの置換 s/ab\nc/xyz\ / s/a\nbc/xyz\ / P D
では、ちょっと説明。
赤字(あるいは太字)にしたコマンドが、処理の流れ・バッファーへの読み込み・出力を制御するためのコマンドたち。緑字(あるいは斜体)の部分は、abc を xyz に置換するための s コマンドである。
流れは以下のごとし。一行におさまっているパターンを置換(さいしょの s コマンド)。次の行をパターンスペースに追加し(N コマンド)、2行にまたがるパターンを置換(2,3 番目の s コマンド)。パターンスペースに埋め込まれた改行以前をプリント(P コマンド)したうえで削除(D コマンド)。また、はじめにもどる。
上記の例で何が起こっているかを詳しく見てみむ。
| コマンド | パターンスペース内容 | 出力内容 |
|---|---|---|
| 1行におさまるパターンの置換 | 1 行目 | |
| N | 1 行目〈改行〉2 行目 | |
| 2行にまたがるパターンの置換 | 1 行目〈改行〉2 行目 | |
| P | 1 行目〈改行〉2 行目 | 1 行目〈改行〉 |
| D | 2 行目 | |
| 1行におさまるパターンの置換 | 2 行目 | |
| N | 2 行目〈改行〉3 行目 | |
| 2行にまたがるパターンの置換 | 2 行目〈改行〉3 行目 | |
| P | 2 行目〈改行〉3 行目 | 2 行目〈改行〉 |
| D | 3 行目 | |
| 1行におさまるパターンの置換 | 3 行目 | |
| N | 3 行目 | 3 行目〈改行〉 |
| 終了 |
(補足1)sed では、ふつう、スクリプト最終行までいくと制御が先頭行にうつり、次の入力行が自動的に読み込まれる。しかし、D コマンドが使われると、制御が先頭に戻りはするが、パターンスペースが空でない限り自動的に行が読み込まれたりはしない。
(補足2)N コマンドは、読み込むべき次の行がないと、パターンスペースを吐き出して sed を終了させる。
(補足3)s コマンドが N コマンドの前後二箇所に分けて書かれいている理由。もし、最初の s コマンドを、2, 3 番目の s コマンドの直前に置くと、一行におさまっているパターンに対して二回置換コマンドが実行されてしまう。その結果、abc を aabc に置換したいような場合、aaabc になってしまう。
(補足4)これは基本的には、前に示した例と同じ考え方であるが、s コマンドが改行を消去したり増やしたりしないことを前提にしているために、コーディングが簡単になっている。
(2) 強引なやり方
以下のやり方は、入力の分量に比例してメモリ使用量が増え、メモリが足りないとヤバイことになる。
:a $!N $!b a # 1 行におさまるパターンの置換 s/abc/xyz/g # 2 行にわたるパターンの置換 s/a\nbc/xyz\ /g s/ab\nc/xyz\ /g
このスクリプトでは、入力をすべて一つにつなげてパターンスペースにぶち込み、それからやおら置換を行っている。
段落内改行の削除
一行20字の原稿を書くときに、20字ごとに改行を入れる人がいる。そして、こうした場合、行頭の全角スペースが改段落の記号となっていることが多い。ワープロの字数設定を知らない人に多いパターンである。
これを sed で「ふつうの原稿」になるよう処理してみよう。つまり、改行は段落の最後のみにつくように、段落内の改行を削除して行をつなげていくのである。じつのところ、これは sed 向きな作業ではない。なぜならば、sed は行指向のエディタである。そして、改行は行の終わりを示す。つまり、行内編集が得意な sed は、二つ以上の行をつなげるという作業が苦手なのだ。
しかし、苦手といえどもできないわけではない。ホールドスペースというものを使うと、なかなかスッキリやってくれる。(スペースのところが見えないが、コピー&ペーストして使えるように、そのままにしておいた。)。
スペースを含まない空行は、まったく削除されてしまうので注意。
#!/bin/sed -nf
# 最終行
${
H
x
s/\n//g
p
b
}
# スペースで始まる最終行以外の行
/^[ ]/{
x
s/\n//g
p
b
}
# スペースで始まらない最終行以外の行
H
また、メールによくあるパターンに、空行を段落の区切りにして、改行は適宜読みやすいように入れるということがある。 LaTeX 用の原稿も、このような形式になっている。原稿がこの手のものであれば、やはり sed で処理できる。
同様のことを ed で行うこともできることをついでに言っておこう。
注意。以下 sed 用スクリプトは、空行じたいは削除する。また、スペース文字だけからなる行も空行として扱う。
#!/bin/sed -nf
# 最終行
${
H
x
s/\n//g
p
b
}
# 空行
/^[ ]*$/{
x
s/\n//g
/[^ ]/p
b
}
# それ以外の行
H
ただし、以上 2 例については ed で行うと、もっと簡単かも。
コマンドたち
これは、各コマンドに「何を期待してよいか」を示そうというわけではなく、各コマンドを利用したとき、「期待を裏切られるのは何故か」を説明せんとしたものであります。
n コマンド
n コマンドと N コマンドは違うものである。ここでは、n コマンドを見てみむ。(N コマンドについては後述)。蛇足なれど、n コマンドと -n オプションは何の関係もなし。
n コマンドは、現在のパターン・スペースの内容を吐き出して、次の行を読み込む。もちろん、このときに現在行の番号もインクリメントされる。現在のパターン・スペースの内容を吐き出す動作は -n オプション使用時には抑制される。
いきなり例を見てみむ。
奇数行・偶数行だけ出力
入力ファイルの奇数行だけを出力するなら
n d
とやればよい。なんと 2 文字のスクリプトなり(-n オプションは不要)。ハイ、説明。
# パターンスペースにあらたな 1 行を読み込む n # パターンスペースを出力して次行をパターンスペースに読み込む d # パターンスペースを削除する # いつも通りパターンスペースを出力しようにも空だ
偶数行だけ出力するなら、
1d n d
これも、-n オプション不要。ハイ、説明。
# パターンスペースにあらたな 1 行を読み込む 1d # もし 1 行目なら、 # パターンスペースを削除し、 # 新たな入力行を読み込み、 # スクリプトの先頭に制御を戻す n # パターンスペースを出力して次行をパターンスペースに読み込む d # パターンスペースを削除する # いつも通りパターンスペースを出力しようにも空だ
暇な人は、「3 の倍数行のときだけ『アホ』と出力」とか試みられたし。……と書かむ思ったけど、つい自分で書いてしまつた。スマヌ。
n n s/.*/aho/
-n オプションは不要なり。
N コマンド
新しい行をパターンスペースに読み込む。以前あった内容と、新しく読み込まれた内容は改行文字によって区切られる。というのが、通り一遍の説明。
予想される通り、N コマンドを行うと、カレント行が進む。
$ cat t.txt 123 456 789
というファイルがあるとしよう。
$ sed -ne "N; 1p" t.txt
これは、何も出力しない。1 行目において、N コマンドが適用されたので、カレント行が 2 行目に移る。したがって、その後に 1 行目をプリントせよ(1p)といってももう遅いのである。(為念。-n オプションにより、明示的なプリントコマンドなき出力を抑制してゐる)
$ sed -ne "N; 2p" t.txt 123 456
N コマンドによりカレント行が 2 行目に移っても、1 行目の内容はパターンスペースに保持されていることがわかる。
$ sed -ne "N; 3p" t.txt
不思議なことに、これは何も出力しない。N コマンドは、読み込むべき入力がないと、現在のパターンスペースの内容を出力して処理を終了する。カレント行を 3 行目にしてスタートしたサイクルは、しょっぱなにN コマンドにぶつかる。ところが、読み込むべき 4 行目が入力にはない。しかたなく N コマンドは現在のパターンスペースを出力してプロセス終了させうとするが、出力は -n オプションで抑制されているので(※)、結局何も出力しないで終了してしまうのだ。
※ -n オプションがある場合、明示的なプリントコマンドによらない出力は行われない。「読み込むべき次行がない場合に実行された N コマンドが、パターンスペースの内容を吐き出す」とい動作は、ここでいう「明示的なプリントコマンド」ではないので、抑制されたのだ。
住所録
フィールド・セパレータに「改行」を使い、決まったフィールド数で 1 レコードとしているデータの処理の例。
001 カビパン男 男性 愛知県 002 甘木 某 男性 東京都 003 甘木 某女 女性 東京都
なんてデータを入力として受け取り、「都道府県、性別、氏名」の順に並べ変えて出力してみむ。
-n オプションをつけずに
N N N s/\(.*\)\n\(.*\)\n\(.*\)\n\(.*\)/\1\n\4\n\3\n\2/
ということになる。N コマンドで 1 レコードぶんをすべてパターンスペースに突っ込んでから、改行を目印にフィールドの並べ換えを行っている。
もし、レコードセパレータが入っているならば、awk で処理したほうが楽じゃよ。
D コマンド
D コマンドは、パターンスペースから埋め込まれた改行以前(改行を含む)を削除する。
$ cat t.txt 123 456
というファイルがあるとしよう。
$ sed -ne "N;D;p" t.txt
これは、456 を出力しそうなものだが、実際は何も出力されない。D コマンドが実行されると、制御が(プログラムの)先頭行に戻るので、このプログラムにおいては p コマンドは一度も実行されないのである。
だがそれだけではない。D コマンドはもっと奥が深いのだ。
$ cat t.txt <1> <2> <3>
というファイルに対して、実験をする。
$ sed -ne "1N;=;p;D" t.txt 2 <1> <2> 2 <2> 3 <3>
= はカレント行番号を表示するもので、出力をわかりやすくしただけ。1N により、1 行目の尻に改行をはさんで 2 行目がつながるので、= コマンドのところで 2 が、p コマンドのところで <1>+改行+<2> が出力されるのは容易にわかる。
その後、D コマンドで、パターンスペースから「<1> + 改行」が削除され、制御がプログラム先頭に戻る。プログラム先頭に戻ったのだから、今度は 3 行目を読み込むかと思いきや = コマンドは再度 2 を出力し、それにつづく p コマンドも、前のサイクルで D コマンドが消し残したパターンスペース後半の内容、つまり <2> を表示している。その理由はこうだ。D コマンドは、制御をプログラム先頭に戻すが、そのとき新しい行が読み込まれる(かつカレント行がインクリメントされる)のを抑止するのである。だが、話はこれだけではない。
次に D コマンドが実行されたとき、パターンスペースは空になる(こんどは N コマンドが実行されず、埋め込まれた改行がパターンスペース中にないからすべてが削除される)。制御はまたプログラム先頭に戻る。D コマンドで戻ったのだから、新たな入力行、つまり 3 行目は読み込まれないかと思えば、そうではない。次の = コマンドは 3 を出力し、パターンスペースの内容を印刷する p コマンドも <3> を出力している。D コマンドでプログラム先頭に飛んだにもかかわらず、新たな行が読み込まれているのだ。これは、次の理由による。D コマンドが実行された結果、、パターンスペースが空になってしまったときは、制御をプログラム先頭に戻し、このとき新しい行が読み込まれる(かつカレント行がインクリメントする)のを抑止しないのである。
三度目の D コマンドにより、パターンスペースは空になり、制御はプログラム先頭に移り、新たな入力行を読み込もうとする。しかし、もう入力行はないので、デフォルトの動作に従って、このプロセスは終了する。
ホールド・スペース
sed はパターン・スペースのほかにホールド・スペースというバッファをもつ。これは、たんに文字列を保存するために使えるスペースで、ホールドスペースに対して働くコマンドは、パターン・スペースからそこに文字列を出し入れするコマンドだけである。退避所のようなものと考えて差し支えない。なお、ホールド・スペースは一つしかない。
h パターン・スペース --> ホールド・スペース コピー(上書き) H パターン・スペース --> ホールド・スペース コピー(追加) g パターン・スペース <-- ホールド・スペース コピー(上書き) G パターン・スペース <-- ホールド・スペース コピー(追加) x ホールド・スペース <--> パターン・スペース 交換
ホールド・スペースへの文字列の出し入れは、D コマンドや N コマンドのように制御の流れを変えることがないので、理解するのがそう難しくない。
ホールド・スペースを利用する典型的な例は、行によってデータの性質が異なる入力の処理である。
奇数行と偶数行の入れ替え
奇数行と偶数行を入れ替えることを考へむ。これは、置換コマンドを使わない愉快で素早いやり方であが、いくらかトリッキーに見えるかもしれない。置換を使う気なら、ホールドスペースを使わずに同様のことができるので、「住所録」を参照されたし。
-n オプションつきで、
h n p g p
とやればよい。ものすごく呪文っぽい! sed って楽しいな、という感じですぞ。では説明。
# まず、最初に新しい行がパターンスペースに読み込まれる h # そいつをホールドスペースにコピーしとく n # -n オプションのおかげでパターンスペースを出力せずに、たんに # 次の一行をあらたにパターンスペースに読み込む p # ここで、パターンスペースを出力しとく g # さっき退避させといたホールドスペースの内容をパターンスペースにコピー p # パターンスペースの内容を出力
行方向のデータ構造のあるものを検索
$ cat words.txt apple and color p.10 dog cat p.11 apple orange p.13 green red apple
という単語帳で、apple という単語が何ページに出てきたのかを知るには、
$ sed -ne "/^p\./h; /apple/ {g; /^$/d; p}" words.txt
p.11
p.13
とやればよい。p. ではじまる行があると、これをホールド・スペースに保管しておき、以下の行で apple を探索し、見つかったときにホールド・スペースにあった内容を吐き出すのである。/^$/d は、1 行目のタイトルにある apple のページ数に対してページ数のかわりに空行が出力されるのを防いでいる。
段落内改行の削除の例の入力においては、段落頭を示す記号(全角スペース)のある行は、ほかの行とは違う特別な意味を示し行方向の構造を持っていると言うことができる。そこで、やはりホールド・スペースを使っている。
余談
sed はあくまでも一行ずつ処理するのが身上なので、列方向のみならず行方向に構造をもっているようなデータに対して選択的な処理をするのは基本的には苦手である。こうした課題は、入力全体をバッファに保持していて、先頭に向かっての探索も可能な ed のほうが向いているかもしれない。
上述の apple 出現ページを探索する例の ed 版は、
$ echo '1;/^p\./,$g/apple/?^p\.?p' | /bin/ed -s words.txt p.11 p.13
ということになるか。GNU ed で試した。残念ながら他の ed がどうなっているか知らない。「1 行目から p. ではじまる行を探索し、その行から最終行までの中で、apple を含む行をすべて探す。そうして見つかった各行からファイル先頭方向に向かって p. ではじまる行を探し出し、見つかった行をプリントする」という意味である。1; は対話環境においてカレント行がどこにあるかわからないから。/^p\./ はこれがないと apple が p. ではじまるどの行よりも先にある場合に最後にある p. ではじまる行が出力されてしまうのを防ぐためである。
ed は /正規表現/ でファイル末尾方向への探索ができるが、このほかに、?正規表現? でファイル先頭方向への探索ができる。処理対象のファイルをすべて格納しておくバッファを持つからできることで、sed には真似のできぬことなり。
