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