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 ./*.txtfind -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 のようにスペースを含んだファイルを扱ってもおかしくならないように、適宜シングルクォートで囲ったりしている。

以上

他にもまだ色んなオプションがあるのだが、よく使われるのはこのぐらいだろう。

-I {}-p オプションさえ覚えてしまえば、安全に xargs を使いこなせそうだ。