xargs 完全理解マニュアル - xargs は多すぎる引数を分割するコマンドって知っていますか？

## はじめに

`xargs` コマンドは「なにか凄そうだけどよく分からないコマンド」としてよく知られています。使う人は使うけど何をやっているのか全くわからないコマンドです。**「ループ処理？そんなものよりワンライナーで書くほうがカッコいいぜ」と思って気軽に使うと空白やクォーテーション文字でエラーになる罠にハマる**使い方がとても難しいコマンドです。この記事はそういうよくわからない `xargs` はどういうコマンドなのか解説します。この記事を読むと `xargs` を「完全に理解した」と言えるようになるでしょう。

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

- **`xargs` は難しすぎるコマンド、可能な限り使うな**
  - 昔と違い今はそんなに重要で必要なコマンドではなくなった
  - 構造化プログラミングが可能になったBourneシェルの誕生以前の古いコマンド
- **`xargs` はシェルスクリプトで反復処理（ループ処理）を行うコマンドではない**
  - 単純なループ処理なら、シェル言語の機能 `for` や `while` を使う
  - カッコつけて難しい `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` の使い方には潜在的なバグが含まれているため理解せずに使うと罠にハマります。

:::note alert
[2025-04-14] この記事の以前の版では「xargs は拡張引数 (extended arguments) の略」と書いていましたが、これは間違った情報でした。開発者のHerb Gellisは **"eXecute command with ARGumentS"** の略であると言っていることを確認しました。訂正してお詫びいたします。記事の内容もあわせｔ修正しています。

