HOME

sed メモ(第二版)

2015-5-26(Tue)

私はこの小さな働き者が大好きだ。

〈目次〉

前の版のメモは、私が sed を使い始めた頃から書き始められたもので、拙いところがあちこちに溜まっていた。そこで、書き直したのであるが、きっと少しばかりスッキリしたことと思う。

改行を挿入したい

入力にない改行の出力は、改行文字をバックスラッシュで隠せばよい。何も難しいことはない。

aaa
bbb
ccc

という入力で、bbb の後に改行を挿入するため

s/bbb/&\
/

のようなスクリプトを実施すると、

aaa
bbb

ccc

のようになる。bbb の後に改行が一つ余計に入り、その結果空行ができた。なお(& はマッチ全体を示す)。

空行を削除したい

空行の削除は、改行だけ削除しようとせずに、行全体を d コマンドで削除せむと考えれば容易。

aaa
bbb

ccc

という入力に、

/^$/d

というスクリプトを実施すると、

aaa
bbb
ccc

という結果になる。

\n で改行にマッチする場合とマッチしない場合

sed は読み込んだ行の行末にある改行を削除してパターンスペースと呼ばれるバッファに格納したうえでテキスト処理をし、最後にパターンスペースの内容に改行をつけて出力するのが基本である。

したがって通常は、\n などのエスケープシーケンスを使っても、正規表現を改行にマッチさせることはできない。

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 コマンド」と「埋め込まれた改行」)。以下、具体例で見ていく。

2 行に渡るパターンを置換したい

改行を無視して探索し、2 行にまたがるパターンに対しても置換を行ないたいという場合がある。たとえば、

____abc_____a
bc__abc____ab
c___abc____ab

という入力に対して、abc を xyz に変換して次のように出力したい。

____xyz_____xyz
__xyz____xyz
___xyz____ab

スクリプトは次のようになる。

# 1 行におさまるパターンの置換
s/abc/xyz/g
N
# 2 行にわたるパターンの置換
s/ab\nc/xyz\
/
s/a\nbc/xyz\
/
P
D

赤字(あるいは太字)にしたコマンドが、処理の流れ・バッファーへの読み込み・出力を制御するためのコマンドたち。緑字(あるいは斜体)の部分は、abc を xyz に置換するための s コマンドである。

N コマンドは 1 行を読み取ってパターンスペースの末尾に挿入し、以前にパターンスペースに入っていた内容との間に改行を埋め込む。こうした「埋め込まれた改行」は \n でマッチさせることができる。これ以上読み込む行がないと N コマンドは sed を終了させる。(-n オプションつきで起動されていない場合、sed は終了する前にパターンスペースの中身を出力する点に注意)

D コマンドはパターンスペース中の埋め込まれた改行とそれ以前を削除する。続いて「埋め込まれた改行以後をパターンスペースに保持したまま、新しい行を読み込まずにスクリプト先頭に制御を移す」ということを行う。なお、D コマンドが行われた結果、パターンスペースが空になった場合は、スクリプト先頭に制御が移ったときに新行が読み込まれる(この例では最後にそれが行われている)。※蛇足ながら、この手のスクリプトでは D コマンドの挙動重要だと思うなり

何が起こっているのかを 1 コマンドごとに追ってみる。赤字(あるいは太字)は、パターンスペースの内容、緑字(あるいは斜体)は出力である。

____abc_____a
↓(1行でおさまるパターンの置換)
____xyz_____a
↓(N コマンド)
____xyz_____a[埋め込み改行]bc__abc____ab
↓(2行にまたがるパターンの置換)
____xyz_____xyz[埋め込み改行]__abc____ab
↓(P コマンドが埋め込み改行以前を出力)
 ____xyz_____xyz[改行]
↓(D コマンドが埋め込み改行以前を削除
 パターンスペースを保持したままスクリプト先頭へ)
