xargs の -I オプションとコマンドライン長の制約
概要
この記事では xargs コマンドの基本的な使いかたと、便利な -I
オプションの紹介をします。
最後に、xargs コマンドの -I <replstr>
オプションを用いたときの置換文字列長の制約についてまとめます。
最後の内容はシェル芸人しか得しない結構ニッチな部分になってしまったと思いますが、-I
オプション自体はかなり便利なので、このオプションだけでも覚えていってもらえれば幸いです。
なんでこんな記事書いたの?
昨日こんな記事を書きました。
しかし、翌日になっていろいろ試していたら MacOS, Linux の各環境で xargs の制約によって動かないケースがあることがわかり、記事修正のためにいろいろ調べていました。その結果、内容が元記事に足すにはボリュームが増えすぎてしまったので、新しく記事にすることにしました。
xargs コマンドのきほん
xargs コマンドは、標準入力から入ってきた文字列をコマンドライン引数に変換して実行するためのコマンドです。
以下、具体例で説明します (そんな基本的なことは知ってるよという方は次の節まで読みとばしてください)。
以下は xargs
と rm
を組み合わせて 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
はかなり便利に使うことができます。
-I
オプションについて
xargs の 普通の 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.png
を c.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;
実際に動きを観察してみます。
yes
は y\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 コマンドを使う際に役に立ててもらえれば幸いです。
以上
Discussion