21
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シェル芸Advent Calendar 2023

Day 21

xargs 完全理解マニュアル - xargs は拡張引数 (extended arguments) の略って知っていますか?

Last updated at Posted at 2023-12-21

はじめに

xargs コマンドは「なにか凄そうだけどよく分からないコマンド」としてよく知られています。使う人は使うけど何をやっているのか全くわからないコマンドです。また、やっていることがわかっても実際に使ってみると、空白やクォーテーション文字でエラーになってしまう使い方がとても難しいコマンドです。この記事はそういうよくわからない xargs はどういうコマンドなのか解説します。この記事を読むと xargs を「完全に理解した」と言えるようになるでしょう。

xargs コマンドが難しい理由は、xargs 自体の設計や実装の問題で古い時代の制限が多いからです。仕様が意味不明で一貫性がなくで他のコマンドと正しく連携するのが困難です。そして本来の目的と違う用途に流用されてばかりです。最初にこの記事の結論を書いておきます。

  • xargs は難しすぎるコマンド、可能な限り使うな
    • 昔と違い今はそんなに重要で必要なコマンドではなくなった
  • ネット・書籍に載っている xargs の使い方の多くは雑で「たまたま動いている」だけ
    • シェルスクリプトで空白を扱えない問題の 1/3 は xargs の雑な使用が原因
  • 特にシェルスクリプトの場合、必要ないなら 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 の本来の「目的」なのです。

10万個の引数を渡して ls コマンドを呼び出すとエラーになる(この問題を解決するのが「目的」)
$ 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 はなく whilegoto は外部コマンドだった
    • シェル言語の機能が貧弱な時代に必要だったもの
  • 当時のコマンドライン上のファイル名展開は 512 バイト(後に 5120 バイト)だった
  • 最大 512 バイトのファイル名展開が、そもそも xargs を書いた理由だった
  • -i オプションなどの機能は便利そうだからと後から追加した
  • 仕様や制限は、当時「これで十分だろう」という理由によるもの

難しく考える必要はありません。xargs コマンドは当時のプログラマの一人が、コマンドライン引数に多くの引数を指定したらエラーになって実行できないという切実な問題を解決するために作ったものです。そしてありがちな話として取りあえず動くものを作り後で便利そうな機能を追加した一つのコマンドでしかありません。出来が良いとは言えないものが、大きく改良されることもなく今も使われ続けているのです。

引数が多すぎるなら分割してコマンドを何回も実行すればいいじゃない

さて、コマンドライン引数が多すぎてエラーになる場合、人間だったらどうやってこの問題を解決するでしょうか?

10万個の引数を渡してエラーになる問題をどうやって解決する?
$ ls file{000001..100000}.txt
-bash: /bin/ls: 引数リストが長すぎます

簡単ですね? 引数の数を分割してコマンドを何回も実行すればよいのです。

5万個の引数で2回の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 コマンドは標準入力からデータを受け取り引数の形に変換して実行するという機能を持っています。

xargsは標準入力からからデータを指定したコマンドの引数に変換する(簡易版)
    標準入力のデータを引数に変換してコマンドを呼び出す変換器
                     |
$ find . -type f | xargs touch
|              |         |   |
+---+----------+         +-+-+
    |                      |
標準出力で出力するコマンド   xargsが呼び出すコマンド  
./file0163.txt               touch ./file0163.txt ./file1288.txt ./file0953.txt ・・・
./file1288.txt    ↗
./file0953.txt
 ︙

xargsの入力データ形式は行指向ではなく複雑な「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 を使わずに多くの引数を扱う方法を思いつくのではないでしょうか?

次のようにすれば xargs の使用をなくせるのでは?
find /tmp/files -type f | touch

# なぜ xargs が必要なのか?
find /tmp/files -type f | xargs touch

xargs が必要な直接的な答えは touch コマンドはファイル名を引数から指定することしかできず、標準入力から読み取るように作られていないからです。しかし touch コマンドを修正して標準入力からファイル名を読み取るよう修正することもできるはずです。そうすれば xargs コマンドを使う必要もありません。逆に言えば xargs コマンドが必要になってしまっている理由は標準入力で引数を与えることができないからです。

ファイル名を引数で指定する代わりに標準入力から受け取るコマンドはあまりありません。まったくないというわけではないです。例えば昔に tar コマンドと同様の目的で使われていた cpio コマンドはファイル名を引数ではなく標準入力から受け取ります。

cpioはファイル名を標準入力から受け取る
$ echo file.txt | cpio -o > archive.cpio
1 block

したがって cpio では xargs が必要ない
$ find . -name "*.txt" | cpio -o > archive.cpio
1 block
tarはファイル名を引数で指定する
$ 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 オプションで引数を挿入したい場所を指定できる
-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 には実装されていません。

