TL;DR
shell のコマンドストリング( -c オプションの後ろの文字列)内で展開します.
渡すコマンドストリングの行数で挙動が変わります.
(例: sh -c 'echo foo; echo bar' は 2行 のコマンドストリングを渡している)
xargs に -P オプションを付ければ高速化も簡単ですね.
Bash
$ echo foo.bar | xargs -I{} bash -c 'echo ${0%.*}' {}
foo
- 位置パラメータの指定
-
$0から
-
- コマンドストリングが1行のとき
- 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的な
execが実行される
- 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的な
- コマンドストリングが2行以上のとき
- こちらの記事 [1] を参照
Zsh
$ echo foo.bar | xargs -I{} zsh -c 'echo ${0%.*}' {}
foo
- 位置パラメータの指定
-
$0から
-
- コマンドストリングが1行のとき
- 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的な
execが実行される -
bashと同じ挙動
- 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的な
- コマンドストリングが2行以上のとき: こちらの記事 [1] を参照
Dash
$ echo foo.bar | xargs -I{} dash -c 'echo ${1%.*}' {}
foo
- 位置パラメータの指定
-
$1から
-
- コマンドストリングの行数に関わらず,shellを立ち上げその子プロセスとして実行
(fish?)
fishの場合も調べたのですがわかりませんでした.
もしもわかる人がいたら教えてください!
この記事について
xargsに渡す引数を変数展開したかったのでメモ
ffmpegを使って aac を格納している m4a ファイルを mp3 に変換するときに,xargsの-Pオプションを指定して高速に処理しようとしました.
しかし,「変数展開で拡張子変えられない!」となったので調べました.
これより以下のコマンドは全てbashで動作させています.
動作環境
$ cat /etc/redhat-release
Fedora release 30 (Thirty)
$ uname -srvmpio
Linux 5.2.15-200.fc30.i686 #1 SMP Mon Sep 16 15:21:49 UTC 2019 i686 i686 i386 GNU/Linux
$ bash --version
GNU bash, version 5.0.7(1)-release (i686-redhat-linux-gnu)
方法
こちらの記事 [1] で紹介されているbashコマンドの使い方を応用します.
以下のように,-cオプションで指定する文字列(以下,コマンドストリング)の後に引数を置くと,$0から順番にコマンドストリング内の位置パラメータへ代入されます.
・引数を置かない場合
$ bash -c 'echo $0'
bash
・引数を置く場合
$ bash -c 'echo $0' 1st_arg
1st_arg
コマンドストリング内では変数展開できるので
$ echo /bin/uname | \
xargs -I{} bash -c \
'echo ${#0} ${2^^} $(basename $1) $($3)' {} {} {}.up {}
10 /BIN/UNAME.UP uname Linux
というようなことが可能です.
使用例
実際にffmpegでファイルの変換をしてみました.
$ ls
01.m4a 02.m4a 03.m4a 04.m4a 05.m4a
$ ffmpeg -i 01.m4a |& grep Audio
Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 127 kb/s (default)
$ find . -name '*.m4a' | \
xargs -P 5 -I{} bash -c 'ffmpeg -i $0 ${1%.*}.mp3' {} {}
$ ls
1.m4a 01.mp3 02.m4a 02.mp3 03.m4a 03.mp3 04.m4a 04.mp3 05.m4a 05.mp3
$ ffmpeg -i 01.mp3 |& grep Audio
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 128 kb/s
変換できていますね.
本当に速いのか?
xargsで並列化していても,bashのプロセスを立ち上げるオーバーヘッドが気になります.
そこで, for ループを回した場合との実行時間を比較しました.
forループの場合
下記のコマンドで実行時間を 10回 測りました.
平均は 54.41秒 でした.
$ ls
01.m4a 02.m4a 03.m4a 04.m4a 05.m4a
$ time ( for i in $(find . -name '*.m4a'); do \
ffmpeg -i $i ${i%.*}.mp3; \
done; )
real 0m57.303s
user 0m56.372s
sys 0m0.193s
xargsを使った場合
下記のコマンドで実行時間を 10回 測りました.
平均は 24.51秒 でした.
xargsの-Pオプションには5を渡して計測しています.
(最大同時実行数が5)
$ ls
01.m4a 02.m4a 03.m4a 04.m4a 05.m4a
$ time ( find . -name '*.m4a' | \
xargs -P 5 -I{} bash -c 'ffmpeg -i $0 ${1%.*}.mp3' {} {} )
real 0m25.671s
user 1m22.154s
sys 0m0.312s
結果
それぞれ 10回 ずつ測定して平均(算術)を計算したものを表にしました.
今回の測定ではxargsを使った場合の方が,2倍以上 速いという結果でした.
| forloop [sec] | xargs [sec] | 比率 (forloop/xargs) |
|---|---|---|
| 54.41 | 24.51 | 2.210 |
ハマりどころ
最初は,
find . -name '*.m4a' | \
xargs -I{} bash -c 'ffmpeg -i $1 ${2%.*}.mp3' {} {}
のように,シェルスクリプトと同じ感覚で最初の引数を$1で指定していました.
当然のようにうまくいかず,必死に色々な方法を試しました.
まさか,最初の引数を$0から指定するとは思わず,気付いたときには「なんでこんな仕様なんだ!」と感じたものです.
shellによって挙動が変わる
詳しくは先に示したこちらの記事 [1] をご覧ください.
参考
1 shellの-cオプションについてUbuntuのsh(dash)、bash、zshはそれぞれ違う挙動をする - Qiita