2008/08/12からのアクセス回数 36655
1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。
彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。
今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。
今回は、「パイプでつなく」をテーマにUNIXの偉大な大発明の中から、
について例題を交えながら説明していきます。
パイプを使った処理の例として、「tr」コマンドのmanページの単語の種類をカウントします。
$ man tr | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 492
このようにUNIXのコマンドをパイプでつなくことによって簡単に必要な処理をこなすことができます。
「単語処理の例」は、別にパイプを使わなくてもファイルを使って逐次的に処理できます。 パイプがファイルと決定的に異なる点は、リアルタイムの処理です。
図は、マウスイベントを別Windowにプロットする例です。
このデモでは、xevコマンドとEvent2Plot, Graphのシェルスクリプトをパイプでつないで、xplotの画面にイベントをリアルタイムでプロットします。
$ xev | Event2Plot | Graph
pipeシステムコールは、本当に不思議な関数です。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成されます。 マニュアルの説明を読んだだけでpipeシステムコールの使い方を理解できる人は少ないでしょう。
パイプでつなぐときに必要なシステムコール
について、おさらいも含めて説明します。
forkシステムコールの仕様を簡単に書くと、
呼び出し形式 #include <unistd.h> pid_t fork(void); 機能 呼び出し元のプロセスをコピーして、新しいプロセスを生成する。 戻り値 成功すると、子プロセスには0が返され、 親プロセスには、子プロセスのプロセスIDが返されます。forkに失敗したら、-1を返します。
です。
forkでは、
ます。
簡単なプログラムで、上記の仕様を確認してみましょう。
#include <stdio.h> #include <unistd.h> main() { int pid; if ((pid = fork()) == -1) { fprintf(stderr, "can't fork\n"); } else if (pid == 0) { // child fprintf(stdout, "this is a child process\n"); fprintf(stderr, "pid(child)=%d\n", pid); } else { // parent fprintf(stdout, "this is a parent process\n"); fprintf(stderr, "pid(parent)=%d\n", pid); } }
以下のようにコンパイルして、実行すると
$ cc -o ex1 ex1.c $ ex1 this is a parent process pid(parent)=6556 this is a child process pid(child)=0
と出力され、分岐とpidの値が正しくセットされていることが分かります。
次に、標準出力(1)とエラー出力(2)をリダイレクトでファイルに保存します。
$ ex1 1>1.out 2>2.out $ more *.out << 1.outの内容 >> this is a parent process this is a child process << 2.outの内容 >> pid(parent)=6570 pid(child)=0
と、親と子プロセスが同じファイルに書き込み、ファイルの共有がされていることが確認できます。
pipeシステムコールの仕様を簡単に書くと、
呼び出し形式 #include <unistd.h> int pipe(int fildes[2]); 機能 パイプを生成し、ペアのファイル記述子を割り当てる。1個目のファイル記述子が読み込み、 2個目が書き込み用となる。 fildes[1]に書き込まれたデータは、fildes[0]から読み込まれる。これにより、 あるプログラムの出力をから他のプログラムの入力にすることができる。 読み込みまたは書き込みのファイル記述子のいずれかがクローズするとwindowed(未亡人)となる。 windowedとなったパイプに書き込むと書き込みプロセスはSIGPIPEシグナルを受け取る。 windowedにすることで、読み込みプロセスにEnd-Of-Fileを送ることができる。 読み手がパイプのデータをすべて読み込んだ後やwindowedになった後のパイプから読み込もうとすると 0が返される。 戻り値 成功すると0を返し、そうでない場合には-1を返し、変数errnoにエラー番号をセットする。
パイプは、
のように機能します。
簡単なプログラムで、上記の仕様を確認します。
// ex2.c : pipe example #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFSIZE (128) main() { int pid; int fd[2]; // create a pipe. if (pipe(fd) == -1) { fprintf(stderr, "Can't create pipe\n"); exit(1); } printf("fd[0]=%d, fd[1]=%d\n", fd[0], fd[1]); if ((pid = fork()) == 0) { // child char buf[BUFSIZE]; int len = 0; // close pipe out. close(fd[1]); // read message. len = read(fd[0], buf, BUFSIZE); printf("len=%d, buf=%s", len, buf); len = read(fd[0], buf, BUFSIZE); close(fd[0]); fprintf(stderr, "child finishied. len=%d\n", len); } else { // parent char buf[] = "hello world\n"; // close pipe in. close(fd[0]); // write message. write(fd[1], buf, sizeof(buf)); close(fd[1]); fprintf(stderr, "parent finishied\n"); } }
プログラムを実行すると
$ ex2 fd[0]=3, fd[1]=4 parent finishied len=13, buf=hello world child finishied. len=0
と出力され、
ます。*1
同様にdupシステムコールの仕様を簡単に書くと、
呼び出し形式 #include <unistd.h> int dup(int fildes); 機能 既存のファイル記述子を複製し、新しい生成されたファイル記述子を返す。 プロセスには、getdtablesize()で返される大きさのファイル記述子テーブルを持ち、 新しいファイル記述子には、未使用のもっとも小さい値が返される。 戻り値 正常終了の場合、0以上の値が返され、そうでない場合には-1を返します。
となります。
UNIXのファイル記述子の割り当てルールは、
ので、特定の値のファイル記述子を割り当てたい時には、
ことで実現できます。 リダイレクトやパイプは、この簡単なルールを使って実現されています。
dupの動作を確認するために、以下のサンプルプログラムを作成します。
// ex3.c : dup example #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFSIZE (128) main() { int pid; int fd[2]; // create a pipe. if (pipe(fd) == -1) { fprintf(stderr, "can't create pipe\n"); exit(1); } if ((pid = fork()) == 0) { // child int in = -1; // close pipe out. close(fd[1]); // close stdin. close(0); in = dup(fd[0]); close(fd[0]); fprintf(stderr, "in=%d\n", in); fprintf(stderr, "child : exec wc\n"); execl("/usr/bin/wc", "wc", 0); } else { // parent int out = -1; // close pipe in. close(fd[0]); // close stdout. close(1); out = dup(fd[1]); close(fd[1]); fprintf(stderr, "out=%d\n", out); fprintf(stderr, "parent: exec echo 'hello world'\n"); execl("/bin/echo", "echo", "hello world", 0); } }
プログラムを実行すると、
$ ex3 out=1 parent: exec echo 'hello world' in=0 child : exec wc 1 2 12
と出力されます。
wcの出力結果は、
$ echo 'hello world' | wc 1 2 12
と同じであり、親プロセスと子プロセスの間でパイプが正常に結ばれたことが確認できました。
パイプのつなぎ方が分かったところで、最初に紹介したパイプのサンプルを使ってパイプを使うことのメリットについて説明します。
パイプの特徴として、以下のことが挙げられます。
いくつかの処理を確認しながら、パイプを増やしていくのがパイプを使ったプログラムの特徴です。 最初のサンプルで、man trの結果から単語を切り出す部分をみてみましょう。
trの処理は、
この結果、英字以外の文字列は、改行(\n)1個に置き換わり、1行に1個の単語が抽出されます。
実際にその処理をみてみましょう。
$ man tr | tr -cs 'A-Za-z' '\n' TR BSD General Commands Manual TR 途中省略 Std POSIX standard BSD July BSD
と出力されます。
単語が抽出できたことを確認し、同じ単語を1つにまとめます。
sortコマンドの
を追加して重複を取り除きます。
$ man tr | tr -cs 'A-Za-z' '\n' | sort -u A ALL AM AN AR ASCII AT Additionally Any As B BI BSD 以下省略
と出力され、単語とは思えない1または2文字が含まれていることが分かります。
これは、manページが、
のように文書整形コマンドによって、強調文字、アンダーラインが付加付加されています。 これをターミナルに出力するために、制御コードが付加されているためです。
$ man tr | od -c | more 0000000 T R ( 1 ) 0000020 B S D G e 0000040 n e r a l C o m m a n d s M 0000060 a n u a l 0000100 T R ( 1 ) \n \n 0000120 N \b N A \b A M \b M E \b E \n 0000140 t \b t r \b r - - t r a n 0000160 s l a t e c h a r a c t e r s 0000200 \n \n S \b S Y \b Y N \b N O \b O P \b 0000220 P S \b S I \b I S \b S \n 0000240 t \b t r \b r [ - \b - C \b C c \b 0000260 c s \b s u \b u ] _ \b s _ \b t _ 0000300 \b r _ \b i _ \b n _ \b g _ \b 1 _ 0000320 \b s _ \b t _ \b r _ \b i _ \b n _ \b 0000340 g _ \b 2 \n t \b t r \b r 以下省略
のように強調文字では、
アンダラインでは、
しているのです。
正しい結果がでるようにするには、バックスペースとその前の文字を削除しなければなりません。
そこで、sedコマンドを追加します。(以下のコマンドで^Hは、Ctrl-vの後にCtrl-Hを入力)
$ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | more A ALL ASCII Additionally Any As BSD C COLLATE COMPATIBILITY CTYPE
とただし単語が抽出できました。
確認が終わった後で、moreをwc -lに変えると
$ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 436
正しい単語の数を得ることができました。
このように一つずつ確認しながらパイプをつなぐことによって短時間に正しい結果を得ることができます。
例えば、WebのアクセスログからGoogleの検索でブログにアクセスしたログを抽出したいとします。
この条件を使って
$ grep http://www.google.co.jp/search /var/log/apache2/access.log | grep index.php 218.228.8.55 - - [09/Aug/2009:06:43:41 +0900] "GET /~take/TakeWiki/index.php?avr %2F%E6%9C%80%E5%88%9D%E3%81%AE%E4%B8%80%E6%AD%A9 HTTP/1.1" 200 33207 "http://www .google.co.jp/search?q=avr+isp&hl=ja&lr=lang_ja&client=firefox-a&rls=org.mozilla :ja:official&start=50&sa=N" "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9 .1.2) Gecko/20090729 Firefox/3.5.2" 以下省略
のように簡単に出力できます。
もちろん、grepの正規表現を使って1つのgrepで検索することもできますが、少しの間違いで検索できなくなります。AかつBのような時、
$ grep A | grep B
のようにパイプでつなぐのが簡単かつ確実な方法です。特に急いでいるときには難しい表現はさけ、簡単な処理をパイプでつないでいく方が堅実です。
2つ目のサンプル、
xev | Event2Plot | Graph
では、Event2PlotとGraphというシェルスクリプトを使いました。
引数やオプションが多い場合には、それをシェルスクリプトに書いて一つにまとめると便利です。 Graphは、graphコマンド*2のオプションを以下のようにまとめたものです。
#! /bin/sh graph -T X -x 0 100 -y 0 100 -m 0 -S 5
エディタでGraphを入力した後に、シェルで実行できるように実行権限をセットします。
$ chmod +x Graph
Graphを起動して、X,Yの座標をブランク区切りで入力すると、リアルタイムに座標が画面にプロットされます。
$ Graph 10 30 50 50 70 20
これでGraphの完成です。*3
次にxevコマンドを実行し、太い線の正方形の中で、マウスを動かしてみてください。
MotionNotify event, serial 23, synthetic NO, window 0xa00001, root 0x1fd, subw 0x0, time 236035801, (68,77), root:(298,252), state 0x0, is_hint 0, same_screen YES MotionNotify event, serial 23, synthetic NO, window 0xa00001, root 0x1fd, subw 0x0, time 236035818, (66,106), root:(296,281), state 0x0, is_hint 0, same_screen YES
のようにMotionNotify eventの次の行に(68,77)のようにマウスの矩形内の座標(左上が原点)が含まれています。
これをawkを使ってX Y座標に変換しているのが、Event2Plotです。 先ほどと同様に
#! /bin/sh awk -F, ' /MotionNotify/ { getline; gsub(/\(/, "", $4); gsub(/\)/, "", $5); printf("%d %d\n", $4, 100 - $5); }'
をEvent2Plotに入力して、
chmod +x Event2Plot
を実行してください。
xevとEvent2Plotをぱいぷでつないで、
$ xev | Event2Plot 6 59 27 85 37 82 42 81 43 80 42 80 41 79 40 79 39 79 38 79 以下省略
最初、ある程度マウスを動かすと画面に座標が表示されます。*4
ここまで、確認できたらGraphと連結してみましょう。
$ xev | Event2Plot | Graph
無事、最初の図と同じように出力されたでしょうか。
グラフにプロットされるデータがどのようになっているか知りたいことはありませんか。 このような場合には、teeコマンドで途中のデータをファイルに出力し、tailコマンドで表示します。
$ xev | Event2Plot | tee /tmp/1 | Graph & $ tail -f /tmp/1 58 50 65 54 73 65 72 71 62 80 57 81 途中省略 終了するには、Ctrl-Cを入力してください
リアルタイムとまではいきませんが、途中結果が表示されます。
初期のUNIXでは、ファイルシステムを跨ぐファイルのコピーには、cpコマンドが使えませんでした。 そこで、tarコマンドとパイプを使って以下のようにコピーしました。
$ tar cf - . | (cd /mnt/bak; tar -xf -)
これまでは、()で括ってパイプを使ったことはありませんでしたが、これはグルーピングと呼ばれる 処理で、()で括ったコマンドは入出力が共通になります。*5
バッククォートで括られたコマンドの出力をパイプでつないで、その結果を別のコマンドの引数にします。
例えば、変数NOWに今の時刻をセットしたい場合に、
$ NOW=`date` $ echo $NOW Wed Aug 12 20:15:11 JST 2009
とすると、変数NOWにdateコマンドの実行結果がセットされ、echo $NOWでセットした時刻が表示されます。
また、あるコマンドの実行結果を別のコマンドの引数にしたい場合にも便利です。 例)変数iの値に1を足した値を表示する場合
$ i=1 $ echo `expr $i + 1` 2
これを使えば、変数の値を変える場合には、
$ i=`expr $i + 1` $ echo $i 2
とすればよいことが分かります。
シェルスクリプト内に記述したドキュメントにシェル変数の置換を行い、その結果を別のコマンドの入力とします。
ヒアドキュメントは、テンプレート(ひな形)して使用されることが多く、sh, awk等のプログラムをシェルスクリプト内で生成し、実行する例が多く見られます。
簡単な例を使ってヒアドキュメントの変数置換を確かめてみます。
$ NOW=`date` $ cat <<EOF > What time is it now? > It's $NOW. > EOF What time is it now? It's Thu Aug 13 17:56:49 JST 2009.
例えば、awkではshと同様に変数を$を使って参照するため、ヒアドキュメントの変数置換がかえって邪魔になることがあります。
そのような場合には、<<\EOFのように<<の後に\を付けます。
$ cat <<\EOF > What time is it now? > It's $NOW. > EOF What time is it now? It's $NOW.
パイプをつないでデータを加工する方法がわかったところで、それをシェルスクリプトにして、いつでも使えるようにしましょう。
まずは、シェルのおさらいからはじめましょう。ここではsh, bashの系列のシェルをベースに説明します。
ここでは、シェルのおさらいも兼ねて、
について、例題を交えながら説明します。
UNIXのCプログラムでは、main関数がプログラムの開始部分(エントリポイント)であり、プログラムmainを呼び出す前にシェルが標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)にそれぞれ、0, 1, 2のファイル記述子がオープンされた状態でmainが呼び出されます。
シェルがプログラムを起動する前に、標準入力、標準出力、標準エラー出力を付け替えることから入出力の動的変更のことをリダイレクトと呼びます。
リダイレクトの仕組みは、dupシステムコールの例題でも示したとおり、
によって実現されています。
主なリダイレクト指定方法は、
があります。
では、実際にリダイレクトを使って簡単なファイルを作成します。
$ echo test > /tmp/test.out $ cat /tmp/test.out test $ echo more test >> /tmp/test.out $ cat /tmp/test.out test more test $ wc </tmp/test.out 2 3 15
ここで>>は、存在しないファイルの場合、新たにファイルを作成し、そのファイルに出力します。 また、>はすでにファイルが存在する場合には、そのファイルに上書きされます。*6
シェルには、ファイル名をメタ記号によって複数のファイルを一括して指定することができます。
メタ記号の一覧を以下に示します。
メタ記号 | 説明 |
? | 任意の一文字にマッチする |
[abef] | []で囲まれた文字のいずれかの一文字にマッチする |
[a-z] | a-zの間に含まれる一文字にマッチする |
[^abc] | [の直後に^が指定されるとそれ以降に指定した文字列以外の文字にマッチする |
* | 任意の文字列にマッチする |
ファイル展開はディレクトリ内のファイルに対して行われます。
例えば、ファイルが.bakで終わるすべてのファイルを削除する場合には、
$ rm *.bak
のように指定します。
UNIXでは、.で始まるファイル、ディレクトリはメタ記号の展開にマッチしません。 それでは、.*とするとどうなるでしょう
$ echo .* . .. .cproject .project .settings
.で始まるファイルの他に、.(カレントディレクトリ)と..(親ディレクトリ)も展開されます。
メタ記号の展開で間違ってカレントディレクトリや親ディレクトリが指定されないように、ファイル展開では.で始まるファイルはメタ記号に含めないようになっているのです、この性質を利用して.profile等各種コマンドのユーザ設定ファイルは.で始まるファイル名を使います。
それでは、.ではじまるファイル名を指定するには、以下のように指定します。
$ echo .[^.]* .cproject .project .settings
シェルでは、同一シェル内だけで有効なシェル変数と子プロセスにも引き継がれる環境変数について説明します。
シェル変数に値をセットするには、変数名と値の間に空白を入れずに=を入れます。代入する値の中に空白が含まれる場合シングルクォート(')またはダブルクォート(")で括ります。シェル変数名にはアルファベット、数字、アンダースコア(_)が使用できます。慣例ではループの添え字を除いて、大文字を使うことが多いです。
簡単なシェル変数の例を以下に示します。
$ HOST_NAME=IES00 $ IN_FILE="" $ PS="ps -ef"
順番に見ていきましょう。
変数を参照するには、変数名の前に$を付けます。 変数は、空白や.などの変数名に使えない文字を区切りとしますが、英字の途中で変数を置換させたいときには、ダブルクォート"や${}で括って参照します。
$ $PS UID PID PPID C STIME TTY TIME CMD 0 1 0 0 0:17.04 ?? 0:24.09 /sbin/launchd 0 15 1 0 0:01.72 ?? 0:01.83 /usr/libexec/kextd 0 16 1 0 0:26.06 ?? 0:41.26 /usr/sbin/DirectoryService 以下省略 $ VAR=hello $ echo $VAR hello $ echo ${VAR}_again hello_again $ echo using"$VAR" usinghello
また、シェル変数には配列指定も可能であり、変数名[添え字]の形式で指定することができます。 すべての配列要素を参照する場合には、${配列名[@]}とし、要素の個数を参照する場合には、${#配列名[@]}とします。
$ LINE[0]=foo $ LINE[1]=bar $ echo ${LINE[@]} foo bar $ echo ${#LINE[@]} 2
変数をクリアするには、unset 変数名を使います
$ unset LINE $ echo ${LINE[@]}
つぎに環境変数の設定に説明します。環境変数を定義するには、以下の2通りの方法があります。
すでに定義されているシェル変数を環境変数に切り替えるには、exportコマンドを使用します。
$ export VAR
新規に環境変数を定義する場合には、exportの後に変数名=値を指定します。
$ export VAR=hello
どうやってシェル変数と環境変数が使い分けられているのか、環境変数の仕組みを見ながら説明します。
Cのmain関数の宣言形式に、
#include <stdio.h> int main(int argc, char * argv[], char * env[])
というのがありますが、最後のenvが環境変数の配列をシェルから引き継ぐための、ポインターです。
簡単なCのサンプルを使って環境変数がシェルから引き継がれることを確かめてみましょう。
// ex4.c : env example #include <stdio.h> int main(int argc, char* argv[], char *env[]) { while (*env != NULL) { printf("%s ", *env++); } printf("\n"); }
makeコマンドでコンパイルし、実行します。
$ make $ ex4 MANPATH=/opt/local/share/man:/usr/share/man:/usr/local/share/man:/usr/X11/man TERM_PROGRAM=Apple_Terminal TERM=xterm-color SHELL=/bin/bash CATALINA_HOME=/Users/take/local/tomcat TMPDIR=/var/folders/Xo/XoxQsiGT2RWIck+BYuxhKU+++TI/-Tmp-/ Apple_PubSub_Socket_Render=/tmp/launch-wSems0/Render TERM_PROGRAM_VERSION=240.2 以下省略
この結果をenvコマンドと比べると
$ echo `env` MANPATH=/opt/local/share/man:/usr/share/man:/usr/local/share/man:/usr/X11/man TERM_PROGRAM=Apple_Terminal TERM=xterm-color SHELL=/bin/bash CATALINA_HOME=/Users/take/local/tomcat TMPDIR=/var/folders/Xo/XoxQsiGT2RWIck+BYuxhKU+++TI/-Tmp-/ Apple_PubSub_Socket_Render=/tmp/launch-wSems0/Render TERM_PROGRAM_VERSION=240.2 以下省略
同じ結果となり、環境変数がサンプルプログラムに引き渡されていることが分かります。
シェルには、シェル変数、環境変数の他に、
の変数操作があります。
シェルで、空白で区切られた文字列を1つの引数にしたり、置換文字の$, *, ?等を引数にそのまま渡したりしたいことがあります。
このようなときには、バックスラッシュ\やシングルクォート'を使って文字をクォーティングします。
$ echo \$ $ # echo '* ?' * ?
シェルのクォーティングルールをきちんと説明した書物は少なく、参考文献のThe UNIX Systemの図4-4から引用します。
メタ文字 | ||||||
' | " | ` | \ | $ | * | |
' | t | n | n | n | n | n |
" | n | t | y | y | y | n |
` | n | n | t | y | n | n |
ここで、
ことを意味します。
ただし、`で囲まれた$は、シェル変数が置換されるので、yの間違いではないかと思われます。
$ HELLO="echo hello" $ echo `$HELLO` hello
この表の意味を簡単に説明すると
皆様のご意見、ご希望をお待ちしております。