前口上
いま従事しているプロジェクトは 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"' \;
↩