xargs ナニモワカラナイ
コマンドの実行結果を利用して、別のコマンドを for
文的に実行できる、という認識でいる xargs
コマンド。恥ずかしながらマジで使い方が分からなくて、コイツだけはググってコピペでしのいで、「何故かコレで動く」状態の認識で使ってきた。何なら怖いから便利そうでも逃げてきたくらいだ。
さすがに格好悪いので、今回は xargs
コマンドの使い方を頑張って勉強する。
目次
xargs は Linux と Mac で仕様が違う
出たよ BSD。なんか変だなと思ったら Mac の xargs
は認識しないオプションがあったよ。
簡単にいうと、-i
も -l
(小文字のエル) も引っかかるポイントがあるから止めとけってことみたい。
以降は Linux 版でも Mac 版でも同じように動作する書き方を紹介していく。
xargs の基本的な使い方 (-L 1
)
xargs
が一見分かりにくいのは、パイプで渡した文字列がどのように挿入されるかが分かりにくいところだろう。
例えば次の例。
$ ls -1 | xargs -L 1 echo 'Name :'
まず、$ ls -1
で、ファイル名一覧を出す。-1
オプションはあってもなくても変わらないんだけど、イメージしやすくなると思うので、1行につき1ファイルで出力するよう指定した。
んでその結果が xargs
に渡る。-L 1
というのは、Linux 版の -l
オプションと同じ意味。引数を1つずつ渡す意味になる。
何のコマンドを実行しているかというと、echo 'Name :'
というコマンド。コレだけ見ると、普通に文字列 Name :
をコンソール出力するだけだ。
コレを xargs
に指定していて、引数が1つ渡されるので、結果は次のようになる。
# まずファイル一覧の結果だけ見てみる
$ ls -1
file-1.txt
file-2.txt
file-3.txt
# 先程のコマンドを実行する
$ ls -1 | xargs -L 1 echo 'Name :'
Name : file-1.txt
Name : file-2.txt
Name : file-3.txt
このようになった。
-L 1
(-l
) で、渡される引数の数を指定して、その回数だけ指定した echo
コマンドが実行されたというワケ。
引数が渡される数を変える
今のままだと、$ ls -1
と大して結果が変わらなくて、イマイチ効果が分かりにくい。しかし、次のような例だと、その効果がイメージしやすいと思う。
# 削除コマンドを使うので、お試し用のディレクトリとファイルを作ります
$ mkdir test-directory && cd $_
$ touch a.txt b.txt c.txt
$ find ./*.txt
a.txt
b.txt
c.txt
# これらのファイルを削除する
$ find ./*.txt | xargs rm
この時、xargs
部分でどういうコマンドが実行されているかというと、
$ rm a.txt b.txt c.txt
と展開されている。
ココで比較したいのは、rm
コマンドで一気に消そうとした場合と、find
コマンドの -exec
オプションを使った場合だ。
rm
オプションで一括削除してみる
通常、$ rm ./*.txt
といったコマンドは成功する。./*.txt
部分が a.txt b.txt c.txt
と展開され、先程 xargs
で展開されたコマンドと同等の結果になる。
しかし、削除対象のファイルが多すぎると、Argument list too long
といったエラーが出て一括削除できないのだ。
xargs
は、このような引数の上限を超えないように自動的にコマンドを分割してくれるのだ。
例えば次のようなコマンドに展開されるワケだ。
$ rm a.txt b.txt c.txt ... (正常にコマンドが成功する数だけ列挙…)
$ rm 10000.txt 10001.txt 10002.txt ... (多い分は別コマンドとして実行するよう xargs が処理してくれる)
この辺をうまいことやってくれるので、わざわざ xargs
をかませておくと便利というワケ。
find -exec
で消してみる
find
コマンドの -exec
オプションを使うと、xargs
っぽい処理をしてくれるのだが、内部的には動きが違う。
$ find ./*.txt -exec rm {} \;
ファイルの削除はできるのだが、このコマンドは次のように展開される。
$ rm a.txt
$ rm b.txt
$ rm c.txt
つまり、ファイルの数だけ rm
コマンドが実行されるので、削除処理が遅いのだ。
ちなみに find
の -delete
オプションは xargs rm
と同等に上手いこと処理してくれて高速なのだが、find
コマンドのバージョンによっては対応していないオプションなので、xargs
を使ったやり方の方がより環境を選ばず、汎用性が高いだろう。
そんなワケで、rm ./*.txt
や find -exec
よりもメリットが多いので、xargs rm
を使う機会があり、xargs
の特徴を活かせているのだ。
区切り文字を指定する (-0
)
ところで、先ほどと同じ find
+ xargs rm
コマンドを、次のようなファイルが存在する状態で実行すると、おかしなことになる。
# スペースを含んだファイル「a b.txt」などを作る
$ touch 'a b.txt' 'c.txt'
$ find ./*.txt
a b.txt
c.txt
# 先ほどと同じコマンドを実行する
$ find ./*.txt | xargs rm
rm: ./a: No such file or directory
rm: b.txt: No such file or directory
# ?
$ find ./*.txt
./a b.txt
察しの良い方は気付いたと思うが、xargs
はコマンドを次のように展開する。
$ rm ./a b.txt ./c.txt
# つまり…
$ rm ./a b.txt ./c.txt
このように、3つのファイルを消そうとするコマンドと勘違いされてしまったのだ。
コレを何とかするには、find
コマンドの結果で使う区切り文字を \0
・ヌル文字という特殊文字に変更し、xargs
もヌル文字を解釈するように設定してやるのだ。
$ touch 'a b.txt' 'c.txt'
# `-print0` オプションを付けるとこうなる・人の目には区切られていないように見える
$ find ./*.txt -print0
./a b.txt./c.txt
# このようにオプション指定すると、ヌル文字で区切って解釈してくれるので、正しくファイルが消せる
$ find ./*.txt -print0 | xargs -0 rm
コレが時々出てきてよく分からない -print0
や -0
の仕組みだ。
この仕組みが分かっていないと、思わぬファイルを削除してしまう恐れもある。よくよく注意しよう。
xargs
を確認しながら実行したい (-p
)
さて、xargs
の有用性や安全に実行するための方法を見てきたが、より安全に実行するために、実行前確認をしたいと思う。
先程の rm
コマンドの場合、思いつくのは rm -i
コマンドだろう。xargs
をかませる時は -o
オプションを付与してやると、rm -i
がうまく実行できるようになる。
$ find ./*.txt -print0 | xargs -0 -o rm -i
# 次のようなコンソールが出る
remove ./a b.txt?
# 「y」を入力すれば削除され、何も入力せず Enter を押せば削除されない
remove ./a b.txt? y
# 次のコンソール出力。何も入力せず Enter を押してみる
remove ./c.txt?
# 削除されたのは `a b.txt` のみ
$ ls
c.txt
もし -o
オプションを付けないと、このようなインタラクティブな動作にはならず、削除に失敗する。--open-tty
の略らしい。
コレは rm
コマンドのオプションを有効にしただけだ。それ以外のコマンドで実行前確認をする場合は、xargs
コマンドの -p
オプションを使うと良いだろう。
# 単純に `-p` オプションを使うと、次のようになる
$ find ./*.txt -print0 | xargs -0 -p echo
echo ./a b.txt ./c.txt?... y
# コレまで見てきた `-L 1` オプションと組み合わせると順次実行できる
$ find ./*.txt -print0 | xargs -0 -p -L 1 echo
echo ./a b.txt?...
echo ./c.txt?...
# 「y」を入力すれば初めてコマンドが実行される。何も入力せず Enter を押せば実行されない
$ find ./*.txt -print0 | xargs -0 -p -L 1 echo
echo ./a b.txt?...y
./a b.txt
echo ./c.txt?...y
./c.txt
あまり意味はないが、-o
と -p
オプションを組み合わせたりもできる。それぞれ、コマンドの展開のされ方が異なる点を見てもらおう。
$ find ./*.txt -print0 | xargs -0 -o -p rm -i
rm -i ./a b.txt ./c.txt?...y
remove ./a b.txt? y
remove ./c.txt? y
# コチラは `-L 1` オプションを指定
$ find ./*.txt -print0 | xargs -0 -o -p -L 1 rm -i
rm -i ./a b.txt?...y
remove ./a b.txt? y
rm -i ./c.txt?...y
remove ./c.txt? y
引数の挿入位置を自分で指定する (-I {}
)
ココまで色々触ってきたが、xargs
で引数が装入される位置を自分で選べたら分かりやすくないだろうか。
そんな時は、-I {}
(Linux 版なら -i
が同義) というオプションが使える。
$ ls | xargs -I {} echo {}
a b.txt
c.txt
$ ls | xargs -I {} echo 'Name : {}'
Name : a b.txt
Name : c.txt
# 1つの引数を複数回使ったり
$ ls | xargs -I {} echo 'Name : {} - {}'
Name : a b.txt - a b.txt
Name : c.txt - c.txt
こんな感じ。{}
という文字列を echo
コマンド内にプレースホルダ的に配置している。echo
コマンドの引数はシングルクォートを使っているが、{}
は「変数」ではないので、ダブルクォートによる変数展開、シングルクォートによるエスケープの影響を受けない。
xargs
で複数のコマンドを実行したい
指定のファイル達を gzip
で圧縮して、指定のディレクトリに移す、といったことを、xargs
を組合せたい時。sh -c
ないしは bash -c
を使ってやる。
# テスト用のファイル・ディレクトリを作成する
$ touch 'a b.txt' 'c.txt' 'd.txt'
$ mkdir zipped
# テキストファイルを gzip 圧縮して zipped ディレクトリに移動する
$ find ./*.txt -print0 | xargs -0 -I {} bash -c "echo 'Exec : {}' && gzip '{}' && mv '{}.gz' ./zipped/ && echo 'Finished : {}'"
Exec : ./a b.txt
Finished : ./a b.txt
Exec : ./c.txt
Finished : ./c.txt
Exec : ./d.txt
Finished : ./d.txt
# 結果確認
$ find . -type f
./zipped/d.txt.gz
./zipped/c.txt.gz
./zipped/a b.txt.gz
各コマンド内で {}
が展開されるので、a b.txt
のようにスペースを含んだファイルを扱ってもおかしくならないように、適宜シングルクォートで囲ったりしている。
以上
他にもまだ色んなオプションがあるのだが、よく使われるのはこのぐらいだろう。
xargs
コマンドは、指定のコマンドに対して引数を上手く分配して効率的に実行してくれるコマンド-l
と-i
オプションは Mac で使えなかったりして混乱の元なので、-L 1
と-I {}
に読み替える・書き換える- 引数がどの位置に装入されるのか分からなくなったら
-I {}
オプションを使って明記する - スペースを含んだ文字列を綺麗に扱うために、
find -print0
やxargs -0
オプションを使う - 実行前確認をしたかったら
xargs -p
オプションを使う - 複数のコマンドを実行したい場合は
sh -c
(bash -c
) を使う
-I {}
と -p
オプションさえ覚えてしまえば、安全に xargs
を使いこなせそうだ。