Linuxでfind, sedを使いながら文字列を再利用してファイル名の途中に文字列を挿入する方法

最近、以下のようなことをやる必要がありました。

ファイル名が

201105_ID.jpg
20120608_ID.jpg
20130704_ID.jpg
201409_ID.jpg

となっているファイル(年月が必ずあり、日が時についている)に対して、
年の後にアンダースコアを入れたい、つまり、

2011_05_ID.jpg
2012_0608_ID.jpg
2013_0704_ID.jpg
2014_09_ID.jpg

としたいわけです。

もし、最初の4文字が全く同一の文字列ならばただ置換すればよいので、全く問題ありません。
たとえば、すべて最初が2011からはじまるのであれば、
renameを使って、

$ find . -name '2011*' | rename 's/^2011/2011_/'
$

とするだけです。

しかし、今回は、4文字ということでは規則性がありますが、それぞれの中はバラバラの値です。

(ブログのコメントを受けて、ここを修正します。ちょうど2015年1月の正月に正規表現の勉強をしていたので、非常にタイミングのよいコメントをいただきました。松森様、どうもありがとうございます)

ただ、規則性はあります。最初は数字4文字、で、その後にアンダースコアを入れて残りの数字をいれたらいいわけです。

正規表現で、数字は\dで表すことができます。そして、繰り返し回数は{n}であらわせます。
今の場合、数字4桁は、\d{4}となるわけですね。

その後の数字は、\d+ とあらわすことができます。+は正規表現で1回かそれ以上ですから。

そして、最初の数字4桁のグループと残りの数字をそれぞれ()で2つのグループにわけます。

ファイルの最初は^であらわせますから、ファイルの最初の数字は以下のように表現できます。

(^\d{4})(\d+)

そして、前半のグループは$1、後半のグループは$2で表現できます。
今、グループ1とグループ2をアンダースコアで接続したもので置換したいので、
renameを使うと以下のように表現できます。

$ rename 's/(^\d{4})(\d+)/$1_$2/' 20*ID.jpg
$

これで一気に置換できます。正規表現でのグルーピングは慣れるととても強力ですね。
このようにネット上で教えてもらえるのはとてもありがたいことです。
完全に独学ですので…。

(以降はオリジナルの記事です)

いろいろ調べる中で、いくつかポイントが見つかりました。

  • findのオプション、-execを組み合わせる
  • findで合致したファイル名を再利用するには、{}を使う
  • sedにおいて検索で使った文字列を再利用するには、&を使う
  • bashで文字列を引数にとるためには、-cオプションを使う
  • find -execオプションは必ず;で終わらないといけないが、bashではエスケープシーケンスが必要になるために、必ず\;となる。

今回うまくいったワンライナーをご紹介します。(といっても上のワンライナーの方がずっとエレガントです。ただ、自分の学習過程を記録しておくことも意味があるので、残しておきます)

$ find . -name '201*' -exec bash -c "mv {} \`echo {} | sed -e 's/201./&_/'\`" \;
$

これを(あとで自分が忘れないために)もう少し解説します。

find . -name '201*'

この部分は、findの基本的な使い方です。
find <検索したい場所> -name <ファイル名> です。
ワイルドカードを使いたい場合は、シングルクオートかダブルクオートでくくらなければいけません。
*の場合は、どちらでも大丈夫です。

-exec bash -c

findのオプションで-execを使うと、コマンドを実行することができます。

今は、複雑なことをやりたいので、シェルを呼び出します。そのために、bashとしています。
man bashとすると以下のように書かれています。

オプション
bash は以下のオプションを起動時に解釈します (組み込みコマンド set の説
明で述べられている 1 文字のシェルオプションも使えます):

-c string -c オプションが指定されると、コマンドが string から読み込まれ
ます。 string の後に引き数があれば、これらは 位置パラメータ
(positional parameter: $0 からはじまるパラメータ) に代入され

今は複数のコマンドを読み込みたいので、-cオプションを指定します。

"mv {} \`echo {} | sed -e 's/201./&_/'\`" \;
 

これが今回のキモです。

まず、bash -cの後は文字列ですので、それを明記するために全体をダブルクオーテーションでくくっています。
そして、find -execの最後は;で終わるとなっているのですが、エスケープシーケンスが必要のために\;とします。

残るは mv {} `echo {} | sed -e ‘s/201./&_/’` です。

mv の後の{} はfindで見つかったファイル名を意味します。ここで、findの結果を利用できるわけです。

そして、`echo {} | sed -e ‘s/201./&_/’`

がmvの後半部分になります。これは全体がバッククオートでくくられています。

バッククオートの中を見ると、

echo {} | sed -e ‘s/201./&_/’

となっていて、先ほどと同様に{}はfindの結果を利用するわけで、echo {}で文字列を表示したうえで、それをsedを使って文字列置換をするわけです。

sedの文字列置換は正規表現を用いることができますので、検索文字列は2011,2012,2013,2014のいずれかですから、201.としました。
そして、その後が大事なのですが、sedの検索文字列をそのまま使える方法が、&となります。
今の場合、見つけた文字列の後にアンダースコアをつけたいので、&_としました。

これで無事にファイル名を変更することができました。数千のファイルをたった1行のコマンドで一気に変更できましたので、かなりすっきりしました。といってもここにたどり着くまでに数週間かかりましたが…。

bashでできる技なので、Linuxに限らず、Macでも同様にできるかと思われます。
ただ、これは我流でたどり着いた方法なので、もっとエレガントな方法がありましたら、ぜひ教えていただきたいです。

Linuxでfind, sedを使いながら文字列を再利用してファイル名の途中に文字列を挿入する方法” へのコメント

  1. ピングバック: ファイル名をまとめて変更する [Linux] – Site-Builder.wiki

  2. find 部分文字列置換で検索してたどり着きました。
    今回の例では、以下のようなシェルスクリプトはどうでしょう。確認したら末尾に「|sh」を付加して実行させます。

    for file in 201[1234]_ID.jpg ; do echo mv ${file} ${file:0:4}_${file:4} ;done

    • 相澤様

      スマートな解法をどうもありがとうございます。

      シェルで変数varに対して

      ${var:n:m}とすると、変数varのn番目からm個の文字を使用する
      ${var:n}とすると、変数varのn番目から最後までの文字を使用する

      という機能を上手に使われているわけですね。

      今の場合だと、最初の4文字を

      ${file:0:4}

      で表示し、その後の文字列は、5番目からはじまるので、最初が0であることを考えると、

      ${file:4}

      で表示できるわけですね。

      変数のブレース展開について実用例を教えて頂いてありがとうございました。
      勉強になります。

    • 松森様

      非常にエレガントな解法をありがとうございます。
      \dとグルーピングを上手に使うとこんなにきれいになるんですね。
      とても参考になります。ありがとうございます。

      あとでブログに追記させていただきます。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください