前口上
いま従事しているプロジェクトは Windows のみなのですが、このまえのプロジェクトは GNU/Linux の Bash と Windows にも Git Bash がいれられていて Bash をふかぼり研究してました。そのときにはまってたまたま発見した解決法を自分自身が納得するためにかきちらかした解説文がでてきたので加筆修正して投稿します。 (PowerShell もおもしろいが、やっぱつぎのプロジェクトは Shell がメインであるのがいい…)
find, xargs コマンドではまったこと
まさに こちらの記事 でふれられていることなのですが、 (記事の例にそくしていうと) 意図としてはカレント ディレクトリーにある拡張子 jpg のファイルを拡張子 jpeg に変更しようと、つぎのようにかいてしまうようなことをして、しばしおもいなやんだわけです1。
ls *.jpg | xargs -I@ mv @ "$(basename @ .jpg).jpeg"
xargs -I@ mv ... はパイプからうけとった ls の出力の一項目ずつを @ に代入しては mv ... を実行するのをくりかえします。たとえば ls *.jpg の結果が
bar.jpg baz.jpg foo.jpg
だったとすると xargs -I@ mv @ "$(basename @ .jpg).jpeg" は
mv bar.jpg "$(basename bar.jpg .jpg).jpeg"
mv baz.jpg "$(basename baz.jpg .jpg).jpeg"
mv foo.jpg "$(basename foo.jpg .jpg).jpeg"
と展開、さらに $() のなかみが実行されて
mv bar.jpg "bar.jpeg"
mv baz.jpg "baz.jpeg"
mv foo.jpg "foo.jpeg"
となることを期待したわけです。なお、 basename foo.jpg .jpg とすると foo.jpg から .jpg を除去できます2。
ところがどっこい、シェルはふつうダブル クオーテーションでかこまれた $(...) のなかみをさきに別プロセスとして実行してしまいます。実際にうちこんでみればわかるように basename @ .jpg の結果は @ です3。つまり、ここでは $(basename @ .jpg) イコール @ です。
したがって、いちばんうえのコマンドラインはじつはまず
ls *.jpg | xargs -I@ mv @ "@.jpeg"
になっていたのでした。ふたたび、たとえば ls *.jpg の結果が
bar.jpg baz.jpg foo.jpg
だとすると xarg -I@ mv @ "@.jpeg" は
mv bar.jpg bar.jpg.jpeg
mv baz.jpg baz.jpg.jpeg
mv foo.jpg foo.jpg.jpeg
となります。これは最初に意図したこととはあきらかにちがいます。
ワーク アラウンド
それで、くだんの筆者さんは for や while read をつかうことになるとおっしゃってるんですが、たまたま業務でつかっていたシェル スクリプトの内容をよんでいて発見した手法なんですけど、この例のコマンドラインにそくしてかくと4
ls *.jpg | xargs -I@ sh -c 'mv @ "$(basename @ .jpg).jpeg"'
sh も -c も 'mv @ ...' もここではまだ xargs -I@ への引数にすぎません。とくに 'mv @ ...' はシングル クォーテーションでくくられているのでシェルはなにもせずそのまま xargs にわたします。したがって、みたび、たとえば ls *.jpg の結果が
bar.jpg baz.jpg foo.jpg
だとすると xargs -I@ sh -c 'mv @ "$(basename @ .jpg).jpeg"' は
sh -c 'mv bar.jpg "$(basename bar.jpg .jpg).jpeg"'
sh -c 'mv baz.jpg "$(basename baz.jpg .jpg).jpeg"'
sh -c 'mv foo.jpg "$(basename foo.jpg .jpg).jpeg"'
となります。 sh -c '文字列' は 文字列 をコマンドラインとして実行させるコマンドです。
なお、
ls *.jpg | xargs -I@ mv @ '$(basename @ .jpg).jpeg'
というのはうまくいきません。たしかに、シングル クオーテーションでかこまれているのでそのまま xargs にわたり
mv bar.jpg $(basename bar.jpg .jpg).jpeg
mv baz.jpg $(basename baz.jpg .jpg).jpeg
mv foo.jpg $(basename foo.jpg .jpg).jpeg
とはなりますが、ここではもはやあくまで xargs の処理するところであり、シェルの $(...) 展開のおよぶところではないので、 foo.jpg がもじどおり $(basename foo.jpg .jpg).jpeg というファイル名に変更されてしまいます。したがって、 sh -c が必要というわけです。
参考文献
-
findならさしずめfind -maxdepth 1 -name *.jpg -type f -exec mv {} "$(basename {} .jpg).jpeg" \;↩ -
basenameコマンドにとってfoo.jpgはたんなる文字列にすぎないのでfoo.jpgというファイルが実在していなくてもべつにエラーにはならない。 ↩ -
文字列としての
@には.jpgはついてないからべつになにも除去しないでそのまま。 ↩ -
findならfind -maxdepth 1 -name *.jpg -type f -exec sh -c 'mv {} "$(basename {} .jpg).jpeg"' \;↩