__abc____ab
↓(1行でおさまるパターンの置換)
__xyz____ab
↓(N コマンド)
__xyz____ab[埋め込み改行]c___abc____ab
↓(2行にまたがるパターンの置換)
__xyz____xyz[埋め込み改行]___abc____ab
↓(P コマンドが埋め込み改行以前を出力)
 __xyz____xyz[改行]
↓(D コマンドが埋め込み改行以前を削除
 パターンスペースを保持したままスクリプト先頭へ)
___abc____ab
↓(1行でおさまるパターンの置換)
___xyz____ab
↓(新たに読み込む行がないので N コマンドは sed を終了させる)
↓(sed は終了前にパターンスペースの中身を出力する)
 ___xyz____ab

あるパターンに続く改行を削除したい

上の「2 行に渡るパターンを置換したい」では、入力と出力の間で行数の増減が起こらなかった。ここでは改行が削除されるので、行数減に対応しないとならない。具体的には t コマンドを使う。

abc
abc
def
def

という入力があるときに、abc に後続する改行を削除し、

abcabcdef
def

という出力を得ることを考える。スクリプトは次のようになる。

:a
N
# 埋め込み改行の削除
s/abc\n/abc/
ta
P
D

これは、前項の「2 行に渡るパターンを置換したい」を少し変えたものである。置換が成功すると、パターンスペースの中から埋め込み改行が消えてしまうので、この場合ラベル :b に飛んで、もう一度 N コマンドのところからやり直すようにしている。

起こっていることを 1 コマンドごとに見ると次のようになる。なお、赤字(あるいは太字)は、その時のパターン・スペースの内容、緑字(あるいは斜体)は出力である。

abc
↓(N コマンド)
abc[埋め込み改行]abc
↓(s コマンドが埋め込み改行を削除)
abcabc
↓(置換が成功しているので t コマンドでラベル:bにジャンプ)
↓(N コマンド)
abcabc[埋め込み改行]def
↓(s コマンドが埋め込み改行を削除)
abcabcdef
↓(置換が成功しているので t コマンドでラベル:bにジャンプ)
↓(N コマンド)
abcabcdef[埋め込み改行]def
↓(s コマンドが埋め込み改行の削除)
↓(置換が失敗しているので t コマンドは何もしない)
↓(P コマンドが埋め込み改行以前を出力)
 abcabcdef[改行]
↓(D コマンドが埋め込み改行以前を削除
↓パターンスペースを保持したままスクリプト先頭へ)
def
↓(もう入力行がないので N コマンドは sed を終了させる)
↓(sed は終了前にパターンスペースの内容を出力する)
 def

あるパターンに先行する改行を削除したい

たとえば、

abc
abc
def
def

という入力があるときに、def に先行する改行を削除し

abc
abcdefdef

という出力を得ることを考える。スクリプトは次のようになる。

:a
N
# 埋め込み改行の削除
s/\ndef/def/
ta
P
D

これは、前項の「あるパターンに続く改行を削除したい」とほとんど同じで、違うところは置換コマンドの内容だけである。

改行をすべて削除したい

最も賢いやり方は、sed を使わずに

cat input.txt | tr -d "\n"

とやることである。敢えて sed でやるとすると、次のようになる。

:a
$! {
 N; ba 
}
s/\n//g

最終行に達するまで N コマンドでひたすら入力をパターン・スペースに詰め込んでいき、最後に g オプションつきの置換コマンドで一気に改行を削除している。

じつは、この例は大きな、そしていくぶん邪悪なポテンシャルを秘めている。最初の 4 行の呪文で、入力全体をパターン・スペースに詰め込むから、置換コマンド部分を書くときには「sed の検索パターンは行末の改行にマッチしない」ということを忘れて、\n が改行にマッチするものと思って差し支えないのである。

なお、「いくぶん邪悪な」と言ったのは、入力のサイズが大きくなると、この方法では sed が使えるメモリの上限を越えてしまう可能性があるからだ。

段落内の改行を削除したい(その1)

たとえば、

123
456
789

abc
def
geh

