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

# TL;DR

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

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


## `Bash`

```bash:bash
$ echo foo.bar | xargs -I{} bash -c 'echo ${0%.*}' {}
foo
```

- 位置パラメータの指定
    - `$0`から
- コマンドストリングが１行のとき
    - 渡されたコマンドに対し，新たなshellプロセスの中で暗黙的な`exec`が実行される
- コマンドストリングが２行以上のとき
    - [こちらの記事 \[1\]][1] を参照


## `Zsh`

```zsh:zsh
$ echo foo.bar | xargs -I{} zsh -c 'echo ${0%.*}' {}
foo
```

- 位置パラメータの指定
    - `$0`から
- コマンドストリングが１行のとき
    - 渡されたコマンドに対し，新たなshellプロセスの中で暗黙的な`exec`が実行される
    - `bash`と同じ挙動
- コマンドストリングが２行以上のとき: [こちらの記事 \[1\]][1] を参照


## `Dash`

```shell: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\]][1] で紹介されている`bash`コマンドの使い方を応用します．
以下のように，`-c`オプションで指定する文字列（以下，コマンドストリング）の後に引数を置くと，`$0`から順番にコマンドストリング内の位置パラメータへ代入されます．

**・引数を置かない場合**

```bash:bash
$ bash -c 'echo $0'
bash
```
**・引数を置く場合**

```bash:bash
$ bash -c 'echo $0' 1st_arg
1st_arg
```
<br>
コマンドストリング内では変数展開できるので

```bash
$ echo /bin/uname | \
      xargs -I{} bash -c \
          'echo ${#0} ${2^^} $(basename $1) $($3)' {} {} {}.up {}
10 /BIN/UNAME.UP uname Linux
```
というようなことが可能です．


# 使用例

実際に`ffmpeg`でファイルの変換をしてみました．

```bash: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: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: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:bash
find . -name '*.m4a' | \
    xargs -I{} bash -c 'ffmpeg -i $1 ${2%.*}.mp3' {} {}
```
のように，シェルスクリプトと同じ感覚で最初の引数を`$1`で指定していました．
当然のようにうまくいかず，必死に色々な方法を試しました．
まさか，最初の引数を`$0`から指定するとは思わず，気付いたときには「なんでこんな仕様なんだ！」と感じたものです．


## shellによって挙動が変わる

詳しくは先に示した[こちらの記事 \[1\]][1] をご覧ください．


# 参考

[1]:https://qiita.com/ukinau/items/410f56b6d777ad1e4e90#dash-bash-zshの-cの違い
[1] [shellの-cオプションについてUbuntuのsh(dash)、bash、zshはそれぞれ違う挙動をする - Qiita][1]