32blogby StudioMitsu

xargs実践ガイド:出力を引数に変換する技術

xargsでコマンドを連携させる方法を実例で解説。-0で安全なファイル名処理、-Pで並列実行、-Iで引数位置指定、find・grepとの組み合わせパターン。

12 min read
目次

xargsは標準入力からアイテムを読み取り、それを引数として別のコマンドに渡す。出力を生成するコマンドと引数を必要とするコマンドをつなぐ接着剤で、いくつかのパターンを覚えるだけで使用頻度が跳ね上がる。

要は command1 | xargs command2 で、command1の出力をcommand2の引数にする。覚えるべきオプションは -0(安全なファイル名処理)、-I {}(引数位置指定)、-n(バッチサイズ)、-P(並列実行)の4つ。

xargsが必要な理由

コマンドには、stdinから入力を受け取れるもの(grepsortwc)と、引数としてしか受け取れないもの(rmmvcp)がある。後者にパイプで渡しても動かない:

bash
# これは動かない — rmはstdinを読まない
find . -name "*.tmp" | rm

rm は引数が必要。ここでxargsの出番:

bash
# これは動く — xargsがstdinを引数に変換する
find . -name "*.tmp" | xargs rm

xargsはstdinの各行(またはスペース区切りのトークン)を受け取り、指定したコマンドの引数として追加する。シンプルだが、応用範囲は広い。

基本: xargsの入力分割

デフォルトではxargsは空白(スペース、タブ、改行)で入力を分割し、全てを一括で引数として渡す:

bash
echo "file1.txt file2.txt file3.txt" | xargs touch
# touch file1.txt file2.txt file3.txt と同じ

-n で1回の呼び出しに渡す引数の数を制御できる:

bash
echo "a b c d e f" | xargs -n 2 echo
# echo a b
# echo c d
# echo e f

-t で実際に実行されるコマンドを確認(トレースモード):

bash
echo "file1 file2" | xargs -t rm
# rm file1 file2    ← 実行前にstderrに表示

スペースを含むファイル名の安全な処理: -0

xargsで一番重要なパターン。デフォルトの空白分割は、スペースを含むファイル名で壊れる:

bash
# 壊れる: "my file.txt" が "my" と "file.txt" の2引数になる
find . -name "*.txt" | xargs rm

修正は null バイト区切り — find -print0xargs -0 のペア:

bash
# 安全: nullバイトで区切るのでスペースが保持される
find . -name "*.txt" -print0 | xargs -0 rm

nullバイト(\0)はファイル名に出現できない(POSIX保証)ので、唯一確実に安全な区切り文字。findからファイル名を処理するときは常に -print0 | xargs -0 を使う。

find 以外の入力には -d でカスタム区切り文字を指定:

bash
# 空白ではなく改行で分割
echo -e "path with spaces\nanother path" | xargs -d '\n' ls -la

引数の位置指定: -I

デフォルトではxargsはコマンドの末尾に引数を追加する。別の位置に入れたい場合は -I を使う:

bash
# .logファイルを.log.bakにコピー
find /var/log -name "*.log" -print0 | xargs -0 -I {} cp {} {}.bak

-I {} はコマンド内の {} を現在の入力アイテムに置き換える。暗黙的に -n 1 となり、1アイテムずつ処理される。

実用例 — URLリストのステータスチェック:

bash
cat urls.txt | xargs -I {} curl -sS -o /dev/null -w "%{http_code} {}\n" {}
# 200 https://example.com
# 404 https://example.com/broken

並列実行: -P

xargsは -P で複数プロセスを同時実行できる:

bash
# 4ファイルずつ並列で圧縮
find . -name "*.log" -print0 | xargs -0 -P 4 -n 1 gzip

-P 4 は最大4つの並列プロセス。-n 1 で各プロセスに1ファイルずつ渡す。-n 1 がないとxargsが全ファイルを1回のgzip呼び出しにまとめてしまい、並列化が効かない。

-P 0 で利用可能な限り並列実行:

bash
# 全コアを使ってPNGをWebPに変換
find images/ -name "*.png" -print0 | xargs -0 -P 0 -I {} \
  cwebp -q 80 {} -o {}.webp

xargs -P vs find -exec

よく聞かれる: find -execfind | xargs のどっちを使うべき?

bash
# find -exec: ファイルごとに新プロセスを起動(遅い)
find . -name "*.tmp" -exec rm {} \;

# find -exec +: xargsと同様にバッチ処理(\; より高速)
find . -name "*.tmp" -exec rm {} +

# xargs: デフォルトでバッチ、-Pで並列化もできる
find . -name "*.tmp" -print0 | xargs -0 rm

直列実行では find -exec {} +xargs の性能はほぼ同等。並列実行が必要な場合は xargs の -P 一択。圧縮や画像変換のようなCPUバウンドな処理では -P の効果は大きい。

実践パターン集

パターン1: ファイルを検索してgrep

