🐐

xargs の -I オプションとコマンドライン長の制約

2024/12/22に公開

概要

この記事では xargs コマンドの基本的な使いかたと、便利な -I オプションの紹介をします。
最後に、xargs コマンドの -I <replstr> オプションを用いたときの置換文字列長の制約についてまとめます。

最後の内容はシェル芸人しか得しない結構ニッチな部分になってしまったと思いますが、-I オプション自体はかなり便利なので、このオプションだけでも覚えていってもらえれば幸いです。

なんでこんな記事書いたの?

昨日こんな記事を書きました。

https://zenn.dev/sankantsu/articles/ef2d277789fa8a

しかし、翌日になっていろいろ試していたら MacOS, Linux の各環境で xargs の制約によって動かないケースがあることがわかり、記事修正のためにいろいろ調べていました。その結果、内容が元記事に足すにはボリュームが増えすぎてしまったので、新しく記事にすることにしました。

xargs コマンドのきほん

xargs コマンドは、標準入力から入ってきた文字列をコマンドライン引数に変換して実行するためのコマンドです。

以下、具体例で説明します (そんな基本的なことは知ってるよという方は次の節まで読みとばしてください)。
以下は xargsrm を組み合わせて png ファイルを全部削除する例です。

$ ls *.png | xargs rm

ここでは、ls *.png の出力は例えば以下のような改行区切りの文字列が入っています。

a.png
b.png
c.png

これを xargs rm に渡すと、標準入力から来たファイル名が rm のコマンドライン引数として渡されて

$ rm a.png b.png c.png

を実行したのと同じことになります。その結果、png ファイルを全部削除することができます。

このように rm のようなコマンドライン引数が重要なコマンドをパイプで繋ぎたいとき xargs はかなり便利に使うことができます。

xargs の -I オプションについて

普通の xargs の使いかただと標準入力からやってきた引数はコマンドの末尾に渡されます。
したがって、コマンドの末尾には固定の引数を渡したいといったケースでは使えなくなってしまいます。

例えば、「全ての png ファイルを img/ ディレクトリに移動させたい」というケースではどうしたら良いでしょうか?
以下はうまくいかない例です。

# ダメな例
$ ls *.png | xargs mv img/
mv: c.png is not a directory

この書きかただと mv img/ a.png b.png c.png のように実行されてしまうため、「img/, a.png, b.pngc.png ディレクトリに移動する」という意味になってしまい、うまくいきません。

このようなときに便利なのが -I オプションです。
-I <replstr> のようにオプションを指定すると、以降のコマンドラインにある <replstr> を標準入力の内容で置きかえてコマンドを実行してくれます。具体的な <replstr> としては個人的には {} という文字列を使うことが多いです。
-I オプションを使うとコマンドラインの途中の部分を標準入力の内容で置換することができます。

以下は、-I {} オプションを用いて全ての png ファイルを img/ ディレクトリに移動させる例です。

# うまくいく例
$ ls *.png | xargs -I{} mv {} img/

なお、このとき実際の呼び出しは mv a.png b.png c.png img/ のように一回で実行されるわけではなく、標準入力の行ごとに置きかえとコマンド実行が行われて、

$ mv a.png img/
$ mv b.png img/
$ mv c.png img/

と実行した場合と同じように 3 回実行されます。

-I {} を用いたときと用いないときのコマンド実行回数の違いは、以下のように確認することができます。

# まとめて一回で実行される
$ ls *.png | xargs echo "files:"
files: a.png b.png c.png

# 行ごとにそれぞれ実行される
$ ls *.png | xargs -I{} echo "file:" {}
file: a.png
file: b.png
file: c.png

この違いは知らないとハマることがあるので注意してください。

置換文字列長の制限

最後に、-I オプションを用いたときの置換後のコマンドラインの長さの制約についてまとめます。

xargs の実装バリエーションについて

MacOS と Linux (Ubuntu 等) の xargs コマンドは名前やおおまかな役割は同じですが、実装は別物です。
Linux 環境に入っている xargs は、たいてい GNU findutils 版ではないかと思います。
一方、MacOS に入っているものは FreeBSD 由来のものです。

