ファイルを一括でリネームしたいことはありませんか。私はあります。ということで作りました。
インストールはHomebrew
brew install itchyny/tap/mmv
または以下のコマンドでできます。
go get github.com/itchyny/mmv/cmd/mmv
スクリーンショットではvimが起動していますが、 $EDITOR
が設定されていればそれを使って編集することができます。
エディターでファイル名を編集して一括でリネームするというのは、新しい発想ではありません。 実際、多くのソフトウェア (特にファイラー) がこの機能を実装しています。
私はvimfilerの一括リネーム機能をよく使っていました。 特に不満はないのですが、ファイラーの付属機能である必要はないと考えて、またVimユーザーに限らず広く使ってもらえるように、独立したコマンドとして実装し直すことにしました。 一括リネームを実装するにはどういう技術的挑戦があるのか調べたかったという気持ちもありました。
実装
一括リネームと言ってもそんなシステムコールは当然ありませんから、順番にリネームしていくことになります。 簡単な一括リネームを実装すると次のようになります。
func Rename(files map[string]string) error { for src, dst := range files { if err := os.Rename(src, dst); err != nil { return err } } return nil }
以上。終わりです。
巡回・スライシング
これで終わったらなんの挑戦もありませんが、厄介なケースがいくつかあります。 まずは巡回しているケースです。
a => b b => c c => a
そして、ずれていくケースです。
a => b b => c c => d
実際のユースケースとしてはあまりなさそうではあるものの、これらを全く考慮せずに実装してしまうと、ファイルを失う事故が起きてしまいます。 事故が起きないようにするには
- 検知してエラーを吐く
- リネームできるようにする
という2つの戦略が考えられます。
頭の中でツールの設計をしているときにこれらのケースをどう扱うかしばらく考えていたのですが、検知できるならリネームする順番もわかるだろうと考えて、最初からリネームできるように実装しました。
それではどのようにリネームすればいいのでしょうか。 まず巡回するケースは、一時的なファイル名を用意すれば、次の順にリネームすることができます。
a => tmp c => a b => c tmp => b
ずれていくケースは、ずれの最後から逆順にリネームすればokです。
c => d b => c a => b
このようにすれば、リストにあるファイルを失うことなく、リネームを行うことができます (前提として、同じファイル名へのリネームと、同じファイル名からのリネームは排除しているとします)。
これは明らかにグラフの問題になっています。 巡回を判定するには辿ったノードをマークしていけばいいわけですし、ずれの最後から辿るというのはグラフの葉ノード (leaf node) から矢印を逆順に辿ることを意味しています。 グラフの分岐、つまり同じファイル名(への|からの)リネームを排除しているので、そんなに難しい処理ではありません。
いずれにしても、実際のユースケースではあまり起きないケースだろうなということは分かっています。 しかしファイル名を編集してリネームするUIでユーザーが行を入れ替えたときは、おそらくファイルの入れ替えを期待するだろうと考えて実装してみました。 一括リネームツールを作ろうとしていたらいつの間にかグラフの問題を解いていたというのは面白いですね。
機能を増やさない信念
このツールは、一括リネームを行うただそれだけのツールです。 エディターを立ち上げて、ファイル名の変更を読み取って、リネームしていくだけです。 ただリネーム先のディレクトリがなければ作るという挙動があり、これは以下のように画像を整理するときに便利だと思います。
やりたかったことは既に実装できています。 このツールに関しては、他の複雑なことは実装しないと思います。 例えばファイルを削除したり、リネームの記録をどこかに保存してundoしたりといったことです。 これらはあまりに複雑で、あまりメンテナンスしたくありません *1。 そしてこれらの機能はmassrenが既に実装してますので、そういうものが欲しい人はmassrenに誘導すればいいかなと思っています。
機能を実装しないというのは、重要な設計指針の一つです。
ソフトウェアは機能が多くなるほど、ユーザーが学ばなければいけないことは増えていきます。 一括リネームなんてそもそもめったにやらないオペレーションなのに、そのツールにいろいろな機能があったとして使いこなせるでしょうか?
何かを作りはじめるときに大事なことは、全部入りを目指すのか、シンプルな独立ツールを目指すのかを決めるということです。 全部入りが悪いと言いたいわけではありません。 一貫したインターフェイスで様々なものを操作できる基盤ツールは素敵です。 素敵ですが、そういうソフトウェアを作るときはより慎重に設計しましょう。 達成したい目標が高いほど、設計の失敗が命取りとなることがあります。
シンプルな独立ツールを目指すのであれば、まずはできる限り機能を削ってみてください。 設定もユーザーとのインタラクションもできるだけ排除しましょう *2。 本当にやりたいことしかできない状態になっても、90%、いえ95%くらいの人はそれで十分でしょう。 ほとんど使われないような機能を足すことは、多くの人にとって難しい印象をもたせてしまいます。 機能が多く難しいという印象をもったツールは使わなくなってしまいます *3。
- 作者:Mike Gancarz
- 発売日: 2001/02/01
- メディア: 単行本
ソフトウェアをメンテナンスする上で、複雑にならないように保つというのはとてもむずかしいことです。 著名なエンジニアから機能要望やpull requestが来て悩むこともあれば、取り込まないと書いたコメントがdown voteされることもあります。 しかし、機能を取り込んだ後にそれをメンテナンスするのは (多くの場合) あなた自身です。 安易に取り入れた機能に対して芋づる式に要望が増えたり、他の機能と干渉してバグを生んだりして、手に負えなくなることもあるでしょう *4。 理にかなっている機能要望かどうかを設計指針と照らし合わせて判断する力、そして時には機能を引く判断、これがOSSを長くメンテナンスしていくための生存戦略につながるのだと考えています。
まとめ
ファイルを一括でリネームするツールを作りました。
- ファイル名の一覧をエディターで編集し、その結果をもとにファイルを移動する
- 移動先のディレクトリが存在しなければ作成してからファイルを移動する
私にとっては既に十分であり、常用するツールになると思います。 そして今のこの気持ちを保ち続けることができたら、一年経ってもこのツールの機能をすべて思い出し、使いこなすことができるでしょう *5。
最後になりましたが、様々なフィードバックをしていただいたvim-jpのみなさま、ありがとうございました。
*1:ファイルを削除するだけなのにそんなに複雑なのかと思われるかもしれません。ファイルの削除という危険な操作は、ユーザーによって様々な設定がされている場合があります。例えば単なる削除ではなくて~/.Trash/への移動にするとか、確認が欲しいとか、そういう話です。こういうものを実装し始めると、それは一括リネームツールの範疇を超えてしまいます。挙動をオンオフできるようにしたり、移動先を指定できるようにしたりといった要望が容易に想像つきます。
*2:逆に、すべてを設定できるようにするという発想もあります。茨の道ですが、そちらのほうが適切なケースもあるでしょう。しかし、設定同士が干渉したときの処理が複雑になり、メンテできなくなることが多い気がします。プログラミング言語のソースコードのフォーマッターは両極端で面白いです。
*3:標準のコマンドに関しては、機能が多くて難しくても使わざるを得ないのが現実でしょう。lsやpsのオプションを使いこなしている人はどれくらいいるのでしょうか。使いこなせなくても使うのは、どんな環境でも入っていて換えが効かないからでしょう。こういう分野で代替ツールがユーザー数を伸ばすのは至難の業です (batやexaはすごい)。
*4:それはあなたの能力が低いからだと言われるかもしれませんが、その通りなのです。会社に勤めていると趣味OSSに割ける時間は限られていますし、睡眠は重要です (このブログを書くために多少睡眠を削っているわけですが…)。バグ報告や機能追加要望が多いと徐々にさばけなくなり、メンテ疲れしてしまいます。うまくいけば報酬を得てより多くの時間を割くとか、他の人にメンテナンスを委譲するとかできるかもしれませんが、多くの場合はうまく行かないので、自分が使える時間と実装能力、機能の需要やソフトウェアの品質などのバランスを取りながら、メンテナンスしていくことになります。自分に手に負えなくなりそうな機能であれば、要望を弾くのも大事です。時には他の代替ツールに引導を渡す判断が重要になる場面もあるかもしれません。
*5:3日前に誕生したばかりのツールです。ユーザーが増えて色々と言われるうちに気が変わって機能追加を始めるかもしれません。