LoginSignup
6
6

More than 3 years have passed since last update.

[Bash, Zsh, Dash] xargsに渡す引数を変数展開(無理やり)

Last updated at Posted at 2019-09-26

TL;DR

shell のコマンドストリング( -c オプションの後ろの文字列)内で展開します.
渡すコマンドストリングの行数で挙動が変わります.
(例: sh -c 'echo foo; echo bar' は 2行 のコマンドストリングを渡している)

xargs-P オプションを付ければ高速化も簡単ですね.

Bash

bash
$ echo foo.bar | xargs -I{} bash -c 'echo ${0%.*}' {}
foo
  • 位置パラメータの指定
    • $0から
  • コマンドストリングが1行のとき
    • 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的なexecが実行される
  • コマンドストリングが2行以上のとき

Zsh

zsh
$ echo foo.bar | xargs -I{} zsh -c 'echo ${0%.*}' {}
foo
  • 位置パラメータの指定
    • $0から
  • コマンドストリングが1行のとき
    • 渡されたコマンドに対し,新たなshellプロセスの中で暗黙的なexecが実行される
    • bashと同じ挙動
  • コマンドストリングが2行以上のとき: こちらの記事 [1] を参照

Dash

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
$ bash -c 'echo $0'
bash

・引数を置く場合

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でファイルの変換をしてみました.

bash
$ 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秒 でした.

bash
$ 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)

bash
$ 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

ハマりどころ

最初は,

bash
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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6