Go to list of users who liked
Share on X(Twitter)
Share on Facebook
xargs -Pで安全に並列処理するシェルスクリプトの書き方
はじめに
xargs コマンドの-P オプションを使用すると、指定したコマンドを並列で実行できます。この記事では次期 POSIX (POSIX.1-2024の次、10年後ぐらい)で標準化される予定の-P オプションを使った並列処理についての注意点をまとめます。
シェルスクリプトで簡単に並列処理を行う場合、xargs コマンドを使うのが簡単です。しかしxargs コマンド自体の使い方は難しいということは知っておいてください。(並列処理の話とは関係なく)find | xargs を使ったパターンをよく見かけますが(xargs と同等程度に速い)find -exec {} + を使ったパターンのほうが簡単です(遅いfind -exec {} \; と混同しないように)。必要ない場合xargs コマンドを使わないほうが良いというのは、この記事のもう一つのテーマです。
最も簡単な並列処理の例
xargs コマンドを使った並列処理の最も簡単な例です。
seq4 | xargs-P 4-I{}sleep2このコマンドはsleep コマンドを同時に 4 プロセス並列で実行します。seq コマンドで 4 つのsleep コマンドを実行していますが、並列で実行されるため、全体の処理は 2 秒で処理は終了します。ちなみに-I{} は本来は実行するsleep コマンドの引数に{} と書いて、入力データをsleep の引数に置き換えるものですが、ここでは省略しているため入力データは捨てられます。
# -I{} の本来の使い方# sleep 1、sleep 2、sleep 3、sleep 4 が実行されるseq4 | xargs-P 4-I{}sleep{}-P 0 は多数のプロセスを生成する
多くの場合、-P 0 の並列実行数は CPU コア数ではありません。例えば次のようなコマンドを実行するとおよそ 2 秒で終了します。つまり 1000 プロセスが並列で実行していることになります。
seq1000 | xargs-P 0-I{}sleep2速く終了するのだから良いじゃないかと思うのは早計で、2 秒で終了するのはsleep コマンドが何もしないコマンドだからです。実際は 1000 個のプロセスが同時に起動して並列処理を行うため、システムリソースを過剰に使用して逆に遅くなってしまう場合があります。-P には最大のプロセス数を制限するようにしましょう。一般的には CPU コア数前後が最適です。
CPU 数を取得するにはnproc コマンドが便利です。nproc コマンドは POSIX で規定されていませんが多くの環境で使えます。POSIX.1-2024 で標準化された方法としてgetconf NPROCESSORS_ONLN でも CPU 数を取得できますが、現時点では Linux や Solaris などでは実装されていない(正確には Linux ではgetconf _NPROCESSORS_ONLN で取得できる)ようです。次のようなコードを書けば、nproc コマンドがない環境(FreeBSD以外のBSD系Unix)でもnproc コマンドが使えるようになります。
if!type nproc>/dev/null 2>&1;thennproc(){ getconf NPROCESSORS_ONLN;}finproc コマンドの呼び出しをxargs コマンドの引数に直接書くのもよくありません。なぜならnproc コマンドの呼び出しが失敗したときに対応できないからです。nproc コマンドの出力は一度変数に入れてから使いましょう。変数に入れると「CPU数 - 1」のような書き方も見やすく書けます。
# xargsコマンドの引数に nproc コマンドを書くと nproc コマンドが失敗した場合に対応できないseq1000 | xargs-P"$(nproc)"-I{}sleep2# nproc による CPU 数が取得できない場合は 4 とするNPROC=$(nproc2>/dev/null)||NPROC=4seq1000 | xargs-P"$NPROC"-I{}sleep2# CPU 数 - 1 にする場合seq1000 | xargs-P$((NPROC-1))-I{}sleep2ちなみに次期 POSIX では、-P 0 を指定したときに「実行時間が最小になるような数を選択する」と規定されており、つまりそれは実質的に未指定であり、現在の無制限から CPU 数に応じて調整するように変更しても構わないような柔軟な文章で規定されています。将来の実装では多数のプロセスが生成されなくなるかもしれません。例えば組み込み用の BusyBox では最大 100 に制限されているようです。
長いファイル名は -I{} で扱えないことがある
一般的に-I オプションは次のように使います。
find.-name"*.txt" | xargs-P 4-I{}gzip{}しかし-I にはサイズ制限があり、長いパスでは使えないことがあります。以下の例ではprintf コマンドを使って長いパスを生成しています。
# 131067 + 5 (echo) = 131072 (128KB)$printf"%0131067d\n" 0 | xargs-I{}echo{}xargs: argument list too long# 補足: いずれにしろLinuxの場合は1つの引数に 128 KB の制限がある$/bin/echo"$(printf"%0131072d\n" 0)(古い?)macOS や一部の BSD 系 Unix の実装では、制限を超えてもエラーにならず、黙って{} として扱われるので問題になることがあります。
$printf"%0254d\n" 0 | xargs-I{}echo{}000000000000000 ... 0000# macOS Ventura 13.4 での出力(エラーにならない、ひどい!)$printf"%0255d\n" 0 | xargs-I{}echo{}{}# FreeBSD 13.3では次のようにエラーになるので最新のmacOSは修正されてるかも$printf"%0255d\n" 0 | xargs-I{}echo{}xargs:commandline cannot be assembled, too longmacOS や FreeBSD や NetBSD ではこのサイズを-S オプションで増やすことができます。ただし-S オプションは POSIX では標準化されておらず、GNU 版 xargs では使えません。
$printf"%0131072d\n" 0 | xargs-S 10000000-I{}echo{}000000000000000 ... 0000しかし、OpenBSD や Solaris では-S オプションが使えず、おそらく-I オプションを使う限りサイズ制限を逃れることはでません。問題になりそうな場合、-I オプションの使用は避けたほうが良いでしょう。
個数制限をしなければ並列実行の効果はない
-I オプションの隠された(?)効果は、1行1データにすることです。つまり-I オプションを指定すると、-L 1 オプションが指定されたのと同じ効果があります。
# -Iオプションを指定すると5個のコマンドが実行される$seq5 | xargs-P 5-I{}echorun{}run 1run 2run 3run 4run 5# -L 1 オプションを指定した場合も同様$seq5 | xargs-P 5-L 1echorunrun 1run 2run 3run 4run 5もし-I または-L 1 を指定しなければ次のようにまとめて実行されます。
$seq5 | xargs-P 5echorunrun 1 2 3 4 5つまりどういうことかというと、-P オプションで 5 並列で実行するように指定していたとしても並列で実行されないということです。xargs コマンドで並列処理を行う場合は-L オプションまたは-n オプションでコマンドに渡す引数の個数を制限するようにしましょう。ちなみに引数の個数は 1 つである必要はありません。場合によっては並列で実行するよりもある程度の個数をまとめて実行し、コマンド実行の数を減らしたほうが速い場合もあります。
-L オプションと -n オプションの違い
-L オプションと-n オプションはどちらも引数の数を制限するためのオプションですが、動作に違いがあります。
# -Lは1行が1データとなる$echo"1 2 3 4 5" | xargs-L 1echorunrun 1 2 3 4 5# -nはホワイトスペースで区切られたものが1データとなる$echo"1 2 3 4 5" | xargs-n 1echorunrun 1run 2run 3run 4run 5どちらを使えばよいかは状況次第ですが、ファイル名にスペースが含まれている場合を考慮すると-L オプションを使った方が良いと言えるでしょう。しかし-L オプションにも問題がないわけではありません。次のコードは 2 行のデータのはずですが、-L オプションは 1 つにまとめて実行してしまいます。これは 1 行目の最後にスペースが有るためです。
# 3の後ろにスペースがあるので、次の行とつながって 1 行として扱われる${echo"1 2 3 ";echo"4 5";} | xargs-L 1echorunrun 1 2 3 4 5ファイルパスの最後にスペースが来ることはまずありませんが注意が必要です。
余談ですが-l や-i のように同じような意味の小文字のオプションがありますが、これは古い Unix で歴史的に使われていたオプションです。オプションの仕様が POSIX に準拠しないという理由で、新たに作られたのが-L や-I です。したがって特に理由がない限り-l や-i を使う必要はありません。
クォーテーションが含まれるとエラーになる
-L オプションを使って 1 行 1 データにしたとしてもファイルパスにクォーテーションが含まれる場合に問題になります。
# シングルクォーテーションが含まれるとエラー$echo"test'test" | xargs-L 1echorunxargs: unterminated quote# ダブルクォーテーションが含まれるとエラー$echo'test"test' | xargs-L 1echorunxargs: unterminated quote他にもバックスラッシュが含まれた場合に問題となります。
# バックスラッシュが消え去る$printf'%s\n''test\test' | xargs-L 1echorunrun testtest# バックスラッシュはxargsコマンドのメタ文字をエスケープするための特殊文字$printf'%s\n''test\"test' | xargs-L 1echorunruntest"testこの問題は-L オプションだけではなく-n オプションにもあります。xargs コマンドは本来シンプルな行を入力するコマンドではなく、xargs 形式を入力するコマンドなのです。
POSIX 準拠の-0 オプションを使う
xargs の話をすると、結局は最後にこの話にたどり着きます。xargs コマンドは本来 xargs 形式を入力するコマンドなので、ファイルパスを入力できません。その問題を解決するためにできたのが-0 オプションで、多くの環境ですでに実装されていることから POSIX.1-2024 でも標準化されました。-0 オプションは入力するデータ形式を xargs 形式からヌル文字(\0)区切りのデータ形式に変更する機能です。正確にはデータの間にヌル文字が含まれるのではなく、データの終わりに改行の代わりにヌル文字が使われるヌル文字終端形式です。
-0 オプションを使うことでデータの区切りは改行やホワイトスペースではなくなります。
$printf'%s\0' 1 2 3 | xargs-0-L 1echorunrun 1run 2run 3$printf'%s\0' 1 2 3 | xargs-0-n 1echorunrun 1run 2run 3-0 オプションを指定したときに、-L オプションと-n オプションの違いはなくなるように思えまが、-0 オプションを指定したときは-n の使用を推奨します。なぜなら-L オプション(とついでに-I オプション)は POSIX で XSI オプションとして規定されており移植性がないことが暗示されるからです。そして実際にBusyBox xargs では-L オプションは使えません。
シェルスクリプトコードを組み立てない
これは並列処理に限らず、よく見かけるバッドプラクティスですが、シェルスクリプトのコードを文字列を加工して組み立てて実行しないようにしてください。それはeval コマンドと同じです。どういうコードかというとこのようなコードです。
# ヌル文字終端を使っていてもパスにダブルクォーテーションが含まれていると# xargs自体は問題なくてもシェルの文法としてエラーが発生するfind.-name"*.tar"-print0 | xargs-0-I{} sh-c'gzip "{}"'# 補足: xargsとは関係ないが、このような使い方も危険find.-name"*.jpeg" |sed-E's/(.*)\.jpg$/mv "&" "\1.jpg"/' | sh良い書き方はコードを組み立てません。
# 末尾の「-」はshコマンドの使用で$0に相当する引数が必要なため# ($0はエラーメッセージなどで使われる。基本的になんでもよい。)find.-name"*.jpeg"-print0 | xargs-0-n1 sh-c'mv "$1" "${1%.*}.jpg"' -OSコマンドインジェクションの脆弱性が発生するのはeval コマンドだけではありません。シェルスクリプトのコードを組み立てるときや、awkコマンドのコードを組み立てるときにも脆弱性は発生します。
# 非推奨: from や to の中に system(...) などの文字列が含まれていると危険awk"$from"' <= $1 && $1 <= '"$to"# 安全な書き方(コードを組み立てない)awk-vfrom="$from"-vto="$to"'from <= $1 && $1 <= to'シェルスクリプト(やawk)のコードを組み立てるときは脆弱性を引き起こさないように、エスケープや文字チェックをしっかり行う必要があります。
結局どう書けばいいの?
xargs コマンドを使って並列処理を行うとき、まず-0 オプションを指定しデータの区切りをヌル文字終端にしてから、制限がある-I オプションを使わずに-n Nでコマンドに渡す個数を指定し、-P N で最大の並列実行数を指定します。そしてシェルスクリプトコードを組み立てないようにします。
NPROC=$(nproc2>/dev/null)||NPROC=4find.-name"*.png"-print0 |\ xargs-0-n 1-P"$NPROC" sh-c'convert "$1" "${1%.*}.jpg"' -# 補足: convert は ImageMagick に含まれる画像変換コマンド対話シェルで実行するだけだから手抜きで十分(Linuxで動けば良い、または長いファイル名がなく、クォーテーションなども含まれない)というのであれば、次のような書き方でも問題ないでしょう。
find.-name"*.tar" | xargs-L1-P4gzipfind.-name"*.tar" | xargs-I{}-P4gzip{}手抜きで短く書くか、きっちり安全に書くか、場合によって使い分けてください。
さいごに
だからxargs コマンドの使い方は難しいんだってば。気軽に使おうとするなよ。これだけで十分だっけ? 足りなければ後で追加します。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme