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
をつけてファイル自体を上書き保存できるところ。