sedでHTML内の特定の拡張子のファイルだけパスを置換するのに苦戦した


やりたかったこと

こんな感じのファイルのjsのパスだけフォルダ名をsedコマンドで一括置換したい。
ファイル内のjsのパスは全て変えたいが、他の拡張子のパスは変えたくない。

target.html
<!-- "./js/hoge.js"みたいな感じにしたい -->
<script src="./アセット/hoge.js" /><link rel="./アセット/hoge.css" /><script src="./アセット/hoge.min.js" />
<link rel="./アセット/hoge.css" />
<img src="./アセット/hoge.png" />
<img src="./アセット/hoge.svg" />
<img src="./アセット/hoge.jpeg" />
<img src="./アセット/hoge.jpg" />

結論

下記の正規表現でできました。めでたしめでたし。

sed -i "" "s/アセット\(\/[^\"]*\.js\"\)/js\1/g" target.html

実行結果はこうなる。jsファイルへのパスだけフォルダ名がjsになっている。

target.html
<script src="./js/hoge.js" /><link rel="./アセット/hoge.css" /><script src="./js/hoge.min.js" />
<link rel="./アセット/hoge.css" />
<img src="./アセット/hoge.png" />
<img src="./アセット/hoge.svg" />
<img src="./アセット/hoge.jpeg" />
<img src="./アセット/hoge.jpg" />

苦戦したところ

結論を書いてしまうと非常にあっさりなのだが実はかなり苦戦した。
1行目のような1行に複数回jsのパスが書いてあるケースを考えると複雑さが増す。

最長一致ではダメ

最初にパッと思いついた正規表現は以下のようになった。

sed -i "" "s/アセット\(.*\.js\)/js/g" target.html

実行結果はこうなる。1行目にある2つ目のjsファイルhoge.min.jsのフォルダ名が置換できていない。

index.html
<script src="./js/hoge.js" /><link rel="./アセット/hoge.css" /><script src="./アセット/hoge.min.js" />
<link rel="./アセット/hoge.css" />
<img src="./アセット/hoge.png" />
<img src="./アセット/hoge.svg" />
<img src="./アセット/hoge.jpeg" />
<img src="./アセット/hoge.jpg" />

これはhoge.min.jsが正規表現にヒットしなかったわけではなく、最初のhoge.jsが最長一致によって過剰にヒットしたためにこうなっている。
上記の正規表現の括弧の内側は「任意の文字の連続 + .js」を検索しているため、1行目は下記の範囲でヒットしている。

hoge.js" /><link rel="./アセット/hoge.css" /><script src="./アセット/hoge.min.js

2つ目のjsファイルまでヒットしてしまったため、1つ目のファイルのフォルダ名だけが置換されてしまっている。

最短一致ならどうか?

最長一致で長すぎるのであれば最短一致ならどうだろうか?

/アセット\/.*?\.js/g

これならば最初に.jsが出てきた時点で一致が止まる。1行目の2つ目のjsが含まれてしまう心配はない。
この正規表現の一致は以下のようになる。

アセット/hoge.js
アセット/hoge.css" /><script src="./アセット/hoge.min.js

確かに1つ目のjsファイルは期待通りの結果になったが、2つ目の一致が大きすぎる
js以外のファイルが挟まっているとそのフォルダ名がマッチしてしまい、そこから.jsまでが一致してしまう。
さらに言うとsedコマンドは最短一致(?記号)をサポートしていないので、そもそもこの正規表現がコマンドで実行できない。

ちなみに、仮にsedで実行できたとしたらHTMLはこうなっていただろう。やはり間違いである。

target.html
<!-- cssのディレクトリがjsになってしまう -->
<script src="./js/hoge.js" /><link rel="./js/hoge.css" /><script src="./アセット/hoge.min.js" />
<link rel="./アセット/hoge.css" />
<img src="./アセット/hoge.png" />
<img src="./アセット/hoge.svg" />
<img src="./アセット/hoge.jpeg" />
<img src="./アセット/hoge.jpg" />

"が出現した時に一致を止める

やりたいことを言い換えれば下記の単位で置換すべきかを確認したいのだ。

"./アセット/hoge.js"
"./アセット/hoge.css"
"./アセット/hoge.min.js"

ダブルクォーテーションの閉じが出現した時に一致を止められれば過剰な長さの一致を防げる
しかし、正規表現に「一致を止める」などという表現は私の知る限りでは存在しない。

なので、言い方を変える。"の内側から始まり「"以外の文字の連続」を検索する正規表現を書けばいい。
「x以外の文字」は文字クラスの先頭に^を書けばいい。[]で囲って先頭に^を書く。

/アセット\/[^\"]*\.js/g

これで「"以外の文字列の連続が.jsで終わる」を表現できた。
あとはこれをsedコマンドに組み込んで、ファイル名を後方参照しつつフォルダ名の部分だけ置換するように書き換えればいい。

sed -i "" "s/アセット\(\/[^\"]*\.js\"\)/js\1/g" target.html

また一歩、正規表現脳を強化できたと思った。