xargsでサクッと
複数の要素を処理したい時、
- データを取ってくる(fetch) -> 抽出(filter) -> 列挙(foreach, map)
という定型的な枠組みの中で処理を行うことが多いと思います。例えば、スクレイピングとかで抜き出した複数の動画、音声ファイルのリストを一括ダウンロードしたいとか。
そういうのは、できるだけ(Pythonやshellなどの)スクリプトとかも書かずに、commandのみでサクッと書いて済ませてしまいたいものです。
データを取ってくる(fetch)に関しては、リモートのものをfetchするならcurlとか、すでにローカルにあるならcatとかいろいろバリエーションはありそうです。
一方で、抽出(filter), 列挙(foreach, map)に関しては汎用的なcommandはまぁ、限られていて
|抽出(filter)|列挙(foreach, map)|
|---|---|---|
|grep, sed|xargs|
で処理することができます1。grep
, sed
はエンジニアの中で一度も使ったことのない人はいないと思いますが、xargs
は敷居の高いコマンドのように感じます。(私には最初、何か使いにくいなぁと感じるコマンドでした)
xargsは名前こそわかりにくいものの、実体はforeachと同じです!
なので、関数型プログラミングに慣れてしまっている人ならば、
seq 1 100 | xargs -I VAR echo VAR
(shell) <-> (1..100).map{|i| puts(i)}
(Ruby)
のように対応関係にあるのを把握しさえすれば半分は理解したようなものなのです。
(xargsの-I
optionを使えば任意の場所で引数を展開してくれ、頭がぼーっとしているときでも使えるので多用してます。)
Note) 慣例で、xargs -I{} command {}
と{}
を用いることが多いですが、別に何でもいいので今回はVAR
のようにしました。
ここまでは、xargsはforeach(またはmap)と同じだよ、というお話でした。後の半分は、「これどうやってxargs使って書けばいいんや?」っていうのをいくつか紹介していきたいと思います。
Note) もし、xargsの-I
optionを用いずにseq 1 10 | xargs echo
と書いたときは、一行一行が各要素として認識してくれないです。
% seq 1 3
1
2
3
% seq 1 3 | xargs echo
1 2 3
# xargsで-n1とoptionを書けば問題ない。t optionは後述
% seq 1 3 | xargs -t -n1 echo
echo 1
1
echo 2
2
echo 3
3
Kenta-Nakajima-no-MacBook-Pro:~
n optionについては、2つ以上の引数を渡したい時
で再掲しますが、とりあえず今は一つの引数を渡したいだけなので、-n1
(-n 1
)とすることだけ気をつければよいです。
xargsの逆引き
基本は、xargs
= foreach(or map)
なのですが、いくつかハマったところがあるので、逆引きという形でまとめていきたいと思います!
各要素ごとにxargsの中身のコマンドがどのように展開されているかを知りたい
xargs -t
とすれば良いです。[command1] | xargs [command2]
のcommand2の中身が複雑だったときに、どのように展開されているか確認したいときに便利です。
一括処理が重いのでなんとかしたい
xargsは-P [並列数]
で並列にcommandを処理することができます。例は https://orebibou.com/2015/07/%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%AA%E3%82%89%E8%A6%9A%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E3%80%8Exargs%E3%80%8F/ の「6.複数プロセスを同時に実行させる」を参考にすれば良さそうです。2
Note) 並列に処理させるコマンドとしては、他にparallelというものがあります。こちらは更に細かくカスタマイズできるようです。 -> http://bicycle1885.hatenablog.com/entry/2014/08/10/143612
2つ以上の引数を渡したい時
xargsに慣れると、1つでなく2つ以上の引数(タプル)を渡してxargsで処理したい場面がよく出てきます。
そんな場面あるの?
例えば、jsonファイルをcommandで処理したい時はjq
というのが非常に便利です3。(WebサービスなどでRESTful APIとして情報が公開されていれば、jsonファイルはcurlなどで取ってこれますよね)
ここで以下のことをしたいとします:
% cat result.json
[{
"title": "title1-1",
"mp3" : "http://example.com/music201801221234567892.mp3",
"thumbnail": "thumbnail1.png"
},{
"title": "title1-2",
"mp3" : "http://example.com/music201801221234567891.mp3",
"thumbnail": "thumbnail2.png"
},{
"title": "title1-3",
"mp3" : "http://example.com/music201801221234567890.mp3",
"thumbnail": "thumbnail3.png"
}]
% cat result.json | jq -r '.[] | [.mp3, .title + ".mp3"] | join(" ")'
http://example.com/music201801221234567892.mp3 title1-1.mp4
http://example.com/music201801221234567891.mp3 title1-2.mp4
http://example.com/music201801221234567890.mp3 title1-3.mp4
# mp3ファイル達をcurlで取ってきてそれぞれtitleにある名前でrenameしたいんだけど?
#% cat result.json | jq -r '.[] | [.mp3, .title + ".mp3"] | join(" ")' | xargs curl ???
上のように mp3ファイル達をcurlで取ってきてそれぞれtitleにある名前でrenameしたい。この場合引数を複数(2つ)使います。
引数が一つの場合だと -I
optionがめっちゃ便利なのでした。引数が2つの場合も同じように、-I
を(複数使って)、以下のように書きたいところですが
# curl url -o filename
# Could not resolve host: VAR1 でエラー
% cat result.json | jq -r '.[] | [.mp3, .title + ".mp3"] | join(" ")' | \
pipe pipe> xargs -IVAR1 -IVAR2 curl VAR1 -o VAR2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (6) Could not resolve host: VAR1
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (6) Could not resolve host: VAR1
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (6) Could not resolve host: VAR1
残念ながらダメそうです。
こんなときは、
% cat result.json | jq -r '.[] | [.mp3, .title + ".mp3"] | join(" ")' | \
xargs -n2 bash -c 'curl $0 -o $1'
とすればよいです。ポイントは3つ:
-
curl $0 -o $1
で各要素で処理したいコマンドを書く。$0
,$1
は引数パラメータ(positional parameter)で、bash -c 'curl $0 -o $1' [引数1] [引数2]
とおんなじです。 -
xargs bash -c '...'
とする。bash -c 'string'
は文字列をevalします。"curl $0 -o $1"
(ダブルクオーテーション)だと$0
や$1
が展開されるので、'curl $0 -o $1'
(シングルクオーテーション)に注意。 -
-n2
(-n 2
)とする。これは、一行ごとにhttp://example.com/musicxxx.mp3 title1-3.mp4
のように引数が2つなので。
curl $0 -o $1
を見てもらえればわかりますが、optionの中身(この場合はcurl -o
option)に引数が展開される場合でも使えるので、汎用的ですね xargs -I
optionの上位互換と思ってもらえれば良いです。(ただし、こっちのほうが複雑ですが)
まとめ
-
xargsはforeach(またはmap)と同じだよ。
-
seq 1 100 | xargs -I VAR echo VAR
<->(1..100).map{|i| puts(i)}
(Ruby)だよ -
xargsの
-t
optionでxargs -t [command1]
のcommand1がどんなふうに展開されてるのかがわかるよ -
[command1] | xargs -n2 bash -c '[command2]'
で2つの引数を渡して処理できるよ。 -
xargsの
-P
optionで並列にcommandを処理できるよ。
参考url
補足
複数の要素を処理する場合に、xargsでなく、shell scriptにてwhile read -r line
で処理するのがよい( https://qiita.com/piroor/items/77233173707a0baa6360#%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97%E5%87%A6%E7%90%86%E3%81%AF%E9%85%8D%E5%88%97%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%82%A4%E3%83%86%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A7 )と説明されている記事があるが、個人的には実務で殆ど使っていないです。理由は
- (私が)すでに関数型言語に慣れてしまっているので、
while
が気持ち悪い - シェルスクリプトを普段日常的に書かないので、バッドノウハウとか忘れてしまっている(例えば、シェルスクリプトの空白の話とか)
- shellのcommandで済ませられるならば、簡単なのでそっちで書けるに越したことはない、という気分の場合。例えば、Dockerfileを作成したいときに、スクリプトファイルを別に用意して..とやるのはちょっとめんどくさい
から。
-
grep, sedは行抽出なので、標準入力は行単位で情報が区切られているとします。 ↩
-
今回のcurlの場合、相手に過度の負荷をかけないのは大人の約束ということで。 ↩
-
brew install jq
で取ってこれる(https://github.com/stedolan/jq) csvならxsv
が良さげです。 ↩