NGW開発BLOG
← 記事一覧へ
·#018

壊さずに直す件数アサート付きの冪等な一括置換

ファイルを丸ごと差し替えられない環境へ同じ修正を手で当てる。件数アサートと冪等性チェックで置換を検証つきの適用に変える型を紹介する。

ファイルを差し替えられない現場で、同じ直しをもう一度当てる

手元で直したのと同じ修正を、本番に近い別環境へもう一度当てる場面がある。しかもファイルを丸ごと差し替えられない、コピーも許されない、という制約つきだ。できるのは、対象の数行だけをその場で書き換えることだけ。

素朴にやると、置換スクリプトを書いて流す。だがこれは思ったより事故が多い。正規表現の特殊文字がうっかり効いて意図しない箇所まで巻き込む。環境差でパターンがマッチせず、0件のまま「エラーも出なかったし成功だろう」と勘違いする。あるいは同じスクリプトを二度流して、一度目で直った箇所をさらに壊す。

厄介なのは、これらがどれも「静かに」失敗することだ。スクリプトは正常終了し、終了コードは0を返す。壊れたことに気づくのは、その環境で誰かが機能を使ったときになる。だから置換は「実行する」ものではなく、「安全性を証明してから適用する」ものにしたい。

事故の正体は「思ったところに当たっていない」

自動置換の事故を分解すると、ほとんどが一点に集約される。狙った場所に、狙った数だけ当たっていない、ということだ。

  • 対象が違う、あるいは既に別の形に変わっていて、0件しか一致しない
  • 想定していなかった別の箇所にも一致して、2件以上に当たる

どちらも「1箇所だけ直したい」という意図から外れている。ならば、その意図をそのままコードに書けばいい。置換の前に「この文字列はちょうど1回だけ出現するか」を確かめ、そうでなければ書き込まずに中断する。0件も2件以上も、等しくエラーとして扱う。

もうひとつ効くのがリテラルマッチだ。置換したい文字列を正規表現として解釈させず、特殊文字を無効化した「ただの文字列」として探す。quotemeta 相当の処理を一枚かませるだけで、.($ が牙をむく余地がなくなる。狙った文字列だけを、文字どおりに対象にできる。

count == 1 を確認してから、はじめて書く

骨子は驚くほど短い。各置換について、変更前の文字列の出現回数を数え、それがちょうど1のときだけ書き換える。冪等性のために、変更後の形が既に存在するなら「適用済み」とみなして無変更で正常終了する。そして全部のチェックが通ってから初めてファイルに書く。部分適用はしない。

text = read_raw(path)   # UTF-8をrawバイトとして扱い、壊さない

for (before, after) in edits:
    if count_literal(text, after) >= 1:
        continue                      # 既に適用済み → 冪等に何もしない
    n = count_literal(text, before)   # 特殊文字は無効化して数える
    assert n == 1, f"想定は1件、実際は{n}件: {before}"
    text = replace_literal(text, before, after)

write_raw(path, text)   # 全チェック通過後にまとめて書く

大事なのは順序だ。数える、確かめる、(すべて確かめ終えてから)書く。assert が途中で1本でも倒れたら、その時点で write に到達しない。だから「1箇所目だけ直って2箇所目でこけた」という中途半端な状態が原理的に生まれない。

count_literal を全文の走査にしておくと、冪等性チェックも件数チェックも同じ物差しで測れる。「変更後が1件以上あるか」で適用済みを判定し、「変更前がちょうど1件か」で安全性を判定する。二度流しても、一度目で after が出現しているから二度目はスキップされる。これが冪等性だ。

文字コードも忘れてはいけない。テキストをUTF-8として気を利かせて再エンコードすると、環境によっては改行コードやBOMが混入して差分が汚れる。読むときも書くときもrawバイトのまま扱い、触った数行以外は1バイトも変えないのが安全だ。

破壊的操作の最小の保険

バックアップを取らない運用にするなら、なおさら「書く前に安全を証明する」ことが効いてくる。取り返しのつかない操作ほど、実行の手前に検証の壁を積むべきだからだ。

出現回数のアサートと冪等性チェックは、そのための最小の保険といっていい。派手な仕組みではない。数えて、1であることを確かめ、通ってから書く。それだけだ。だがこの三行の規律が、「思ったところに当たっていない」という事故のほぼ全種類を、書き込みの前で堰き止める。

手作業には再現性と可監査性がつきまとう。誰がいつ何を当てたか、本当に当たったのか。それを人間の注意力ではなく、スクリプト自身の自己検証で担保する。置換を「実行」ではなく「検証つきの適用」に変える。壊さずに直すとは、結局そういうことだ。