batch command to rename a fileをMacで作る|保存して繰り返す形にする
「batch command to rename a file」という言い方には出どころがあります。Windowsでは、命令を並べたテキストファイルを .bat という拡張子で保存し、ダブルクリックで走らせる。その中で名前を変える行が ren です。その組み立てのままMacに来ると、最初の1分で2つの壁に当たります。その命令が入っていないことと、ワイルドカードの扱いが違うことです。どちらにも置き換えがあります。そして本当に作る価値があるのは、その場で打つ1行ではなく、バッチファイルがそうだったように、来月も同じ作業を構文を思い出さずに実行できる保存されたファイルのほうです。
探している命令は、そもそも入っていない
ターミナルで which ren と打っても何も返りません。rename も同じで、素のmacOSには入っていません。Linux向けの記事を先に読んだ人がここで止まります。ren はWindowsのコマンドインタプリタが内部に持っている命令で、UNIX側に対応するものがありません。rename は多くのLinuxディストリビューションに付いてくるPerl製のスクリプトで、macOSには同梱されていません。
Macにあるのは mv です。ファイルを移動する命令で、移動先を同じフォルダにしたときの見え方が「名前の変更」になります。
mv report-draft.pdf report-final.pdf
1件だけならこれで終わりです。名前を変える専用の動詞は存在しませんし、最初からありませんでした。UNIX系のファイルシステムでは、名前はファイルの属性ではなくディレクトリの中の項目だからです。名前を変えることと、別のフォルダへ移すことは、内部では同じ操作になります。だから1つの命令で両方を兼ね、フォルダをまたぎながら同時に名前も変える処理が追加の手間なしで書けます。
Windowsのワイルドカードは、そのままでは通らない
Windowsでは ren *.txt *.md が通ります。命令が2つのパターンをそのまま受け取り、自分で照合するからです。同じ行をMacで打つと、mv に届く前に止まります。
mv *.txt *.md
zsh: no matches found: *.md
違いは、アスタリスクを誰が展開するかです。Macではコマンドが動く前にシェルがパターンを展開し、一致するファイル名の一覧に置き換えます。1つ目のパターンは既存のテキストファイルの一覧になります。2つ目は、まだ .md のファイルが無いので何にも一致せず、シェルはアスタリスクをそのまま渡すのではなくエラーで止まります。
仮に .md のファイルが偶然いくつか存在していた場合のほうが危険です。エラーにならず、mv は平らに並んだ名前の一覧を受け取り、全部を最後の1件の中へ移そうとします。
この一点で、バッチファイルの移植が失敗する理由のほとんどが説明できます。Macではパターンは引数ではありません。引数に展開されるものであって、名前の変更の「変更後」の側にパターンを置くことはできません。ファイル1件ごとに新しい名前を計算する何かが必要で、それがループです。
1行をループに置き換える
for f in *.txt; do
mv -- "$f" "${f%.txt}.md"
done
短い3行ですが、それぞれが仕事をしています。${f%.txt} は変数の末尾から拡張子を落とす書き方で、外部のツールではなくシェルの機能です。"$f" の引用符は、空白を含むファイル名が複数の引数に割れるのを防ぎます。Macでは空白入りの名前が例外ではなく普通なので、これは省略できません。-- は「ここから先はファイル名」と mv に伝える印で、ハイフンで始まる名前をオプションと誤解させないためのものです。
引用符を省くのが、ファイルを失う最も多い経路です。Q3 report.txt を引用符なしで渡すと、mv には2つの別々の引数として届き、エラーになるか、意図しない場所へ移動します。
拡張子を落とすより複雑なことをしたいときは、zshに専用の道具が入っています。最初から入っていて、読み込まれていないだけです。
autoload -U zmv
zmv -n '(*)-draft.pdf' '$1-final.pdf'
丸括弧が元の名前の一部を捕まえ、新しい名前の側で $1 $2 として使えます。-n を付けると、何も動かさずに実行予定だけを出力します。
コマンドをファイルにして、来月も使えるようにする
ここがバッチファイルの発想がそのまま活きる場所です。命令をファイルに保存して再利用する習慣は正しく、macOSはそれを直接支えています。
シェルスクリプトは、1つ以上のUNIXコマンドを含むテキストファイルです。コマンドラインで入力できるコマンドを、シェルスクリプトを実行することで実行します。 出典: support.apple.com
必要なのは、命令を書いたファイルと、1行目のシェルの指定と、実行の許可だけです。
#!/bin/zsh
#使い方: retitle.sh <変更前の拡張子> <変更後の拡張子>
for f in *."$1"; do
[ -e "$f" ] || continue
mv -n -- "$f" "${f%.$1}.$2"
done
retitle.sh として保存し、chmod 755 retitle.sh を実行すれば動く状態になります。この手順はAppleの説明にもそのまま載っています。以降は、作業したいフォルダで ./retitle.sh txt md と打つだけで済み、構文を思い出す必要がなくなります。[ -e "$f" ] || continue の行は、一致するファイルが1件も無かったときの保険です。これが無いと、パターンそのものをファイル名として1回ループが回り、意味の分かりにくいエラーが出ます。
ダブルクリックで走る形にする
Windowsの .bat はダブルクリックで走ります。macOSにも同じ仕組みが、別の拡張子で用意されています。.command という拡張子を付けて実行可能にしたスクリプトは、Finderから開くとターミナルが立ち上がって実行されます。
これは慣習ではなく登録です。filename.command のファイル種別を調べると com.apple.terminal.shell-script という識別子が返り、ターミナル自身がその種別を「Shell」という役割で自分のものだと宣言しています。
使う前に知っておきたい点が2つあります。
- 実行時のカレントディレクトリは、スクリプトが置かれているフォルダではありません。同じフォルダのファイルを対象にしたいなら、最初の行に
cd "$(dirname "$0")"を入れる必要があります - ダウンロードやメールで受け取った
.commandには隔離の印が付き、最初に開くときに警告が出ます。自分の手元で書いたものと、人に渡したものとで挙動が変わります
フォルダ全体ではなく、Finderで選択したファイルだけを対象にしたい場合は、ショートカットアプリの「シェルスクリプトを実行」を使い、クイックアクションとして保存する経路があります。この場合は拡張子の話が丸ごと不要になります。
人が走らせる形にするときに足す4行
その場で打つコマンドには、書いた本人しか知らない前提が乗っています。どのフォルダに立っていたか、そのパターンで何を指すつもりだったか、出力がおかしければ気づけたはずだ、という前提です。ファイルに保存した瞬間にその前提は消えます。うっかり実行できるようになった時点で、同じ1行の危険度は変わります。
#!/bin/zsh
set -euo pipefail
cd "$(dirname "$0")" || exit 1
[ "$#" -eq 2 ] || { echo "使い方: $0 <変更前の拡張子> <変更後の拡張子>"; exit 1 }
set -e は最初に失敗した時点で止めます。set -u は未定義の変数をエラーにします。打ち間違えた変数名が空文字に展開されて、パスがディスクのルートまで縮む事故を防ぐのはこの指定です。cd の行はスクリプトを自分のフォルダに縛り、開いていた窓のカレントディレクトリで動いてしまうのを防ぎます。最後の行は、引数が足りないときにそもそも走らせません。
バッチファイルの習慣で1つ引き継ぐ価値があるのは、冒頭のコメントです。名前だけ並んだファイル群は半年後に読めません。シェルスクリプトでは行頭に番号記号を置いた行がコメントになります。使い方を1行書いておくだけで、ループを読み解く手間が消えます。
手元のバッチファイルをそのまま持ってきた場合
Windowsで書いた .bat の中身をコピーして、Macで保存し直しても動きません。命令が違うのは分かっていても、見えないところでもう1つ引っかかります。改行コードです。
Windowsのテキストファイルは行末に復帰と改行の2文字を置きます。Macを含むUNIX系は改行の1文字だけです。この状態のファイルを実行可能にして走らせると、次のように止まります。
./crlf.sh
zsh: bad interpreter: /bin/zsh^M: no such file or directory
1行目のシェルの指定の末尾に余分な文字が付いたまま解釈されるため、そんなシェルは無いと言われます。ファイルの見た目は何ともないので、原因にたどり着くまでに時間を取られやすい種類の失敗です。file スクリプト名 を実行して「CRLF line terminators」と出れば確定で、テキストエディタ側の改行コードの設定をLFに変えて保存し直せば通ります。
書き換えの対応も、覚えるべきものは多くありません。
| バッチファイル | シェルスクリプト |
|---|---|
| 1行目の @echo off | 1行目に #!/bin/zsh |
| %1 %2 の引数 | $1 $2 |
| FOR %%f IN (*.txt) DO | for f in *.txt; do |
| REM のコメント | 行頭の番号記号 |
| ren 変更前 変更後 | mv 変更前 変更後 |
消える前に確かめる3つの手順
ターミナルに取り消しはありません。上書きで消えたファイルはゴミ箱にも入りません。次の3つでほとんどの危険は消えます。
| 手順 | やり方 | 防げること |
|---|---|---|
| 空打ち | mv の前に echo を置く | パターン違い、対象の取り違え |
| 上書きの拒否 | mv -n を使う | 2件が同じ名前に着地する事故 |
| 変更前の控え | ls を先にファイルへ書き出す | 元に戻す必要が出たとき |
2つ目には注意点があります。macOSの mv -n は既存の宛先を上書きしませんが、失敗したことも報告しません。すでに存在する名前に対して実行すると、何も動かさずに成功として返ります。スクリプト側は終了状態から判断できないので、ループの半分が黙って飛ばされたまま正常終了することがあります。実行の前後でファイル数を数えるのが、現実的な確認方法です。
置き場所を決めると、書き直さなくなる
書いたフォルダにそのまま置いたスクリプトは行方不明になります。置き場所は2つに決まっています。自分だけが使うものはホームフォルダの中に bin を作り、PATH に足す。全アカウントで使うものは /usr/local/bin に置く。後者は保護されたディレクトリの中で唯一書き込める場所で、macOSのアップデートでも消えません。
PATH に載った時点で、呼び出しはパスではなく単語になります。人が実際に使い始めるのはこの段階からで、それまでは毎回ループを書き直すことになります。
残っているのは構文ではなく、窓の往復
ここまでで構文の問題は片づきます。残るのは作業の形のほうです。片方の窓でフォルダを確かめ、もう片方でコマンドを打ち、結果を見るためにまた戻る。これを1件の名前変更のたびに繰り返しています。空打ちで結果を確かめる手順を足すと、この往復はさらに1回増えます。1回の切り替えは数秒でも、確認を丁寧にするほど回数が増える構造になっている点が問題です。
フォルダとターミナルとAIが同じ窓にある形にすると、空打ちの出力を読みながら同じ画面でフォルダの中身を確かめられるため、確認を増やしても往復は増えません。何ができるようになるかはできることに整理してあり、同じ用途の他の道具との違いは他のファイル管理との比較で条件ごとに並べています。作業の続きを手元を離れてから確かめたい場合はiPhone・iPadから続きをも参考になります。
よくある質問
Macに rename コマンドが入っていないのはなぜですか?
ren はWindowsのコマンドインタプリタ専用の命令で、Linuxの記事によく出てくる rename はPerl製のスクリプトです。どちらもmacOSには同梱されていません。名前の変更は mv で行います。UNIX系では名前がディレクトリの項目なので、移動と名前の変更が同じ操作になるためです。
ren \*.txt \*.md と同じことをMacでやるにはどう書きますか?
mv *.txt *.md はエラーになります。Macではコマンドが動く前にシェルが両方のパターンを展開し、まだ存在しない .md に一致するものが無いためです。for でファイルを1件ずつ回し、${f%.txt}.md のように新しい名前をその都度作る書き方に置き換えます。
スクリプトをダブルクリックで実行できるようにするには?
拡張子を .command にして、chmod 755 で実行可能にします。macOSはこの拡張子をターミナル用のシェルスクリプトとして登録しているので、Finderから開くとターミナルが起動して実行されます。置いてあるフォルダを対象にしたい場合は、先頭に cd "$(dirname "$0")" を足してください。
mv -n を付けておけば上書きの心配はありませんか?
上書きは防げますが、飛ばしたことを知らせてくれません。macOSでは宛先がすでに存在する場合、何も動かさずに成功として返るため、スクリプト側は終了状態から気づけません。実行の前後でファイル数を比べるか、先に echo を付けて空打ちし、名前がぶつかる組を確認してください。