sed の落とし穴・・・sed の s/// は何もしない? 実はパターンスペースを破壊する
目次
sed のデバッグをしている時、落とし穴に遭遇したので備忘録として残す。
例えば、以下のスクリプトは、行頭に複数の「:」があれば一個だけに置換し、「チェック」を「判定」に置換することを想定したもの。
1:top
2s/^:*/:/
3s/チェック/判定/
4t top
実はこのスクリプトはバグが潜んでおり、そのまま実行すると無限ループとなり、いつまで経っても処理が終わらずプロンプトが表示されない。 (バグが潜んでいるが、当記事の本題ではないので、ひとまず脇に置いておく)
1$ echo '::: t コマンドは、直前の s コマンドの成否だけをチェックしているわけではない' | sed '
2> :top
3> s/^:*/:/
4> s/チェック/判定/
5> t top'
6^C ← 終わらないので Ctlr + C で強制終了した
7$
当初の私の想定では、t コマンドは直前の s コマンドの成否に応じて分岐するものと思い込んでいたので、1回目の s/チェック/判定/ は成功して :top にジャンプするが、2回目のs/チェック/判定/ はすでに置換対象がないので失敗し、ジャンプは発生せず、スクリプト処理が終了するものと思っていた。
そこで、t コマンドの直前に、必ず置換が失敗すると見込んだコマンド s/// を追加して、ジャンプしないことを確認しようとしたのだった。
1$ echo '::: t コマンドは、直前の s コマンドの成否だけをチェックしているわけではない' | sed '
2:top
3s/^:*/:/
4s/良い/悪い/
5s///
6t top'
7^C ← 終わらないので Ctlr + C で強制終了した
8$
ところが、予想に反してこちらも無限ループ状態となり、処理が終わらなかった。
基本に立ち返り、sed のマニュアルを精読したところ、以下の記述を見つけた。
https://www.gnu.org/software/sed/manual/sed.html#The-_0022s_0022-Command
4.3 selecting lines by text matching/regexp/
This will select any line which matches the regular expression regexp.
(中略)
The empty regular expression ‘//’ repeats the last regular expression match (the same holds if the empty regular expression is passed to the s command).🔶 翻訳結果
/regexp/
これは、正規表現 regexp にマッチするすべての行を選択する。
(中略)
空の正規表現//は、直前に使用した正規表現によるマッチを繰り返す(この挙動は、sコマンドに空の正規表現を与えた場合も同様である)。
つまり、s/// は「直前に使われた正規表現を、空文字列に置換することになる。s/// は s/<直前の正規表現>// と同じだ。
だから、直前の正規表現にマッチすることがあればその部分が削除される、すなわち、置換が成功したことになるのだ。
例.
1$ echo 'abc123DE45f6' | sed 's/[0-9][0-9]*//; s///'
2abcDEf6
s/[0-9][0-9]*//123を削除- パターンスペース:
abcDE45f6
s///- 直前の正規表現
[0-9][0-9]*を再利用 →45を削除 - パターンスペース:
abcDEf6
- 直前の正規表現
可読性が極端に下がるため、 意図的でない限り s/// は使わない方が安全だ
最後に、元のスクリプトのバグを修正してこの記事を締める。「:」の置換コマンドをループの直前に移動するだけだ。
1s/^:*/:/
2:top
3s/チェック/判定/
4t top
元々、s/^:*/:/ がループ内(:top と t top の間)に存在していたわけだが、それだと常に置換が成功するため、t コマンドで置換成功と判定されて無限ループに陥っていたのだ。
実行結果は以下。
1$ echo '::: t コマンドは、直前の s コマンドの成否だけをチェックしているわけではない' | sed '
2> s/^:*/:/
3> :top
4> s/チェック/判定/
5> t top'
6: t コマンドは、直前の s コマンドの成否だけを判定しているわけではない
7$