OS 問わず Bash で一括リネームする

Bash でファイル名を一括でリネームしようと思い、調べ始めたらなかなか混乱していたので、整理する。

目次

Windows Git SDK、Ubuntu Linux、MacOS で同じコマンドを使いたい

ファイルの一括リネームを実現するにあたって、OS の差異があるのは鬱陶しいので、どの環境でも同じコマンドでリネームできる方法を探すことにする。

Windows 環境では、自分は Git For Windows (GitBash) の上位互換である Git SDK を使っているのでその環境を対象にし、また、WSL Ubuntu でも同様に動くようにしたい。

同じく MacOS でも同じコマンドが動くようにしたい。GNU 系のコマンドを別途 Homebrew でインストールして利用する前提とする。

rename コマンドは OS 間で全く違うので使わない

「ファイルの一括リネーム」を実現する、rename というドンズバなコマンドが存在する。しかしこのコマンド、よくある名前なので混沌としていて、OS (導入環境) によって全く別のコマンドがインストールされる。以下、--help オプションの出力内容を比較してみよう。

$ rename --help

使い方:
 rename [options] <expression> <replacement> <file>...

ファイル名を変更します。

オプション:
 -v, --verbose       explain what is being done
 -s, --symlink       act on the target of symlinks
 -n, --no-act        do not make any changes
 -o, --no-overwrite  don't overwrite existing files
 -i, --interactive   prompt before overwrite

 -h, --help          このヘルプを表示します
 -V, --version       バージョンを表示します

詳しくは rename(1) をお読みください。

$ rename --version
rename from util-linux 2.35.2
$ apt install rename

$ rename --help
Usage:
    rename [ -h|-m|-V ] [ -v ] [ -0 ] [ -n ] [ -f ] [ -d ]
    [ -e|-E perlexpr]*|perlexpr [ files ]

$ rename -V
/usr/bin/rename using File::Rename version 1.10
$ brew install rename

$ rename --help
Usage:
    rename [switches|transforms] [files]

    Switches:

    --man (read the full manual)
    -0/--null (when reading from STDIN)
    -f/--force or -i/--interactive (proceed or prompt when overwriting)
    -g/--glob (expand "*" etc. in filenames, useful in Windows™ CMD.EXE)
    -k/--backwards/--reverse-order
    -l/--symlink or -L/--hardlink
    -M/--use=Module
    -n/--just-print/--dry-run
    -N/--counter-format
    -p/--mkpath/--make-dirs
    --stdin/--no-stdin
    -t/--sort-time
    -T/--transcode=encoding
    -v/--verbose

    Transforms, applied sequentially:

    -a/--append=str
    -A/--prepend=str
    -c/--lower-case
    -C/--upper-case
    -d/--delete=str
    -D/--delete-all=str
    -e/--expr=code
    -P/--pipe=cmd
    -s/--subst from to
    -S/--subst-all from to
    -x/--remove-extension
    -X/--keep-extension
    -z/--sanitize
    --camelcase --urlesc --nows --rews --noctrl --nometa --trim (see manual)

# バージョン表示のオプションはないようなので man を抜粋する
$ rename --man
NAME
    rename - renames multiple files

VERSION
    version 1.601

…それぞれ全くオプションが違うので、各環境で使い方を覚えないといけない。Mac の rename なんかはまぁまぁ使いやすいし、find -exec rename といった組み合わせ方もあるのだが、いずれも他の環境で使えないので今回はコレ以上深堀りしない。

findsedxargs mv でリネームする

そこで調べ直すと、

  1. find でリネーム対象を絞り込み、
  2. sed でリネーム後のファイル名を組み立て、
  3. 最後に xargs mv で実際にリネームする

という方法が、どの環境でも同じように処理できそうだと思った。

なお、MacOS の場合は findsed ともに BSD 版であるため、細かなオプションの差異を吸収するために GNU 版を入れておくと良い。

$ brew install findutils gnu-sed
$ alias find='gfind'
$ alias sed='gsed'

find で処理対象を絞り込む

まずは find コマンドによる絞り込みの方法を押さえておく。

# カレントディレクトリ直下のファイルを全取得する
$ find . -type f -maxdepth 1

-maxdepth オプションは、BSD 版だと -depth-d オプションとも書けるが、GNU 版は -maxdepth と書かないと有効にならない。

コレをベースに、-name オプションなんかを使って絞り込んでやると良いだろう。

# 前方一致
$ find . -type f -maxdepth 1 -name 'prefix*'
# 部分一致
$ find . -type f -maxdepth 1 -name '*file-name*'
# 後方一致
$ find . -type f -maxdepth 1 -name '*-suffix.txt'

…こんな感じで、アスタリスク * を使うと絞り込みやすい。

$ find . -type f -maxdepth 1 -name 'prefix*'
./prefix-001.jpg
./prefix-002.jpg

こんな風に、リネーム対象とするファイル名が列挙できた。

sed でリネーム後の文字列を作る

続いて、リネーム後のファイル名文字列を sed で作る。p スクリプトコマンドを使うと、「処理前・(改行)・処理後」の順で出力できるので、コレを使う。

$ find . -type f -maxdepth 1 -name 'prefix-*' | sed 'p;s/prefix-/new-/'
./prefix-001.jpg
./new-001.jpg
./prefix-002.jpg
./new-002.jpg

正規表現を使いたい場合は -r オプションを使う。普通の正規表現と違って、キャプチャした文字列を参照する場合は $1$2 (ドルマーク) ではなく \1\2 とバックスラッシュで書く。

# キャプチャを使って、ハイフン部分だけアンダースコアに変更する
$ find . -type f -maxdepth 1 -name 'prefix-*' | sed -r 'p;s/(prefix)-([0-9]*).jpg/\1__\2.jpeg/'
./prefix-001.jpg
./prefix__001.jpeg
./prefix-002.jpg
./prefix__002.jpeg

大文字・小文字変換は \L\U で行える。このオプションは BSD sed では使えないので、GNU 版 gsed を使おう。

# 小文字にする
$ find . -type f -maxdepth 1 -name 'prefix-*' | sed 'p;s/\(.*\)/\L\1/'

# 大文字にする
$ find . -type f -maxdepth 1 -name 'prefix-*' | sed 'p;s/\(.*\)/\U\1/'
./prefix-001.jpg
./PREFIX-001.JPG
./prefix-002.jpg
./PREFIX-002.JPG

ココまでで、リネームしたいファイル名とリネーム後の文字列が交互に列挙されることになる。ココまでで止めておくと、Dry-Run 的に置換結果を確認だけできる。

xargs mv で実際に置換する

最後に、xargs -n2 mv を繋げれば一括リネームが実行できる。

# 全ファイルの「BEFORE」部分を「AFTER」に置換する
$ find . -type f -maxdepth 1 | sed 'p;s/BEFORE/AFTER/' | xargs -n2 mv

# 一部ファイルを絞り込み正規表現を使って置換する
$ find . -type f -maxdepth 1 -name 'prefix-*' | sed -r 'p;s/(prefix)-([0-9]*).jpg/\1__\2.jpeg/' | xargs -n2 mv

こんな感じ。

以上

findsedxargs mv の組み合わせで、イイカンジに対象ファイルを絞り込み、置換文字列を組み立て、実行できた。3つのコマンドの基本的なオプションしか使っておらず、ほぼ定型的に実装できるので、オプションを沢山覚えて使いこなす必要がなくシンプルだ。Dry-Run したくなった時も、オプションだなんだと考えずに xargs mv を外すだけで良いので、分かりやすいであろう。