マニアックを突き進め、プロセス置換

zshにはプロセス置換(Process Substitution)と呼ばれる機能がある。これはファイルを指定すべき場所や、パイプとして処理すべきところを「コマンドやコマンドの組み合わせそのもの」を記述して処理するいう機能だ。コマンドをファイルのように扱えるものといってもよい。

表1.1 プロセス置換の記述方法

記述方法 内容
<(コマンドリスト) コマンドの結果をファイルとして扱う
>(コマンドリスト) 出力先をコマンドに渡す

コマンドで指定するファイルをファイルではなくコマンドとして直接指定することで、一時ファイルを作成する必要がなくなる。どれだけ使うシーンがあるかと問われると頭を捻ってしまうが、ログ解析や差分チェックなどを使っている人からは「けっこう便利に使っている」という話も聞く。

さあ、今回はこのマニアックな機能を堪能していこう。ちなみに今回の内容はその多くをzshマニュアルの「Process Substitution」の項を参考にしている。コマンド例もこれを参考にしたものだ。より詳しく知りたい読者の方は、そちらも合わせてご覧いただきたい。

例えばこういった場合

CSVファイルを編集する例を考えてみよう。表2.1のようなテーブルデータを用意したとしよう。テキスト状態ではプロンプト2.1のようになっている。

表2.1 CSVファイルの例

氏名 年齢 性別 備考
Zsher01 32 Male Both bash
Zsher02 32 Male   
Zsher03 35 Female   
Zsher04 32 Male From tcsh
Zsher05 35 Male From bash

プロンプト2.1 テキストファイルの場合

% cat sheet01.csv
"氏名","年齢","性別","備考"
"Zsher01",32,"Male","Both bash"
"Zsher02",32,"Male",
"Zsher03",35,"Female",
"Zsher04",32,"Male","From tcsh"
"Zsher05",35,"Male","From bash"
%

例えばこのファイルから、氏名と年齢の2列だけを取り出したいとする。これにはさまざまな方法があるが、一例としてcut(1)コマンドを使えばプロンプト2.2のようになる。

プロンプト2.2 cut(1)コマンドの使用例

% cut -f 1,2 -d, sheet01.csv
"氏名","年齢"
"Zsher01",32
"Zsher02",32
"Zsher03",35
"Zsher04",32
"Zsher05",35
%

プロセス置換で実現してみよう

わざわざプロセス置換で同じことをする必要はないわけだが、説明を分かりやすくするために、同じことをプロセス置換でやってみる。cut(1)コマンドはプロンプト3.1やプロンプト3.2のようにすれば1列ずつ取り出せる。これらを組み合わせてプロンプト2.2のようにしてみよう。

プロンプト3.1 1列目だけ取り出す

% cut -f 1 -d, sheet01.csv
"氏名"
"Zsher01"
"Zsher02"
"Zsher03"
"Zsher04"
"Zsher05"
%

プロンプト3.2 2列目だけ取り出す

% cut -f 2 -d, sheet01.csv
"年齢"
32
32
35
32
35
%

paste(1)コマンドで出力結果をマージすればプロンプト3.3のようになる。これを見れば、コマンドの出力がそのままファイルを指定した場合のように扱われていることが分かるだろう。

プロンプト3.3 プロセス置換を使った場合

% paste <(cut -f 1 -d, sheet01.csv) <(cut -f 2 -d, sheet01.csv)
"氏名" "年齢"
"Zsher01" 32
"Zsher02" 32
"Zsher03" 35
"Zsher04" 32
"Zsher05" 35
%

プロセス置換は使われるシーンやシステムに用意されている機能などによって、場合によって/dev/fd/?のファイルへの置換、FIFO(名前付きパイプ)、パイプなどの機構に置き換えて実行される。つまりプロンプト3.3の場合、paste(1)コマンドとしてはファイルが指定された場合と処理は何ら変わっていないということだ。試しにdiff(1)コマンドなどで実行してみれば、実際にはどういったファイルが使われているかが分かる。

プロンプト3.4 diff(1)で実行した場合 - プロセス置換の部分がファイルに置き換えられていることが分かる

% diff -u <(cut -f 1 -d, sheet01.csv) <(cut -f 2 -d, sheet01.csv)
--- /tmp/zshiA99oo Thu May 10 19:57:03 2007
+++ /tmp/zshYfMoLl Thu May 10 19:57:03 2007
@@ -1,6 +1,6 @@
-"氏名"
-"Zsher01"
-"Zsher02"
-"Zsher03"
-"Zsher04"
-"Zsher05"
+"年齢"
+32
+32
+35
+32
+35
%

例えばプロセス置換に"; sleep 10"あたりを仕込んで「diff -u <(cut -f 1 -d, sheet01.csv; sleep 10) <(cut -f 2 -d, sheet01.csv)」のように実行し、プロンプト3.5のようにfile(1)コマンドを使えば、プロセス置換がFIFO(名前付きパイプ)に置き換えられていることが分かる。

プロンプト3.5 プロセス置換の部分に"; sleep 10"を仕込んでfile(1)コマンドでチェック

% file /tmp/zsh*
/tmp/zshiA99oo: fifo (named pipe)
/tmp/zshYfMoLl: fifo (named pipe)
%

要するに一時ファイルはzshが適当に用意してくれるわけだ。

もう1つの方も

コマンドの出力をファイルではなく、逆に出力をコマンドに流し込む指定が「>(コマンドリスト)」だ。コマンドの出力を複数のコマンドリストに流し込むことができる。少々分かりにくいかもしれないが、プロンプト3.3のコマンドの出力を2つの処理に渡して処理する例をプロンプト4.1に示す。

プロンプト4.1 出力結果をコマンドへ流し込む

% atoZ() { tr "[a-z]" "[A-Z]" }
% Atoz() { tr "[A-Z]" "[a-z]" }
% paste <(cut -f 1 -d, sheet01.csv) <(cut -f 2 -d, sheet01.csv) \
> >(grep 32 | atoZ) > >(grep 35 | Atoz)
"ZSHER01" 32
"ZSHER02" 32
"ZSHER04" 32
"zsher03" 35
"zsher05" 35
%

「>(コマンドリスト)」についてだが、コマンドにそのまま流し込みたいので「> >(コマンドリスト)」のようにリダイレクトも併用している。これでパイプで流した場合と同じような効果がある。このあたりは理解しにくいかもしれないが、「>(コマンドリスト)」に対してリダイレクト「>(コマンドリスト)」しているので「> >(コマンドリスト)」だ。パイプで同じことをするなら「| tee >(コマンドリスト) > /dev/null」のようにすればよい。

プロセス置換はけっこう便利なことがある

コマンドによっては引数にファイルしか受け取らず、パイプ、要するに標準入力を受け付けないものがある。代わりに出力に標準出力(やパイプ)を指定できずファイルしか指定できないものがある。プロセス置換を使うと、そういったファイルに対しても一時ファイルを作成することなく処理ができるわけだ。パイプが使えないコマンドに対する代替処置ととらえてもよいだろう。

プロセス置換をゴリゴリ活用するときがあるかと言われると、はっきり言って筆者にはほとんどない。しかしながら使っている人の話によると、使えば便利な機能だそうだ。また、都合によりサーバで一時ファイルを作成したくない(または作成できない)場合や、シェルスクリプトにおいて一時ファイルを考えたくない場合などには、便利な機能である。