はじめに

複数のファイルの名前をあるルールで一括して変換する方法について、xargsfor + sedxargs + sedの順にシェルでの実現方法を書いていき、複雑度が増す場合にRubyでシンプルに実現する方法も書く。

動作確認 RedHat6.2

RedHat系はrenameは不向き

簡単なルールであればrenameコマンドを使用すればいい。

rename from to files
# files:ワイルドカード使用可能

しかし正規表現を使用しなければいけないようなルール、たとえば末尾に.csvをつけるといったようなことはできない。
ネットではsedのように 's/regex/regex/' 形式で指定できると書いてあったが、RedHat6.2に入っているrenameコマンドは対応していなかった。
Linuxには2つのrenameコマンドが存在し、正規表現が使えるのはperlで作られたDebian系のrenameの方であり、RedHat系はutil-linuxに含まれるrenameコマンドなので、複雑なルールには不向き。

find + xargs

findやlsの結果をxargsで受けて、一つずつ処理するのが汎用的に使える。

【要件】test_で始まる複数のファイル名の末尾に.csvをつける。
【方法】

find -name 'test_*' -printf '%f¥n' | xargs -I % mv % %.csv

【説明】find -printf '%f\n'でtest_で始まるファイルのファイル名部分だけを抜き出し、ファイル名を一つずつmvする。

さらにfor文とsedを合わせて柔軟な正規表現変換

for文とsedを合わせて使うと、今回のように末尾に文字を付けたり外したりするだけでなく、より柔軟な変換が可能になる。

【要件】_ を . に置き換えてファイル名を変換する。
【方法】

for org in `find . -name "*_*" -printf '%f\n'`
do
  replaced=`echo ${org} | sed -e 's/_/./g'`
  mv ${org} ${replaced}
done

for文を使わずsedだけでワンライナー実行

正規表現で柔軟に変換するにはfor文を合わせて使うと書いたが、sedだけでワンライナー実行もできる。※ただし少し複雑になる。

【要件】_ を . に置き換えてファイル名を変換する。
【方法】

$ ls -1 | xargs -I % echo -e '%\n%' | sed -r '2~2s/_/./g' | xargs -n 2 mv

【説明】コマンドを分解して説明する。

$ ls
test_1_1_1  test_1_2_1  test_1_2_2

#lsの結果を縦一列に並べるため、オプション -1 を指定する
#xargsに改行のみを区切りとみなす-Iオプションをつけるため
#xargs -n 1をxargs -Iの前に実行して改行を実現してもいいのだが
$ ls -1
test_1_1_1
test_1_2_1
test_1_2_2

#改行を挟んで同じ文字列を出力する。
$ ls -1 | xargs -I % echo -e '%\n%'
test_1_1_1
test_1_1_1
test_1_2_1
test_1_2_1
test_1_2_2
test_1_2_2

#sedの 2~2 で2行目以降2の倍数のみを変換する
#xargs -n 2で標準出力から2つ分ずつ引数を処理するようにする
$ ls -1 | xargs -I % echo -e '%\n%' | sed -r '2~2s/_/./g' | xargs -n 2 echo
test_1_1_1 test.1.1.1
test_1_2_1 test.1.2.1
test_1_2_2 test.1.2.2

#echoにより変換過程を確認したので、本当に行いたいmvに変えて実行する
$ ls -1 | xargs -I % echo -e '%\n%' | sed -r '2~2s/_/./g' | xargs -n 2 mv
$ ls
test.1.1.1  test.1.2.1  test.1.2.2

※sedの拡張正規表現-r-Eの場合もある。今回の例だと拡張でなくてもいいので-eでも可。
※sedの偶数行を取得する2~2s/_/./gn;s/_/./gでも可。

オプション 説明
n N 入力の次の行をパターンスペースに読み込む/追加する。
addr1,~N addr1 から、addr1 以降の、入力行番号が N の倍数の行までマッチする。
addr1,+N (参考) addr1 から、addr1 以降の N 行にマッチする。

シェルスクリプトではなくRubyで置換とrenameを行う

for文とsedを合わせて柔軟な正規表現変換を行うまでは十分に読みやすいシェルコマンドだったが、ワンライナーで実現しようとすると途端に複雑になってしまった。ワンライナーでも簡単な記述で実現するためにRubyを使う。
lsの結果をRubyに渡して置換とrenameを行う方法と、rename対象を選別するところから全てRubyで行う方法の二通りで書いてみる。

ls + Ruby

【要件】_ を . に置き換えてファイル名を変換する。
【方法】

$ ls | ruby -nle 'File.rename($_, $_.gsub(/_/, "."))'

# あるいは
$ ls | ruby -r 'fileutils' -nle 'FileUtils.mv($_, $_.gsub(/_/, "."))'

【説明】まずはlsの結果を渡す方法を過程も含めて詳しく見ていく。

$ ls
test_1_1_1    test_1_2_1    test_1_2_2
$ ls | ruby -ne 'p $_'
"test_1_1_1\n"
"test_1_2_1\n"
"test_1_2_2\n"

#改行が含まれてしまっているので、lオプションを入れるか、$_.chompをするかで改行を取る
#改行が含まれていることに気付かず以降のrename処理を行ってしまうと、改行をファイル名の最後にもつファイルが存在しないため、
#No such file or directoryというエラーが出る
$ ls | ruby -nle 'p $_'
"test_1_1_1"
"test_1_2_1"
"test_1_2_2"

#次のようにgsubで置換することでmvのfromとtoができる
$ ls | ruby -nle 'puts $_ + " " + $_.gsub(/_/, ".")'
test_1_1_1 test.1.1.1
test_1_2_1 test.1.2.1
test_1_2_2 test.1.2.2

#実際に置換してみる
$ ls | ruby -nle 'File.rename($_, $_.gsub(/_/, "."))'
$ ls
test.1.1.1    test.1.2.1    test.1.2.2

File#renameを使用してもいいがFileUtils#mvも使える。

$ ls | ruby -r 'fileutils' -nle 'FileUtils.mv($_, $_.gsub(/_/, "."))'

全てRubyで

次にrename対象を選別するところから全てRubyで行う方法も書いてみる。

【要件】_ を . に置き換えてファイル名を変換する。
【方法】

$ ruby -e 'Dir.glob("*").each{|f| File.rename(f, f.gsub(/_/, "."))}'

【説明】Dir.globを使ってファイルの一覧を取得している。

$ ruby -e 'p Dir.glob("*")'
["test_1_1_1", "test_1_2_1", "test_1_2_2"]

$ ruby -e 'Dir.glob("*").each{|f| File.rename(f, f.gsub(/_/, "."))}'

Rubyプログラムとしてはこちらの方が正統なのだろうが、ワンライナーとしてははじめの方が好み。

Rubyでのワンライナーのまとめ

最後にRubyワンライナーのまとめとして3つの方法を再掲する。

$ ls | ruby -nle 'File.rename($_, $_.gsub(/_/, "."))'
$ ls | ruby -r 'fileutils' -nle 'FileUtils.mv($_, $_.gsub(/_/, "."))'
$ ruby -e 'Dir.glob("*").each{|f| File.rename(f, f.gsub(/_/, "."))}'