[[FrontPage]] 2008/08/12からのアクセス回数 &counter; #contents * パイプでつなぐ [#g6c751b8] 1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。 彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のマイコンチップ以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。 今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。 ** UNIXの3大発明 [#i61b3a24] 今回は、「パイプでつなく」をテーマにUNIXの偉大な大発明の中から、 - fork : 子プロセスの生成 - pipe : パイプ - リダイレクト について例題を交えながら説明していきます。 *** パイプを使った処理の例 [#a948992b] - 単語処理の例 パイプを使った処理の例として、「tr」コマンドのmanページの単語の種類をカウントします。 #pre{{ $ man tr | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 492 }} このようにUNIXのコマンドをパイプでつなくことによって簡単に必要な処理をこなすことができます。 - リアルタイムの例 「単語処理の例」は、別にパイプを使わなくてもファイルを使って逐次的に処理できます。 パイプがファイルと決定的に異なる点は、リアルタイムの処理です。 図は、マウスイベントを別Windowにプロットする例です。 &ref(demo_pipeline.jpg); このデモでは、xevコマンドとEvent2Plot, Graphのシェルスクリプトをパイプでつないで、xplotの画面にイベントをリアルタイムでプロットします。 #pre{{ $ xev | Event2Plot | Graph }} ** パイプのつなぎ方 [#pfaae892] pipeシステムコールは、本当に不思議な関数です。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成されます。 マニュアルの説明を読んだだけでpipeシステムコールの使い方を理解できる人は少ないでしょう。 - 最初にパイプを生成します。 &ref(pipe.jpg); - 次にforkシステムコールを使って、子プロセスを生成します &ref(fork.jpg); - 親のin、子のoutをクローズします &ref(close.jpg); - 親のstdoutをクローズし、dupします、子のstdinをクローズしてdupします &ref(dup.jpg); ** パイプを実現する3つのシステムコール [#wece4043] パイプでつなぐときに必要なシステムコール - fork - pipe - dup について、おさらいも含めて説明します。 *** fork システムコールのおさらい [#mcf73ea6] forkシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #include <unistd.h> pid_t fork(void); 機能 呼び出し元のプロセスをコピーして、新しいプロセスを生成する。 戻り値 成功すると、子プロセスには0が返され、 親プロセスには、子プロセスのプロセスIDが返されます。forkに失敗したら、-1を返します。 }} です。 forkでは、 - 子プロセスは、変数、ファイルなどのリソースを親システムと共有 - 親と子プロセスは、forkのリターン値で別の処理に切り分けられる ます。 簡単なプログラムで、上記の仕様を確認してみましょう。 #pre{{ #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); } } }} 以下のようにコンパイルして、実行すると #pre{{ $ 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)をリダイレクトでファイルに保存します。 #pre{{ $ 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 システムコールの使い方 [#ka7a7042] pipeシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #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にエラー番号をセットする。 }} パイプは、 - 単一方向のデータフローを持つバッファです - バッファが満杯になった場合には、書き込みプロセスはスリープ状態になり、十分な空きができるまで待ちます - 読み込みプロセスは、バッファに書き込まれるまでスリープ状態で待ちます - 書き込み側がクローズするとパイプからの読み込みでは0(End-Of-File)が返されます - 読み手側でクローズされたパイプに書き込もうとするとSIGPIPEシグナルが発生します のように機能します。 簡単なプログラムで、上記の仕様を確認します。 #pre{{ // 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"); } } }} プログラムを実行すると #pre{{ $ ex2 fd[0]=3, fd[1]=4 parent finishied len=13, buf=hello world child finishied. len=0 }} と出力され、 - pipeシステムコールによってfdに3, 4のファイル記述子が割り当てられ - 親プロセスがhello world\nを書き込み、書き込み用ファイル記述子をクローズする - 子プロセスが、hello world\nを読み込み、次の読み込みをすると0が返され ます。((最初の読み込みで、len=13となっているのは、文字列の終わりの0が含まれているためです)) *** dup システムコールの使い方 [#ic4ba39d] 同様にdupシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #include <unistd.h> int dup(int fildes); 機能 既存のファイル記述子を複製し、新しい生成されたファイル記述子を返す。 プロセスには、getdtablesize()で返される大きさのファイル記述子テーブルを持ち、 新しいファイル記述子には、未使用のもっとも小さい値が返される。 戻り値 正常終了の場合、0以上の値が返され、そうでない場合には-1を返します。 }} となります。 UNIXのファイル記述子の割り当てルールは、 - 新しいファイル記述子には、未使用のもっとも小さい値が返される ので、特定の値のファイル記述子を割り当てたい時には、 - dupの直前にそのファイル記述子をクローズする ことで実現できます。 リダイレクトやパイプは、この簡単なルールを使って実現されています。 dupの動作を確認するために、以下のサンプルプログラムを作成します。 #pre{{ // 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); } } }} プログラムを実行すると、 #pre{{ $ ex3 out=1 parent: exec echo 'hello world' in=0 child : exec wc 1 2 12 }} と出力されます。 - 親プロセスのパイプの出力用ファイル記述子は、1(標準出力)に割り当てられ - echo 'hello world'を実行します - 子プロセスのパイプの入力用ファイル記述子は、0(標準入力)に割り当てられ - echoの結果'hello world'を標準入力から読み込みwcを実行します wcの出力結果は、 #pre{{ $ echo 'hello world' | wc 1 2 12 }} と同じであり、親プロセスと子プロセスの間でパイプが正常に結ばれたことが確認できました。 ** パイプをつなぐサンプル [#hbf674b0] パイプのつなぎ方が分かったところで、最初に紹介したパイプのサンプルを使ってパイプを使うことのメリットについて説明します。 パイプの特徴として、以下のことが挙げられます。 - 確認しながら、パイプをつなぐ - 複雑な処理を、単純な処理をパイプでつなぐ - 途中経過を確認しながら、パイプでつなぐ *** 確認しながら、パイプをつなぐ [#mb5f3fb6] いくつかの処理を確認しながら、パイプを増やしていくのがパイプを使ったプログラムの特徴です。 最初のサンプルで、man trの結果から単語を切り出す部分をみてみましょう。 trの処理は、 - -cオプションは、指定されたパターン以外のも文字列を置換の対象ととし、 - -sオプションは、同じ文字に置換された場合、それを1個にまとめるようにします この結果、英字以外の文字列は、改行(\n)1個に置き換わり、1行に1個の単語が抽出されます。 実際にその処理をみてみましょう。 #pre{{ $ man tr | tr -cs 'A-Za-z' '\n' TR BSD General Commands Manual TR 途中省略 Std POSIX standard BSD July BSD }} と出力されます。 単語が抽出できたことを確認し、同じ単語を1つにまとめます。 sortコマンドの - -uオプションは、入力をソートするとき重複する行を1つにまとめます を追加して重複を取り除きます。 #pre{{ $ 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ページが、 &ref(man.jpg); のように文書整形コマンドによって、強調文字、アンダーラインが付加付加されています。 これをターミナルに出力するために、制御コードが付加されているためです。 #pre{{ $ 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 以下省略 }} のように強調文字では、 - N \b Nのように文字を表示し、バックスペースでもどし、再度文字を出力 アンダラインでは、 - _ \b s のように先に_を表示し、バックスペースでもどし、文字を出力 しているのです。 正しい結果がでるようにするには、バックスペースとその前の文字を削除しなければなりません。 そこで、sedコマンドを追加します。(以下のコマンドで^Hは、Ctrl-vの後にCtrl-Hを入力) #pre{{ $ 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に変えると #pre{{ $ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 436 }} 正しい単語の数を得ることができました。 このように一つずつ確認しながらパイプをつなぐことによって短時間に正しい結果を得ることができます。 *** 複雑な処理を、単純な処理をパイプでつなぐ [#t78619d9] 例えば、WebのアクセスログからGoogleの検索でブログにアクセスしたログを抽出したいとします。 - Googleの検索からアクセスされたログには、http://www.google.co.jp/searchという文字が含まれ - PukiWikiを使ったブログには、index.phpという文字が含まれます この条件を使って #pre{{ $ 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 のようにパイプでつなぐのが簡単かつ確実な方法です。特に急いでいるときには難しい表現はさけ、簡単な処理をパイプでつないでいく方が堅実です。 *** 途中経過を確認しながら、パイプでつなぐ [#i6e9f386] 2つ目のサンプル、 #pre{{ xev | Event2Plot | Graph }} では、Event2PlotとGraphというシェルスクリプトを使いました。 引数やオプションが多い場合には、それをシェルスクリプトに書いて一つにまとめると便利です。 Graphは、graphコマンド((GNU plotutils パッケージに含まれています))のオプションを以下のようにまとめたものです。 #pre{{ #! /bin/sh graph -T X -x 0 100 -y 0 100 -m 0 -S 5 }} - 1行目の#! は、使用するシェル(コマンド)を指定するおまじないです。ここでは/bin/shを使っています - graphのコマンドとオプションをセットしています。 エディタでGraphを入力した後に、シェルで実行できるように実行権限をセットします。 #pre{{ $ chmod +x Graph }} Graphを起動して、X,Yの座標をブランク区切りで入力すると、リアルタイムに座標が画面にプロットされます。 #pre{{ $ Graph 10 30 50 50 70 20 }} これでGraphの完成です。((Macの場合MacPortのgraphはXウィンドウをサポートしていないので、xtermから-T tekオプションで起動してください)) 次にxevコマンドを実行し、太い線の正方形の中で、マウスを動かしてみてください。 #pre{{ 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です。 先ほどと同様に #pre{{ #! /bin/sh awk -F, ' /MotionNotify/ { getline; gsub(/\(/, "", $4); gsub(/\)/, "", $5); printf("%d %d\n", $4, 100 - $5); }' }} をEvent2Plotに入力して、 chmod +x Event2Plot を実行してください。 xevとEvent2Plotをぱいぷでつないで、 #pre{{ $ xev | Event2Plot 6 59 27 85 37 82 42 81 43 80 42 80 41 79 40 79 39 79 38 79 以下省略 }} 最初、ある程度マウスを動かすと画面に座標が表示されます。((ある程度バッファに溜まるまで出力されないです)) ここまで、確認できたらGraphと連結してみましょう。 #pre{{ $ xev | Event2Plot | Graph }} 無事、最初の図と同じように出力されたでしょうか。 グラフにプロットされるデータがどのようになっているか知りたいことはありませんか。 このような場合には、teeコマンドで途中のデータをファイルに出力し、tailコマンドで表示します。 #pre{{ $ xev | Event2Plot | tee /tmp/1 | Graph & $ tail -f /tmp/1 58 50 65 54 73 65 72 71 62 80 57 81 途中省略 終了するには、Ctrl-Cを入力してください }} リアルタイムとまではいきませんが、途中結果が表示されます。 ** その他のパイプ(pipe)の使われ方 [#j46d5541] *** パイプ(|) [#r0f82b15] 初期のUNIXでは、ファイルシステムを跨ぐファイルのコピーには、cpコマンドが使えませんでした。 そこで、tarコマンドとパイプを使って以下のようにコピーしました。 #pre{{ $ tar cf - . | (cd /mnt/bak; tar -xf -) }} これまでは、()で括ってパイプを使ったことはありませんでしたが、これはグルーピングと呼ばれる 処理で、()で括ったコマンドは入出力が共通になります。((実装上は別のシェルプロセスを起動し、コマンドを実行するので、このようなことが可能になります)) *** バッククォート(`) [#p3d27cbb] バッククォートで括られたコマンドの出力をパイプでつないで、その結果を別のコマンドの引数にします。 例えば、変数NOWに今の時刻をセットしたい場合に、 #pre{{ $ NOW=`date` $ echo $NOW Wed Aug 12 20:15:11 JST 2009 }} とすると、変数NOWにdateコマンドの実行結果がセットされ、echo $NOWでセットした時刻が表示されます。 また、あるコマンドの実行結果を別のコマンドの引数にしたい場合にも便利です。 例)変数iの値に1を足した値を表示する場合 #pre{{ $ i=1 $ echo `expr $i + 1` 2 }} これを使えば、変数の値を変える場合には、 #pre{{ $ i=`expr $i + 1` $ echo $i 2 }} とすればよいことが分かります。 *** ヒアドキュメント(<<EOF) [#u89a4f77] シェルスクリプト内に記述したドキュメントにシェル変数の置換を行い、その結果を別のコマンドの入力とします。 ヒアドキュメントは、テンプレート(ひな形)して使用されることが多く、sh, awk等のプログラムをシェルスクリプト内で生成し、実行する例が多く見られます。 簡単な例を使ってヒアドキュメントの変数置換を確かめてみます。 #pre{{ $ 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. }} - 変数NOWに現在時刻がセットされ、 - <<EOFがヒアドキュメントの開始です。<<の後には任意の文字列を指定します。ここではEOFとしました - スクリプトではなく、端末などで実行すると2次プロンプトの> が出ます - 出力は、期待通りNOWが設定時刻に置換されています 例えば、awkではshと同様に変数を$を使って参照するため、ヒアドキュメントの変数置換がかえって邪魔になることがあります。 そのような場合には、<<\EOFのように<<の後に\を付けます。 #pre{{ $ cat <<\EOF > What time is it now? > It's $NOW. > EOF What time is it now? It's $NOW. }} * 簡単シェルスクリプト [#vf5f7efd] パイプをつないでデータを加工する方法がわかったところで、それをシェルスクリプトにして、いつでも使えるようにしましょう。 まずは、シェルのおさらいからはじめましょう。ここではsh, bashの系列のシェルをベースに説明します。 ** おさらい [#b051ca79] ここでは、シェルのおさらいも兼ねて、 - リダイレクト - ファイル名展開 - シェル変数と環境変数 - 特殊変数 - クォーティング について、例題を交えながら説明します。 *** リダイレクト [#l97cf876] UNIXのCプログラムでは、main関数がプログラムの開始部分(エントリポイント)であり、プログラムmainを呼び出す前にシェルが標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)にそれぞれ、0, 1, 2のファイル記述子がオープンされた状態でmainが呼び出されます。 シェルがプログラムを起動する前に、標準入力、標準出力、標準エラー出力を付け替えることから入出力の動的変更のことをリダイレクトと呼びます。 リダイレクトの仕組みは、dupシステムコールの例題でも示したとおり、 - 新しい接続先のオープン - 切り替える入出力のクローズ - dupシステムコールを使った入出力の付け替え によって実現されています。 主なリダイレクト指定方法は、 - < を使った標準入力の指定 - > を使った標準出力の指定 - >> を使ったファイルへの追加出力指定 があります。 では、実際にリダイレクトを使って簡単なファイルを作成します。 #pre{{ $ 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 }} ここで>>は、存在しないファイルの場合、新たにファイルを作成し、そのファイルに出力します。 また、>はすでにファイルが存在する場合には、そのファイルに上書きされます。((rwのファイル属性は変更ありません)) *** ファイル展開 [#k8dc7d65] シェルには、ファイル名をメタ記号によって複数のファイルを一括して指定することができます。 メタ記号の一覧を以下に示します。 |メタ記号|説明| |?|任意の一文字にマッチする| |[abef]|[]で囲まれた文字のいずれかの一文字にマッチする| |[a-z]|a-zの間に含まれる一文字にマッチする| |[^abc]|[の直後に^が指定されるとそれ以降に指定した文字列以外の文字にマッチする| |*|任意の文字列にマッチする| ファイル展開はディレクトリ内のファイルに対して行われます。 例えば、ファイルが.bakで終わるすべてのファイルを削除する場合には、 #pre{{ $ rm *.bak }} のように指定します。 UNIXでは、.で始まるファイル、ディレクトリはメタ記号の展開にマッチしません。 それでは、.*とするとどうなるでしょう #pre{{ $ echo .* . .. .cproject .project .settings }} .で始まるファイルの他に、.(カレントディレクトリ)と..(親ディレクトリ)も展開されます。 メタ記号の展開で間違ってカレントディレクトリや親ディレクトリが指定されないように、ファイル展開では.で始まるファイルはメタ記号に含めないようになっているのです、この性質を利用して.profile等各種コマンドのユーザ設定ファイルは.で始まるファイル名を使います。 それでは、.ではじまるファイル名を指定するには、以下のように指定します。 #pre{{ $ echo .[^.]* .cproject .project .settings }} *** シェル変数 [#hf55402a] シェルでは、同一シェル内だけで有効なシェル変数と子プロセスにも引き継がれる環境変数について説明します。 シェル変数に値をセットするには、変数名と値の間に空白を入れずに=を入れます。代入する値の中に空白が含まれる場合シングルクォート(')またはダブルクォート(")で括ります。シェル変数名にはアルファベット、数字、アンダースコア(_)が使用できます。慣例ではループの添え字を除いて、大文字を使うことが多いです。 簡単なシェル変数の例を以下に示します。 #pre{{ $ HOST_NAME=IES00 $ IN_FILE="" $ PS="ps -ef" }} 順番に見ていきましょう。 - シェル変数HOST_NAMEに"IES00"をセットします - シェル変数IN_FILEに空文字をセットします - シェル変数PSに"ps -ef"をセットします 変数を参照するには、変数名の前に$を付けます。 変数は、空白や.などの変数名に使えない文字を区切りとしますが、英字の途中で変数を置換させたいときには、ダブルクォート"や${}で括って参照します。 #pre{{ $ $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 }} また、シェル変数には配列指定も可能であり、変数名[添え字]の形式で指定することができます。 すべての配列要素を参照する場合には、${配列名[@]}とし、要素の個数を参照する場合には、${#配列名[@]}とします。 #pre{{ $ LINE[0]=foo $ LINE[1]=bar $ echo ${LINE[@]} foo bar $ echo ${#LINE[@]} 2 }} 変数をクリアするには、unset 変数名を使います #pre{{ $ unset LINE $ echo ${LINE[@]} }} *** 環境変数 [#t79199e5] つぎに環境変数の設定に説明します。環境変数を定義するには、以下の2通りの方法があります。 すでに定義されているシェル変数を環境変数に切り替えるには、exportコマンドを使用します。 #pre{{ $ export VAR }} 新規に環境変数を定義する場合には、exportの後に変数名=値を指定します。 #pre{{ $ export VAR=hello }} どうやってシェル変数と環境変数が使い分けられているのか、環境変数の仕組みを見ながら説明します。 Cのmain関数の宣言形式に、 #pre{{ #include <stdio.h> int main(int argc, char * argv[], char * env[]) }} というのがありますが、最後のenvが環境変数の配列をシェルから引き継ぐための、ポインターです。 簡単なCのサンプルを使って環境変数がシェルから引き継がれることを確かめてみましょう。 #pre{{ // ex4.c : env example #include <stdio.h> int main(int argc, char* argv[], char *env[]) { while (*env != NULL) { printf("%s ", *env++); } printf("\n"); } }} makeコマンドでコンパイルし、実行します。 #pre{{ $ 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コマンドと比べると #pre{{ $ 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 以下省略 }} 同じ結果となり、環境変数がサンプルプログラムに引き渡されていることが分かります。 *** 特殊変数 [#n0f58ed5] シェルには、シェル変数、環境変数の他に、 - 位置変数 - 特殊変数 - 特殊置換 の変数操作があります。 :位置変数 | シェルやシェル関数への引数を$0, $1, $2, ...$nのように引数の位置を数字nで参照する方法です。 :特殊変数 | プロセスID、引数の個数などの特殊な情報を参照するときに使用します。以下の特殊変数がよく使われます。 -- $* : $1から始まるすべての位置変数を展開します -- $0 : 実行コマンドの名前を返します -- $# : 位置変数の数を返します -- $$ : 実行中のプロセスIDを返します -- $? : 直前のコマンドの終了ステータスを返します : 特殊置換 | ${変数:=省略値}の形式で指定し、変数が定義されていない場合には省略値を参照します。 *** クォーティング [#w5202abd] シェルで、空白で区切られた文字列を1つの引数にしたり、置換文字の$, *, ?等を引数にそのまま渡したりしたいことがあります。 このようなときには、バックスラッシュ\やシングルクォート'を使って文字をクォーティングします。 #pre{{ $ echo \$ $ # echo '* ?' * ? }} シェルのクォーティングルールをきちんと説明した書物は少なく、参考文献のThe UNIX Systemの図4-4から引用します。 |>|>|>|>|>|>|CENTER:メタ文字|h ||'|"|`|\|$|*| |'|t|n|n|n|n|n| |"|n|t|y|y|y|n| |`|n|n|t|y|n|n| ここで、 - t : 終端文字 - y : シェルによって解釈される - n : シェルによって解釈されず、そのまま渡される ことを意味します。 ただし、`で囲まれた$は、シェル変数が置換されるので、yの間違いではないかと思われます。 #pre{{ $ HELLO="echo hello" $ echo `$HELLO` hello }} この表の意味を簡単に説明すると - ' は、すべての特殊文字を無効にします - "は、変数置換、\を有効にし、`によって返された結果を1つの引数にまとめる時に使用します - `は、変数置換、\を有効にします ** おきまりのパターン [#a20dec8e] シェルは、コマンドを組み合わせるだけではなく、その使い方を拡張するためにも利用できます。 ここでは、標準入力から第一引数に指定されたパターンを検索するプログラムtgrepを複数ファイルの検索に拡張方法を以下のような手順で説明します。 - 引数のチェック - 各ファイルに対する繰り返し処理 tgrepは、簡単なCのプログラムです。 #pre{{ #include <stdio.h> #include <string.h> #define BUFSIZE (512) main(int argc, char *argv[]) { char* pattern = *(++argv); char line[BUFSIZE]; while(fgets(line, BUFSIZE, stdin) != NULL) { if (strstr(line, pattern) != NULL) printf(line); } } }} これから作成するシェルスクリプトの名称をTgrep.shとします。 主な機能は、 - 引数が1個の場合には、標準入力から検索し引数で指定された文字列を検索します - 引数が2個以上の場合には、第2引数以降の各ファイルに対して第1引数で指定された文字列を検索します *** 引数チェック [#j066a3a2] 引数のチェックでよく使用されるシェルの文法がcase文です。 シェルのケース文には正規表現が指定できるので、Cプログラミング言語のcase文よりも使い易くなっています。 まず、引数の数を調べるところは、特殊シェル変数の$#で引数の個数を取り出し、case文で処理を振り分けます。各条件に適応する処理は、2個のセミコロン(;;)で区切られる。 #pre{{ #! /bin/sh case $# in 1) echo pattern: $1 ;; [2-9]) echo pattern: $1; shift; echo files: $* ;; esac }} まずは、このようにシェルスクリプトが正しい動作をするかechoを使って引数をチェックします。 #pre{{ $ Tgrep.sh 1 pattern: 1 $ Tgrep.sh 1 2 3 4 pattern: 1 files: 2 3 4 }} うまく動作しているように見えますが、これでは10個以上の引数が与えられた正常に動作しません。 そこで、引数のパターンを0個、1個、それ以外に変えてみましょう。 #pre{{ #! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) echo pattern: $1 ;; *) echo pattern: $1; shift; echo files: $* ;; esac }} ここで、shiftコマンドは、位置引数を1個左にシフトコマンドです。shiftコマンドで2個目以降の引数を$*で表せることになります。 引数が10個以上でも正常に動作するか、引数の個数を変えて確かめてみましょう。 #pre{{ $ Tgrep.sh 1 2 3 4 5 6 7 8 9 10 11 pattern: 1 files: 2 3 4 5 6 7 8 9 10 11 $ Tgrep.sh 1 pattern: 1 $ Tgrep.sh invalid argument. }} すべてうまくいきました。これで、引数チェックは完成です。 *** 各ファイルに対する繰り返し [#scf2ecb0] 次は、検索するファイル数が複数になったときの処理を追加します。 同じ処理を何回か繰り返す場合には、for文を使用します。 引数のテストではechoを使っていましたが、これからはtgrepに替え、2個以上のファイルに対する処理にfor文を入れてみましょう。 #pre{{ #! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) tgrep $1 ;; *) PATTERN=$1; shift; for i in $* do tgrep $PATTERN < $i; done ;; esac }} できたスクリプトを動かしてみます。 #pre{{ $ Tgrep.sh BUFSIZE *.c #define BUFSIZE (128) char buf[BUFSIZE]; len = read(fd[0], buf, BUFSIZE); len = read(fd[0], buf, BUFSIZE); #define BUFSIZE (128) #define BUFSIZE (512) char line[BUFSIZE]; while(fgets(line, BUFSIZE, stdin) != NULL) { }} うまく検索しているみたいですが、これではどのファイルにパターンがマッチしたのか分かりません。 そこで、Tgrep.shの出力行の先頭にファイル名を追加することにします。出力行の処理には、while文とread文を使用します。出力行をread文でLINE変数にセットして、それをファイル名と一緒にechoコマンドで出力します。 #pre{{ #! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) tgrep $1 ;; *) PATTERN=$1; shift; for i in $* do tgrep $PATTERN < $i | while read LINE do echo $i: $LINE done done ;; esac }} 出力結果は、以下のようになります。 #pre{{ $ Tgrep.sh BUFSIZE *.c ex2.c: #define BUFSIZE (128) ex2.c: char buf[BUFSIZE]; ex2.c: len = read(fd[0], buf, BUFSIZE); ex2.c: len = read(fd[0], buf, BUFSIZE); ex3.c: #define BUFSIZE (128) tgrep.c: #define BUFSIZE (512) tgrep.c: char line[BUFSIZE]; tgrep.c: while(fgets(line, BUFSIZE, stdin) != NULL) { }} 少しずつ動作を確認しながら、機能を拡張することで難なく目標とするシェルスクリプトが完成しました。 このようにわずか13行のシェルスクリプトで単純な検索プログラムtgrepが実際に活用できるコマンドとなることがシェルスクリプトの魅了です。 ** 制御コマンド [#zd2f10a8] 詳しい説明もせずにfor文、case文、while文を使ってしまったので、ここで詳しくシェルの制御コマンドについて説明します。 シェル制御コマンドのおもしろいところは、ifならfi、caseはesacのようにコマンドの終わりが、制御文字を逆順にした単語となっているものが多い点です。 *** for文 [#ua1c3ffc] for 文は、次の形式で使用します。 #pre{{ for name in word_list do block_stmt; done }} - word_listには空白で区切った単語の列を指定し、指定された順で変数nameにセットされ - doとdoneの間のblock_stmtが実行されます - in word_listが省略された場合には、すべての引数($*と同等)が展開されます 簡単な例で動作をみてましょう。 #pre{{ # 10回ループを回す場合の簡単な例 for i in 1 2 3 4 5 6 7 8 9 10 do echo $i; # ループカウンタの値を出力する done }} 端末からこれを実行すると、 #pre{{ $ for i in 1 2 3 4 5 6 7 8 9 10 > do > echo $i; > done 1 2 3 4 5 6 7 8 9 10 }} doneが入力されるまでは、第2プロンプトの> が出力され、その後1から10までが出力されます。 シェルスクリプトでは、Cのfor文のような指定回数だけループを回すような処理をfor文では書けません。 そこで、pythonのrangeと同様のプログラムを作成し、for文で指定回数ループが書けるようにしてみましょう。 rangeの仕様は、以下の通りとします - 引数が1個の場合、0以上、指定された値より小さい整数を出力します - 引数が2個の場合、第1引数を初期値とし、初期から第2引数より小さい整数を出力します - 引数が3個の場合、第1引数を初期値、第2引数を最大値、第3引数をステップ量と、初期値から最大値未満の整数をステップ単位で出力します range.cは、以下のようになります。 #pre{{ // range.c : range program. #include <stdio.h> main(int argc, char* argv[]) { int init = 0; int max = 0; int step = 1; int i; if (argc == 2) { max = atoi(argv[1]); } else if (argc == 3) { init = atoi(argv[1]); max = atoi(argv[2]); } else if (argc == 4) { init = atoi(argv[1]); max = atoi(argv[2]); step = atoi(argv[3]); } if (step > 0) for (i = init; i < max; i += step) printf("%d ", i); else for (i = init; i > max; i += step) printf("%d ", i); printf("\n"); } }} 動作を確認してみましょう。 #pre{{ $ range 1 10 1 2 3 4 5 6 7 8 9 $ range -1 1 -1 0 $ range 10 2 -1 10 9 8 7 6 5 4 3 $ range 3 0 1 2 }} となります。 それでは、rangeを使って与えられた数をカウントダウンするスクリプトEx5.shを作成してみましょう。 ((^Gは、Ctrl-vのあとにCtrl-gを入力してください)) #pre{{ # /bin/sh for i in `range $1 0 -1` do echo $i ^G; sleep 1; done }} では、実行権限を与えて、5からカウントダウンしてみましょう。 #pre{{ $ chmod +x Ex5.sh $ Ex5.sh 5 5 4 3 2 1 }} 1秒ごとにブザーが鳴って、5から1までカウントします。 *** case文 [#ta6d2577] case文は、パターンに合致した実行文を処理します。 #pre{{ case word in pattern1) block_stmt ;; # pattern1に合致した場合、 .... pattern2) block_stmt ;; # pattern2に合致した場合、 *) block_stmt ;; # 上記のいずれでもない場合、 esac }} 処理の終わりには、;ではなく;;を指定します。また、*)とすることで上記以外という指定になります。 pattern1, pattern2のパターン指定には、shの正規表現を使うことができます。 通常、case文は引数のチェックに使用され、その場合には位置引数をシフトするshiftコマンドが一緒に使用されます。 UNIXには、オプションを処理する便利なコマンドgetoptがあるので、これを使うとオプション #pre{{ $ Ex6.sh -cf 1 2 3 }} は #pre{{ $ Ex6.sh -c -f 1 -- 2 3 }} のように変換されます。 例として、コマンドEx6.shのオプションがvfのいずれかで、fオプションにはファイル名が後に続くような場合、 #pre{{ #! /bin/sh set -- `getopt cf: $*`; # オプションのチェックと展開して、位置引数に再セットする C_FLAG=false FILE="" for i in $* do case $i in -c) C_FLAG=true ;; -f) FILE=$2; shift ;; --) break ;; -?) echo invalid option; exit 1;; esac shift; done echo C_FLAG=$C_FLAG FILE=$FILE ARGS=$* }} となります。 これを実行すると #pre{{ $ chmod +x Ex6.sh $ Ex6.sh -cf 1 2 3 C_FLAG=true FILE=1 ARGS=2 3 }} となります。 *** if文 [#i8ed2aa3] 次にif文です。if文は、testコマンドと一緒に使うことが多いです。 if文の形式は、 #pre{{ if expr_list then ; block_stmt [else block_stmt] fi }} の形式を取ります。 expr_listの最後にセミコロンを付けない場合には、 #pre{{ if expr_list then block_stmt else block_stmt fi }} のように改行してthenを記述します。((私はこちらの形式を使用しています)) expr_listの終了コードが0以外の場合に条件が真となります。 if分がネストする場合には、elif文を使用します。 #pre{{ if expr_list then block_stmt elif expr_list then block_stmt else block_stmt fi }} if文の例を以下に示します。 #pre{{ if [ $? -ne 0 ] # 直前のコマンドが正常に終了しなかった場合 then if [ -e $TMP_FILE ] # 一時ファイルが存在すれば、 then rm -f $TMP_FILE; # 一時ファイルを削除し、処理を終了する exit 1; fi fi }} *** testコマンド [#q9f5e194] testは、引数で指定された条件を確認するコマンドです。 shでは、慣例として"["((testのシンボリックリンク))も利用することができます。この場合には終わりを"]"で囲みます。(([および]はコマンドであるため、expr_listとの間に空白を入れる必要があります)) よく使われるtestコマンドオプションを以下に示します。 : = | 文字列が等しい場合に、真を返します : != |文字列が等しくない場合、真を返します : -eq | 数値が等しい場合に、真を返します : -ne | 数値が等しくない場合に、真を返します : -d | ディレクトリの場合に、真を返します : -e | ファイルが存在する場合に、真を返します : -z | 文字列の長さが0の場合に、真を返します。シェル変数に値がセットされているか否かを調べるときに使用します : -n | 文字列の長さが0でない場合に、真を返します *** while文 [#t4b48c96] while文は、条件が真の間、ループを回します。 while文の仕様は、 #pre{{ while expr_list do block_stmt; done }} でexpr_listの終了条件が真の場合に、ループを通過しblock_stmtを実行します。 よくあるパターンは、ファイルからの入力処理であり、read文と組み合わせて使用します。 #pre{{ i=0; # 添え字iを初期化する cat /tmp/test.dat | while read line # /tmp/test.datの各行をlineに読み込む do IN_LINE[$i]=$line; # 入力ファイルの各行を配列IN_LINEにセットする i=`expr $i + 1`; done }} ** シェルスクリプトTIP集 [#x2bd3c3c] *** パイプをまとめる括弧 [#d87910de] *** 縦をよこにする [#zd916bb7] *** 一意なファイル名の生成 [#w23470d6] *** 出力の結合 [#ze550214] *** 特殊ファイル [#mdaa9e59] * 参考文献 [#k43cbbaa] - The Unix System ((私は、この本でUNIXを勉強しました。これ1冊でshの使い方、システムコールを使ったC言語プログラミング、UNIXコマンドの使い方が習得できます。さすがshを作った人が書いた本です!)) http://www.amazon.com/exec/obidos/ASIN/0201137917/ - Lions’ Commentary on UNIX ((初期のUNIXのソースコードを使ってUNIXカーネルの仕組みを解説した名著です。)) http://www.amazon.co.jp/exec/obidos/ASIN/4756118445/ 皆様のご意見、ご希望をお待ちしております。 - この記事は、「コマンドを組み合わせるのりとしてのシェルスクリプト」を再編成したものです。 -- [[竹本 浩]] &new{2009-08-15 (土) 16:00:35}; #comment