はじめに
xargs
コマンドは「なにか凄そうだけどよく分からないコマンド」としてよく知られています。使う人は使うけど何をやっているのか全くわからないコマンドです。また、やっていることがわかっても実際に使ってみると、空白やクォーテーション文字でエラーになってしまう使い方がとても難しいコマンドです。この記事はそういうよくわからない xargs
はどういうコマンドなのか解説します。この記事を読むと xargs
を「完全に理解した」と言えるようになるでしょう。
xargs
コマンドが難しい理由は、xargs
自体の設計や実装の問題で古い時代の制限が多いからです。仕様が意味不明で一貫性がなくで他のコマンドと正しく連携するのが困難です。そして本来の目的と違う用途に流用されてばかりです。最初にこの記事の結論を書いておきます。
-
xargs
は難しすぎるコマンド、可能な限り使うな- 昔と違い今はそんなに重要で必要なコマンドではなくなった
-
ネット・書籍に載っている
xargs
の使い方の多くは雑で「たまたま動いている」だけ- シェルスクリプトで空白を扱えない問題の 1/3 は
xargs
の雑な使用が原因
- シェルスクリプトで空白を扱えない問題の 1/3 は
- 特にシェルスクリプトの場合、必要ないなら
xargs -I
よりもfor
/while
を使え -
ファイルパスを扱うなら
xargs
は不要。find ... -exec {} +
を使え-
xargs
を多用する人は、十中八九find ... -exec
の存在を知らない人 -
find ... -exec {} +
はfind | xargs
の上位互換 -
find ... -exec {} +
は速くて多数のファイルがあってもエラーにならない
-
- 並列実行 (
-P
) などでどうしてもxargs
が必要な場合は-0
(POSIX準拠)を使え- GNU Parallel などの代替ソフトウェアの利用も考慮する
補足 「たまたま動いている」とは、サンプルコードのデータでは問題ないが、それ以外の空白やクォートが含まれているデータや特定の環境だとエラーになったり期待したとおりに動かない可能性がある「潜在的なバグ」が含まれている使い方という意味です。ネット・書籍に載っている多くの xargs
の使い方には潜在的なバグが含まれているため理解せずに使うと罠にハマります。
xargs の「目的」を知り本質を見抜く!
xargs
を理解するには、xargs
が何のために作られたコマンドなのかを知ることが一番です。
xargs は拡張引数(extended arguments)の略
xargs
コマンドがよく分からない理由は、xargs
コマンドがそもそも一体何のためにあるコマンドなのかを理解していないためです。なんのためのコマンドなのかは実は名前が適切に表現しています。xargs
とは 拡張引数(extended arguments)の略であり、その名の通り引数を拡張するものです。extended とは引き伸ばすや広げると言った意味を持つ言葉です。expansion(展開)や extension(追加機能)ではなく extended です。といっても引数を拡張(広げる)とは一旦何のことなんだ?と思うことでしょう。
その前に一つ重要な注意点があります。それは xargs
はもともと引数を拡張するために作られたコマンドなのですが、現在では xargs
の当初の目的とは違う使い方がよくされているということです。これは cat
コマンドが本来はファイルを結合 (concatenate) するために作られたコマンドなのにファイルを画面に出力するためによく使われているのと同じようなものです。拡張引数の略である xargs
という名前から推測できる「目的」ではなく xargs
が行っている「機能」が使われているから「なにか凄そうだけどよく分からないコマンド」になってしまっているのです。
物事を理解するためにまず原点に戻りましょう。xargs
の拡張引数とは OS が持っているコマンドライン引数のサイズの制限を拡張する(広げる) ということです。拡張すると言っても OS の制限をただの一コマンドが変更することは出来ないので、実際には拡張したかのように使えるようにすることです。すなわちコマンドを実行するときに多くの引数を渡すとサイズ制限に引っかかってしまいエラーになってしまうという問題を解決するために作られたのが xargs
コマンドです。これが xargs
の本来の「目的」なのです。
$ ls file{000001..100000}.txt
-bash: /bin/ls: 引数リストが長すぎます
補足: 上記はシェルの機能(ブレース展開)によって以下のように10万個の引数に展開される
ls file000001.txt file000002.txt ... file100000.txt
どれくらいの引数を渡せるかは OS によって異なり ARG_MAX
の値(getconf ARG_MAX
で取得可能)で決まっています。制限値は引数の個数ではなくサイズ(バイト数)なので注意してください。また ARG_MAX
の制限値いっぱい使えるわけではなく、ここから実行コマンドのパスや環境変数のサイズなどを引いたサイズが制限値です。この制限値をどう解釈するかは OS によって異なるため ARG_MAX
の値から引数で渡せるサイズを正確に計算することは出来ません。ようするに ARG_MAX
の値は目安でしかないということです。
OS | ARG_MAX | OS | ARG_MAX |
---|---|---|---|
Ubuntu 22.04.3 | 2,097,152 (2 MB) | OpenBSD 7.4 | 524,288 (512 KB) |
macOS 13.4 | 1,048,576 (1 MB) | Solaris 10 | 1,048,320 (1 MB - 256) |
FreeBSD 14.0 | 524,288 (512 KB) | Solaris 11 | 2,096,640 (2 MB - 512) |
NetBSD 10.0 | 262,144 (256 KB) | POSIX の制限値 | 4,096 (4 KB) |
ちなみに Linux では「一つの引数」に 128 KB のサイズ制限があります。したがって引数全体が 2 MB に達していなくても長い引数一つでエラーになります。
ざっくりですが、私の環境では Linux は 15 文字のファイル名が 9万2000 個あたりでエラーになりました。macOS はその半分です。上記の中で最も少ない NetBSD ではおそらく 9200 個程度でしょう。これだけの引数を渡せるなら十分ではないか? と思うかもしれませんが、一番最後の項目、POSIX で要求されている制限値を見てください。わずか 4 KB、つまり 4 KB しか渡せない OS を作っても POSIX に準拠したことになります。計算上は 150 個程度です。もちろん今どきたった 4KB しか引数で渡せない環境はないと思われますが POSIX の制限が 4 KB ということは、POSIX が策定された 1990 年頃にはそのような環境が実在したか、もしくは想定の範囲内だったことを意味しています。xargs
が作られた時代、それは 1977 年ですが、当時はサイズ制限の小ささは切実な問題だったのです。
xargs は引数サイズの制限を拡張するためのコマンド
xargs
コマンドが開発された経緯は、開発者によって語られています。保存を兼ねて引用しておきます。
引用
Herb Gellis 1988/02/17 7:15:42
Oy veh!
As the original author of xargs, I will respond ...
I wrote xargs about 10 (TEN) years ago when I worked at Bell Labs in
Piscataway NJ. At that time the Bourne Shell didn't exist yet. The original
shell was relatively primitive, and was being greatly enhanced (by John Mashey).
There were lots of other "primitive" aspects of UNIX, such as sizes of things
(max of 512 bytes for file name expansions on the command line... later
changed to 5120 byes), etc. etc. This latter aspect of max 512 byte file
name expansion was the major reason I wrote xargs in the first place, and
then added -i, etc. as new features seemed useful.
The funny (small) sizes are merely choices made at the time, some reflecting
"real world" or "convenient" sizes FOR THE TIME (long ago, and far away...).
There were other silly reasons for some things... By using simple I/O
(avoiding printf, e.g.) xargs was kept at under the "magic" size of 4K
and was able to load very quickly (magic back then before sizes in the
file system were revamped). Obviously sizes should have been changed
as UNIX changed around it, but I doubt very much that xargs was ever enhanced
(I left B.T.L. in 1981)...
I also wrote the man page from which you speak (and I agree it is an abortion,
but I couldn't stand looking at it anymore either) and obviously it has never
changed either (the same man page is in our HP-UX). Oh well. Sorry about
those core dumps... I've never seen any.
To attempt to answer your questions specifically (based on whatever
design decisions I think I remember making mucho years ago):
>of the SVID remarkably obscure. For example, are empty lines to be
>discarded in the sense that they aren't *counted* by the -l option,
>or are they to be discarded only in the sense that they don't
>contribute any arguments? {Don't suggest running the program; what I
Both, don't count them for -l and discard them as they don't contribute.
>want to know is what it is *supposed* to do.} If a line has a space
>or tab before the new-line, is it still counted by -l? What about
No. This meant the line was continued to the next physical line
but was logically one line. (I realize that was a dumb way to do it,
should have used the \ escaped newline method)
>new-lines quoted by \ ? We are told that blanks and tabs are allowed
^^^^^^^^^^^^^
>in arguments, if quoted, but what about new-lines?
^^^^^^^^^^^^^^^^^^^^^
No on both counts.
> -- why will "-i" accept only five *arguments* containing replstr,
> given that any one argument may contain any number of instances
> of replstr? Would anything break if this limit were raised?
Why 5? Because I wrote it that way. Don't remember if it was a static
array, but this is purely arbitrary, and could be changed at will.
>-- why may constructed arguments grow to only 255 characters
That's how big the (static) buffer was.
>The XARGS(SD_CMD) section also says that "xargs will terminate if ...
>it receives a return code of -1 from ... command". This is bizarre,
Didn't seem unreasonable back in 1977 or thereabouts.
>So, is there a clear description of xargs anywhere?
No comment! (sorry)
Herb
. .
|=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-| "What will I be when I grow up?
| Herb Gellis | You are already grown up.
| {ucbvax,...}!hplabs!hpindda!herb | You mean this is as UP as I will get?..."
|=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-|
| | David Gerrold, "When HARLIE Was One"
--- ---
このメールの内容から次のことがわかります。
-
xargs
はメールの送信日の 10年前(1977年頃)に書かれた- 補足: 世の中に Unix が広まり始めたのは 1979 年以降
- 原始的なシェル(PWBシェル)が用いられておりBourne シェルはまだない
- 補足: PWBシェルはスクリプト言語として使うに機能不足
- パイプはあったが
for
はなくwhile
やgoto
は外部コマンドだった - シェル言語の機能が貧弱な時代に必要だったもの
- 当時のコマンドライン上のファイル名展開は 512 バイト(後に 5120 バイト)だった
- 最大 512 バイトのファイル名展開が、そもそも
xargs
を書いた理由だった -
-i
オプションなどの機能は便利そうだからと後から追加した - 仕様や制限は、当時「これで十分だろう」という理由によるもの
難しく考える必要はありません。xargs
コマンドは当時のプログラマの一人が、コマンドライン引数に多くの引数を指定したらエラーになって実行できないという切実な問題を解決するために作ったものです。そしてありがちな話として取りあえず動くものを作り後で便利そうな機能を追加した一つのコマンドでしかありません。出来が良いとは言えないものが、大きく改良されることもなく今も使われ続けているのです。
引数が多すぎるなら分割してコマンドを何回も実行すればいいじゃない
さて、コマンドライン引数が多すぎてエラーになる場合、人間だったらどうやってこの問題を解決するでしょうか?
$ ls file{000001..100000}.txt
-bash: /bin/ls: 引数リストが長すぎます
簡単ですね? 引数の数を分割してコマンドを何回も実行すればよいのです。
$ ls file{000001..050000}.txt
$ ls file{050001..100000}.txt
xargs
がやっているのもこれと同じです。ちなみに xargs
はきっちり半分にしているわけではなく、適切なサイズに調整しながらなるべく多くの引数を渡してコマンドを実行します。ちなみに私の Linux の環境ではおよそ 8700 個 (128KB) 単位で 12 回のコマンド呼び出しが行われました。これが xargs
コマンドの目的を実現するためのアイデアです。
一般的に多くの引数とはファイル名です。ファイル名以外をコマンドライン引数にたくさん書くことはあまりないでしょう。多くの引数を手書きするなんてやってられませんからね。カレントディレクトリにファイルがたくさんあり、それらのファイルに対してなにかのコマンドを実行するときに、コマンドライン引数の制限が問題になってきます。
$ ls *.txt
補足: シェル言語の機能にコマンドライン引数のサイズ制限はない
シェル言語の機能: シェル関数やビルトインコマンドの引数、パス名展開や for の in 部分など
忘れてはいけないことなのでもう一度述べておきます。xargs
コマンドの本来の目的は「ファイル数(引数)が多い場合のエラーの回避」です。そして「コマンドは複数回実行される事がある」ということです。
xargsは標準入力のデータを引数に変換してコマンドを呼び出す変換器
さて xargs
の目的とアイデアは出揃いました。次に考えるべきは実現方法です。このような場合にファイル数が多くてもエラーにならずに目的を達成するにはどうしたらよいでしょうか? その場合に取りうる手段はあまりありません。引数が使えない、それなら標準入出力を使おうと xargs
の開発者は考えました。
ファイル名の一覧は ls
コマンドや find
コマンドなどを使って標準出力に出力することができます。当然ですが標準出力に出力するファイル数には制限はありません。
$ ls /tmp/files # 補足: ls はソートされるので遅い
file0000.txt
file0001.txt
file0002.txt
︙
$ find /tmp/files -type f # 補足: find はソートされないので速い
/tmp/files/file0163.txt
/tmp/files/file1288.txt
/tmp/files/file0953.txt
︙
そしてコマンドはファイル名を引数で受け取ります。ここでは例として touch
コマンドを実行します。繰り返しますがコマンドライン引数の数には制限があります。
$ touch file0000.txt file0001.txt file0002.txt ...
「標準出力でデータを出力するコマンド」と「引数にデータを指定して実行するコマンド」の二つをつなげることができれば、コマンドライン引数のサイズ制限を回避することができます。xargs
コマンドが行っていることは、この二つのコマンドをつなげることです。xargs
コマンドは標準入力からデータを受け取り引数の形に変換して実行するという機能を持っています。
標準入力のデータを引数に変換してコマンドを呼び出す変換器
|
$ find . -type f | xargs touch
| | | |
+---+----------+ +-+-+
| |
標準出力で出力するコマンド xargsが呼び出すコマンド
./file0163.txt touch ./file0163.txt ./file1288.txt ./file0953.txt ・・・
./file1288.txt ↗
./file0953.txt
︙
xargsの入力データ形式は行指向ではなく複雑な「xargs 形式」
上記の図は簡易版です。xargs
が行う実際の入力データ形式は行単位ではなくもっと複雑です。少し詳細に書き直しましょう。
$ any-command | xargs touch
| | | | |
+---+----------+ | +-+-+
| | |
arg1 arg2 arg3 arg1 touch arg1 arg2 arg3 'arg 4' arg5
'arg 4' arg2
arg"5" → arg3 ↗
︙ arg 4
arg5
xargsコマンドは入力データを解析(パース)して引数リストを生成する
xargs
を行単位でデータを読み込んでいるコマンドだと勘違いしている人が多いと感じますが、xargs
コマンドは本来 xargs
が決めた専用のデータ形式でデータを渡さなければいけないコマンドです。CSV 形式が一見カンマで区切られただけの単純なデータ形式に見えて実際にはクォートなどのパースが必要な複雑な形式であるのと同じように、xargs
コマンドも単純な行単位のデータではなく専用の複雑な「xargs 形式」を入力データ形式として使うコマンドです。多くの Unix コマンドが行単位でデータを出力するのに対して xargs
の入力データは行単位ではないので、データ形式に完全な互換性がありません。これが xargs
での引数解釈のトラブルを引き起こしています。後ほど解説しますが、更に面倒なことに「xargs 形式」は一種類ではなく複数の種類があります。「xargs 形式」でデータを渡された xargs
コマンドは入力データをパースして引数リストを生成します。最後に引数リストをエラーにならない単位で分割して引数をコマンドに渡して実行します。
多くのコマンドはファイル名を標準入力から読み取らない
鋭い人は xargs
を使わずに多くの引数を扱う方法を思いつくのではないでしょうか?
find /tmp/files -type f | touch
# なぜ xargs が必要なのか?
find /tmp/files -type f | xargs touch
xargs
が必要な直接的な答えは touch
コマンドはファイル名を引数から指定することしかできず、標準入力から読み取るように作られていないからです。しかし touch
コマンドを修正して標準入力からファイル名を読み取るよう修正することもできるはずです。そうすれば xargs
コマンドを使う必要もありません。逆に言えば xargs
コマンドが必要になってしまっている理由は標準入力で引数を与えることができないからです。
ファイル名を引数で指定する代わりに標準入力から受け取るコマンドはあまりありません。まったくないというわけではないです。例えば昔に tar
コマンドと同様の目的で使われていた cpio
コマンドはファイル名を引数ではなく標準入力から受け取ります。
$ echo file.txt | cpio -o > archive.cpio
1 block
したがって cpio では xargs が必要ない
$ find . -name "*.txt" | cpio -o > archive.cpio
1 block
$ tar -cf archive.tar file.txt
後に追加された -T オプションを使えば tar でも標準入力からファイルを受け取ることができる
$ echo file.txt | tar -c -f archive.tar -T-
$ find . -name "*.txt" | tar -c -f archive.tar -T -
このようなコマンドであれば、xargs
コマンドを使うことなく多くの引数(ファイル名)を処理することができるのですが、このようなコマンドはどちらかといえば例外です。一般的に標準入力からはファイル名ではなくファイルの中身を読み取ります。例えば grep
コマンドがそうです。
# 引数はファイル名を指定する(複数のファイル名に対応している)
grep 'pattern' file1.txt file2.txt file3.txt ...
# ⭕ 標準入力からはファイルの「中身」を読み取る
cat file.txt | grep 'pattern'
# ❌ 標準入力からファイル名を指定することはできない
find . -name "*.txt" | grep 'pattern'
なかなかおもしろい違いだと思いませんか?
この話から、xargs
はファイル名を標準入力から受け取ることができないコマンドのための回避策とも言えることがわかります。なぜ標準入力から引数を読み取るように作らないのでしょうか? いえ、私も作ろうとは思いません。標準入力と引数には使い方に違いがあると感じているからです。標準入力からデータを受け取る(そして標準出力で出力する)コマンドはフィルタと言われますが、フィルタは「ひとかたまり」のデータを加工するものです。引数は複数のデータを渡すことが出来ますが、標準入力ではそれができません。複数のデータをひとかたまりにしてしまうと情報(個々のファイル名など)が抜け落ちてしまうため、複数のデータを処理するコマンドはフィルタにはなり得ません。
(本家ではない)Unix 哲学に「すべてのプログラムをフィルタとして設計する 」という設計方針を提唱しているものがありますが、フィルタにできるのはひとかたまりのデータを扱うプログラムだけです。「すべてのプログラム」をフィルタにすることはできません。実際にフィルタでないコマンドは多数ありますよね?特にファイル管理を行うコマンドに多いです。それらはフィルタではないから Unix 哲学を満たしていないということでしょうか? いいえ、違います。「すべてのプログラムをフィルタとして設計する」という設計方針の方が間違っているのです。そもそも本家の Unix 哲学にはそのような設計方針はありません。xargs
コマンドは「引数に変換してコマンドを実行する」ものであるため「標準入力からデータを受け取らないフィルタではないコマンドがある」という前提があって初めて意味があるコマンドです。
xargsはエンドユーザー用ではなく開発者用のツールだった!?
xargs
が難しいというのは、エンドユーザー用ではなく開発者用のツールとして見なされていたという事実からからもわかります。これは POSIX xargs の RATIONALEで述べられています。
The xargs utility was usually found only in System V-based systems; BSD systems included an apply utility that provided functionality similar to xargs -n number. The SVID lists xargs as a software development extension. This volume of POSIX.1-2017 does not share the view that it is used only for development, and therefore it is not optional.
訳: xargs
ユーティリティは、通常 SYstem V ベースのシステムでのみに存在した。BSD システムでは xargs -n number
に近い機能を提供する apply
ユーティリティが含まれていた。SVID では xargs
はソフトウェア開発拡張としてリストされている。POSIX.1-2017 では開発のみに使用されるという見解を共有しておらず、したがってオプションではない。
SVID とは System V Interface Definition の略で(POSIX ではなく)AT&T の UNIX System V のための標準規格です。AT&T による標準規格なので、ある意味 UNIX 公式の標準規格といえます。xargs
が System V のみに存在した(下記補足参照)ということは、つまり xargs
を開発した UNIX では、xargs
をソフトウェア開発ツールとして扱っていたといえます。POSIX では開発のみに使われるものではないと判断されましたが、少し判断が変わればソフトウェア開発拡張のオプション機能だった可能性があるようなものです。だから xargs
は使うのが難しくて当たり前なんです。
ちなみに SVID の内容は以下のアドレスから参照することができます。xargs
コマンドは Volume 3 に記載されています。
補足: xargs
コマンドは BSD Unix では 1991 年の 2.11BSD、1989 年の 4.3BSD NET/2 にはあるようで、おそらく POSIX.2-1992 の策定中に POSIX の定義に従って実装されたのだと思われます。System V のみに存在したというのは、それ以前の話をしているのでしょう。
xargsは目的を見失い新しい「機能」を追加してしまった
xargs
コマンドは元々は多くの引数を指定したときに、コマンドライン引数のサイズ制限でエラーになる問題を解決するために作られました。多くのコマンドに共通する話ですが、コマンドはバージョンアップするたびに機能が追加されていきます。そして元々の目的とは関係ない別の機能が追加されることもあります。例えば cat
コマンドはファイルを結合するコマンドであり -v
オプション(印刷できない文字をASCII文字で出力する)を cat
コマンドに追加するのは良くない考えだと言われてきました(参照 UNIX Style, or cat -v Considered Harmful)。
本家の Unix 哲学には「それぞれのプログラムが一つのコトをうまくやる。新しい仕事をするには新しい機能を追加して古いプログラムを複雑にするのではなく新しく作る」という考え方があります。
Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new "features".
xargs
が「なにか凄そうだけどよく分からないコマンド」になってしまったのは、本来の目的とは関係ない新しい仕事をするときに、新しいコマンドを作るのでなく新しい機能を追加してしまったせいです。私は Unix 哲学の考え方が完全に正しいとは思っておらず、本来の目的とは違っていても便利な機能を追加したほうが良い場合もあると考えていますが、実際 xargs
がよくわからないコマンドになってしまったのは、追加された機能(オプション)にあることは間違いないでしょう。
「-n number」や「-l lines」や「-s size」によるサイズ制限
xargs
コマンドは多くの引数をエラーが出ないサイズ毎に分割するコマンドです。しかしここで xargs
の開発者は考えました。分割するサイズを指定できれば便利ではないかと。それが -n
(最大個数)、-l
(最大行数)、-s
(最大バイト数)による制限です。
たしかに便利そうではあります。なにが便利かと言うと xargs
にデータを渡す側が遅い場合に、標準の分割サイズに達する前に、途中でコマンドを実行することができるからです。
# 5 秒経たないとなにも出力しない(xargs はコマンドを実行しない)
seq -f "file%04g.txt" 5 | while read -r file; do
echo "$file"
sleep 1 # 1 秒毎にデータを出力する
done | xargs
# => file0001.txt file0002.txt file0003.txt file0004.txt file0005.txt
# 2 秒(引数最大2個)毎に出力させることができる
seq -f "file%04g.txt" 5 | while read -r file; do
echo "$file"
sleep 1 # 1 秒毎にデータを出力する
done | xargs -n2
# => file0001.txt file0002.txt
# => file0003.txt file0004.txt
# => file0005.txt
xargs
はデフォルトでは標準入力から多くのデータを溜め込んでからコマンドを実行します。データが溜まるまでコマンドは実行されないので xargs
へデータを渡す側と xargs
でコマンドを実行する側はほとんど並列で動くことはありません。-n
、-l
、-s
を指定すると引数を溜める量を制限することができるため、xargs
へデータを渡す側が遅い場合に、引数を渡す処理とコマンドの実行を並列で実行することができます。
それならサイズ制限は小さくしたほうが並列動作して良いのかと言うとそうとは限らず、コマンドの実行回数が多くなるため、サイズ制限を小さくしすぎると逆に遅くなることがあります。もっとも高いパフォーマンスを出せる適切なサイズは場合によってさまざまなので、xargs
は「このような使い方をするとパフォーマンスが出せる」とは一概には言えません。
癖のある -i / -I オプションによる「挿入モード」
xargs
コマンドはデフォルトでは呼び出すコマンドの末尾に引数を追加してコマンドを呼び出します。
$ find . -type f | xargs touch 《ここに引数を追加する》
touch コマンドの末尾に引数(ファイル名)を追加してコマンドを呼び出す。
例: touch file1.txt file2.txt file3.txt
しかし引数は必ずしも最後に追加したいとは限りません。例えば mv
コマンドで指定したディレクトリにファイルを移動する場合は、途中に引数を入れ込むことになります。
$ find . -type f | xargs mv 《ここに入れたい》 /tmp/trash/
このように、途中に入れたい(挿入したい)ときに使うのが -i
や -I
オプションによる「挿入モード」です。これも本来の多すぎる引数をエラーにならないように分割する機能とは関係のない機能です。なぜなら入力データの一行ごとにコマンドを呼び出すように動作が変わるからです。
-iは《ここに入れたい》場所に {} と書く
$ find . -type f | xargs -i mv {} /tmp/trash/
次のように 3 回実行される
mv file1.txt /tmp/trash/
mv file2.txt /tmp/trash/
mv file3.txt /tmp/trash/
補足: -Iは挿入する場所を示す文字を省略できないという違いがあるだけ
$ find . -type f | xargs -I{} mv {} /tmp/trash/
一般的に -i
/ -I
オプションを使用するとコマンドの実行回数が増えるためパフォーマンスは大きく低下します。{}
の場所に複数のファイルを入れて欲しいものですが、それが可能なのは 2001 年にリリースされた FreeBSD 4.4 で実装され、他の BSD 系 Unix にも広まった -J
オプションです。-J
オプションの方が -I
オプションよりも優れているのですが、残念ながら GNU 版の xargs
や System V 版の xargs
には実装されていません。
$ find . -type f | xargs -J{} mv {} /tmp/trash/
次のように 1 回で実行できるから -i / -I よりもパフォーマンスが良い
mv file1.txt file2.txt file3.txt /tmp/trash/
BSD 版には昔は「挿入モード」なんてなかった
挿入モード(-i
オプション)1977 年の PWD/UNIX 時代からある機能(PWB/UNIX 1.0 xargs)ですが、BSD 系 Unix では実装されていません。そして 2001 年の -J
オプションよりも後の 2002 年に、-I
オプションが FreeBSD 4.7 で実装されました(参考)。OpenBSD では 2003 年の OpenBSD 3.4、NetBSD では 2007 年の NetBSD 4.0 で実装されました。-I
の方が実装が遅いというのは面白い話ですね。
移植性(POSIX)はどうなってるんだ?と思うかもしれませんが、挿入モードは POSIX xargs ではオプションの XSI 拡張機能、すなわち System V 系 Unix でのみ必須のオプションで、BSD 系 Unix では必須ではなく移植性はなかったものです。XSI 拡張の -I
(と -L
)は、現在の BSD 系 Unix では、すでにどちらも実装されているので POSIX の標準機能に変更しても良い気がしますね。
POSIXで「-e, -i, -l」が「-E、-I、-L」に変更された
初期の PWB/UNIX 版と System V 版にはこれだけのオプションがありました。
【PWB/UNIX版】
xargs [-ptx] [ -e[eof] ] [ -i[repl] | -l | -n[num] ] [ -s[size] ] [utility [args...]]
【System V版】(-l で行数を指定できるようになった)
xargs [-ptx] [ -e[eof] ] [ -i[repl] | -l[num] | -n[num] ] [ -s[size] ] [utility [args...]]
初期の BSD 系 Unix にはこれだけのオプションしか有りませんでした。
【4.3BSD (1990) 版】
xargs [-t] [-n num] [-s size] [utility [args ...]]
以下は無いもの
-p: プロンプトモード(実行前に実行してよいか聞くだけ)
-t: トレースモード(実行前に実行するコマンドを表示するだけ)
-e: 論理終了行
-i: 挿入モード
-l: 最大行数
【FreeBSD 1.0 (1993) 版】(-x が追加)
xargs [-t] [-n num [-x]] [-s size] [utility [args ...]]
初期の POSIX の xargs
の仕様は単純でこれだけのオプションしかありませんでした。
xargs [−t] [−n num [−x]] [−s size] [utility [args...]]
厳密には POSIX ではありませんが、後に Single UNIX Specification として POSIX に統合される XPG Issue 4, Version 2 では、拡張機能として初期の xargs
コマンドにあった -p
オプションに加えて -e
、-i
、-l
が復活しました。ただし廃止予定としての復活であり、それらを大文字にした -E
、-I
、-L
が新しい書き方として追加されています。
xargs [-t] [-n num [-x]] [-s size] [utility [args...]]
拡張機能: [-p] [-E eof] [-I repl] [-L num]
廃止予定: [-e[eof]] [-i[repl]] [-l[num]]
この流れを簡単に説明すると、xargs
の POSIX の仕様は 1977 年頃に作られたものが POSIX.1-2018 までの間に -e
、-i
、-l
が廃止され、同等の -E
、-I
、-L
に変化しただけなのです。
xargs [-ptx] [-E eof] [-n num] [-s size] [utility [args...]]
【XSI拡張】
xargs [-ptx] [-E eof] [-I repl | -L num | -n num] [-s size] [utility [args...]]
補足: POSIX.1-2001 (Issue 6) で -e, -i, -l はで削除、-p, -E は標準機能となった
-e
、-i
、-l
と -E
、-I
、-L
の違いは書式です。 -e
、-i
、-l
では省略可能なオプション引数が使われています。省略可能なオプション引数はオプション名とオプション引数に間にスペースを入れることはできません(例 -i
や -i{}
とは書けるが -i {}
は -i
と解釈される)。また省略可能なオプション引数は POSIX の引数のガイドラインに従っていません。
-e[eof]: eof は論理終了文字列。eof を省略すると論理終了文字列の機能は無効になる
-i[repl]: 挿入モードで挿入箇所に書く文字列。repl を省略すると {} を指定した意味となる
-l[num]: 行数。num を省略すると 1 を指定した意味となる
-E eof: 意味は -e と同じ。eof は省略できない。空文字を指定すると論理終了文字列の機能は無効になる
-I repl: 意味は -i と同じ。repl は省略できない
-L num: 意味は -l と同じ。num は省略できない
POSIX は小文字のオプションと同等の機能の少し異なるオプションを新たに大文字で追加して標準化するということがよくあります。POSIX では小文字のオプションは廃止されましたが、実際の実装は互換性を保つために残します。そのため GNU や System V 系の xargs
コマンドは同等の意味なのに互換性を保つ小文字のオプションと、POSIX で指定された大文字のオプションの二種類が残っているのです。POSIX のやりたいことも理解できますが、利用者から見れば同じようなオプションが2つもある、なんでや?となって混乱してしまいます。大文字だからといって必ずしも POSIX が標準化したオプションとは限りませんが、小文字と大文字で似たような機能のオプションがある場合は、小文字は互換性を保つためのもので、大文字が移植性のために POSIX が追加したオプションということになります。ちなみに POSIX が目指す所は異なる環境への移植性であり後方互換性ではありません。POSIX は後方互換性を実現可能な余地を与えるだけで、実際の後方互換性の実現は各 OS ベンダーに任せています。
なお、2024 年に完成する見込みの POSIX Issue 8 では -r
オプションと -0
オプションが追加されて以下のように拡張されています。ようやく POSIX で標準化された範囲でもまともにデータを扱えるようになったなぁという感じです。
xargs [-prtx] [-E eof | -0] [-n num] [-s size] [utility [args...]]
【XSI拡張】
xargs [-prtx] [-E eof | -0] [-I repl | -L num | -n num] [-s size] [utility [args...]]
POSIXはfor
ループを使えといって「挿入モード」を削除した
ここでいう「挿入モード」とは厳密には -i
オプションのことです。オプションの XSI 拡張機能の -I
のことではありませんが、-i
と -I
は本質的に同じものなので -I
も同じ理由が当てはまります。POSIX が -i
を削除した理由は POSIX xargs の RATIONALE に書いてあります。
Several other xargs options were removed because simple alternatives already exist within this volume of POSIX.1-2017. For example, the -i replstr option can be just as efficiently performed using a shell for loop. Since xargs calls an exec function with each input line, the -i option does not usually exploit the grouping capabilities of xargs.
訳: 他のいくつかの xargs
オプションは削除された。なぜなら POSIX.1-2017 本編の中にシンプルな代替方法がすでに存在するからである。例えば -i replstr
オプションはシェルの for
ループで同じように効率的に実行できる。xargs
は exec()
関数を行ごとに呼び出し、xargs
のグループ化機能を利用しないからである。
グループ化機能というのは、この記事で散々繰り返している、多すぎる引数をエラーにならない単位に分割して呼び出すことです。-i
/ -I
オプションは行ごとに一つの引数として実行するため、xargs
の本来の目的と関係ない機能で POSIX の言うグループ化機能を使いません。POSIX は xargs
の本質をちゃんと見抜いており、必要のない「挿入モード」を削除したのです。
だからね、-i
や -I
は xargs
コマンドにいらないものなんですよ。BSD 系 Unix の-J
は改良されており POSIX の言うグループ化機能を使うので含まれませんが、GNU 版や System V 版でしか使えません。また -i
/ -I
も -J
も実は別の書き方でもっと形で実装できるので必須というものでもありません。別の書き方はこの記事で後ほど説明します。
入力データの中に _
だけの行があると打ち切るのは便利やろ?
「便利そうだからデフォルトでこの機能を有効にしておくよ!」 と xargs
の開発者は考えたようです。いやぁ、とても便利ですね(皮肉)。
$ xargs << 'HERE'
arg1
arg2
_ 👈 この行でデータ読み取り終了、以下の行は無視
arg3
arg4
HERE
arg1 arg2 👈 2行目までしか処理されていない
この機能がデフォルトで有効なのは System V 系 Unix (Solaris、AIX など) です。便利そうだからと言って余計な機能を追加して複雑にし、さらにデフォルトにしないで欲しいです。今の人ならそう考えると思いますが、まあ 1977 年当時ですから、便利そうで機能を追加することもあるでしょう。
POSIX をよく読むと「-E
を指定しない場合の動作は、論理終了文字列が _
であるか、論理終了文字列の機能が無効であるかは指定されません」と書いてあります。つまりデフォルトではどっちの動作なのかわかりません。POSIX はこのように移植性がないものを探すために読むドキュメントです。POSIX に準拠してどの環境でも同じように動くように xargs
コマンドを使おうと考えるならば -E ''
を必ず指定しなければいけないということです。GNU 版や BSD 系 Unix では昔から無効なので知らなかった人も多いと思います。誰も論理終了文字列の機能なんて使おうと思わないでしょう? でもデフォルトで有効の環境があるわけです。しかし xargs
コマンドを使うときに -E
を指定して明示的に無効にしている例なんてまず見たことありません。移植性が重要と言いながら誰も POSIX の規格書を読んでいないことがわかります。正直私も読みたくありません。他の言語はすでに POSIX を気にせずどの環境でも動くアプリケーションを作れるようになっています。いずれシェルスクリプトでも同じように POSIX を気にしなくても良くなるでしょう。そうなれば GNU と BSD の違いとか POSIX の知識は不要になってしまいます。POSIX の規格書はアプリケーション(シェルスクリプト)の開発者が読むものではなく、言語の処理系やライブラリの開発者が読むものであるべきです。
ちなみに論理終了文字列の機能無効にするのではなく、逆に使いたい人は文字を指定する必要があります。FreeBSD 13.x までは -E
に文字を指定した場合に正しく動かないバグがありましたが(参照)、2023年11月にリリースされた FreeBSD 14.0 で修正されました。NetBSD にもそのうちマージされるでしょう。試してみればすぐに気づくようなバグにこれまで誰も気づいていなかったということは、この機能を使っている人がほとんどいなかったということなのでしょう。これからは論理終了文字列の機能は正しく動くよ、よかったね。
$ printf '%s\n' a b '' d e | xargs -E_
a b
$ printf '%s\n' a b 'c ' d e | xargs -E_
a b c
$ printf '%s\n' a b c d e | xargs -Ecc
a b
並列実行機能の -P オプションが追加された
xargs
本来の多すぎる引数をエラーにならないように分割する機能と全く関係ないのが -P
オプションです。誰かが引数の数を制限して実行できるなら、並列で実行できた方が便利だと考えたのでしょうね。
-P
オプションを指定する場合、引数の数に制限を設ける必要があります。例えば以下のような使い方は殆どの場合意味がありません。
$ find . -name "*.txt" | xargs -P 4 mycmd
なぜなら xargs
はコマンドライン引数のサイズ制限でエラーにならない多くの引数でコマンドを実行するからです。最初の方で私の Linux 環境では 8700個ぐらいの引数(ファイル名)でコマンドが実行されたと書きましたが、この mycmd
も同じようにサイズ制限を超えない限り1回しか呼び出されないので -P 4
で並列実行を指定しても無意味です。-P
オプションを使う場合、-n
オプションなどで引数の数を制限する必要があります。
$ find . -name "*.txt" | xargs -P 4 -n 1 mycmd
この時 mycmd
は一つのファイルの処理に時間が掛かるコマンドが望ましいです。例えばファイルの削除みたいなものを並列で実行しても、コマンドの起動時間が増えるだけで並列の効果はあまりありません。-n
オプションで引数の数を制限するとコマンド実行回数は多くなるので、パフォーマンスが低下する場合もあります。
要するに -P
オプションは xargs
コマンドの動きと実行するコマンドの特性をよく考えて、効果がある場合を見極めて使うものだということです。指定するだけでなんでも並列に実行して速くしてくれるものではありません。
xargs
の並列実行は確かに利用が簡単で便利なものですが、厳密に処理を制御しようとすると機能不足であることがわかります。例えばデータの順に標準出力を出力することは出来ませんし、各コマンドの標準エラー出力の内容や終了ステータスを個別に取ることは出来ません。ラッパースクリプトを作れば不可能ではありませんが、そのような機能を標準で持っていないというのは、結局のところ xargs
コマンドが並列実行のために作られたコマンドではないからです。個人的に多機能すぎてあまり好きではありませんが、並列実行のために作られた専用コマンドには GNU Parallel があります。そういったコマンドの利用を検討したり、シェルスクリプトであればバックグラウンドプロセスを使って並列実行を行うことも出来ます。簡単に使えるという点で(まだ)xargs -P
には価値がありますが、コマンドを並列化したいだけで、xargs
コマンドの複雑な仕様に悩まされることになるのを考えると、いずれはもっと良いコマンドに置き換えるべきでしょう。
xargsの難しい引数解釈の仕様を、誰もまともに説明しやしない
xargs
コマンドに関するトラブルは、標準入力から受け取ったデータの引数解釈の仕様をちゃんと理解してないことによって起こります。
しかしながら、xargs
の難しい引数解釈の仕様をちゃんと説明しているところなんてまず見たことがありません。ネットの記事だけではありません。書籍ですらまともに説明していません。誰も説明しないのは xargs
の引数解釈の仕様があまりにも難しくて誰も説明できないのだと思います。man xargs
でしっかりドキュメントを読めという考えかもしれませんが、実際の所 man xargs
をちゃんと読んでいる人なんていないでしょう?
多くの人が「動いてるからコレでいいやろ」という雑な考え方で使っているわけです。それでは空白を正しく扱えないのは当たり前です。そのような考え方で信頼性高いシェルスクリプトを書くことはできません。xargs
の難しい引数解釈の仕様を理解してから使いましょう。もっと良い提案があります。そもそも難しい xargs
、使うのやめませんか? やめないのであれば続きを読んでください。
最もトラブルが多い標準モード(デフォルト)の引数解釈
xargs
でいろんなオプションを指定しない標準モードの引数解析の仕様はもっともルールが複雑なものです。
xargs は空白・タブ・改行を区別せず区切り記号として扱う
おそらく多くの人がうすうす気づいている仕様だと思いますが、xargs
は行単位でデータを扱うコマンドではありません。Unix 哲学らしからぬ(?)仕様です。xargs
は空白・タブ・改行のいずれかの文字の連続(混ざっていても良い)を一つの区切り記号とみなして、区切り記号で区切られた項目を一つの引数として扱います。
$ printf 'a b c' | xargs printf '[%s]\n'
[a]
[b]
[c]
$ xargs printf '[%s]\n' << 'HERE'
a b
c
HERE
[a]
[b]
[c]
「xargs cmd
」で空白・タブ・改行を正しく扱う方法
もし標準モードで空白・タブ・改行を正しく扱うにはバックスラッシュでエスケープする必要があります。
$ printf '%s\n' 'a\ b\ c' | xargs printf '[%s]\n'
[a b c]
$ xargs printf '[%s]\n' << 'HERE'
a\ \ \ \ b\
\
\ \ \ c\
\
HERE
[a b
c
]
言うまでもなく ls
コマンドや find
コマンドはこのようなエスケープを行いません。この仕様は多くの人が何となく気づいてと思いますが「よし xargs
で正しく扱えるようにエスケープするぞ!」と考えて書いているようなコードを見ることは少ないです。
「xargs cmd
」でクォーテーションを正しく扱う方法
xargs
のデフォルトで空白・タブ・改行をそのまま扱えないのはよく知られていると思いますが、実際にはシングルクォーテーションとダブルクォーテーションもそのままでは扱えません。
$ xargs printf '[%s]\n' << 'HERE'
'a a a'
HERE
[a a a]
$ xargs printf '[%s]\n' << 'HERE'
"b b b"
HERE
[b b b]
これをみて「空白やタブが含まれる場合はクォーテーションで括ればいいんだ」と考えると思います。間違いではないのですが改行には通用しません。
$ xargs printf '[%s]\n' << 'HERE'
'a a
a'
HERE
xargs: unterminated quote
$ xargs printf '[%s]\n' << 'HERE'
"b b
b"
HERE
xargs: unterminated quote
この例からわかるように xargs
のクォーテーションの解釈はシェルのクォーテーションの解釈とは異なります。似てる部分もあります。例えばシングルクォーテーションで括られた中にシングルクォーテーションを入れることはできないので、一旦シングルクォーテーションで文字列を閉じて、別の方法でシングルクォーテーションを入れる必要があります。
$ xargs printf '[%s]\n' << 'HERE'
'a a \' a'
HERE
xargs: unterminated quote
補足: 一旦閉じる必要がある
$ xargs printf '[%s]\n' << 'HERE'
'a a '\'' a'
HERE
[a a ' a]
ダブルクォートの中にダブルクォーテーションを入れることもできませんが、シェルとは異なりバックスラッシュでダブルクォーテーションをエスケープしようとしても無意味です。
$ xargs printf '[%s]\n' << 'HERE'
"a a \" a"
HERE
xargs: unterminated quote
補足: 一旦閉じる必要がある
$ xargs printf '[%s]\n' << 'HERE'
"a a "\"" a"
HERE
[a a " a]
ダブルクォートの中に $
や "
はそのまま入れることができます。またバックスラッシュもダブルクォートの中にそのまま入れることができます。これらもシェルのダブルクォートとは異なる仕様です。
だから xargs
の引数解釈の仕様はシェルの引数解釈の仕様とは別物なのです。
xargs
の引数サイズは 2048 バイト以下
Solaris 11 のコマンドライン引数のサイズ制限はおよそ 2MB ですが、xargs
コマンドの引数サイズの制限は 2048 バイト(正確には LINE_MAX
)以下です。-s
オプションを指定しても増やすことはできません。
$ printf '%02048d' 0 | xargs
xargs: A single arg was greater than the max arglist size of 2048 characters
$ printf '%02048d' 0 | xargs -s 3000
xargs: 0 < max-cmd-line-size <= 2048: 3000
$ getconf LINE_MAX
2048
POSIX でもそのように定義されています。
一般的なコマンドは xargs
形式での出力に対応していない
xargs
コマンドの標準モードの入力形式はこのように複雑です。このような xargs
形式で出力する一般的なコマンドはありません。つまり ls
コマンドや find
コマンドなど、文字列に空白やクォーテーションが入る文字列を、そのまま xargs
コマンドに読み込ませるとたいがいトラブルになるということです。
「xargs
形式で出力する一般的なコマンドはありません」と言いましたが、一つだけ例外があります。それは NetBSD 版の find
コマンドです。NetBSD 版の find
コマンドには -printx
という xargs
で安全に使える形式で出力する評価式があります。しかし -printx
は FreeBSD でも OpenBSD でも使えません。NetBSD を使っている人だけが使える機能です。
xargs
コマンドをお手軽に使っている例をよく見かけますが、潜在的なバグが含まれていることがザラであり「まあ大概のデータならそれで動くよね。少し特殊なデータが含まれると動かないけど。」というものばかりです。
行数指定時(-l
, -L
)の追加の特別仕様、末尾の空白・タブは継続行
-l
または -L
を指定すると、引数の解釈を「指定した行数ごと」に打ち切ってコマンドを実行します。
$ xargs -L1 echo "line" << 'HERE'
foo 100
bar 200
HERE
line foo 100 👈 1行ごとに1回のechoコマンドの実行
line bar 200 👈 1行ごとに1回のechoコマンドの実行
本当に「指定した行数ごと」だと思いましたか? 特別な追加仕様があります。行の末尾に空白・タブがある場合は、次の行に継続しているという意味となります。
$ xargs -L1 echo "line" << 'HERE'
foo 100 👈 末尾に空白があると次の行をつなげる
bar 200
HERE
line foo 100 bar 200 👈 1行扱いなので1回のechoコマンドの実行
一行(指定数行)ごとにコマンドを実行したいけど、たまに次の行に分けたいと言うときに便利ですね(皮肉)
理解に苦しむ仕様ですが、作られた当時が 1977 年ですから、他のコマンドが長すぎる行を扱えなかったとか、テレタイプで紙に印刷しなければいけなかった都合上、きっと当時はこのような仕様が妥当だったのでしょう。
挿入モード(-i
, -I
-J
)の引数解釈は標準モードとは違う
さて次は挿入モードです。これも気軽に使われていますが、ちゃんと引数の仕様わかっているのかなぁという使われ方ばかり見かけます。
挿入モード(-i
, -I
) では、一行が一引数になる
挿入モードは標準モードよりも引数の解釈は簡単です。基本は一行が一引数です。空白やタブが入っていても区切られることはありません。引数の解釈は簡単になっていますが標準モードと解釈の違いがあるので混乱します。なんでこんな違いを作っちゃったんでしょうね?
$ xargs -i printf '[%s]\n' {} << 'HERE'
foo bar
baz
HERE
[foo bar]
[baz]
-i
は BSD 系 Unix では使えません。-I
を使う必要がありオプション引数は省略できません。伝統的に System V 系 Unix では -i
を使用しており置き換える文字はデフォルトの {}
が使われていたという経緯から -I{}
がよく使われていますが別の文字でも構いません。
$ xargs -I{} printf '[%s]\n' {} << 'HERE'
foo bar
baz
HERE
[foo bar]
[baz]
挿入モード(-i
, -I
)は末尾と途中の空白・タブは無視しないが先頭は無視する
この違い、覚えておきましょう。
$ xargs -I{} printf '[%s]\n' {} << 'HERE'
foo bar 👈 末尾に空白がある
HERE
[foo bar ]
挿入モード(-i
, -I
)は標準モードよりも大幅に遅い
標準モードではコマンドライン引数のサイズ制限を超えない可能な限り多くの引数を渡してコマンドを実行しますが、挿入モードでは基本的に一行ごとにコマンドを実行します。コマンドの起動は遅い処理なので、引数が多い場合に挿入モードを使うと大幅に遅くなる場合があります。
$ time seq 10000 | xargs echo > /dev/null
real 0m0.012s
user 0m0.010s
sys 0m0.006s
$ time seq 10000 | xargs -I{} echo {} > /dev/null
real 0m7.975s
user 0m6.387s
sys 0m1.754s
挿入モード(-i
, -I
)でもクォーテーションの扱いは標準モードと同じ
挿入モードでもクォーテーションの仕様は標準モードと同じです。同じ話なので説明は省略します。
挿入モード(-i
, -I
) で使える {}
は 5つまで
挿入モード(-i
, -I
) で使える {}
は 5つまでです。 xargs
が作られたのがはるか昔ですからね。当時の実装上の制限です。この仕様が残っているのは BSD 系 Unix と System V 系 Unix で GNU 版にはありません。BSD 系 Unix では -R
オプションを指定すると置き換える数を変更することが出来きます。
$ echo 1 | xargs -I{} echo {} {} {} {} {} {}
1 1 1 1 1 {}
$ echo 1 | xargs -I{} -R 10 echo {} {} {} {} {} {}
1 1 1 1 1 1
$ echo 1 | xargs -I{} -R -1 echo {} {} {} {} {} {} # -1: 無制限
1 1 1 1 1 1
$ echo 1 | xargs -I{} echo {} {} {} {} {} {}
xargs: too many args with {}
まあ普通は 5 つも使わないのでこの制限が問題になることはないでしょう。
挿入モード(-i
, -I
) の {}
で展開できる文字数は255文字未満
挿入モード(-i
, -I
) の {}
で展開できる文字数は 255文字未満です。 xargs
が作られたのがはるか昔ですからね。当時の実装上の制限です。この仕様が残っているのは BSD 系 Unix と System V 系 Unix で GNU 版にはありません。BSD 系 Unix では -S
オプションを指定すると展開できる文字数を変更することが出来ます。
$ printf '%0255d' 0 | xargs -I{} echo {}
xargs: command line cannot be assembled, too long
$ printf '%0255d' 0 | xargs -S 1000 -I{} echo {}
000 ...省略... 000
$ printf '%0255d' 0 | xargs -I{} echo {}
xargs: Maximum argument size with insertion via {}'s exceeded
BSD 系 Unix の挿入モード(-J
) では空白で区切られる
BSD 系 Unix の挿入モード(-J
) は、おそらく -i
や -I
の一貫性のない仕様という大きな問題点を解決するために作られています。つまり -i
や -I
よりも -J
の方が優れているということです。
-i
や -I
と比較しての「素晴らしい点」ですが、-J
は {}
は一つしか使えません。また {}
の前後に文字をつけることができません。単独の引数として {}
を使う必要があります。-i
や -I
では例えば以下のような良くない書き方ができてしまったのですが、-J
ではそれができないようになっています。-i
や -I
にあった 255 文字未満までしか展開できないという制限もありません。
$ seq 5 | xargs -I{} sh -c 'echo {}' # 良くない書き方
$ seq 5 | xargs -J{} sh -c 'echo "$@"' sh {} # 適切な書き方
そして最も素晴らしいのは標準モードと同じように引数を解釈することです。
$ xargs printf '[%s]\n' << 'HERE'
foo bar
baz
HERE
[foo]
[bar]
[baz]
$ xargs -J{} printf '[%s]\n' {} end << 'HERE'
foo bar
baz
HERE
[foo]
[bar]
[baz]
[end]
$ xargs -I{} printf '[%s]\n' {} end << 'HERE'
foo bar
baz
HERE
[foo bar]
[end]
[baz]
[end]
標準モードと一貫性があるため -i
や -I
よりも -J
を使うべきなのですが、残念なことに現時点では BSD 版 Unix 専用の機能で GNU 版や System V 系 Unix では使えません。
最も簡単なヌル文字終端形式(-0
)を使え
ということで、ここまで見てきたように昔からある xargs
の標準モードや、-l
または -L
を指定した行数指定や -i
または -I
または -J
の引数解析の仕様は、xargs
を簡単に扱うには難しいものです。
なんで、こんな仕様になってしまったのか? やっぱり 1977 年製という古いコマンドだからでしょうね。古いからと言って必ずしもダメというわけではありませんが、xargs
の仕様は一貫性がなく他のコマンドと組み合わせて使えるような仕様になっておらず、当時の一般的なユースケースに限ってたまたま使えていたというべきでしょう。それもごく普通の人がコンピューターを使うようになり、ファイル名にスペースやクォーテーションなどを使いだして破綻しました。
そこで新しく作られたのがヌル文字終端形式の -0
です。-0
を使うと複雑な引数解釈の仕様がなくなり、区切り文字(正確には終端文字)はヌル文字(\0
)のみになります。
ヌル文字終端形式は POSIX で標準化された
ヌル文字終端形式は POSIX Issue 8 (POSIX.1-202x) で標準化されました。POSIX.1-202x はまだ完成していませんが、現在機能が完成された Draft 4 の段階まで来ているので来年中(2024年中)には完成するでしょう。つまり今使えるならば、使って構わないということです。
おかしな使い方や間違った使い方が xargs の理解を妨げる
xargs
は引数を拡張するコマンドです。しかし別の目的に使うこともできます。別の目的に使うことができますが、そのような使い方が広まることで、xargs
への理解が妨げられるようになります。
引数を標準入力で指定することを目的にしない
なぜこんなことをしてるのかよくわからない xargs
の使い方は、たった一つの引数をわざわざ xargs
を使って指定する書き方です。次のような例です。
$ echo "http://example.com" | xargs curl
このような使い方は全く意味がありません。次のように書けば良いからです。
$ curl "http://example.com"
コードの意味を分かりづらくする原因は、必要とは思えないようなものを付け加えることです。(後から読む自分自身を含めた)コードの読者はなんのためにこんな無駄なことをしてるんだ?と悩むことになります。
その他の例えとして拡張子を jpeg
から jpg
に置き換えるコマンドの例を示します。次の書き方はごく自然ですね?
$ mv image.jpeg image.jpg
元は ksh93 の拡張機能で、bash、mksh、yash、zsh、OpenBSD sh などで広く採用されたブレース展開の機能を用いると、次のようにさらに簡潔に書くことができます。
$ mv image.{jpeg,jpg}
シェルスクリプトで変数を使う場合次のように書くこともできます。これは POSIX で標準化された機能なので、すべての sh で動作します。
from="image.jpeg"
mv "$from" "${from%.jpeg}.jpg"
これを無駄に xargs
を使った書き方はこのようになります。
$ echo "image.jpeg" | sed -E 's/(.*)\.jpeg$/& \1.jpg/' | xargs mv
はっきりいって、こんな書き方はダサいですよ。パフォーマンスが悪いと言いたくなりますが、そんな些細なパフォーマンスよりも明らかにわかる「コードが無駄」と指摘しなければならないですね。他にも引数解釈のルールに照らし合わせれば、ファイル名にスペースやクォートなどが入った時に問題なく動きますか? と問いたくなります。
最小限でわかりやすく、問題の置きない書き方をしましょう。頭を悩ませるコードを書いてはいけません。コマンドは引数で指定できるようになってるのですから、引数で指定しましょう。
無駄にかっこいい使い方でこのようなものを見かけました(実際にはもっと複雑で、別のコマンドからファイル名を出力しています)。さてこの見慣れない -o
オプションはなんのために必要なのでしょうか?
$ echo "file.txt" | xargs -o vi
答えは、次のようなエラーが出るのを防ぐためです。
$ echo "file.txt" | xargs -o vi
Vim: Warning: Input is not from a terminal
そんな説明が必要なことをするぐらいなら、素直に書きましょう。
$ vi $(echo "file.txt")
もちろん別のコマンドからファイル名を出力しないのであればこれでよい
$ vi "file.txt"
xargs は複数行を一行にまとめることはできない
xargs
は複数行を一行にするためにたびたび使われています。
$ seq 5
1
2
3
4
5
$ seq 5 | xargs
1 2 3 4 5
便利だと思いますし、わかった上で使うのは構いませんが、この使い方は xargs
の本来の使い方、多すぎる引数を分割してコマンドを呼び出す場合にうまくいきません。
まず xargs
は呼び出すコマンドを省略したときに echo
コマンドを実行します。以下の実験から、GNU xargs は実際に外部コマンドの echo
コマンドを呼び出し、それ以外では内蔵の echo
コマンドを呼び出しているようです。
$ seq 5 | PATH=/dev/null /usr/bin/xargs # GNU xargs ではエラーになる
/bin/xargs: echo: No such file or directory
$ seq 5 | PATH=/dev/null /usr/bin/xargs # macOS、BSD 系・System V系
1 2 3 4 5
xargs
コマンドは複数の引数にして echo
コマンドを呼び出しているだけで、複数の引数をスペース区切りで出力するのは echo
コマンドの機能です。この方法で複数行を一行にまとめることができる・・・ように思えます。
$ seq 5 | xargs
| | |
+-----+ +---- xargs は複数行の入力を得て
1 それぞれを一つの引数としてコマンドを呼び出す
2 echo 1 2 3 4 5
3
4
5
seqコマンドは上記のように複数行で出力する
確かに多くの場合はこの方法で一行にできるでしょう。しかしこれは本来の xargs
の使い方ではありません。xargs
の本来の使い方は「多くの引数を分割して『複数回』のコマンド呼び出しに変換すること」です。つまり echo
コマンド呼び出しが『複数回』になる場合は 1 行にはなりません。
$ seq 10000 | xargs | wc -l # xargs は一行にするコマンド?
1
$ seq 100000 | xargs | wc -l # xargs では 1行にできない
5
$ seq 1000000 | xargs | wc -l # 10万行は53行になる
53
xargs
を使って引数を一行にまとめるというのは、本来の目的である「引数の拡張」が行われない場合にのみ成立する裏技的な使い方です。しかしですね、引数の拡張が必要ないのであればコマンド置換を使って書くこともできます。
seq 10 | xargs # seq + xargs + echo で実現
echo $(seq 10) # seq + echo のみで実現
文字数は変わりませんが xargs
コマンドを使って一行にするというのは、実は xargs
コマンドをわざわざ使わなくても良い例です。またメモリ使用量も変わりません。xargs
は標準入力から読み取ったデータをメモリに溜めてからコマンドを呼び出すからです。
ちなみにちゃんと一行にするには次のように paste
コマンドを使います。
$ seq 1000000 | paste -sd ' ' - | wc -l # 10万行でも1行になる
1
正直な所 paste
コマンドも適切なコマンド名とは思えませんが、他に適切な名前のコマンドはありません。他にもやり方はあるかもしれませんが、おそらく専用のコマンドを作らない限り xargs
よりも長くはなるでしょう。そういう理由もあって、複数行を「だいたい」一行にできるコマンドとして xargs
がお手軽な方法として使われています。
「xargs -n2」は「引数を2個ずつコマンドに渡す」ではない
標準入力からのデータをペアにして -n 2
を指定して 2個の引数でコマンドを呼び出すとします。例えばこのような例です。
$ find . -name '*.jpeg' | sed -E 's/(.*)\.jpeg$/& \1.jpg/' | xargs -n2 mv
意味
- 拡張子 jpeg のファイルを列挙し
- sed で一つのファイルを foo.jpeg foo.jpg という2つの引数を作り出し
- xargs -n 2 を指定することで、
- mv foo.jpeg foo.jpg のように実行する
ファイルパスならおそらく問題ないでしょう。しかし、-n 2
の正しい意味は「2個ずつ渡す」ではなく「渡す数を最大2個に制限する」 です。したがって以下のように1個で呼び出される場合があります。
$ printf "%01000d\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
2
$ printf "%02000d\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
1
1
$ printf "%065520d\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
2
$ printf "%065550d\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
1
1
$ printf "%0522000d\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
2
$ printf "%0523000dd\n" 1 2 | xargs -n2 sh -c 'echo $#' sh
1
1
-n 2
が必ず 2個ずつ呼び出すものであると考えているとデータによってはずれてしまうことがあるということです。もちろん引数のサイズが十分小さければ「だいたい動くはず」 です。しかし環境ごとに異なるさまざまな制限によって必ず動くという保証はありません。
だからですね。xargs
の本来の目的とはサイズ制限を超えるものを分割して呼び出すのであって、-n
で指定した数ごとで呼び出すのではなく、-n
は引数の最大個数の指定なのです。厳密には xargs
はいくつの引数ごとに呼び出すか指定できないものと考える必要があります。
「xargs -L 2」は「2行毎に呼び出す」ではない
-n
と同じ話です。-L
(-l
も同様)もサイズ制限があるので「2行毎に呼び出す」ではなく「最大2行ごとに呼び出す」です。それに加えて行末に空白やタブがある場合は次の行に継続するという仕様がありましたね。
「xargs -I{} sh -c 'cmd {}'
」なんて書いてはいけない
しばしば見かけますが、これは良くない書き方です。-0
を指定して xargs -0 -I{} sh -c 'cmd {}'
のような書き方をしても駄目です。なぜならシェルのメタ文字を正しく扱えないからです。
$ printf '%s\0' 'a"b' | xargs -0 -I{} sh -c 'echo {}'
sh: 2: Syntax error: Unterminated quoted string
このような書き方は、コードを文字列で組み立てていることになるので、実質 eval
を使っているのと同じなのでとんでもないことが起こり得ます。
$ printf '%s\0' 'a"b' | xargs -0 -I{} sh -c 'echo {}'
は
$ sh -c 'echo a"b'
を実行する
$ printf '%s\0' 'foo > bar' | xargs -0 -I{} sh -c 'echo {}'
は
$ sh -c 'echo foo > bar'
を実行するからファイルに書き込む
じゃあどうすればよいか? {}
は単独の引数にしてください。{}
はクォートして '{}'
や "{}"
と書いても構いません。
$ printf '%s\0' 'a"b' | xargs -0 -I{} sh -c 'echo "$1"' sh {}
a"b
ちなみにBSD 版 Unix の改良された -J
オプションは {}
は単独の引数にしかできません。
$ printf '%s\0' 'a"b' | xargs -0 -J{} sh -c 'echo {}'
{}
だから、安全で優れているのです。
「xargs -I{} sh -c 'cmd "$0"' {}
」のように$0
を使ってはいけない
$0
を引数の代わりに使ってはいけません。これは xargs
というよりも sh
の仕様なのですが $0
はコマンド名を入れるところであって引数を入れるところではありません。
# 良くない書き方
xargs -I{} sh -c 'echo "$0"' {}
# 良い書き方
xargs -I{} sh -c 'echo "$1"' sh {}
なかなか不思議な仕様なのですが、シェルで -c
を指定してシェルスクリプトコード(下記の command_string)を文字列で指定した場合、引数(argument)の前にコマンド名(command_name)を要求します。$0
はコマンド名であって引数ではありません。
sh -c [options] command_string [command_name [argument ...]]
$0
を引数として使うと上手く行かない例を紹介します。
# FreeBSD の場合
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' {}
# => 何も出力されない
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' - {}
# => [-n]
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' -- {}
# => [-n]
# Linux などの場合
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' {}
# => [-n]
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' - {}
# => [-]
printf '%s\n' '-n' | xargs -I{} sh -c 'echo "[$0]"' -- {}
# => [--]
$0
はエラーメッセージでコマンド名として使われます。コマンド名には適切と思われる名前(ただしハイフンで始まらない名前)を指定することでエラーメッセージが適切なものになります。
$ echo 12345 | xargs -I{} sh -c '() "[$0]"' {}
12345: 1: Syntax error: ")" unexpected
$ echo 12345 | xargs -I{} sh -c '() "[$1]"' myprog {}
myprog: 1: Syntax error: ")" unexpected
他にも $0
は $@
に含まれないなどの問題があるので、$0
を引数を入れるところとして使わないようにしましょう。
bash 専用のシェル関数の呼び出し方
特にシェルスクリプトでは、xargs
コマンドでシェル関数を呼び出すという使い方できます。基本的な書き方は次のようになります。
#!/usr/bin/env bash
func() {
for i in "$@"; do
echo "arg: $i"
done
}
# 子プロセスのbashで指定した関数を使えるようにエクスポートする
# エクスポートされた関数は関数の中身が文字列として環境変数に設定され
# 子プロセスのbashが環境変数を参照して自動的にシェル関数を定義する
export -f func
# bash 専用機能なので bash コマンドを実行する
seq 5 | xargs bash -c 'func "$@"' myprog
-I
オプションを使う場合の書き方は次のようになります。この例ではデータとして数値しか生成されないので問題ありませんが、すでに述べたように一般論として「xargs -I{} bash -c 'cmd {}'
」は良くない書き方です。
# -I オプションを使うときの書き方
seq 5 | xargs -I{} bash -c 'func "$@"' myprog {}
# 推奨しない(空白やクォーテーションなどが含まれない前提でのみ使える)
seq 5 | xargs -I{} bash -c 'func {}'
そもそも挿入モード(-i
, -I
)を使わない
そもそも挿入モード(-i
, -I
)は、多すぎる引数をエラーにならない単位で分割することをせず、一行ごとに実行するのでパフォーマンスが悪いです。引数を途中に挿入したいのであれば sh
コマンドを使って実現できます。
$ find . -name '*.txt' | xargs sh -c 'mv "$@" /tmp/trash' sh
ファイルを一つづつリネームする例(foo.jpeg
を foo.jpg
に変更する例)
$ find . -name '*.jpeg' | sed -E 's/(.*)\.jpeg$/& \1.jpg/' | xargs -n2 mv
は、以下のように書きます。
$ find . -name '*.jpeg' | xargs -n1 sh -c 'mv "$1" "${1%.jpeg}.jpg"' sh
もっと良い書き方
$ find . -name '*.jpeg' -print0 | xargs -0 -n1 sh -c 'mv -- "$1" "${1%.jpeg}.jpg"' sh
シェルスクリプトだと for
や while
ループを使って書くこともできます。頭を悩ませるコードよりも愚直なコードを書きましょう。
# for を使った例(補足: in の部分にコマンドライン引数のサイズ制限はない)
for file in *.jpeg; do
mv -- "$file" "${file%.jpeg}.jpg"
done
# while を使った例
find . -name '*.jpeg' | while IFS= read -r file; do
mv -- "$file" "${file%.jpeg}.jpg"
done
# while を使ったもっと良い例(POSIX Issue 8 で標準化されたが使えないシェルがある)
find . -name '*.jpeg' -print0 | while IFS= read -d '' -r file; do
mv -- "$file" "${file%.jpeg}.jpg"
done
シェルスクリプトで書くとパフォーマンスが下がるのではないかと思うかもしれませんが、シェルスクリプトのパフォーマンスの問題は外部コマンドの実行回数(とサブシェル)です。この場合は外部コマンド(mv
コマンド)の実行回数は同じなのでパフォーマンスに影響はありません。これが POSIX で挿入モードが不要とされた理由です。
xargs が速いのではなく外部コマンドの実行回数で速度が決まる
xargs
は速いとときどき言われますが、実際には xargs
を使うかどうかではなく、外部コマンドの実行回数が速度を左右しています。次の test1.bash
と test2.bash
は外部コマンド(touch
コマンド)の実行回数が同じなので速度の差はほとんどありません。
#!/usr/bin/env bash
for i in file{0000..9999}.txt; do
touch "$i"
done
#!/usr/bin/env bash
seq -f 'file%05g' 0 9999 | xargs -I{} touch {}
$ time ./bash test1.bash
real 0m8.353s
user 0m6.525s
sys 0m2.110s
$ time ./test2.bash
real 0m8.195s
user 0m6.511s
sys 0m1.788s
コマンドの実行回数を減らせば、どちらも同じように速くなります。
#!/usr/bin/env bash
touch file{0000..9999}.txt
#!/usr/bin/env bash
seq -f 'file%05g' 0 9999 | xargs touch
$ time ./test3.bash
real 0m0.068s
user 0m0.031s
sys 0m0.038s
$ time ./test4.bash
real 0m0.063s
user 0m0.022s
sys 0m0.044s
理由よくわからないけど xargs
を使えば速くなるんだのように考えるのはやめて、速くなる理由まで調べるようにしてください。
データがないときにコマンドを実行したくない場合は-r
を指定しろ
xargs
の本来の目的は多すぎる引数を実行するときに分割してコマンドを実行するものです。では引数が全く無い場合、コマンドは実行するべきでしょうかしないべきでしょうか?
すべてのコマンドは引数がなくても実行できるので xargs
を介したとしても実行できるのがあるべき仕様です。これは最初の xargs
コマンドのバージョンからそういう仕様です。その結果、ファイルが見つからない場合はエラーになります。
$ find . -name '*.jpeg' | xargs touch
touch: missing file operand
Try 'touch --help' for more information.
しかし、エラーになるのは GNU 版と System V 系 Unix です。意図的なのか知りませんが、BSD 系 Unix ではファイルが見つからない場合はコマンドが実行されないように仕様を変えたようです。-r
オプションを指定すると BSD 系 Unix と同じようにファイルが見つからない場合にコマンドが実行されなくなります。
$ find . -name '*.jpeg' | xargs -r touch
-r
は POSIX Issue 8 (POSIX.1-202x) で標準化されたオプションです。標準化されたばかりでまだ実装されていない場合がある? それならば find
コマンドを使用しましょう。find
コマンドは見つけたファイルに対してコマンドを実行するという xargs
の本来の目的とは違うものなので、ファイルが見つからない場合はコマンドを実行しません。
並列実行で -P 0
を使わない、-n
でサイズを指定しろ
-P 0
はできる限り多くのプロセスを起動するオプションです。データが多いと大量のプロセスが起動して負荷をかけすぎてしまうことがあります。また -n
(-l
でも可)でサイズを指定しないと、大量の複数の引数を一つのプロセスに渡すだけになってしまい、並列実行が行われない可能性があります。
正しい使い方を知るほど、ダメな xargs
の使い方が目につく
ここまで読んだ人なら、もうとっくに xargs
の仕様にうんざりしていると思います。xargs
を理解すればするほど、ネットの記事やシェルスクリプト関係の書籍の雑な xargs
の使い方 が目に付きます。書いている本人は「この例では上手く動くからいいやろ」なのでしょう。
で、その例を見て、同じように真似て書いた人が、空白やクォートが含まれたデータや長い文字列を扱って罠にハマるわけです。
もちろん「今回あつかうデータは問題ない」という前提であれば、雑な xargs
の使い方をしても構わないのですが、それができるのは罠を知っている人だけです。扱うデータに問題があるかないかを判断できる人だけが、手を抜いた xargs
の使い方ができます。
xargs
の正しい使い方はこれだけだ!
xargs
を正しく使うのは容易ではありません。簡単に使う方法をまとめましょう。
脱xargs! - 可能な限りxargsを使わずに「find -exec {} +」を使え
xargs
の使い方と言っておきながら、xargs
を使わない方法を書くのは矛盾していますが、xargs
を使わないでいいなら使うのはやめましょう。
xargs
は 1977 年に、コマンドライン引数のサイズ制限の問題を回避するために、一貫性がなく使いやすくもない仕様で作られたものです。それよりも改良された新しい find
コマンドの -exec {} +
のほうが優れています。書き換えは組み合わせるコマンドを一つ減らすだけの簡単なものです。以下の例を参照してください。
$ find /tmp/files -type f | xargs touch
| | | |
+-----------------------+ +---+
標準出力で出力するコマンド xargs xargs が呼び出すコマンド
$ find /tmp/files -type f -exec touch {} +
| | | |
+-----------------------+ +--------+
標準出力で出力するコマンド find が呼び出すコマンド
この find -exec {} +
の使い方には、空白・タブ・改行の問題も、クォートの問題も、パフォーマンスの問題も、ファイル数が多すぎる場合の問題もありません。ファイルが一つもない場合にコマンドが実行される問題もありません。ファイルを扱う場合にしか使えないという制限はありますが、引数が多すぎる場合のほとんどはファイルを扱う場合です。ファイルを扱う場合は find
コマンドから xargs
につなぐ代わりに -exec
を使うだけです。
xargs を使うなら「xargs -E '' -r -0
」ぐらいは指定しよう
どのオプションを指定するかは要件次第ですが、入力データが安全だという保証がある場合を除きこれぐらいは指定しておくべきでしょう。論理終了文字列の機能を無効化して、データがない場合はコマンドの実行しないようにして、引数の区切りはヌル文字終端です。こららを使わない場合は、xargs
の仕様をよく理解して使う必要があります。
いちいち書くのがめんどくさい? そんなもの alias
やシェル関数を使えば簡単です。
xargs() {
command xargs -E '' -r "$@"
}
find . -name "*.txt" | xargs -0
挿入モード -i
や -I
を使うのは控えましょう。使用する場合は仕様を理解してパフォーマンスに十分注意しましょう。{}
は単独の引数にします。「xargs -0 -I{} sh -c 'echo {}'
」のようなコードを文字列で組み立てる使い方はダメです。
シェルスクリプトでは for や while を使おう
必要もないのに無理に xargs
コマンドで頑張る必要はありません。xargs
コマンドの目的は引数が多すぎる場合に分割することです。引数が少ないのであれば、ほぼ確実に xargs
を使う必要はありません。特に -i
や -I
を使うのであれば、for
や while
で十分です。
xargs
コマンドを使うときに頑張って考えていませんか? 頑張って考えたコードは難しいコードだということです。後で読み返したときに何をやっているのか分からなくなります。賢いコードを書こうとせずにまずは愚直なコードを書きましょう。パフォーマンスはそれが重要なときに考慮しましょう。
さいごに
xargs
コマンドにはいくつもの問題があります。xargs
コマンドに変わる新しいコマンドを作るべきです。引数解析の仕様はなくして、行指向の一行一引数かヌル文字終端のデータ形式のみ、挿入モードは BSD 系 Unix の -J
ベースに変更。並列化処理は別のコマンドに分離(注意 GNU Parallel がすでにある)。余計な機能やナンセンスな制限はなくしてシンプルなコマンドにすべきでしょう。
xargs
の仕様を完全に理解するのはまず無理ですので、xargs
をなるべく使わないことを目標にしましょう。さて最後の最後に一言、この記事は間違いなく「xargs
完全理解マニュアル」です。詳しくは「ダニングクルーガー効果」とやらを調べてください。