- [bug-findutils: An interesting thing: about the naming of xargs](https://lists.nongnu.org/archive/html/bug-findutils/2023-03/msg00000.html)
- [GNU findutils: 13.2 History of xargs](https://www.gnu.org/software/findutils/manual/html_mono/find.html#History-of-xargs)
:::

## xargs の「目的」を知り本質を見抜く！

`xargs` を理解するには、`xargs` が何のために作られたコマンドなのかを知ることが一番です。

### xargs は多すぎる引数を分割するために作られた

`xargs` コマンドがよく分からない理由は、`xargs` コマンドがそもそも一体何のためのコマンドなのかを理解していないためです。`xargs` はもともと引数を拡張するために作られたコマンドなのですが、現在では `xargs` の当初の目的とは違う使い方がよくされているということです。これは `cat` コマンドが本来はファイルを結合 (con**cat**enate) するために作られたコマンドなのにファイルを画面に出力するためによく使われているのと同じようなものです。`xargs` の「目的」を知らずして `xargs` が行っている「機能」が使われているから「なにか凄そうだけどよく分からないコマンド」になってしまっているのです。

物事を理解するためにまず原点に戻りましょう。`xargs` とは OS が持っているコマンドライン引数のサイズの制限を回避するコマンドです。回避すると言っても OS の制限をただの一コマンドが変更することは出来ないので、実際には回避したかのように使えるようにすることです。すなわち**コマンドを実行するときに多くの引数を渡すとサイズ制限に引っかかってしまいエラーになってしまうという問題を解決するために作られたのが `xargs` コマンド**です。これが `xargs` の本来の「目的」なのです。

```console: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` コマンドが開発された経緯は、開発者によって語られています。保存を兼ねて引用しておきます。

https://groups.google.com/g/comp.unix.questions/c/vG2keVnENmo/m/LkZN5orQrwcJ

<details>
<summary>引用</summary>

```txt
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"
--- ---
```

</details>

このメールの内容から次のことがわかります。

- `xargs` はメールの送信日の 10年前（1977年頃）に書かれた
  - 補足: 世の中に Unix が広まり始めたのは 1979 年以降
- 原始的なシェル（PWBシェル）が用いられておりBourne シェルはまだない
  - 補足: PWBシェルはスクリプト言語として使うに機能不足
  - パイプはあったが `for` はなく `while` や `goto` は外部コマンドだった
  - シェル言語の機能が貧弱な時代に必要だったもの
- 当時のコマンドライン上のファイル名展開は 512 バイト（後に 5120 バイト）だった
- 最大 512 バイトのファイル名展開が、そもそも `xargs` を書いた理由だった
- `-i` オプションなどの機能は便利そうだからと後から追加した
- 仕様や制限は、当時「これで十分だろう」という理由によるもの

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

（2025-04-14追記）もう一つの開発者本人による説明は、ここに記載されています。最初の用途は、Cプログラムのコンパイルで名前の合計が512バイトを超えてしまう問題を回避することだったようです。

https://www.gnu.org/software/findutils/manual/html_mono/find.html#History-of-xargs

上記によると、xargsコマンドは "eXecute command with ARGumentS" の略ですが、そもそもは、当時は x で始まるコマンドがないことに気づいたことから思いついたようなことが書かれています。

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

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

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

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

```console:5万個の引数で2回のlsコマンド呼び出しに分ける
$ ls file{000001..050000}.txt
$ ls file{050001..100000}.txt
```

`xargs` がやっているのもこれと同じです。ちなみに `xargs` はきっちり半分にしているわけではなく、適切なサイズに調整しながらなるべく多くの引数を渡してコマンドを実行します。ちなみに私の Linux の環境ではおよそ 8700 個 (128KB) 単位で 12 回のコマンド呼び出しが行われました。これが **`xargs` コマンドの目的を実現するためのアイデア**です。
 
一般的に多くの引数とはファイル名です。ファイル名以外をコマンドライン引数にたくさん書くことはあまりないでしょう。多くの引数を手書きするなんてやってられませんからね。カレントディレクトリにファイルがたくさんあり、それらのファイルに対してなにかのコマンドを実行するときに、コマンドライン引数の制限が問題になってきます。

```console:ファイルがどれだけあるかわからず、多数のファイルに展開される場合にエラーになる
$ ls *.txt

補足: シェル言語の機能にコマンドライン引数のサイズ制限はない
シェル言語の機能: シェル関数やビルトインコマンドの引数、パス名展開や for の in 部分など
```

忘れてはいけないことなのでもう一度述べておきます。`xargs` コマンドの本来の目的は「ファイル数（引数）が多い場合のエラーの回避」です。そして「コマンドは複数回実行される事がある」ということです。

### xargsは標準入力のデータを引数に変換してコマンドを呼び出す変換器

さて `xargs` の目的とアイデアは出揃いました。次に考えるべきは実現方法です。このような場合にファイル数が多くてもエラーにならずに目的を達成するにはどうしたらよいでしょうか？ その場合に取りうる手段はあまりありません。引数が使えない、それなら標準入出力を使おうと `xargs` の開発者は考えました。

ファイル名の一覧は `ls` コマンドや `find` コマンドなどを使って標準出力に出力することができます。当然ですが標準出力に出力するファイル数には制限はありません。

```console
$ 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` コマンドを実行します。繰り返しますがコマンドライン引数の数には制限があります。

```console
$ touch file0000.txt file0001.txt file0002.txt ...
```

「標準出力でデータを出力するコマンド」と「引数にデータを指定して実行するコマンド」の二つをつなげることができれば、コマンドライン引数のサイズ制限を回避することができます。`xargs` コマンドが行っていることは、この二つのコマンドをつなげることです。**`xargs` コマンドは標準入力からデータを受け取り引数の形に変換して実行する**という機能を持っています。

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

### xargsの入力データ形式は行指向ではなく複雑な「xargs 形式」

上記の図は簡易版です。**`xargs` が行う実際の入力データ形式は行単位ではなく**もっと複雑です。少し詳細に書き直しましょう。

```console: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` を使わずに多くの引数を扱う方法を思いつくのではないでしょうか？

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

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

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

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

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

したがって cpio では xargs が必要ない
$ find . -name "*.txt" | cpio -o > archive.cpio
1 block
```

```console: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` コマンドがそうです。

```sh
# 引数はファイル名を指定する（複数のファイル名に対応している）
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](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/xargs.html) の 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 に記載されています。

https://www.sco.com/developers/devspecs/

補足: `xargs` コマンドは BSD Unix では 1991 年の [2.11BSD](https://man.freebsd.org/cgi/man.cgi?query=xargs&manpath=2.11+BSD)、1989 年の [4.3BSD NET/2](https://man.freebsd.org/cgi/man.cgi?query=xargs&manpath=4.3BSD+NET%2f2) にはあるようで、おそらく POSIX.2-1992 の策定中に POSIX の定義に従って実装されたのだと思われます。System V のみに存在したというのは、それ以前の話をしているのでしょう。

## xargsは目的を見失い新しい「機能」を追加してしまった

`xargs` コマンドは元々は多くの引数を指定したときに、コマンドライン引数のサイズ制限でエラーになる問題を解決するために作られました。多くのコマンドに共通する話ですが、コマンドはバージョンアップするたびに機能が追加されていきます。そして元々の目的とは関係ない別の機能が追加されることもあります。例えば `cat` コマンドはファイルを結合するコマンドであり `-v` オプション（印刷できない文字をASCII文字で出力する）を `cat` コマンドに追加するのは良くない考えだと言われてきました（参照 [UNIX Style, or cat -v Considered Harmful](https://harmful.cat-v.org/cat-v/)）。

本家の [Unix 哲学](https://en.wikipedia.org/wiki/Unix_philosophy)には「それぞれのプログラムが一つのコトをうまくやる。新しい仕事をするには新しい**機能**を追加して古いプログラムを複雑にするのではなく新しく作る」という考え方があります。

> 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` にデータを渡す側が遅い場合に、**標準の分割サイズに達する前に、途中でコマンドを実行する**ことができるからです。

```sh
# 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` コマンドはデフォルトでは呼び出すコマンドの末尾に引数を追加してコマンドを呼び出します。

```console
$ find . -type f | xargs touch 《ここに引数を追加する》

touch コマンドの末尾に引数（ファイル名）を追加してコマンドを呼び出す。
例: touch file1.txt file2.txt file3.txt
```

しかし引数は必ずしも最後に追加したいとは限りません。例えば `mv` コマンドで指定したディレクトリにファイルを移動する場合は、途中に引数を入れ込むことになります。
 
```console
$ find . -type f | xargs mv 《ここに入れたい》 /tmp/trash/
```

このように、途中に入れたい（挿入したい）ときに使うのが `-i` や `-I` オプションによる「挿入モード」です。これも本来の多すぎる引数をエラーにならないように分割する機能とは関係のない機能です。なぜなら**入力データの一行ごとにコマンドを呼び出す**ように動作が変わるからです。

```console:-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` には実装されていません。

```console:-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](https://www.tuhs.org/cgi-bin/utree.pl?file=PWB1/usr/man/man1/xargs.1)）ですが、BSD 系 Unix では実装されていません。そして 2001 年の `-J` オプションよりも後の 2002 年に、`-I` オプションが FreeBSD 4.7 で実装されました（[参考](https://man.freebsd.org/cgi/man.cgi?query=xargs&manpath=FreeBSD+4.7-RELEASE)）。OpenBSD では 2003 年の OpenBSD 3.4、NetBSD では 2007 年の NetBSD 4.0 で実装されました。`-I` の方が実装が遅いというのは面白い話ですね。

移植性（POSIX）はどうなってるんだ？と思うかもしれませんが、**挿入モードは [POSIX xargs](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/xargs.html) ではオプションの XSI 拡張機能**、すなわち System V 系 Unix でのみ必須のオプションで、BSD 系 Unix では必須ではなく移植性はなかったものです。XSI 拡張の `-I` （と `-L`）は、現在の BSD 系 Unix では、すでにどちらも実装されているので POSIX の標準機能に変更しても良い気がしますね。

### POSIXで「-e, -i, -l」が「-E、-I、-L」に変更された

初期の PWB/UNIX 版と System V 版にはこれだけのオプションがありました。

```txt:初期の 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 にはこれだけのオプションしか有りませんでした。

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

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

```txt: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` に変化しただけなのです。

```txt: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 で標準化された範囲でもまともにデータを扱えるようになったなぁという感じです。

```txt: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](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/xargs.html) に書いてあります。

> 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` の開発者は考えたようです。いやぁ、とても便利ですね（皮肉）。

```console:論理終了文字列 _ があると、そこでデータ読み取り終了
$ 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` に文字を指定した場合に正しく動かないバグがありましたが（[参照](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=270867)）、2023年11月にリリースされた FreeBSD 14.0 で修正されました。NetBSD にもそのうちマージされるでしょう。試してみればすぐに気づくようなバグにこれまで誰も気づいていなかったということは、この機能を使っている人がほとんどいなかったということなのでしょう。これからは論理終了文字列の機能は正しく動くよ、よかったね。

```console: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` オプションを指定する場合、引数の数に制限を設ける必要**があります。例えば以下のような使い方は殆どの場合意味がありません。

```console:4並列で実行しているつもりかもしれないが一般的に1並列でしか動かない
$ find . -name "*.txt" | xargs -P 4 mycmd
```

なぜなら `xargs` はコマンドライン引数のサイズ制限でエラーにならない多くの引数でコマンドを実行するからです。最初の方で私の Linux 環境では 8700個ぐらいの引数（ファイル名）でコマンドが実行されたと書きましたが、この `mycmd` も同じようにサイズ制限を超えない限り**1回しか呼び出されない**ので `-P 4` で並列実行を指定しても無意味です。`-P` オプションを使う場合、`-n` オプションなどで引数の数を制限する必要があります。

```console:並列で実行させたいなら引数の数を指定する必要がある
$ 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` は空白・タブ・改行のいずれかの文字の連続（混ざっていても良い）を一つの区切り記号とみなして、区切り記号で区切られた項目を一つの引数として扱います。

```console
$ 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`」で空白・タブ・改行を正しく扱う方法

もし標準モードで空白・タブ・改行を正しく扱うにはバックスラッシュでエスケープする必要があります。

```console
$ 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` のデフォルトで空白・タブ・改行をそのまま扱えないのはよく知られていると思いますが、実際には**シングルクォーテーションとダブルクォーテーションもそのままでは扱えません**。

```console:シングルクォーテーションやダブルクォーテーションは消える
$ xargs printf '[%s]\n' << 'HERE'
'a a a'
HERE
[a a a]

$ xargs printf '[%s]\n' << 'HERE'
"b b b"
HERE
[b b b]
```

これをみて「空白やタブが含まれる場合はクォーテーションで括ればいいんだ」と考えると思います。間違いではないのですが改行には通用しません。

```console:クォーテーションがあっても改行は区切り記号として扱われる
$ 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` のクォーテーションの解釈はシェルのクォーテーションの解釈とは異なります**。似てる部分もあります。例えばシングルクォーテーションで括られた中にシングルクォーテーションを入れることはできないので、一旦シングルクォーテーションで文字列を閉じて、別の方法でシングルクォーテーションを入れる必要があります。

```console:シングルクォーテーションの中にシングルクォーテーションは入れられない
$ xargs printf '[%s]\n' << 'HERE'
'a a \' a'
HERE
xargs: unterminated quote

補足: 一旦閉じる必要がある
$ xargs printf '[%s]\n' << 'HERE'
'a a '\'' a'
HERE
[a a ' a]
```

ダブルクォートの中にダブルクォーテーションを入れることもできませんが、シェルとは異なりバックスラッシュでダブルクォーテーションをエスケープしようとしても無意味です。

```console:ダブルクォーテーションの中にダブルクォーテーションは入れられない
$ 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` オプションを指定しても増やすことはできません。

```console: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` を指定すると、引数の解釈を「指定した行数ごと」に打ち切ってコマンドを実行します。

```console
$ xargs -L1 echo "line" << 'HERE'
foo    100
bar    200
HERE
line foo 100       👈 1行ごとに1回のechoコマンドの実行
line bar 200       👈 1行ごとに1回のechoコマンドの実行
```

本当に「指定した行数ごと」だと思いましたか？ 特別な追加仕様があります。行の末尾に空白・タブがある場合は、次の行に継続しているという意味となります。

```console
$ xargs -L1 echo "line" << 'HERE'
foo    100     👈 末尾に空白があると次の行をつなげる
bar    200
HERE
line foo 100 bar 200      👈 1行扱いなので1回のechoコマンドの実行
```

一行（指定数行）ごとにコマンドを実行したいけど、たまに次の行に分けたいと言うときに便利ですね（皮肉）

理解に苦しむ仕様ですが、作られた当時が 1977 年ですから、他のコマンドが長すぎる行を扱えなかったとか、テレタイプで紙に印刷しなければいけなかった都合上、きっと当時はこのような仕様が妥当だったのでしょう。

### 挿入モード（`-i`, `-I` `-J`）の引数解釈は標準モードとは違う

さて次は挿入モードです。これも気軽に使われていますが、ちゃんと引数の仕様わかっているのかなぁという使われ方ばかり見かけます。

#### 挿入モード（`-i`, `-I`) では、一行が一引数になる

挿入モードは標準モードよりも引数の解釈は簡単です。基本は一行が一引数です。空白やタブが入っていても区切られることはありません。引数の解釈は簡単になっていますが標準モードと解釈の違いがあるので混乱します。なんでこんな違いを作っちゃったんでしょうね？

```console:挿入モードでは空白やタブで区切られず一つの引数として扱われる
$ xargs -i printf '[%s]\n' {} << 'HERE'
foo    bar
baz
HERE
[foo    bar]
[baz]
```

`-i` は BSD 系 Unix では使えません。`-I` を使う必要がありオプション引数は省略できません。伝統的に System V 系 Unix では `-i` を使用しており置き換える文字はデフォルトの `{}` が使われていたという経緯から `-I{}` がよく使われていますが別の文字でも構いません。

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

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

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

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

#### 挿入モード（`-i`, `-I`）は標準モードよりも大幅に遅い

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

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

```console: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
```

```console: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` オプションを指定すると展開できる文字数を変更することが出来ます。

```console: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
```

```console: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 文字未満までしか展開できないという制限もありません。

```sh:-Jでは適切な書き方しかできない
$ seq 5 | xargs -I{} sh -c 'echo {}' # 良くない書き方

$ seq 5 | xargs -J{} sh -c 'echo "$@"' sh {} # 適切な書き方
```

そして最も素晴らしいのは標準モードと同じように引数を解釈することです。

```console:標準モードと同じように引数を解釈する
$ 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` を使って指定する書き方です。次のような例です。

```console
$ echo "http://example.com" | xargs curl
```

このような使い方は全く意味がありません。次のように書けば良いからです。

```console
$ curl "http://example.com"
```

コードの意味を分かりづらくする原因は、**必要とは思えないようなものを付け加えること**です。（後から読む自分自身を含めた）コードの読者はなんのためにこんな無駄なことをしてるんだ？と悩むことになります。

その他の例えとして拡張子を `jpeg` から `jpg` に置き換えるコマンドの例を示します。次の書き方はごく自然ですね？

```console
$ mv image.jpeg image.jpg
```

元は ksh93 の拡張機能で、bash、mksh、yash、zsh、OpenBSD sh などで広く採用されたブレース展開の機能を用いると、次のようにさらに簡潔に書くことができます。

```console
$ mv image.{jpeg,jpg}
```

シェルスクリプトで変数を使う場合次のように書くこともできます。これは POSIX で標準化された機能なので、**すべての sh で動作**します。

```sh
from="image.jpeg"
mv "$from" "${from%.jpeg}.jpg"
```

これを無駄に `xargs` を使った書き方はこのようになります。

```console
$ echo "image.jpeg" | sed -E 's/(.*)\.jpeg$/& \1.jpg/' | xargs mv
```

はっきりいって、こんな書き方はダサいですよ。パフォーマンスが悪いと言いたくなりますが、そんな些細なパフォーマンスよりも明らかにわかる「コードが無駄」と指摘しなければならないですね。他にも引数解釈のルールに照らし合わせれば、**ファイル名にスペースやクォートなどが入った時に問題なく動きますか？** と問いたくなります。

最小限でわかりやすく、問題の置きない書き方をしましょう。**頭を悩ませるコードを書いてはいけません**。コマンドは引数で指定できるようになってるのですから、引数で指定しましょう。

無駄にかっこいい使い方でこのようなものを見かけました（実際にはもっと複雑で、別のコマンドからファイル名を出力しています）。さてこの見慣れない `-o` オプションはなんのために必要なのでしょうか？
 
```console:-o は何ために必要？
$ echo "file.txt" | xargs -o vi
```

答えは、次のようなエラーが出るのを防ぐためです。

```console
$ echo "file.txt" | xargs -o vi
Vim: Warning: Input is not from a terminal
```

そんな説明が必要なことをするぐらいなら、素直に書きましょう。

```console
$ vi $(echo "file.txt")

もちろん別のコマンドからファイル名を出力しないのであればこれでよい
$ vi "file.txt"
```


### xargs は複数行を一行にまとめることはできない

`xargs` は複数行を一行にするためにたびたび使われています。

```console
$ seq 5
1
2
3
4
5

$ seq 5 | xargs
1 2 3 4 5
```

便利だと思いますし、わかった上で使うのは構いませんが、この使い方は `xargs` の本来の使い方、多すぎる引数を分割してコマンドを呼び出す場合にうまくいきません。

まず `xargs` は呼び出すコマンドを省略したときに `echo` コマンドを実行します。以下の実験から、GNU xargs は実際に外部コマンドの `echo` コマンドを呼び出し、それ以外では内蔵の `echo` コマンドを呼び出しているようです。

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

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

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

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

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

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

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

```sh:複数行をちゃんと一行にする方法（「-」はBSD・SystemV版pasteで必要）
$ seq 1000000 | paste -sd ' ' - | wc -l # 10万行でも1行になる
1
```

正直な所 `paste` コマンドも適切なコマンド名とは思えませんが、他に適切な名前のコマンドはありません。他にもやり方はあるかもしれませんが、おそらく専用のコマンドを作らない限り `xargs` よりも長くはなるでしょう。そういう理由もあって、複数行を「だいたい」一行にできるコマンドとして `xargs` がお手軽な方法として使われています。

### 「xargs -n2」は「引数を2個ずつコマンドに渡す」ではない

標準入力からのデータをペアにして `-n 2` を指定して 2個の引数でコマンドを呼び出すとします。例えばこのような例です。

```console
$ 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個で呼び出される場合があります。

```console: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
```

```console: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
```

```console: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 {}'` のような書き方をしても駄目です。なぜなら**シェルのメタ文字を正しく扱えない**からです。

```console:-0 オプションを指定していたとしてもエラーになる
$ printf '%s\0' 'a"b' | xargs -0 -I{} sh -c 'echo {}'
sh: 2: Syntax error: Unterminated quoted string
```

このような書き方は、コードを文字列で組み立てていることになるので、実質　`eval` を使っているのと同じなのでとんでもないことが起こり得ます。

```console:{} の場所に引数をそのまま埋め込むだけ
$ 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'
を実行するからファイルに書き込む
```

じゃあどうすればよいか？ **`{}` は単独の引数にしてください**。`{}` はクォートして `'{}'` や `"{}"` と書いても構いません。

```console:{} は単独の引数にする
$ printf '%s\0' 'a"b' | xargs -0 -I{} sh -c 'echo "$1"' sh {}
a"b
```

ちなみにBSD 版 Unix の改良された **`-J` オプションは `{}` は単独の引数にしかできません**。

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

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

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

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

```sh:$0を引数を入れる場所として使わない
# 良くない書き方
xargs -I{} sh -c 'echo "$0"' {}

# 良い書き方
xargs -I{} sh -c 'echo "$1"' sh {}
```

なかなか不思議な仕様なのですが、シェルで `-c` を指定してシェルスクリプトコード（下記の command_string）を文字列で指定した場合、引数（argument）の前にコマンド名（command_name）を要求します。`$0` はコマンド名であって引数ではありません。

```txt
sh -c [options] command_string [command_name [argument ...]]
```

`$0` を引数として使うと上手く行かない例を紹介します。

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

```console:データ（12345）がコマンド名として扱われてしまっている
$ echo 12345 | xargs -I{} sh -c '() "[$0]"' {}
12345: 1: Syntax error: ")" unexpected
```

```console:$0にはコマンド名を指定する
$ echo 12345 | xargs -I{} sh -c '() "[$1]"' myprog {}
myprog: 1: Syntax error: ")" unexpected
```

他にも `$0` は `$@` に含まれないなどの問題があるので、`$0` を引数を入れるところとして使わないようにしましょう。

### bash 専用のシェル関数の呼び出し方

特にシェルスクリプトでは、`xargs` コマンドでシェル関数を呼び出すという使い方できます。基本的な書き方は次のようになります。

```sh
#!/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 {}'`」は良くない書き方です。

```sh
# -I オプションを使うときの書き方
seq 5 | xargs -I{} bash -c 'func "$@"' myprog {}

# 推奨しない（空白やクォーテーションなどが含まれない前提でのみ使える）
seq 5 | xargs -I{} bash -c 'func {}'
```

### そもそも挿入モード（`-i`, `-I`）を使わない

そもそも挿入モード（`-i`, `-I`）は、多すぎる引数をエラーにならない単位で分割することをせず、一行ごとに実行するのでパフォーマンスが悪いです。引数を途中に挿入したいのであれば `sh` コマンドを使って実現できます。

```console:挿入モードを使わないパフォーマンスの良い方法（複数のファイルの移動）
$ find . -name '*.txt' | xargs sh -c 'mv "$@" /tmp/trash' sh
```

ファイルを一つづつリネームする例（`foo.jpeg` を `foo.jpg` に変更する例）

```console:良くない例（-n2を指定しても2個単位で実行される保証がないから）
$ find . -name '*.jpeg' | sed -E 's/(.*)\.jpeg$/& \1.jpg/' | xargs -n2 mv
```

は、以下のように書きます。

```console:良い例
$ 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` ループを使って書くこともできます。**頭を悩ませるコードよりも愚直なコード**を書きましょう。

```sh
# 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` コマンド）の実行回数が同じなので速度の差はほとんどありません。

```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 {}
```

```console
$ 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
```

```console
$ 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` コマンドのバージョンからそういう仕様です。その結果、ファイルが見つからない場合はエラーになります。

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

```console:-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 {} +` のほうが優れています。書き換えは組み合わせるコマンドを一つ減らすだけの簡単なものです。以下の例を参照してください。

```console: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` やシェル関数を使えば簡単です。

```sh
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` 完全理解マニュアル」です。詳しくは「[ダニングクルーガー効果](https://www.google.com/search?q=%E3%83%80%E3%83%8B%E3%83%B3%E3%82%B0+%E3%82%AF%E3%83%AB%E3%83%BC%E3%82%AC%E3%83%BC+%E5%AE%8C%E5%85%A8+%E7%90%86%E8%A7%A3&tbm=isch)」とやらを調べてください。