2008/08/12からのアクセス回数 36461 はじめに †Amazonの協調フィルタリングで話題になった「集合知(collective intelligence)」ですが、 そこにはこれが正解というものはなく、仮説と検証のサイクルを何度も繰り返しながら新たな方式が 日々研究開発されています。 仮説と検証のサイクルでは、生のデータから分析可能なデータに加工する作業が幾度となく繰り返され その処理内容は様々です。このため、データ加工では短期間に適切なデータを作成するというハードな 業務です。 そんな時に役にたつのが、これからご紹介します
です。 シェルには、sh, ksh, bash, csh, tcsh等いろいろな種類がありますが、ここではshを使って説明します。 *1 パイプでつなぐ †1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMultics と呼ばれる大型のオペレーティングシステムを開発していた頃、 「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。 彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11 (システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト) という現在のマイコンチップ以下のハードウェアを購入し、 その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。 今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。 この機会に、UNIXの「パイプでつなぐ」を試してみてください。 UNIXの3大発明 †ここでは、「パイプでつなく」をテーマに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システムコールの使い方を理解できる人は少ないでしょう。
パイプを実現する3つのシステムコール †パイプをつなぐときに必要なシステムコール
について、おさらいも含めて説明します。 fork システムコールのおさらい †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)をリダイレクトでファイルに保存します。 shでは n> のようにリダイレクトするファイル記述番号を指定することができます。
しています。 $ 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 システムコールの使い方 †pipeシステムコールの仕様を簡単に書くと、 呼び出し形式 #include <unistd.h> int pipe(int fildes[2]); 機能 パイプを生成し、ペアのファイル記述子を割り当てます。1番目のファイル記述子(fildes[0])が読み込み、 2番目(fildes[1])が書き込み用となります。 fildes[1]に書き込まれたデータは、fildes[0]から読み込まれまる。 これにより、あるプログラムの出力を他のプログラムの入力にすることができます。 読み込みまたは書き込みのファイル記述子のいずれかがクローズするとwidowed(未亡人)となります。 widowedとなったパイプに書き込むと書き込みプロセスはSIGPIPEシグナルを受け取ります。 widowedにすることで、読み込みプロセスにEnd-Of-Fileを送ることができます。 読み手がパイプのデータをすべて読み込んだ後やwidowedになった後のパイプから読み込もうとすると 0が返されます。 戻り値 成功すると0を返し、そうでない場合には-1を返します。 パイプは、
のように機能します。 簡単なプログラムで、上記の仕様を確認します。 // 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; // 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 と出力され、
ます。*2 dup システムコールの使い方 †同様に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; // 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; // 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 と出力されます。 *3
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コマンドの
先ほどのパイプラインに、sort -uを追加して重複を取り除きます。 $ 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コマンド*4のオプションを以下のようにまとめたものです。 #! /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の完成です。*5 次に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 以下省略 最初、ある程度マウスを動かすと画面に座標が表示されます。*6 ここまで、確認できたらGraphと連結してみましょう。 $ xev | Event2Plot | Graph 無事、最初の図と同じように出力されます。 グラフにプロットされるデータの途中経過がどのようになっているか知りたいことはありませんか。 このような場合には、teeコマンドで途中のデータをファイルに出力し、tailコマンドで表示します。 *7 $ 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)の使われ方 †グルーピング †初期のUNIXでは、ファイルシステムを跨ぐファイルのコピーには、cpコマンドが使えませんでした。 そこで、tarコマンドとパイプを使って以下のようにコピーしました。 $ tar cf - . | (cd /mnt/bak; tar -xf -) これまでは、()で括ってパイプを使ったことはありませんでしたが、これはグルーピングと呼ばれる 処理で、()で括ったコマンドは入出力が共通になります。 *8 バッククォート(`) †バッククォートで括られたコマンドの出力をパイプでつないで、その結果を別のコマンドの引数にします。 例えば、変数NOWに今の時刻をセットしたい場合に、 $ NOW=`date` $ echo $NOW Wed Aug 12 20:15:11 JST 2009 とすると、変数NOWにdateコマンドの実行結果がセットされ、echo $NOWでセットした時刻が表示されます。 また、あるコマンドの実行結果を別のコマンドの引数にしたい場合にも便利です。 例)変数iの値に1を足した値を表示する場合(シェル内の四則演算等にはexprを使います) $ i=1 $ echo `expr $i + 1` 2 これを使えば、変数の値を変える場合には、 $ i=`expr $i + 1` $ echo $i 2 とすればよいことが分かります。 ヒアドキュメント(<<EOF) †シェルスクリプト内に記述したドキュメントにシェル変数の置換を行い、その結果を別のコマンドの入力とします。 ヒアドキュメントは、テンプレート(ひな形)として使用されることが多く、 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 存在しないファイルに>>を使った場合、新たにファイルを作成し、そのファイルに出力します。 また、すでにファイルが存在する場合、>を使ったリダイレクトはそのファイルに上書きします。 *9 ファイル展開 †シェルでは、メタ記号を使って複数のファイル名に展開することができます。 メタ記号の一覧を以下に示します。
ファイル展開はディレクトリ内のファイルに対して行われます。 例えば、ファイルが.bakで終わるすべてのファイルを削除する場合には、 $ rm *.bak のように指定します。 サンプルソースのディレクトリには、以下のファイルがあります。 $ ls -a . 2.out Graph ex2 images .. Event2Plot Makefile ex2.c range .cproject Ex5.sh Pipeline.txt ex3 range.c .project Ex6.sh Tgrep.sh ex3.c tgrep .settings Ex7.sh ex1 ex4 tgrep.c 1.out Ex8.sh ex1.c ex4.c しかし、echo *では $ echo * 1.out 2.out Event2Plot Ex5.sh Ex6.sh Ex7.sh Ex8.sh Graph Makefile Pipeline.txt Tgrep.sh ex1 ex1.c ex2 ex2.c ex3 ex3.c ex4 ex4.c images range range.c tgrep tgrep.c となり、.で始まるファイルが含まれていません。 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[]) { int i; for (i=0 ; env[i] != NULL ; ++i) { printf("%s ", env[i]); } printf("\n"); } コンパイルし、実行します。*10 $ cc -o ex4 ex4.c $ 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から引用します。
ここで、
ことを意味します。 ただし、`で囲まれた$は、シェル変数が置換されるので、yの間違いではないかと思われます。 $ HELLO="echo hello" $ echo `$HELLO` hello 以上のことから、シェルのクォーティングルールを簡単に説明すると
となります。 おきまりのパターン †シェルは、コマンドを組み合わせるだけではなく、その使い方を拡張するためにも利用できます。 ここでは、単純な検索するプログラムtgrepを複数ファイルの検索に拡張する方法を以下のような手順で説明します。
tgrepは、
をファイルの終わりまで繰り返す、簡単なプログラムです。 #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とします。 主な機能は、
引数チェック †引数のチェックでよく使用されるシェルの文法がcase文です。 シェルのcase文には正規表現を使うことができるので、C言語のcase文よりも使い易くなっています。 まず、引数の数を調べるところは、特殊シェル変数の$#で引数の個数を取り出し、 case文で処理を振り分けます。各条件で実行する処理は、2個のセミコロン(;;)で区切ります。 #! /bin/sh case $# in 1) echo pattern: $1 ;; [2-9]) echo pattern: $1; shift; echo files: $* ;; esac ここで、shiftコマンドは、位置引数を1個左にシフトコマンドです。shiftコマンドで2個目以降の引数を$*で表せることになります。 まずは、このようにシェルスクリプトが正しい動作をするかechoを使って引数をチェックします。 $ Tgrep.sh 1 pattern: 1 $ Tgrep.sh 1 2 3 4 pattern: 1 files: 2 3 4 うまく動作しているように見えますが、これでは10個以上の引数が与えられた正常に動作しません。 そこで、引数のパターンを0個、1個、それ以外に変えてみましょう。 #! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) echo pattern: $1 ;; *) echo pattern: $1; shift; echo files: $* ;; esac 引数が10個以上でも正常に動作するか、引数の個数を変えて確かめてみましょう。 $ 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. すべてうまくいきました。これで、引数チェックは完成です。 各ファイルに対する繰り返し †次は、検索するファイル数が複数になったときの処理を追加します。 同じ処理を何回か繰り返す場合には、for文を使用します。 引数のテストではechoを使っていましたが、これからはtgrepに替え、 2個以上のファイルに対する処理にfor文を入れてみましょう。 #! /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 できたスクリプトを動かしてみます。 $ 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コマンドで出力します。 #! /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 出力結果は、以下のようになります。 $ 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が実際に活用できるコマンドとなることがシェルスクリプトの魅了です。 制御コマンド †詳しい説明もせずにfor文、case文、while文を使ってしまったので、ここで詳しくシェルの制御コマンドについて説明します。 シェル制御コマンドのおもしろいところは、ifならfi、caseはesacのようにコマンドの終わりが、制御文字を逆順にした単語となっているものが多い点です。 for文 †for 文は、次の形式で使用します。 for name in word_list do block_stmt; done
簡単な例で動作をみてましょう。 # 10回ループを回す場合の簡単な例 for i in 1 2 3 4 5 6 7 8 9 10 do echo $i; # ループカウンタの値を出力する done 端末からこれを実行すると、 $ 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の仕様は、以下の通りとします
range.cは、以下のようになります。 // 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"); } 動作を確認してみましょう。 $ 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を作成してみましょう。 *11 # /bin/sh for i in `range $1 0 -1` do echo $i ^G; sleep 1; done では、実行権限を与えて、5からカウントダウンしてみましょう。 $ chmod +x Ex5.sh $ Ex5.sh 5 5 4 3 2 1 1秒ごとにブザーが鳴って、5から1までカウントダウンしました。 case文 †case文は、パターンに合致した実行文を処理します。 case word in pattern1) block_stmt ;; # pattern1に合致した場合、 .... pattern2) block_stmt ;; # pattern2に合致した場合、 *) block_stmt ;; # 上記のいずれでもない場合、 esac 処理の終わりには、;ではなく;;を指定します。また、*)とすることで上記以外という指定になります。 pattern1, pattern2のパターン指定には、shの正規表現を使うことができます。 通常、case文は引数のチェックに使用され、その場合には位置引数をシフトするshiftコマンドが一緒に使用されます。 UNIXには、オプションを処理する便利なコマンドgetoptがあり、これを使うとオプションは $ getopt cf: -cf 1 2 3 -c -f 1 -- 2 3 となり、
に変換してくれるので、シェルスクリプトの引数処理がとても簡単になります。 getoptを使った例として、 コマンドEx6.shのオプションがvfのいずれかで、fオプションにはファイル名が後に続くような場合、 #! /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=$* となります。 これを実行すると $ chmod +x Ex6.sh $ Ex6.sh -cf 1 2 3 C_FLAG=true FILE=1 ARGS=2 3 となります。 if文 †次にif文です。if文は、testコマンドと一緒に使うことが多いです。 if文の形式は、 if expr_list then ; block_stmt [else block_stmt] fi の形式を取ります。 expr_listの最後にセミコロンを付けない場合には、 if expr_list then block_stmt else block_stmt fi のように改行してthenを記述します。*12 expr_listで最後に実行したコマンドの終了コードが0の場合に条件が真となります。 if分がネストする場合には、elif文を使用します。 if expr_list then block_stmt elif expr_list then block_stmt else block_stmt fi if文の例を以下に示します。 if [ $? -ne 0 ] # 直前のコマンドが正常に終了しなかった場合 then if [ -e $TMP_FILE ] # 一時ファイルが存在すれば、 then rm -f $TMP_FILE; # 一時ファイルを削除し、処理を終了する exit 1; fi fi testコマンド †testは、引数で指定された条件を確認するコマンドです。 shでは、慣例として"["*13も利用することができます。この場合には終わりを"]"で囲みます。*14 よく使われるtestコマンドオプションを以下に示します。
while文 †while文は、条件が真の間、ループを回します。 while文の仕様は、 while expr_list do block_stmt; done でexpr_listで最後に実行したコマンドの終了条件が0(真)の場合に、ループ内のblock_stmtを実行します。 以下のEx7.shコマンドではread文と組み合わせて使用します。 #! /bin/sh i=0 while read line do IN_LINE[$i]=$line; i=`expr $i + 1`; done echo IN_LINE=${IN_LINE[@]}; Ex7.shを実行すると $ Ex7.sh <<EOF > abc efg > 12345 > EOF IN_LINE=abc efg 12345 のようにシェル変数IN_LINEの値が出力されます。 シェルスクリプトTIP集 †縦をよこにする †ディレクトリの特定のファイルのみを削除する場合、削除するファイルのリストをファイルに保存し、このファイルの情報を使ってrmコマンドを実行すると便利です。
では実際にやってみましょう。 これまでのCのサンプルファイルがあるディレクトリでオブジェクトファイルを作成し、それを削除してみます。 $ cc -c *.c $ ls *.o >/tmp/rm.lst $ cat /tmp/rm.lst ex1.o ex2.o ex3.o ex4.o range.o tgrep.o $ rm `cat /tmp/rm.lst` $ ls *.o ls: *.o: No such file or directory rm `cat /tmp/rm.lst`のように一覧ファイルをバッククォート` で実行すると縦に並んでいたファイルのリストが横一覧のコマンド引数に変換することができます。 一意なファイル名の生成 †シェルスクリプト中で一時ファイルを作成する場合、一意なファイル名を一時ファイル名としたいことがあります。 このような場合特殊変数$$を使ってシェルのプロセスIDを取得し、ファイル名の一部に付けることで一意なファイル名を作成します。 特にヒアドキュメントのテンプレート機能を使ってawk等のスクリプトをシェルスクリプト中に生成する場合に使用します。 以下のシェルスクリプト(Ex8.sh)のようなパターンになります。 #! /bin/sh TMP_FILE=/tmp/temp.$$ NAME='Hiroshi TAKEMOTO' cat << EOF > $TMP_FILE My name is $NAME. This is a simple shell script example. EOF echo `cat $TMP_FILE` rm $TMP_FILE Ex8.shを実行すると $ Ex8.sh My name is Hiroshi TAKEMOTO. This is a simple shell script example. $ ls /tmp/temp.* ls: /tmp/temp.*: No such file or directory 一時ファイルは、実行後に削除されています。 出力の結合 †shでは、標準エラー出力や標準出力の指定を2, 1のように記述番号を使って指定することができます。 標準出力と標準エラー出力を一緒にファイルに出力する場合には、2>&1を使って出力をまとめることができます。 例)test.shの標準出力と標準エラー出力を/tmp/stdou_and_stderr.outする場合、 $ test.sh 2>&1 >/tmp/stdou_and_stderr.out 特殊ファイル †出力内容を捨てたい時やファイルを空にしたい時に特殊ファイル/dev/nullを使用します。 標準出力をリダイレクトで/dev/nullに送れば、そのメッセージが捨てられます。 大量のメッセージを出力するプログラムでそのメッセージが必要ない場合にこの方式を使用することによって処理スピードが格段に速くなます。 また、ファイルを空にするときには、 $ cp /dev/null file_name のようにするとfile_nameで指定されたファイルのサイズが0になります。 まとめ †パイプでつなぐ(パイプライン処理)ことによって、短時間にデータを加工する方法をご紹介しました。 更にシェルスクリプトを使うことによって
といったことを組み合わせることができます。 ここで紹介したデータ加工のパイプライン処理とシェルスクリプトの活用が、 日常の業務で皆様のお役に立てば幸いです。 謝辞 †竹中章子さん、戸張一夫さんには原稿に対し貴重なコメントを頂きました。感謝申し上げます。 参考文献 †
皆様のご意見、ご希望をお待ちしております。
Tweet |