2008/08/12からのアクセス回数 36777
Amazonの協調フィルタリングで話題になった「集合知(collective intelligence)」ですが、 そこにはこれが正解というものはなく、仮説と検証のサイクルを何度も繰り返しながら新たな方式が 日々研究開発されています。
仮説と検証のサイクルでは、生のデータから分析可能なデータに加工する作業が幾度となく繰り返され その処理内容は様々です。このため、データ加工では短期間に適切なデータを作成するという過酷な業務が 任されています。
ここでご紹介するのは、
について、例題を多く取り入れながら説明していきます。
1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMultics と呼ばれる大型のオペレーティングシステムを開発していた頃、 「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。
彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11 (システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト) という現在のマイコンチップ以下のハードウェアを購入し、 その上に現在の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)をリダイレクトでファイルに保存します。 *1
$ 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を返します。
パイプは、
のように機能します。
簡単なプログラムで、上記の仕様を確認します。
// 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
と出力され、
ます。*2
同様に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
と出力されます。 *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を入力してください
リアルタイムとまではいきませんが、途中経過が表示されます。
初期の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
とすればよいことが分かります。
シェルスクリプト内に記述したドキュメントにシェル変数の置換を行い、その結果を別のコマンドの入力とします。
ヒアドキュメントは、テンプレート(ひな形)として使用されることが多く、 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
シェルでは、メタ記号を使って複数のファイル名に展開することができます。
メタ記号の一覧を以下に示します。
| メタ記号 | 説明 |
| ? | 任意の一文字にマッチする |
| [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");
}
コンパイルし、実行します。*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から引用します。
| メタ文字 | ||||||
| ' | " | ` | \ | $ | * | |
| ' | 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
以上のことから、シェルのクォーティングルールを簡単に説明すると
となります。
シェルは、コマンドを組み合わせるだけではなく、その使い方を拡張するためにも利用できます。 ここでは、標準入力から第一引数に指定されたパターンを検索するプログラムtgrep を複数ファイルの検索に拡張方法を以下のような手順で説明します。
tgrepは、簡単なCのプログラムです。
#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 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 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文は、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は、引数で指定された条件を確認するコマンドです。 shでは、慣例として"["*13も利用することができます。この場合には終わりを"]"で囲みます。*14
よく使われるtestコマンドオプションを以下に示します。
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の値が出力されます。
ディレクトリの特定のファイルのみを削除する場合、削除するファイルのリストをファイルに保存し、このファイルの情報を使って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になります。
パイプでつなぐ(パイプライン処理)ことによって、短時間にデータを加工する方法をご紹介しました。 更にシェルスクリプトを使うことによって
といったことを組み合わせることができます。 ここで紹介したデータ加工のパイプライン処理とシェルスクリプトの活用が、 日常の業務での皆様のお役に立てば幸いです。
皆様のご意見、ご希望をお待ちしております。