-J オプションを使えば途中に複数の引数を挿入できる
$ 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 版にはこれだけのオプションがありました。

初期の UNIX 版の xargs コマンド
【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 の仕様は単純でこれだけのオプションしかありませんでした。

初期の POSIX
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 が新しい書き方として追加されています。

XPG Issue 4, Version 2
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 に変化しただけなのです。

POSIX Issue 7 (POSIX.1-2008 ~ POSIX.1-2018)
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 で標準化された範囲でもまともにデータを扱えるようになったなぁという感じです。

POSIX Issue 8 (POSIX.1-202x)
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 ループで同じように効率的に実行できる。xargsexec() 関数を行ごとに呼び出し、xargs のグループ化機能を利用しないからである。

グループ化機能というのは、この記事で散々繰り返している、多すぎる引数をエラーにならない単位に分割して呼び出すことです。-i / -I オプションは行ごとに一つの引数として実行するため、xargs の本来の目的と関係ない機能で POSIX の言うグループ化機能を使いません。POSIX は xargs の本質をちゃんと見抜いており、必要のない「挿入モード」を削除したのです。

だからね、-i-Ixargs コマンドにいらないものなんですよ。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 にもそのうちマージされるでしょう。試してみればすぐに気づくようなバグにこれまで誰も気づいていなかったということは、この機能を使っている人がほとんどいなかったということなのでしょう。これからは論理終了文字列の機能は正しく動くよ、よかったね。

FreeBSD 13.x までは -E にバグあり、論理終了文字列が適切に動かない
$ 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 オプションを指定する場合、引数の数に制限を設ける必要があります。例えば以下のような使い方は殆どの場合意味がありません。

4並列で実行しているつもりかもしれないが一般的に1並列でしか動かない
$ 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 オプションを指定しても増やすことはできません。

Solaris 11 上での実行結果
$ 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{} がよく使われていますが別の文字でも構いません。

BSD 系 Unix では -i ではなく -I を使う
$ xargs -I{} printf '[%s]\n' {} << 'HERE'
foo    bar
baz
HERE
[foo    bar]
[baz]

挿入モード(-i, -I)は末尾と途中の空白・タブは無視しないが先頭は無視する

この違い、覚えておきましょう。

BSD 系 Unix では -i ではなく -I を使う
$ xargs -I{} printf '[%s]\n' {} << 'HERE'
   foo    bar    👈 末尾に空白がある
HERE
[foo    bar    ]

挿入モード(-i, -I)は標準モードよりも大幅に遅い

標準モードではコマンドライン引数のサイズ制限を超えない可能な限り多くの引数を渡してコマンドを実行しますが、挿入モードでは基本的に一行ごとにコマンドを実行します。コマンドの起動は遅い処理なので、引数が多い場合に挿入モードを使うと大幅に遅くなる場合があります。

-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 オプションを指定すると置き換える数を変更することが出来きます。