のような、段落が空行で仕切られている入力を

123456789

abcdefgeh

のような形にしたい。N コマンドで入力行をパターンスペースに詰め込みながら埋め込み改行を削除していくが、空行を読み込んだ時に限り(N コマンドの結果パターンスペース末尾が埋め込み改行になる時に限り)、パターンスペースを出力する。スクリプトは次のようになる。

:a
N
/\n$/{
 p;d;
}
s/\n//
ba

何が起こっているのかを 1 コマンドごとに追ってみる。赤字(あるいは太字)は、パターンスペースの内容、緑字(あるいは斜体)は出力である。

123
↓(N コマンド)
123[埋め込み改行]456
↓(/\n$/にマッチせず)
↓(埋め込み改行の削除)
123456
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンド)
123456[埋め込み改行]789
↓(/\n$/にマッチせず)
↓(埋め込み改行の削除)
123456789
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンド)
123456789[埋め込み改行]
↓(/\n$/にマッチするので
 p コマンドがパターンスペースを出力。
  123456789[埋め込み改行][改行]
 d コマンドがパターンスペースを削除)
パターンスペースは空
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンド)
[埋め込み改行]abc
↓(/\n$/にマッチせず)
↓(埋め込み改行の削除)
abc
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンド)
abc[埋め込み改行]def
↓(/\n$/にマッチせず)
↓(埋め込み改行の削除)
abcdef
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンド)
abcdef[埋め込み改行]geh
↓(/\n$/にマッチせず)
↓(埋め込み改行の削除)
abcdefgeh
↓(b コマンドで無条件にスクリプト先頭にジャンプ。
 このとき、新行は自動的には読み込まれない)
↓(N コマンドが読むべき新行がないので sed が終了)
↓(sed は終了前にパターンスペースを出力)
  abcdefgeh

段落内の改行を削除したい(その2)

たとえば、

<p>123
456
789
<p>abc
def
geh

のような入力を

<p>123456789
<p>abcdefgeh

のような形にしたい。これは前項の例と異なり、段落のはじめにしか目印<p>がない。次行を先読みをして段落の終了を知る必要がある。

スクリプトは、

:a
N
/\n<p>/{P; D;}
s/\n//
$!ba

のようになる。

起こっていることを 1 コマンドごとに見ると次のようになる。なお、赤字(あるいは太字)は、その時のパターン・スペースの内容、緑字(あるいは斜体)は出力である。

<p>123
↓(N コマンド)
<p>123[埋め込み改行]456
↓(/\n<p>/ にマッチせず)
↓(s コマンドが埋め込み改行を削除)
<p>123456
↓(最終行でないのでラベル :a にジャンプ)
↓(N コマンド)
<p>123456[埋め込み改行]789
↓(/\n<p>/ にマッチせず)
↓(s コマンドが埋め込み改行を削除)
<p>123456789
↓(最終行でないのでラベル :a にジャンプ)
↓(N コマンド)
<p>123456789[埋め込み改行]<p>abc
↓(/\n<p>/ にマッチ
 P コマンドが [埋め込み改行] までを出力
  <p>123456789[改行]
 D コマンドが [埋め込み改行] までを削除)
<p>abc
↓(s コマンドが埋め込み改行を削除:マッチせず)
↓(最終行でないのでラベル :a にジャンプ)
↓(N コマンド)
<p>abc[埋め込み改行]def
↓(/\n<p>/ にマッチせず)
↓(s コマンドが埋め込み改行を削除)
<p>abcdef
↓(最終行でないのでラベル :a にジャンプ)
↓(N コマンド)
<p>abcdef[埋め込み改行]geh
↓(/\n<p>/ にマッチせず)
↓(s コマンドが埋め込み改行を削除)
<p>abcdefgeh
↓(最終行なので b コマンドは実行されず、
 原則通り制御がスクリプト一行目に移り新行を読み込もうとするが、
 もはや読み込むものがないのでスクリプトが終了)