ふだん使いではこれらの違いはあまり気にならないことが多いかもしれません。
例えば、ここまでの内容は基本的に GNU findutils 版でも FreeBSD 版でも同じように動くはずです。

しかし、これらの実装の間には実は細かい動きに差があったり、オプションが一方にしか無かったりといったことがあります。残りの部分では、-I <replstr> オプションを使ったときの置換文字列長の制限についての違いを確認します。

MacOS (FreeBSD) 版 xargs の置換文字列長制限

MacOS (BSD) 版の xargs では、デフォルトでは -I オプションによって置換する文字列の長さが 255 文字以上になると置換できません。FreeBSD manpage (man xargs(1) には以下のような記載があります [1]

The resulting arguments, after replacement is done, will not be allowed to grow beyond replsize (or 255 if no -S flag is specified) bytes;

実際に動きを観察してみます。
yesy\n を繰り返す出力を生成しているコマンドで、ここでは改行を削除した上で head -c` と組み合わせ、任意のバイト数の文字列を生成するのに使っています。

$ yes | tr -d '\n' | head -c 254 | xargs -I{} echo {}
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
$ yes | tr -d '\n' | head -c 255 | xargs -I{} echo {}
{}

標準入力の大きさが 255 文字になるとプレースホルダの部分が置換されなくなり、{} がそのまま出力されているのがわかります。

この制約は -S <replsize> オプションで回避できます。こちらはかなり大きい値にしても問題無いようです [2]
実際に試してみます。 ここでは -S に適当に大きい値 (1000000) を指定しています。

$ yes | tr -d '\n' | head -c 255 | xargs -S 1000000 -I{} echo {}
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
$ yes | tr -d '\n' | head -c 100000 | xargs -S 1000000 -I{} echo {}
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy...()

-S オプションの指定によりデフォルトの 255 byte の制限が緩くなり、より長い文字列を -I オプションで置換できるようになっていることが確認できました。
なお、この -S オプションは BSD 固有拡張であり、GNU findutils 版では使えません。

GNU findutils 版 xargs の置換文字列長制限

Linux システムで多く使われる GNU findutils 版の xargs だと、デフォルトでは FreeBSD 版の 255 byte に比べればだいぶ制約がゆるいです。それでもあまり置換文字列が長いと xargs: argument list too long というエラーで失敗してしまいます。
これは、xargs が内部で使う buffer の大きさから来る制約のようです。--show-limits というオプションで制約を確認することができます。
私の環境 (Ubuntu 24.04) では以下のようになっていました。

$ xargs --show-limits </dev/null
Your environment variables take up 2943 bytes
POSIX upper limit on argument length (this system): 2092161
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2089218
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647

コマンドの長さ (+ NULL 終端?) も含めて 131072 文字を超えると失敗しているようです。

$ yes | tr -d '\n' | head -c 131066 | xargs -I{} echo {} | wc -c
131067
$ yes | tr -d '\n' | head -c 131067 | xargs -I{} echo {} | wc -c
xargs: argument list too long
0

まとめ

この記事では xargs の基本的な使いかたからはじめて、-I オプションによるコマンドラインの任意の位置での置換についても紹介しました。最後には、-I オプションの実装ごとの制約について具体的に確認しました。
最後の内容はかなりニッチなものになってしまったかと思いますが、この記事の内容の何かひとつでも xargs コマンドを使う際に役に立ててもらえれば幸いです。
以上

脚注
  1. MacOS 版と FreeBSD 版でまったく実装が同じものかどうかは正確にはわかりませんが、少なくとも今回関係ある箇所についての記載は手元の MacOS manpage と差異が無いことを確認しています ↩︎

  2. 10^9 まで実験した範囲では問題無さそう。筆者の環境ではあまり出力が長すぎる場合 (1MB ぐらい) -S オプションの制約に引っ掛る前に xargs: insufficient space for argument という別のエラーで落ちていました ↩︎

GitHubで編集を提案

Discussion