BSD 系 Unix では 5 つ以上の {} は無視される(-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
System V 系 Unix では 5 つ以上の {} はエラーになる
$ 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 オプションを指定すると展開できる文字数を変更することが出来ます。

BSD 系 Unix では 255文字以上はエラーになる(-Iオプションで変更可能)
$ printf '%0255d' 0 | xargs -I{} echo {}
xargs: command line cannot be assembled, too long

$ printf '%0255d' 0 | xargs -S 1000 -I{} echo {} 
000 ...省略... 000
System V 系 の Unix では 255文字以上はエラーになる
$ 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 文字未満までしか展開できないという制限もありません。

-Jでは適切な書き方しかできない
$ 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 オプションはなんのために必要なのでしょうか?

-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 コマンドの機能です。この方法で複数行を一行にまとめることができる・・・ように思えます。

xargsは標準入力からからデータを受け取り、指定したコマンドの引数に変えて呼び出す
$ seq 5 | xargs
|     |     |
+-----+     +---- xargs は複数行の入力を得て
1                 それぞれを一つの引数としてコマンドを呼び出す
2                 echo 1 2 3 4 5
3
4
5            
seqコマンドは上記のように複数行で出力する

確かに多くの場合はこの方法で一行にできるでしょう。しかしこれは本来の xargs の使い方ではありません。xargs の本来の使い方は「多くの引数を分割して『複数回』のコマンド呼び出しに変換すること」です。つまり echo コマンド呼び出しが『複数回』になる場合は 1 行にはなりません。

xargsは一行にすることはできない
$ seq 10000 | xargs | wc -l # xargs は一行にするコマンド?
1

$ seq 100000 | xargs | wc -l # xargs では 1行にできない
5

$ seq 1000000 | xargs | wc -l # 10万行は53行になる
53

xargs を使って引数を一行にまとめるというのは、本来の目的である「引数の拡張」が行われない場合にのみ成立する裏技的な使い方です。しかしですね、引数の拡張が必要ないのであればコマンド置換を使って書くこともできます。

引数の拡張が行われない前提ならコマンド置換で良い(以下の2行は同じ意味)
seq 10 | xargs # seq + xargs + echo で実現
echo $(seq 10) # seq + echo のみで実現

文字数は変わりませんが xargs コマンドを使って一行にするというのは、実は xargs コマンドをわざわざ使わなくても良い例です。またメモリ使用量も変わりません。xargs は標準入力から読み取ったデータをメモリに溜めてからコマンドを呼び出すからです。

ちなみにちゃんと一行にするには次のように paste コマンドを使います。

複数行をちゃんと一行にする方法(「-」はBSD・SystemV版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個で呼び出される場合があります。

Solaris での実行結果(出力している数字は引数の数)
$ 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
Linux での実行結果(出力している数字は引数の数)
$ 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
macOS での実行結果(出力している数字は引数の数)
$ 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 {}' のような書き方をしても駄目です。なぜならシェルのメタ文字を正しく扱えないからです。

-0 オプションを指定していたとしてもエラーになる
$ 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 オプションは {} は単独の引数にしかできません

-J オプションはこのような書き方はできないようになっている
$ printf '%s\0' 'a"b' | xargs -0 -J{} sh -c 'echo {}'
{}

だから、安全で優れているのです。

xargs -I{} sh -c 'cmd "$0"' {}」のように$0を使ってはいけない

$0 を引数の代わりに使ってはいけません。これは xargs というよりも sh の仕様なのですが $0 はコマンド名を入れるところであって引数を入れるところではありません。

$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 はエラーメッセージでコマンド名として使われます。コマンド名には適切と思われる名前(ただしハイフンで始まらない名前)を指定することでエラーメッセージが適切なものになります。

データ(12345)がコマンド名として扱われてしまっている
$ echo 12345 | xargs -I{} sh -c '() "[$0]"' {}
12345: 1: Syntax error: ")" unexpected
$0にはコマンド名を指定する
$ 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.jpegfoo.jpg に変更する例)

良くない例(-n2を指定しても2個単位で実行される保証がないから)
$ 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 

シェルスクリプトだと forwhile ループを使って書くこともできます。頭を悩ませるコードよりも愚直なコードを書きましょう。

# 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.bashtest2.bash は外部コマンド(touch コマンド)の実行回数が同じなので速度の差はほとんどありません。

test1.bash
#!/usr/bin/env bash
for i in file{0000..9999}.txt; do
  touch "$i"
done
test2.bash
#!/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

コマンドの実行回数を減らせば、どちらも同じように速くなります。

test3.bash
#!/usr/bin/env bash
touch file{0000..9999}.txt
test4.bash
#!/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 コマンドのバージョンからそういう仕様です。その結果、ファイルが見つからない場合はエラーになります。

ファイルが見つからない場合はエラーになる(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 と同じようにファイルが見つからない場合にコマンドが実行されなくなります。

-r オプションを指定すればファイルがない場合にコマンドを実行しない
$ 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 {} + のほうが優れています。書き換えは組み合わせるコマンドを一つ減らすだけの簡単なものです。以下の例を参照してください。

xargsは標準入力からからデータを受け取り、指定したコマンドの引数に変えて呼び出す
$ 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 を使うのであれば、forwhile で十分です。

xargs コマンドを使うときに頑張って考えていませんか? 頑張って考えたコードは難しいコードだということです。後で読み返したときに何をやっているのか分からなくなります。賢いコードを書こうとせずにまずは愚直なコードを書きましょう。パフォーマンスはそれが重要なときに考慮しましょう。

さいごに

xargs コマンドにはいくつもの問題があります。xargs コマンドに変わる新しいコマンドを作るべきです。引数解析の仕様はなくして、行指向の一行一引数かヌル文字終端のデータ形式のみ、挿入モードは BSD 系 Unix の -J ベースに変更。並列化処理は別のコマンドに分離(注意 GNU Parallel がすでにある)。余計な機能やナンセンスな制限はなくしてシンプルなコマンドにすべきでしょう。

xargs の仕様を完全に理解するのはまず無理ですので、xargs をなるべく使わないことを目標にしましょう。さて最後の最後に一言、この記事は間違いなく「xargs 完全理解マニュアル」です。詳しくは「ダニングクルーガー効果」とやらを調べてください。

21
21
1

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
21
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?