↓(sed がパターンスペースを出力)
 <p>abcdefgeh

あるパターンを含む行の前行で置換をしたい

B
B
A
B
B
A

のような入力があったときに、A を含む行の前の行において B を C に置換して、

B
C
A
B
C
A

のような出力を得たい。置換を行うかどうかは、次行の先読みを行わないといけない。そして、先読みした行で置換が起こらないようにするために、これをホールドスペースと呼ばれる一時バッファに退避させておく必要がある。

/A/{ x; s/B/C/; x; }
x
2,$ p
${ g; p; }

パターンスペースに読み込まれた n 行目から A を探して、マッチしたらホールドスペースに蓄えられている n-1 行目を置換。1 行目に A があっても x;s/B/C/;x は何も引き起こさないことに注意。n 行目を読み込んだサイクルでホールドスペース中の n-1 行目がプリントされるようになっているので、最終行を読み込んだサイクルでは、最終行の前行だけでなく、ホールドスペースに保管した最終行自身出力している。

マッチした文字列を抽出したい

sed で正規表現にマッチした文字列を削除するのは容易だが、マッチした文字列だけを抽出するのは難しい。これは削除したい部分にマッチする正規表現が必要になるからだ。しかし、素朴に削除したい部分を .* でマッチさせようとすると、.* が最長マッチをした結果、必要な部分まで食ってしまうかもしれない。

次のような入力があったとする。

URLはhttp://kabipan.com/densan/sed.htmlで、目次は http://www.kabipan.comです。
作者のtwitterのプロフィールページはhttp://twitter.com/kabipanotokoになります。

ここから URL だけを抽出して、

http://kabipan.com/densan/sed.html
http://www.kabipan.com
http://twitter.com/kabipanotoko

という出力を得たい。スクリプトは次のようになる。緑字(あるいは太字)の部分は URL にマッチする正規表現だ(かなり不完全だと思うけど)。

