一年前、ファイルをエディターで一括リネームするmmvコマンドを作りました。
mmv
はリネームの依存関係を解析して順番を決定します。例えば
a => b b => c
というリネームを愚直に上から実行すると、ファイルb
が消えてしまいます。
mmv
はリネーム先がリネームの対象となっている場合は、そちらのリネーム (上の例では b => c
) を先に実行します。
もし
a => b b => c c => a
のようにリネームが循環している場合は、一時的なパスに一旦退避してからリネームします。
a => tmp c => a b => c tmp => b
このツールを作ったときはこれで機能的には十分だと思っていましたが、去年の12月に面白いissueが立ちました (#11)。
mmv $(find .)
のようにディレクトリツリーを対象にするとうまく動かないというのです。
a => b a/x => b/x a/y => b/z a/z => c/z
mmv
はリネームの対象の中でパスが他のパスのディレクトリとなっているケースは想定していませんでした。
リネームを実行すると以下のような問題が起きてしまいます。
a/x => b/x
をa => b
より先に実行してしまうと、最初のリネームが先にb/
を作成してしまうのでa => b
が失敗し、a/
以下の他のファイルは移動されないa => b
をa/z => c/z
より先に実行してしまうと、a/z
はすでにb/z
に移動しているので対象ファイルが見つからない
この問題を1か月ほど悩んだ末に、先日ようやく実装することができました。
例えば以下のようなリネームを行う場合は
a => b a/x => b/x a/y => b/z a/z => c/z
mmv
は以下の順番でリネームを行います。
a/y => tmp1 a/z => tmp2 a => b tmp1 => b/z tmp2 => c/z
ディレクトリ a
のリネームが行われるので、その中のファイルを一旦退避しています。
ただし a/x => b/x
は a => b
のリネームに任せることで、リネームの数を抑えています。
このように書いてしまえばシンプルな処理に見えますね。
しかし、mmv
は親ディレクトリの解析と、先述の依存関係の解析を同時に行います。
また、愚直に実装してしまうと O(n²) になってしまうので、先にディレクトリの深さでパスをグルーピングする実装を行いました。
色々なケースを想像していると重い腰が上がらず、1か月もかかってしまいました。
このツールを作ったときは機能を増やさない信念と称してツールの機能が膨れ上がらないようにと思っていました。
実際、git mv
したいとか -dry-run
したいとか言う要望は、mmv
の信念にはそぐわないので却下しています *1。
しかし親ディレクトリの解析はmmv
の中で実装するのに適切な機能であり、また面白そうなチャレンジだったので実装しました。
昨日リリースしたv0.1.3よりこの機能をお使いいただけます。 是非お使いください。 現場からは以上です。