bash
# 全Pythonファイルから"TODO"を検索
find . -name "*.py" -print0 | xargs -0 grep -n "TODO"

xargsがファイル名をまとめてgrepに渡すので、find -exec grep より高速。

パターン2: ファイルの一括リネーム

bash
# 設定ファイルに.bak拡張子を追加
find /etc/myapp -name "*.conf" -print0 | xargs -0 -I {} mv {} {}.bak

パターン3: 画像の並列処理

bash
# JPEG画像を最大1920px幅にリサイズ(8並列)
find photos/ -name "*.jpg" -print0 | \
  xargs -0 -P 8 -I {} convert {} -resize "1920>" {}

パターン4: 30日以上前のファイルを削除

bash
find /tmp -type f -mtime +30 -print0 | xargs -0 rm -f

パターン5: ファイルの各行に対してコマンド実行

bash
# ホストリストの各ホストにping(3並列)
cat hosts.txt | xargs -P 3 -I {} ping -c 1 -W 2 {}

パターン6: Git — パターンに一致するファイルをステージ

bash
git diff --name-only | grep "\.tsx$" | xargs git add

パターン7: 一括APIコール

bash
# URLリストのHTTPステータスを確認(10並列)
cat endpoints.txt | xargs -P 10 -I {} \
  curl -sS -o /dev/null -w "%{http_code} {}\n" {}

よくある間違い

findで-0を忘れる

bash
# 間違い — スペース入りファイル名で壊れる
find . -name "*.txt" | xargs wc -l

# 正しい
find . -name "*.txt" -print0 | xargs -0 wc -l

不要な-Iの使用

bash
# 遅い — 1ファイルずつ処理
find . -name "*.log" | xargs -I {} gzip {}

# 速い — 全ファイルを一括でgzipに渡す
find . -name "*.log" -print0 | xargs -0 gzip

ARG_MAX制限を無視する

極端に長いファイルリストはOSの引数上限を超える可能性がある。xargsはこれを自動的に複数回の呼び出しに分割して処理する。手動の $() 展開よりxargsの方が安全:

bash
# "Argument list too long" で失敗する可能性
rm $(find . -name "*.tmp")

# 安全 — xargsが自動分割
find . -name "*.tmp" -print0 | xargs -0 rm

FAQ

xargsはパイプだけでは何ができないの?

パイプはstdoutをstdinに送る。でも多くのコマンド(rm、mv、cp、mkdir、chmod)はstdinではなく引数を期待する。xargsがstdinをコマンドライン引数に変換することでそのギャップを埋める。

xargs -0 はいつ使うべき?

find からファイル名を処理するときは常に。-print0 / -0 の組み合わせが、スペース・改行・特殊文字を含むファイル名を安全に扱う唯一の方法。

xargs -P と GNU Parallel の違いは?

xargs -P はシンプルでどこでも使える(findutilsの一部)。GNU Parallelはジョブログ、レジューム、リモート実行、プログレスバーなどの機能がある。ローカルでの単純な並列化なら xargs -P で十分。複雑なジョブ配信が必要ならGNU Parallelを検討。

入力が空のときxargsはどうなる?

デフォルトでは引数なしでもコマンドを1回実行する。--no-run-if-empty(GNU xargsでは -r)で防げる:

bash
find . -name "*.nonexistent" -print0 | xargs -0 --no-run-if-empty rm

find -exec と find | xargs、どっちを使うべき?

単純なケースでは find -exec {} + で十分(xargsと同様にバッチ処理、構文もシンプル)。-P(並列化)や -n(カスタムバッチ)が必要な場合、またはfindと最終コマンドの間にフィルタを挟みたい場合は find | xargs

macOSでも動く?

動くが、macOSにはBSD版xargsが入っていてフラグが微妙に違う。主な違い: --no-run-if-empty がBSD版ではデフォルト動作。この記事のパターンはほぼ両方で動作する。

xargsが実行しているコマンドをデバッグするには?

-t で実行前にコマンドを表示、-p で実行前に確認を求める。複雑なパイプラインを組み立てるとき必須。

xargsで複数コマンドを実行できる?

直接はできないが、シェルを呼び出せばいい:

bash
find . -name "*.md" -print0 | xargs -0 -I {} sh -c 'echo "Processing {}"; wc -l {}'

まとめ

xargsはシンプルだけど、パイプラインの組み立て方を根本的に変えるコマンドだ。覚えておくべきパターン:

  • find -print0 | xargs -0 — ファイル名の安全な処理。毎回使う
  • xargs -I {} — 引数の位置を制御したいとき
  • xargs -P N -n 1 — CPUバウンドな処理の並列実行
  • xargs -t — 実際に何が実行されているかのデバッグ

すでにgrepやripgrepsed/awkを使っているなら、xargsはそれらをつなぐピースになる。fzfと組み合わせればインタラクティブな選択もでき、大抵のファイル処理はターミナルから出ずに完結する。