Linux コマンドラインのコンセプト: xargs
この記事は、シリーズ 機械学習エンジニアのためのLinux の第9回です。
前回の復習
前回の記事では、画像編集ツール ImageMagick の convert
コマンドを紹介した。
この記事では、xargs
というコマンドを紹介する。
このコマンドは、大量のファイルを操作する際に欠かせないコマンドだ。
xargs
とは
xargs
とは「標準入力から受け取った内容を引数にして、別のコマンドを実行する」コマンドだ。
…といってもイメージが湧きづらいと思うので、実例を見ながら説明しよう。
❓ 例題
find
コマンドで数百枚の画像ファイルを検索し、それらの画像の情報を ImageMagick のidentify
コマンドで表示したい。 どうすればよいか。
まず、コマンドの説明から始めよう。
第5回の記事 で説明したように、find
は検索条件に合致するファイルパスを出力する。
$ # find コマンドの実行例
$ # カレントディレクトリ (.) を再帰的に検索し、
$ # 末尾が '.JPG' のファイルパスを出力する
$ find . -name '*.JPG'
./train/good/0001.JPG
./train/good/0002.JPG
./train/good/0003.JPG
./train/damaged/1001.JPG
./train/damaged/1002.JPG
./train/damaged/1003.JPG
...
一方 identify
は、画像ファイルのパスを引数で指定すると、その画像の情報を表示するコマンドだ。
次のコマンド例のように、画像フォーマットやピクセル数、色空間などの情報を表示できる(オプションとして -verbose
を指定すると、EXIF情報を含む詳細なデータを表示できる)。
$ # identify コマンドの実行例
$ # 画像フォーマット: JPEG
$ # 画像サイズ: 3840x2160
$ # 色空間: 8-bit sRGB
$ identify ./train/good/0001.JPG
0001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.75087MiB 0.010u 0:00.009
では、次のように find
で検索したすべてのファイルに対して identify
を実行するには、どうすればよいだろうか、というのがこの例題である。
$ (???)
0001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.75087MiB 0.010u 0:00.009
0002.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 3.62436MiB 0.010u 0:00.010
0003.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 3.55877MiB 0.000u 0:00.009
1001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.54995MiB 0.010u 0:00.010
1002.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.33384MiB 0.000u 0:00.010
1003.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.31245MiB 0.000u 0:00.000
...
ここで登場するのが xargs
だ。
find
と xargs
をパイプで接続し、xargs
の引数として identify
を渡す。
こうすると、find
で検索した結果が identify
の引数として渡され、実行される。
$ # find, xargs, identify を組み合わせた例
$ find . -name '*.JPG' | xargs identify
0001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.75087MiB 0.010u 0:00.009
0002.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 3.62436MiB 0.010u 0:00.010
0003.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 3.55877MiB 0.000u 0:00.009
...
言い換えると、上記のコマンド例は find
が出力したファイルパスをコピーして、
identify
の引数にペーストして実行するのと等価だ。
$ # 上記のコマンドは、次のコマンドと等価
$ identify ./train/good/0001.JPG ./train/good/0002.JPG ./train/good/0003.JPG ./train/damaged/1001.JPG ...
...
数十個程度のパスであればコピー&ペーストでも問題ないが、数百万個のファイルを処理するのはたいへんだ。
このような時に xargs
が役に立つ。
xargs
のオプション
xargs
にはいくつかのオプションがある。代表的なものを見ていこう。
-n
オプション: 引数の数を指定する
identify
コマンドに一度に渡す引数の数は、-n
オプションで指定できる。
例えば -n1
と指定すると、xargs
は JPEGファイルのパス1つずつに対して identify
コマンドを実行する。
$ # -n1 を指定すると...
$ find . -name '*.JPG' | xargs -n1 identify
$ # 次のように実行される
$ identify ./train/good/0001.JPG
$ identify ./train/good/0002.JPG
$ identify ./train/good/0003.JPG
$ identify ./train/damaged/1001.JPG ...
identify
は複数のファイルパスを引数として渡せるから -n
を指定しなくてもよいし、指定しないほうがよい(複数のidentify
プロセスが実行されるので、処理時間が長くなってしまう)。
しかし、1つのパスしか渡せないコマンドを実行するときや、決められた個数の引数をセットで渡すときは -n
を使う価値がある。
-I
オプション: コマンドへの引数の渡し方を指定する
コマンドへの引数の渡し方を細かく指定したい場合には、-I
オプションが役立つ。
-I{}
のように指定すると、xargs
で呼び出すコマンドの引数に含まれる {}
がすべて xargs
から渡される引数に置き換えられる。
…これも、説明より実例を見たほうが分かりやすいと思う。
❓ 例題
ImageMagick の
convert
コマンドを使って、数百枚の画像のサムネイルを作成したい。 サムネイルは、元の画像を 256×256 px 四方にリサイズした画像とする。
- 変換前の画像ファイルは
original/
ディレクトリ直下にある。- サムネイルは
thumbnails/
直下に保存したい。
ImageMagick の convert
コマンドでは、変換前と変換後の2つの画像パスを指定する必要がある。
1枚のサムネイルを作成するコマンドは次のようになる。
$ # 画像1枚のサムネイルを作成する例
$ # `original/0001.JPG` の サムネイルを `thumbnails/0001.JPG` に書き出す
$ convert original/0001.JPG -resize '256x256!' thumbnails/0001.JPG
これを xargs
でバッチ化するのだが、可変の引数としたい箇所 (0001.JPG
) が2箇所ある。
このようなときに活躍するのが -I
オプションだ。
まず、可変の引数であるファイル名を生成するパイプラインを構築する。
$ # 引数のうち、可変部分(ファイル名)を出力するパイプライン
$ find original -maxdepth 1 -name '*.JPG' | cut -d/ -f2
0001.JPG
0002.JPG
0003.JPG
...
そして、xargs -I{}
をこの後ろにくっつける。
$ # 可変部分を、convert の引数(original/{}, thumbnails/{})に埋め込む
$ find original -maxdepth 1 -name '*.JPG' | cut -d/ -f2 |
xargs -I{} convert original/{} -resize '256x256!' thumbnails/{}
こうすると、convert
コマンドの引数のうち、中括弧 ({}
)
でマークした2箇所がファイル名(0001.JPG
、0002.JPG
、…)で置き換えられてから実行される。
言い換えると、上記のパイプラインは次のコマンドと等価になる。
$ # 上記のパイプラインは、次のように実行される
$ convert original/0001.JPG -resize '256x256!' thumbnails/0001.JPG
$ convert original/0002.JPG -resize '256x256!' thumbnails/0002.JPG
$ convert original/0003.JPG -resize '256x256!' thumbnails/0003.JPG
...
このように、パイプラインから受け取った出力を引数の複数箇所に登場させたり、引数の一部にピンポイントで埋め込んだりする場合には、-I
オプションが有用だ。
ちなみに、置き換える文字列には中括弧を使うことが多いが、-I@@
のように他の文字列を指定してもよい。
xargs
が必要なのは、どんなとき?
第7回の記事 では find
・tr
・cut
という3つのコマンドを直接パイプで接続する例を紹介した。
それに対して identify
コマンドを find
と組み合わせるときは xargs
を挟む必要がある。
xargs
なしでパイプ接続できるコマンドと、xargs
を必要とするコマンドの違いは何だろうか?
データの渡し方: 標準入出力と引数
この違いを理解するうえでの鍵となるのが、コマンドへのデータの渡し方だ。
各種の Linux コマンドは、標準入力や引数などいろいろなチャネルから、処理に必要なデータを受け取っている。
例えば cut
や tr
などのフィルタコマンドは、標準入力からデータを受け取るタイプのコマンドだ。
任意のコマンドと cut
をパイプでつなぐと、cut
コマンドは指定したデータだけを抽出してくれる。
$ # cut の標準入力に、処理したいデータをパイプ経由で渡す
$ <任意のコマンド> | cut
一方、先ほど見た identify
をはじめ、ls
や convert
などのコマンドは、操作対象のファイルを引数として指定する。
$ # identify の引数として、処理対象のファイルパスを渡す
$ identify /path/to/file
標準入力と引数のどちらからデータを渡してもよい、というコマンドもある。
例えば cut
コマンドには、標準入力からデータを渡す代わりに、データの入ったファイルのパスを引数として指定することもできる。
$ # cut には引数として、処理するテキストのファイルパスを渡してもよい
$ cut /path/to/text-file
例外もあるが、処理するデータ自体は標準入力として渡し、ファイルパスは引数として渡す、というのが一般的だ。
コマンドの組み合わせパターン
複数のコマンドをパイプで組み合わせる場合、代表的な例として次の2パターンが考えられる。
- コマンドAがデータを出力 → コマンドB(
cut
やtr
など)でデータを加工 - コマンドAがファイルパスを出力 → コマンドBでそのファイルを処理
xargs
が必要になるのは、後者の「コマンドAが出力したファイルパスを使って、コマンドBでファイル自体を処理する」パターンだ。
記事の序盤で紹介した find
と identify
の組み合わせの例も、このパターンに当てはまる。
このパターンのとき、コマンドA (find
など) の出力をそのままコマンドB (identify
など) にパイプ接続すると、ファイルパスはコマンドBに標準入力として渡されてしまう。
多くの場合、ファイルパスは引数として渡す必要があるから、これではコマンドBが動作しない。
$ # find がファイルパスを出力する
$ find
./train/good/0001.JPG
./train/good/0002.JPG
...
$ # command_B でファイルを処理する
$ identify ./train/good/0001.JPG
$ # ❌ これは期待通りに動作しない
$ # (ファイルパスが identify の標準入力に渡される)
$ find | identify
そこで xargs
の出番だ。
xargs
は、標準入力から引数にデータを移す役割をもっている。
find
と xargs identify
を接続すると、xargs
は find
が出力するパスを受け取り、それを identify
に引数として渡して実行する。
このため、「find
で検索したファイルパスに対して identify
を実行する」という動作ができるようになる。
$ # ✅ これは正しく動作する
$ # (ファイルパスが identify に引数として渡される)
$ find | xargs identify
並列化
xargs
には「標準入力を引数に移す」機能のほかに、コマンドの並列化(マルチプロセシング)という機能ももっている。
これを活用すると、時間のかかる処理を高速化することができる。
並列化を有効にするには、xargs
の -P
オプションを使う。
-P
にプロセス数を指定すると、その数だけコマンドが並列実行される。
xargs -P<プロセス数> <実行するコマンド>
例えば、次のコマンドは「拡張子 .JPG の画像を 256px 四方にリサイズして上書き保存する」コマンドだ。
find
、xargs
、ImageMagick の mogrify
という3つのコマンドを使っている。
$ # 拡張子 .JPG の画像を256px四方に縮小し、上書き保存する
$ # mogrify は1つのみ立ち上げ
$ find . -name '*.JPG' | xargs mogrify -resize '256x256!'
上記の例で、同時に実行される mogrify
プロセスは1つだけだ。
これを4つのプロセスで並列実行してみよう (-P4
)。
$ find . -name '*.JPG' | xargs -P4 mogrify -resize '256x256!'
こうすると、4つの mogrify
プロセスが同時に画像を処理する。
なお mogrify
の場合は、残念ながら並列化の恩恵は受けられない(mogrify
自体がマルチスレッドによる複数ファイルのリサイズ処理に対応している)。
しかし、複数コアCPUをもつマシンでマルチスレッド非対応のコマンドを実行する場合は、処理を大幅に高速化できる。
「空白文字」という落とし穴
xargs
を使ううえで注意したいのが 空白文字の処理 だ。
ファイル名に含まれる空白文字に気をつけないと、意図しない処理が実行されて大事故を起こすことがある。
これまでと同様に、find
の出力を xargs
にパイプするケースを考えてみよう。
ただし、これまでの事例と異なり、ファイル名にはスペースが含まれている。
$ find . -name '*.JPG'
./train/fish/2020-09-18 DCIM0001.JPG
./train/fish/2020-09-18 DCIM0002.JPG
./train/fish/2020-09-22 DCIM0003.JPG
./train/dog/2020-10-07 DSC_1001.JPG
この出力を xargs identify
にパイプすると、「ファイルが存在しない(No such file or directory)」というエラーになってしまう。
。
$ # ❌ find の出力を直接 xargs に渡すとエラーになる
$ find . -name '*.JPG' | xargs identify
identify-im6.q16: unable to open image `./train/fish/2020-09-18': No such file or directory @ error/blob.c/OpenBlob/2874.
identify-im6.q16: no decode delegate for this image format `' @ error/constitute.c/ReadImage/560.
identify-im6.q16: unable to open image `DCIM0001.JPG': No such file or directory @ error/blob.c/OpenBlob/2874.
identify-im6.q16: unable to open image `DCIM0001.JPG': No such file or directory @ error/blob.c/OpenBlob/2874.
...
このエラーが起こる原因は、xargs
が引数を区切る方法に関係している。
xargs
は標準でスペースや改行などの「空白文字」を引数の区切りとみなす。
このため、スペースが含まれているファイルパスを渡すと、スペースの前後でファイルパスを区切って identify
に渡してしまう。
つまり、"./train/fish/2020-09-18 DCIM0001.JPG
" というファイルパスを、xargs
は “./train/fish/2020-09-18
” と “DCIM0001.JPG
” という2つの引数と解釈してしまい、エラーになってしまう。
$ # xargs は次のように identify を呼び出すので、エラーになる
$ identify ./train/fish/2020-09-18
$ identify DCIM0001.JPG
$ identify ./train/fish/2020-09-18
$ identify DCIM0002.JPG
...
このような理由から、空白文字が含まれる可能性のあるファイルパスを扱う場合は -0
オプションを使うのが安全だ。
-0
オプションをつけると、xargs
は空白文字の代わりに「ヌル文字」とよばれる記号を引数の区切りとして扱う。
Linux ではヌル文字をファイル名に含めることができないので、エラーを心配せずにファイルパスを扱える。
この場合、 find
コマンドの側でもファイルパス間にヌル文字を出力させる必要がある。
find
は、デフォルトではファイルパス間に改行を出力するが、-print0
をつけるとヌル文字が出力される。
find
に -print0
、xargs
に -0
をつけて再実行すると、意図したとおりにコマンドが実行される。
$ # ✅ 引数間の区切りをヌル文字にすると、うまくいく
$ find . -name '*.JPG' -print0 | xargs -0 identify
./train/fish/2020-09-18 DCIM0001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.39637MiB 0.010u 0:00.010
./train/fish/2020-09-18 DCIM0002.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.54445MiB 0.000u 0:00.000
./train/fish/2020-09-22 DCIM0003.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2.68571MiB 0.010u 0:00.000
./train/dog/2020-10-07 DSC_1001.JPG JPEG 3840x2160 3840x2160+0+0 8-bit sRGB 2811470B 0.000u 0:00.000
📔 ノート
上記の例では
find
とxargs
の2つによるパイプ処理を紹介したが、パイプラインに他のコマンドが含まれる場合は、それらのコマンドもヌル文字を区切り文字として扱うように設定する必要がある。例えば、
cut
コマンドはデフォルトで改行を区切り文字としているが、find -print0
の結果をcut
に渡す場合は、cut
の-z
オプションをつける必要がある。$ # ヌル文字区切りの出力を cut に渡すときは、-z をつける $ find . -name '*.JPG' -print0 | cut -z -d/ -f4
まとめ
今回の記事では xargs
コマンドについて説明した。
コマンドライン上で大量のデータを操作するとき、xargs
のようなコマンドは2つの理由で重要な役割を果たす。
xargs
は、ファイルパスを出力するコマンドと、そのファイルパスを引数として処理をするコマンドの橋渡しをしてくれる。xargs
によってコマンド処理を並列化し、処理時間を短縮できる。
次回の記事では、前回の記事で提示した「大量の画像に対するサムネイルを作成する」という例題の解決方法を紹介しよう。
xargs
を使うことで、この例題のような複雑な処理も可能になる。
それでは、次回の記事をお楽しみに!
🎯 まとめ
xargs
: 標準入力から受け取った値を、別のコマンドの引数に渡して実行するコマンドxargs
のオプション
-n<数値>
: コマンドに一度に渡す引数の個数を指定する-I<文字列>
: 指定した文字列を引数に置き換える-0
: ヌル文字を引数間の区切りとする(引数として渡す値にスペースなどの空白文字が含まれる場合に必要)
❓ 練習課題
- 次のコマンドがどのような処理をするか、説明してみよう。
$ find samples/ -name '*.wav' -print0 | xargs -0 ls -l
- Linux において、ファイル名として使うことが禁止されている文字はなにか、調べてみよう。