HOME > 電算 > sed メモ
sed メモ
最終更新:2008-4-30
私はこの小さな働き者が大好きだ。
gnu.org にある "sed, a stream editor" (http://www.gnu.org/software/sed/manual/sed.html) が何かと有用。日本語訳が、http://www.bookshelf.jp/texi/sed/sed-ja_toc.html にあったよ。
目次
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 には真似のできぬことなり。
数字をインクリメントする
sed は変数を持たないので「数えもの」に使えないかと思いきや、そうでもない。http://www.gnu.org/software/sed/manual/sed.html#Examples に counting xxx という例がいくつか載っているので参考になる。
さて、ここでは
$ echo 199 | sed -f increment.sed 200
というようなことをやりたい。
expr でも使えと。ハイ、ごもっとも。ただ、この作業を sed でやってみると、簡単なことがかなり複雑になるので喜びがある(わけないか)。方針としては、1 桁目が 9 でないなら、これに 1 を足す。全桁が 9 なら、これをすべて 0 に置換して、数字の頭に 1 を書き加える。それ以外の場合は、1 ケタ目から上のケタに向かって連続している 9 をすべて 0 に置き換え、0 でない限りのいちばん下の桁に 1 を足す。
1 ケタ目から上のケタに向かって連続する 9 をすべて 0 に置き換える、という作業が出てくるのだが、これが意外に難しい。私はホールドスペースの世話になるやり方しか考えつかなかったが、http://www.gnu.org/software/sed/manual/sed.html#Increment-a-number に素敵なやり方が載っていた。つまり、1 の位から上に向かって連続している 9 をすべて _ に置換しておき、_ のすぐ上にある数字(これは当然 9 以外である)を 1 増やす置換をする(最上位が 9 のときだけ、その上の桁として 1 をつけ加える)。最後に、y コマンドを使い、_ を 0 に置換するのである。このやり方は、ホールドスペースを使わないために、応用範囲が広い。
以下は最初に私が思いついたダサいやり方。
#!/bin/sed -f
/\([0-9]*\)[0-8]$/ {
s/\([0-9]*\)8$/\19/
s/\([0-9]*\)7$/\18/
s/\([0-9]*\)6$/\17/
s/\([0-9]*\)5$/\16/
s/\([0-9]*\)4$/\15/
s/\([0-9]*\)3$/\14/
s/\([0-9]*\)2$/\13/
s/\([0-9]*\)1$/\12/
s/\([0-9]*\)0$/\11/
q
}
/^99*$/ {
y/9/0/
s/^/1/
q
}
h
s/\(.*[^9][^9]*\)99*$/\1/
s/\([0-9]*\)8$/\19/
s/\([0-9]*\)7$/\18/
s/\([0-9]*\)6$/\17/
s/\([0-9]*\)5$/\16/
s/\([0-9]*\)4$/\15/
s/\([0-9]*\)3$/\14/
s/\([0-9]*\)2$/\13/
s/\([0-9]*\)1$/\12/
s/\([0-9]*\)0$/\11/
x
s/.*[^9]\(99*\)$/\1/
y/9/0/
H
g
s/\n//
「SedWord 貧文書入門(sed で組版)」(では、この仕組みを利用して、Postscript の DSC コメント中で使うページ番号等を生成している。
○○行ごとに区切る
たとえば、テキスト10行ごとに「---------------」を挿入したいとする。方針としては、10行まで N コマンドでパターンスペースにため込んでいき、10行たまったらそれを吐き出すことにすればよい。なお、最後だけは10行に満たなくても、吐き出したい。
N N N N N N N N N N a\ --------------------------------------------
次項の「一行の字数をそろえる」と併せて使うと結構な整形ができるであります。
20 行ごとに区切るためには、N を 20 回書かなくてはいけない。そこでスクリプト中に 20 と数字で書いて済ませようと思ったら、案外面倒だった。
#!/bin/sed -f
:a
$ b b
/\([^\n]*\n\)\{19\}/! {
N
b a
}
:b
s/$/\
---------------/
p
以下、説明
#!/bin/sed -nf
:a # ラベル
$ b b # 最終行なら :b にジャンプしてパターンスペースの内容を出力する
/\([^\n]*\n\)\{19\}/! { # もし、パターンスペースに埋め込まれた改行が19個入っていないなら
# それは、20行ぶんに満たないのであるから、
N # 次の行を読み込む。
b a # そして、スクリプト先頭(:a)へ戻る
}
:b # ラベル
s/$/\ # ここから3行が実行されるのは、最終行を読み込んだか
---------------/ # パターンスペースに二つの埋め込まれた改行があるかのどちらか
ちょっと気をつけたいのは、N コマンドは、「改行+改行なしの次の行」をパターンスペースに追加するので、20行がたまったかどうかは、19個の改行が含まれているかどうか調べなくてはならない。19泊20日ですな。なお、入力が最終行であるかどうかの判断は、$ ではじまる行でなされるが、そのときにはすでに最終行の内容は前のサイクルの N コマンドによってパターンスペースに詰め込まれているという点に注意。
この仕組みを応用したのが、「sed でテキストをポストスクリプトに」。これは、a2ps とか mpage のようなことを、sed だけでやるスクリプト。
一行の字数をそろえる
一行 20 字にそろうように、改行を挿入してテキストを整形したい。 このスクリプトは -n オプション(明示しない限り出力を抑制)とともに実行すること。
なお、このスクリプトでは、オリジナルのテキストにあった改行はそのままいじらない。つまり、改行は追加されるだけで、削除されることはない。この点で Emacs の fill や、nkf -f、fmt -w とは動作が異なる(それらは、空行を作っている以外の改行を、文書構造とは関係のないものとして随意に削除する)。
残念ながら、ASCII 文字も日本文字も同じく一つと数えるので(私の使っている sed では)、英数字が混ざると行が短かくなってしまう。
#!/bin/sed -nf
:a
/^.\{,20\}[。、」』)}.,)]*$/! s/^\(.\{20\}\)/\1\
/
/[(「『{]/ s/\([(「『{(][(「『{(]*\)\n/\
\1/
s/\n\([。、」』)}.,)][。、」』)}]*\)/\1\
/
P
D
b a
次は、整形した例。
例えばこのようなテキストがあったとしよう。 もとのテキストは段落のおわりにしか改行が ない。やりたい事は、こうしたテキストを 「一行が二十字になるように、整形すること。」 である。句読点や括弧閉じが文頭に来る場合、 これを前行にぶら下げて組み、括弧開きが文 末にくるような場合には、次行に追い出した い。
いくらか説明してみる。方針としては、長い行の前から 20 字数えて、その後に改行を入れる。もし、句読点などが行頭にきたり、括弧開きが行末にくるようなら、改行位置をずらす。こうして挿入した改行以前を出力した後、残りに対して同じことを繰り返す。以下縷々見るなり。
01: #!/bin/sed -nf
02: :a ジャンプ先ラベル
03: /^.\{,20\}[。、」』)}.,)]*$/! s/^\(.\{20\}\)/\1\
04: /
20字以下+句読点だけからなる行はいじらない
そうでなければ行頭から数えて連続する20字の後に改行を挿入
この結果、パターンスペースの中身は
「テキスト20字+埋め込まれた改行+残り」となる
05: /[(「『{]/ s/\([(「『{(][(「『{(]*\)\n/\
06: \1/
「テキスト+追い出し文字+改行」を「テキスト+改行+追い出し文字」に置換
することにより、行末に来た [(「『{( を次行行頭に追い出す。
ちなみに、パターンスペース中に埋め込まれた改行は \n がマッチする。
最初に /[(「『{]/ で s コマンドの適用行を限定しているが、これは、
続く正規表現のコストが高いから、少しでもマッチ回数を減らすため。
なお、20文字以下の行は、03 行目が適用されないので埋め込まれた改行を持たず、
この行の s コマンドの影響を受けない(これは 07 行目も同じこと)。
07: s/\n\([。、」』)}.,)][。、」』)}]*\)/\1\
08: /
「テキスト+改行+ぶらさげ文字」を「テキスト+ぶらさげ文字+改行」に置換
することにより、行頭に来た 。、」』)}., を前行にぶら下げる
(ちなみに 05 行目の s コマンドのマッチより、この s コマンドのマッチのほうが
はるかにコストが低い。ワイルドカードの前に \n があるか後ろに \n があるかの
差が、処理速度に大きな影響を与える――私の使っている sed では)
09: P パターンスペースの改行以前を出力
埋め込まれた改行がなければ、パターンスペースをすべてを出力
10: D パターンスペースの改行以前を削除
埋め込まれた改行がなければ、パターンスペースをすべて削除
この結果パターンスペースが空になると11行目は実行されず
次の入力行を読んであらたに先頭からサイクルを開始
11: b a スクリプト先頭に戻る
次の行を読み込んでから前の行に対する処理を決めるという必要がないので、ホールドスペースを利用せず、比較的スッキリしたスクリプトになっている(え?)。/.*a/ のようなマッチは、/a.*/ に比べて大変なコストがかかるのであるが、これは実装がいかなるアルゴリズムを採用しているかに依存している。
テキストファイルから URL だけを抽出
テキストファイルから、URL だけ抽出する。この課題をやると、どうして sed ではなくて Perl や Python が好まれるようになったのか、その理由の一端がわかる。
行を前から見ていって、最初に出てくる abc より前の部分を削除するにはどうしたらよいか。 話の順序としてここから始めてみる。 これは、sed ではかなり面倒である。たとえば、0abc1abc2abc3 から、最初の 0 だけを削除して、abc1abc2abc3 としたい。 ただし、0,1,2,3 の部分が、予測不可能なパターンでも機能するようにしたいし、abc がもっと複雑な表現であっても耐えられるように考えてみたい。
s/.*\(abc.*\)/\1/
だと、行頭から「最後に出現する abc の直前」までが削除されてしまう。 * が最長マッチをするからである。* は貪欲で、すさまじく食べるのである。 ただ、最後の abc まで食べるとマッチが失敗してユーザに怒られるから、そこまでは食べない :-) また、sed には「abc というパターン以外」という表現はないし、 「マッチした部分以前」という表現もない。 最初に出てきた abc を消去するのは簡単だが、それを残してそれより前を消すというのは難しいのである。 こうすればできる。
:a /^abc/! s/^.// t a
これは、先頭が abc であるかどうかを検査して、No であれば、先頭一文字を消去の上、もう一度検査する。 検査の結果が Yes であれば、そこで終了する。ひどく格好悪いが、* の食欲を回避する方法が他に思いつかない。 (いいアイディアがあれば教えてくだされ)
次に、先頭にある abc を出力することを考えてみる。先頭にあれば食いしんぼうの * を使わなくてもよいので、簡単である。
s/\(abc\).*/\1/
さて、最初の abc だけでなくて、二番目以後の abc も次々と出力するにはどうしたらよいだろうか。
:s /^abc/! s/^.// t s h s/\(abc\).*/\1/p g s/abc\(.*\)/\1/ t s
ホールドスペースを利用した複雑な形となった。以下説明。
:s ラベル
/^abc/! s/^.// 先頭が abc でないなら先頭一文字を削除
t s 上の置換が成功したらラベル s(スクリプト先頭)に戻る
先頭に abc が出ているるかもはや一文字も残っていないときにループから抜けることになる
h パターンスペースをホールドスペースにコピー
s/\(abc\).*/\1/p パターンスペース先頭にある abc だけを残して他を削除し、出力
g ホールドスペースにコピーしておいたものをパターンスペースに戻す
s/abc\(.*\)/\1/ パターンスペース先頭にある abc だけを削除する
t s 上の置換が成功したらラベル s(スクリプト先頭)に戻る
行末にいるとパターンスペース先頭に abc がないので、ここでこの行の処理が終了する
それでは、テキストファイルから、URL だけを抜き出すスクリプトにこれを応用してみよう。上で abc としたところを、URL を表現する正規表現に変えてやる(この正規表現は完全ではないと思う)。
:s
/^s\{0,1\}https\{0,1\}:\/\/[a-zA-Z0-9.$,;:&=?!*~@#_()\/]*/! s/^.//
t s
h
s/\(s\{0,1\}https\{0,1\}:\/\/[a-zA-Z0-9.$,;:&=?!*~@#_()\/]*\).*/\1/p
g
s/s\{0,1\}https\{0,1\}:\/\/[a-zA-Z0-9.$,;:&=?!*~@#_()\/]*\(.*\)/\1/
t s
なおこれは、二つの sed コマンドをパイプでつなげば、もっと簡単にできる(はじめに言え)。最初の sed プロセスで、URL の前後に改行を挿入して、次の sed プロセスでは、URL にマッチする行だけ出力しているのである。
cat some.txt |
sed -e 's/s\{0,1\}https\{0,1\}:\/\/[a-zA-Z0-9.$,;:&=?!*~@#_()\/]*/\
&\
/g |
sed -ne '/s\{0,1\}https\{0,1\}:\/\/[a-zA-Z0-9.$,;:&=?!*~@#_()\/]*/ p'
もちろん、Perl や Python を使えば、とても簡単にできますよw
おまけ
sed でパディング
シェルスクリプト中で、1 を 001 に、12 を 012 に、123 をそのままにしたい。つまり 3 桁になるよう、数字の頭に必要な数の 0 をつけ加えたい。
$ echo 123 | sed -e 's/^[0-9][0-9]$/0&/;t;s/^[0-9]$/00&/'
t は、前置されるアドレスで置換が成功した場合に、後置されるラベルに飛ぶ。前置されるアドレスがなければ、当該行と解釈され、後置されるラベルがなければ、スクリプト末尾と解釈される。つまり、複数の s コマンドを書いたときに、それぞれの後ろにいちいち t とだけ書いておけば、スクリプト頭から s コマンドを実行していって、置換が成功されたところで処理が終了する。ようするに 2 ケタの数字を 3 ケタにする s コマンドが置換に成功すれば、1 ケタの数字を 3 ケタにする s コマンドは実行されずに済むので、所期の目的が達成される。

