GNU sedを対象に書くが、最後にBSD sedでの対策も書く。

はじめの行だけ置換する

ファイルに同じ文字列が複数行に存在するものの、一番はじめに出てきた行の文字列だけを置換する方法について。

単純にsed 's/置換対象/置換後/'を実行するとマッチする全行が置換されてしまう。
一番はじめだけ置換するには、sedの対象行を0行目から置換対象の行までに絞ればいい。つまりsed '0,/置換対象/ s/置換対象/置換後/'とする。

以下のファイルを使って挙動を確認する。

$ cat txt 
aaa
aaa
aaa
bbb
bbb
ccc

一番はじめに出てきたbbbだけをBBBに変える。

$ sed '0,/bbb/ s/bbb/BBB/' txt
aaa
aaa
aaa
BBB
bbb
ccc

ファイルの一行目に出てくるaaaでも試してみる。

$ sed '0,/aaa/ s/aaa/AAA/' txt
AAA
aaa
aaa
bbb
bbb
ccc

sed '0,/置換対象/ s/置換対象/置換後/'でうまく動いていることがわかったが、なぜ1行目からではなく0行目から対象にしなければならないのか、つまり、sed '1,/置換対象/ s/置換対象/置換後/'では問題があるのかについて確認する。

$ sed '1,/aaa/ s/aaa/AAA/' txt
AAA
AAA
aaa
bbb
bbb
ccc

この通り、1,/置換対象/としてしまうと、1行目の次に出てくるaaaを対象としてしまい1行目と2行目が対象になってしまう。置換対象がファイルの1行目に一致しなければ1,/置換対象/でも問題ないが、ファイルの1行目に一致することを考えて0,/置換対象/とする必要がある。

参考

GNU sed は、次の特殊な 2 アドレス形式もサポートする。

0,addr2

「先頭アドレスにマッチした状態」で開始し、addr2 が見つかるまでその状態を維持する。これは、1,addr2 に類似しているが、次の点において挙動が異なる。addr2 が入力の先頭行にマッチする場合、0,addr2 形式ではアドレス範囲の終了位置にあるとみなされるが、1,addr2 形式ではアドレス範囲の開始位置にあるとみなされる。このアドレス指定は、addr2 が正規表現の場合にのみ機能する。

BSD sedではどうするか

参考で引用した通り、アドレス0を扱ってくれるのはGNU sedとなる。MacだとBSD sedが入っているが、先ほどの解法通り0行目から置換対象までと書いても一切マッチしなくなってしまう。

仕方がないので、1,/置換対象/を使用する。置換対象がファイルの1行目にある場合を考慮してファイルの1行目に無理やり空行を入れて、最後に1行目を消している。

$ cat <(echo) txt | sed -e '1,/aaa/ s/aaa/AAA/' -e '1 d'
AAA
aaa
aaa
bbb
bbb
ccc

素直にGNU sedをMacに入れた方がいいかな。。。

awkやRubyではどうするか

Macの場合、sedは諦めてawkで処理するのもいいかもしれない。
matchedという変数を用意する。sub()で置換し、置換に成功したとき戻り値1がmatchedにセットされるので、一度だけしかsub()が実行されないようにしている。最後に1を書くことで、各行必ずprintされるようにしている。

$ awk '!matched {matched=sub(/aaa/,"AAA")} 1' txt

Rubyで書くとすると次のようになる。

$ ruby -e 'puts ARGF.read.sub(/aaa/,"AAA")' txt

readで文章全体を一つの文字列として読み込み、subで一番はじめにマッチしたものを置換している。
Rubyのいいところとして、sedと同じように-iをつけてファイル自体を上書き保存できるところ。