# URL の前後に改行を挿入
s/https\{0,1\}:\/\/[-a-zA-Z0-9.$,;:&=?!*~@#_()/]*/\
&\
/g

# 改行が挿入されなかった行を削除
/\n/!d

# 「文字列1,改行,文字列2,改行」の並びを「文字列2,改行」にする
s/[^\n]*\n\([^\n]*\)\n/\1\
/g

# 行末直前の「改行,文字列」を削除する
s/\n[^\n]*$//

ここでは、いったん URL の直前直後に改行を挿入し、次にこの目印を元に不要部分へのパターンマッチングを行っている。必要文字列にマッチする正規表現は上のスクリプトで緑字になっているので、その部分だけ変えれば他のパターンを抽出するのにも使える。

行内で置換の範囲を限定したい

"Mr " に続くアルファベット一文字だけを大文字に変換したいとする。Mr tanaka を Mr Tanaka にするように。この問題の最も簡単な解決法は、

s/Mr a/Mr A/g
s/Mr b/Mr B/g
...
s/Mr z/Mr Z/g

にほかならない。これは面白味はないが、初心者が見ても意図するところがすぐにわかるという利点がある。私は同僚のためにこういう優しいコードを書く人が好きだが、面白味のほうを優先して楽しむのも悪くはない。y コマンドを使ってこの問題を解決できないだろうか?

小文字を大文字に変換するコマンドは

y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

でいいが、これではすべての文字が大文字になってしまう。つまり、y コマンドを使おうとすると、われわれは行内においてコマンドを適用する範囲を制限するにはどうしたらいいか、という問題に遭遇する。(もちろん、コマンドをの対象としたい範囲が行単位であれば、/FromHERE/,/ToHERE/ のように行アドレスで限定してやればよい。しかしこの場合は行の中の一部にだけコマンドを適用したので、それは使えない。)

一つの解決策は、パターンスペースに大文字に変換したい一文字だけを残し、あとの部分をホールドスペースに避難させておくことだ。

:a
/Mr [a-z]/{
h
s/.*Mr \([a-z]\).*/\1/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
x
s/\(.*Mr \)[a-z]\(.*\)/\1\
\2/
G
s/\(.*\)\n\(.*\)\n\(.*\)/\1\3\2/
}
t a

このスクリプトは次のようにして働く。"Mr "の後に小文字一文字が続くパターンのある行(/Mr [a-z]/)において、行の内容をホールドスペースに退避(h)した上で、「Mr に続く一文字」以外を削除(s/.*Mr \([a-z]\).*/\1/)。その一文字を大文字に変換(y)する。ホールドスペースとパターンスペースを交換(x)し、今度は「Mr に続く一文字」を削除し、それより前と後ろを改行によって区切る(sコマンド)。ホールドスペースにある一文字をパターンスペースに追加(G)したうえで、改行で分けられた3つの部分の順番を正しく直す(s/\(.*\)\n\(.*\)\n\(.*\)/\2\1\3/)。もし置換が成功していたなら(t)、ラベル a に飛び、これを繰り返す(t a)

最後に再度言うならば、たいがいの場合この方法よりずっとうまいやり方が見つかるだろう。

sed スクリプト中で別のプロセスを起動する(GNU sed)

GNU sed には、邪悪と言えるほど強力なコマンドがある。e コマンドだ。(念のため言っておくと、-e オプションのことではない)。e コマンドにパラメータをつけないで(つまり、ただ e とだけ書いて)おくと、パターンスペースの内容をシェルに送り、そのアウトプットでパターンスペースを書き換える。つまり、sed スクリプトから他のプロセスを起動できてしまうのである。

この危険を実感したければ、$ echo "whoami" | sed "e" などとやってみるといい。これは、無害なワンライナーだが、想像力のある人なら危険な使い方を思い浮かべることができるだろう。

e コマンドは、sed スクリプトならばアウトプットファイルとして指定されたファイル以外に何の影響も与えないという安心感を覆す邪悪なコマンドだと思う。人からもらった sed のスクリプトを GNU sed で実行するならば、e コマンドが含まれていないかどうかをチェックするか --posix オプションをつけて実行することをお勧めする次第である。

で、この e コマンドを積極的に使うと、たとえば文書で @today と書いておいたところを sed に通して実際の今日の日付けに置換できたりする。

$ echo "Today is @today, isn't it? 
Yes, it's @today !" | sed -f date.sed 
Today is 2016年  4月  7日 木曜日 10:12:05 JST, isn't it? 
Yes, it's 2016年  4月  7日 木曜日 10:12:05 JST !

こんな感じ。

これを実現するのはいくらか複雑で、

:a
/today/{
h
s/.*@today.*/date/
e
x
s/\(.*\)@today\(.*\)/\1\
\2/
H
g
s/\(.*\)\n\(.*\)\n\(.*\)/\2\1\3/
}
t a

のような感じだ。e コマンドはパターンスペース全体をシェルに送ってしまうので、@today 以外の部分をいったんホールドスペースに退避させている(これは、前に使ったテクニックだ)。また、同じ行に 2 回以上 @today が出てくることを考えて、ラベルを使ったループを作っている。

もちろん、次のようなシェルスクリプトのほうがずっと読みやすくて歓迎されるだろう。

$ echo "Today is @today, isn't it? 
Yes, it's @today !" | sed "s/@today/`date`/g"

もう一つ e コマンドを使ったものを考えてみる。

1 2
3 4

というファイルがあるとき、各行の合計を求めたいとする。もちろん、常識的には awk '{print $1+$2}' とやればいいのである。これを gnu sed でやると

s/  */ + /
s/^/expr /
e

ということになる。たぶん、だれも喜ばないと思うけど。

改行コードの変換

改行コードを sed で変えられないかという話をしばしば耳にする。LF と CR と CR LF との相互変換という話題である。これを sed でやるには、いくつか問題点がある。Unix/Linux な環境における場合を考えてみる。

第一の問題は、sed が基本的には印刷可能なテキストを相手にしたものであること。改行を示すエスケープシーケンスをスクリプト中に書くことは、すべての sed が能くすることではない。GNU sed だと、\x を用いた 16 進による表記ができるから、LF は \x0a、CR は \x0d と書くことができる(参考 http://www.gnu.org/software/sed/manual/sed.html#Escapes )。しかし、こうしたことは、たとえば Mac にはじめから入っている sed ではできない。(この問題はシェルの機能を使って回避できることもある。たとえば、bash では $'\x0d' と書いて CR を表すが如きを用いるのである。だが、これはシェルに依存する。たとえば、私のところでは、bash 用に書いたスクリプトが sh ではうまく動かないという邪悪なことが起こる。)

第二の問題は、第一の問題よりも深刻である。sed はスクリプトを行に適用する前に改行を削除して、処理後改行をつけ加えて出力するのが仕様である(ファイル末尾を除く)。基本的には、Linux 環境で使われる sed なら行末の LF を取り除いて置換等の処理を行った後 LF を付加して出力される。これは取りも直さず、sed で改行コードの変更する可からず、と言っているようなものだ。ただし、この問題に悩まされない変換もある。一例を挙げると、Linux 上で LF → CR LF をやるときは、CR だけを加えればよいから sed でも可能だ。(この問題を回避するために、すべての行をつなげて読み込んで処理することもできる。:a;$!N;$!ba という例のおまじないだ。ただ、こちらのやり方は、巨大なファイルを処理する場合問題を起こすだろう)

第三の問題は、CR だけの改行は Linux では改行として認識されずに、複数行からなるファイルがものすごく長い行 1 行だと解釈されるということだ。巨大なファイルだと、メモリを食いつくすかもしれない。

sed が何を改行コードとして認識するかには環境によって LF, CR, CRLF の 3 つのパターンがあり、また、LF, CR, CRLF 間の相互の変換に 6 通りあり、sed の種類の問題あるいはシェルのエスケープシーケンスの取り扱いの問題があり、ファイルを一気にバッファに読み込まれる危険についての判断があり——であるから、sed による改行コードの変更はなかなか大変だ。

Unix/Linux 環境での結論

 

本の原稿 1 冊ぶんほどの小さなファイルなら nkf のオプションで変換してしまうのが楽だと思う。以下は重量級のファイルを相手にする場合のことを念頭に置く。ファイルサイズが大きくてもメモリ不足にならず、その限りで最も速く、かつ多くの環境で用意されているツールを優先して使うと、次のようになった。

なおこれは、私の Linux 上での話で、基本的に改行コードが LF となっている環境用のメモである。

tr 最強。でも、LF→CRLF、CR→CRLF はできない。LF→CRLF は Perl さんか GNU sed でできる。CR→CRLF は、tr で CR→LF にしてから Perl さんか GNU sed で LF → CRLF にする二段構え。CR→LF にしているのは、私の環境だと Perl さんや GNU sed が LF しか改行と認識せず、CR を無視してすべて一気にバッファに読み込もうと欲張るから。

(1)LF → CR tr "\n" "\r"
perl -p -e 's/\n/\r/'
(2)LF → CRLF sed -e 's/$/\x0d/' # GNU sed のみ
perl -p -e 's/\n/\r\n/'
(3)CRLF → LF tr -d "\r"
sed -e 's/\x0d$//' # GNU sed のみ
perl -p -e 's/\r//'
(4)CRLF → CR tr -d "\n"
perl -p -e 's/\n//'
(5)CR → LF tr "\r" "\n"
(6)CR → CRLF tr "\r" "\n" | sed -e 's/$/\x0d/' # GNU sed のみ
tr "\r" "\n" | perl -p -e 's/\n/\r\n/'

行の長さを揃える

一行の字数を短かく揃えるためのコマンドとしては、fmt, pr が有名だが、私の環境では日本語のように単語をスペースで区切らない言語に対しては無力である。この用途では、nkf -f を用いるのがよいと思う。一応 sed でそれらしいことをやってみた。

一行 20 字にそろうように、改行を挿入して次のようにテキストを整形したい。

例えばこのようなテキストがあったとしよう。
もとのテキストは段落のおわりにしか改行が
ない。やりたい事は、こうしたテキストを
「一行が二十字になるように、整形すること。」
である。句読点や括弧閉じが文頭に来る場合、
これを前行にぶら下げて組み、括弧開きが文
末にくるような場合には、次行に追い出した
い。

スクリプトは次のようになる。なお、-n オプション(明示しない限り出力を抑制)とともに実行すること。このスクリプトは、オリジナルのテキストにあった改行はそのままいじらない。また、半分の幅しかな文字(a,b,c,1,2,3)も日本文字も同じく一つと数えるので、そうした文字が混ざると行が短かくなってしまう。

#!/bin/sed -nf
:a
/^.\{,20\}[。、」』)}.,)]*$/! s/^\(.\{20\}\)/\1\
/
/[(「『{]/ s/\([(「『{(][(「『{(]*\)\n/\
\1/
s/\n\([。、」』)}.,)][。、」』)}]*\)/\1\
/
P
D
b a

02行目:ジャンプ先ラベル

03〜04行目:20字以下+句読点だけからなる行はいじらないそうでなければ行頭から数えて連続する20字の後に改行を挿入。この結果、パターンスペースの中身は「テキスト20字+埋め込まれた改行+残り」となる。

05〜06行目:「テキスト+追い出し文字+改行」を「テキスト+改行+追い出し文字」に置換することにより、行末に来た [(「『{( を次行行頭に追い出す。ちなみに、パターンスペース中に埋め込まれた改行は \n がマッチする。最初に /[(「『{]/ で s コマンドの適用行を限定しているが、これは、続く正規表現のコストが高いから少しでもマッチ回数を減らすため。なお、20文字以下の行は、03 行目が適用されないので埋め込まれた改行を持たず、この行の s コマンドの影響を受けない(これは 07 行目も同じこと)。

07〜08行目:「テキスト+改行+ぶらさげ文字」を「テキスト+ぶらさげ文字+改行」に置換することにより、行頭に来た 。、」』)}., を前行にぶら下げる(ちなみに 05 行目の s コマンドのマッチより、この s コマンドのマッチのほうがはるかにコストが低い。ワイルドカードの前に \n があるか後ろに \n があるかの差が、処理速度に大きな影響を与える——私の使っている sed では)

09行目:パターンスペースの改行以前を出力埋め込まれた改行がなければ、パターンスペースをすべてを出力

10行目:パターンスペースの改行以前を削除埋め込まれた改行がなければ、パターンスペースをすべて削除この結果パターンスペースが空になると11行目は実行されず次の入力行を読んであらたに先頭からサイクルを開始

11行目:スクリプト先頭に戻る

さかさ言葉

たとえば「abc」を「cba」に変換したい。まず入力文字列の最後に改行をつけておき、「改行の直前の 1 文字を改行の直後にもっていく」ということを、改行が先頭に出てくるまで繰り返す。

s/$/\n/
:a
s/\([^\n]\)\(\n.*\)/\2\1/
ta
s/\n//

文字グループの中にブラケット自身を書きたい

[ ] こういう括弧をブラケット(bracket)と呼ぶ。正規表現で文字グループを書くときに、たとえば [a-z] などとするが、その前後についている括弧が、ブラケットだ。

この文字グループの中に、ブラケット自身を書きたい場合どうするか。すぐに思いつくのが、[\[\]] であるが、これはうまくマッチしないことがある。

[ または ] ということを表現するには、[][] としなくてはならない。これは、実に奇妙に見える。中身が空の二つの文字グループが連続しているようだが、そうではない。[ 直後に書かれた ] は、] そのものを示す。そして、[ はエスケープしなくても [ そのものを表す。つまり、[ ] の中に ][ が入っているのが、[][] なのである。

奇数行・偶数行だけ出力

入力ファイルの奇数行だけを出力するなら

n
d

とやればよい。なんと 2 文字のスクリプト。n コマンドは、現在のパターン・スペースの内容を吐き出して、次の行を読み込む。このときに現在行の番号もインクリメントされる。

   # パターンスペースにあらたな 1 行を読み込む
n  # パターンスペースを出力して次行をパターンスペースに読み込む
d  # パターンスペースを削除する
   # いつも通りパターンスペースを出力しようにも空だ

偶数行だけ出力するなら、

1d
n
d

となる。

   # パターンスペースにあらたな 1 行を読み込む
1d # もし 1 行目なら、
   #   パターンスペースを削除し、
   #   新たな入力行を読み込み、
   #   スクリプトの先頭に制御を戻す
n  # パターンスペースを出力して次行をパターンスペースに読み込む
d  # パターンスペースを削除する
   # いつも通りパターンスペースを出力しようにも空だ

暇な人は、「3 の倍数行のときだけ『アホ』と出力」とか試みられたし。……と書かむ思ったけど、つい自分で書いてしまつた。

n
n
s/.*/aho/

奇数行と偶数行の入れ替え

奇数行と偶数行を入れ替えることを考へむ。これは、置換コマンドを使わない愉快で素早いやり方であるが、いくらかトリッキーに見えるかもしれない。

-n オプションつきで

h
n
p
g
p

というスクリプトを使えよい。

   # まず、最初に新しい行がパターンスペースに読み込まれる
h  # そいつをホールドスペースにコピーしとく
n  # -n オプションのおかげでパターンスペースを出力せずに、たんに
   # 次の一行をあらたにパターンスペースに読み込む
p  # ここで、パターンスペースを出力しとく
g  # さっき退避させといたホールドスペースの内容をパターンスペースにコピー
p  # パターンスペースの内容を出力

N コマンドの勉強

念のため言っておくと、N コマンドと 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 コマンドが、パターンスペースの内容を吐き出す」とい動作は、ここでいう「明示的なプリントコマンド」ではないので、抑制されたのだ。

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 コマンドにより、パターンスペースは空になり、制御はプログラム先頭に移り、新たな入力行を読み込もうとする。しかし、もう入力行はないので、デフォルトの動作に従って、このプロセスは終了する。

ホールドスペースに「メモ」しておく例

$ 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

「1 行目から p. ではじまる行を探索し、その行から最終行までの中で、apple を含む行をすべて探す。そうして見つかった各行からファイル先頭方向に向かって p. ではじまる行を探し出し、見つかった行をプリントする」という意味である。1; は対話環境においてカレント行がどこにあるかわからないから。/^p\./ はこれがないと apple が p. ではじまるどの行よりも先にある場合に最後にある p. ではじまる行が出力されてしまうのを防ぐためである。

ed は /正規表現/ でファイル末尾方向への探索ができるが、このほかに、?正規表現? でファイル先頭方向への探索ができる。処理対象のファイルをすべて格納しておくバッファを持つからできることで、sed には真似のできぬことなり。

(おまけ)

カタカナをひらがなに変換。

y/ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヽヾ/ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖゝゞ/

ただし、カタカナしかない文字「ヷヸヹヺ」および「ヿ」(コト)は変換せず。また、長音記号(オンビキ)は、カタカナひらがな兼用のため変換せず。また、カタカナにはひらがなの「ゟ」(より)に当たるものなし。中黒「・」は Unicode ではカタカナに分類されているようだ。 また、濁点「 ゛」(U+309B)や半濁点「 ゜」(U+309C)は合字を作るためのもの U+3099 および U+309A とは別にあり、いずれもカタカナ、ひらがなの区別がない。

いわゆる半角カタカナをカタカナに変換。ただし、濁音、半濁音、「ヱヲヰ」のたぐいは半角カタカナが存在しないので変換せず。

y/ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン/ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン/

おわり