sed の落とし穴・・・sed の t は「最後の s だけ」を見ているわけではない

sed のデバッグをしている時、初見の落とし穴に遭遇したのでメモ。
t コマンドって、s コマンドで置換が成功した場合に指定のラベルに分岐するわけだが、思い通りに分岐判定できず、無限ループに陥ってしまった。 そこで試しにと思い、そのループ内に s/// という無意味そうな置換コマンドを追加して、tコマンドが効かずにループ脱出できることを確認しようとしたところ、相変わらず無限ループに陥った・・・というのが調査の経緯。

結論から言うと:

s コマンドのどれか1つでも置換が成功していれば、t コマンドは分岐する。

t コマンドの仕様(重要)

sedt label は次の条件で分岐する。

直前の入力行の読み込み、または直前の t の実行以降に、 1回でも置換(s)が成功していれば分岐する

ここでのポイントは:

  • 「直前の s が成功したか」ではない
  • **「それまでに成功した s が1つでもあるか」**を見ている

内部的には 置換成功フラグ(substitution flag)を sed が持っていて、 t はそれをチェックする。

スクリプトのサンプル

1s/foo/FOO/
2s/baz/BAZ/
3t success
4s/.*/NO MATCH/
5b end
6:success
7s/bar/BAR/
8:end
動作の流れ
  1. s/foo/FOO/ : foo を FOO に置換。
  2. s/baz/BAZ/ : baz を BAZ に置換。
  3. t success :
    • 1, 2 のいずれかの置換が成功 → :success にジャンプ → bar を BAR に置換。
    • 1, 2 のどちらも置換が失敗 → NO MATCH に置換 → :end にジャンプ。

実行例

t コマンドの前方の(直前でない) s コマンドが成功する場合

 1$ printf "foo bar\n" | sed '
 2s/foo/FOO/
 3s/baz/BAZ/
 4t success
 5s/.*/NO MATCH/
 6b end
 7:success
 8s/bar/BAR/
 9:end
10'
11FOO BAR
  • s/foo/FOO/ は成功 → フラグ ON
  • s/baz/BAZ/ は失敗 → フラグ 変更なし
  • t top → フラグ ON なので、:success にジャンプ
  • s/bar/BAR/ は成功
  • 終了

スクリプトの一個目と四個目の置換が成功して、FOO BAR が出力される。

【参考】 t コマンドの直前の s コマンドが成功する場合

 1$ printf "foo baz\n" | sed '
 2s/foo/FOO/
 3s/baz/BAZ/
 4t success
 5s/.*/NO MATCH/
 6b end
 7:success
 8s/bar/BAR/
 9:end
10'
11FOO BAZ
  • s/foo/FOO/ は成功 → フラグ ON
  • s/baz/BAZ/ は成功 → フラグ ON
  • t top → フラグ ON なので、:success にジャンプ
  • s/bar/BAR/ は失敗
  • 終了

スクリプトの一個目と二個目の置換が成功して、FOO BAZ が出力される。

【参考】 t コマンドの前方の s コマンドが全て失敗する場合

 1$ printf "xxx bar\n" | sed '
 2> s/foo/FOO/
 3> s/baz/BAZ/
 4> t success
 5> s/.*/NO MATCH/
 6> b end
 7> :success
 8> s/bar/BAR/
 9> :end
10> '
11NO MATCH
  • s/foo/FOO/ は失敗 → フラグ OFF
  • s/baz/BAZ/ は失敗 → フラグ OFF
  • t top → フラグ OFF なので、:success にジャンプしない
  • s/.*/NO MATCH/ は成功
  • b end:end にジャンプ
  • 終了

スクリプトの三個目の置換が成功して、「NO MATCH」 が出力される。

関連する注意点

  • t が分岐すると 成功フラグはリセット
  • そのため同じ t に無限ループし続けることはない(通常は)

関連ページ