LoginSignup
4
3

More than 3 years have passed since last update.

[Shell] find, xargs コマンドのおとしあなを詳説してみる

Last updated at Posted at 2020-01-01

前口上

いま従事しているプロジェクトは 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

となります。これは最初に意図したこととはあきらかにちがいます。

ワーク アラウンド

それで、くだんの筆者さんは forwhile 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 が必要というわけです。

参考文献


  1. find ならさしずめ find -maxdepth 1 -name *.jpg -type f -exec mv {} "$(basename {} .jpg).jpeg" \; 

  2. basename コマンドにとって foo.jpg はたんなる文字列にすぎないので foo.jpg というファイルが実在していなくてもべつにエラーにはならない。 

  3. 文字列としての @ には .jpg はついてないからべつになにも除去しないでそのまま。 

  4. find なら find -maxdepth 1 -name *.jpg -type f -exec sh -c 'mv {} "$(basename {} .jpg).jpeg"' \; 

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3