今回は,シェルとプログラムとの間のインタフェースについて理解しよう. そして,既存の Unix コマンドと組み合わせて協調的に利用できるような, 柔軟性のあるプログラムを作成してみよう.
Unix の文字端末にログインする(端末を開く)と, コマンドの入力を受け付けてくれる. これは,日常的すぎて気付きづらいことだが,実は, Cのプログラムで文字列の入力を処理しているのと 同じ状態になっている.
つまり,ログイン直後から動いている特別なプログラムがあり, それがコマンドの入力・実行の処理をしてくれているわけだ. このようなプログラムは,シェルと呼ばれている.
シェルのソースコード断片: char cmd[256], arg[256]; printf("[prompt]$ "); // プロンプト表示 scanf("%s %s", cmd, arg); // コマンド入力 ...
このソースコードはイメージです.
実在のシェルのソースコードではありません.
|
シェルの実行例: [prompt]$ mkdir C/0716 ... |
なお,シェルのコマンドには, 内部コマンドと外部コマンドの2種類がある:
たとえば,exit コマンドを入力するとシェルを終了する. また,よく使うコマンド cd 等も内部コマンドである.
たとえば,ls コマンドを入力すると, /bin/ls プログラム (ディレクトリ /bin にある実行形式ファイル ls) を実行する. また,自分で作ったプログラムの実行コマンド ./a.out を入力すると, カレントディレクトリにある a.out プログラムが実行される.
ちなみに,ls や cp のような Unix の標準的なコマンドのプログラムファイルは, /bin や /usr/bin などのディレクトリに保管されている. プログラムファイルの存在を確認してみよう.
$ ls /bin | less ... cp ... ls ...
ところで,外部コマンドでは, シェルプログラムから他のプログラムを呼び出していることになる. また,実は,シェルは端末ウィンドウとは別のプログラムであり, 端末プログラムがシェルプログラムを呼び出していることにもなる. つまり,PC の中では,様々な状況で, 複数のプログラムが連携動作しているわけだ.
なお今回は,このような連携動作の一部について, プログラミングしてみよう.
シェルの外部コマンドによって,プログラムを実行する場合について考える. 多くのコマンドでは, 実行時にコマンドライン引数を与えることによって, その動作をコントロール(切り替えたり,調整したり)できる.
たとえば,ls は,ファイルの一覧を表示するコマンドであるが, 多種多様なコマンドライン引数によって, 様々な表示形式を選べるようになっている:
$ ls # カレントディレクトリのファイルを表示 $ ls /bin # ディレクトリ /bin のファイルを表示 $ ls -w 40 /bin # 幅 40 文字以内で表示 $ ls -l /bin # ファイルの詳細情報を表示 ...
コマンドライン引数の種類としては, オプションスイッチ(上記の「-w」,「-l」), オプションパラメータ(上記の「40」,「/bin」), 等がある.
オプション(option)は, 選択的(必須ではない,省略も可能)というような意味.
なお,コマンドによっては, 必須のコマンドライン引数をもつものもある. (例:cc コマンドでは, 引数としてファイル名を与える必要がある.) つまり,「コマンドライン引数=オプション」というのは誤解.
Cでは,これらのコマンドライン引数を main( ) 関数の引数として, プログラム内に取り込むことになる. この機能を利用するためには, main( ) 関数のプロトタイプを次の形式とすればよい:
int main(int argc, char *argv[])
この場合,まず,引数の個数が 変数 argc(argument count)に代入される. そして,argc 個の引数文字列が 配列 argv(argument vector)に代入される.
正確には,argv は「文字列ポインタの配列」なので, 引数文字列の「内容」ではなく「アドレス」が配列要素に代入される. 文字列の内容はメモリ上のどこかに記録される. (が,具体的にどこなのか?は気にしなくてよい.) 文字列ポインタについては, 以前の説明も参照せよ.
より正確には,実は, argv は「文字列ポインタへのポインタ」なのだが, これだと混乱し易いので, この授業では,「ポインタの配列」とみなすことにしておこう. 詳しくは,教科書 pp.139-140 を参照せよ.
List 1 は,コマンドライン引数を表示するプログラムである. これは,echo コマンドのクローンでもある.
#include <stdio.h> int main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) { // 引数の個数分だけ反復 printf("%s ", argv[i]); // 各引数の内容を表示 } printf("\n"); return (0); }
コマンドライン引数をいくつか与えて実行してみよう:
$ echo over 200 times over 200 times
この実行結果と List 1 の内容を比較してみれば, 各変数の値が次のようにセットされていることが容易に理解できるハズだ:
int argc = 4; // 引数の個数(コマンド名も勘定に含む) char *argv[4]; argv[1] = "over"; // 1 番目の引数 argv[2] = "200"; // 2 番目の引数 argv[3] = "times"; // 3 番目の引数
ちなみに,argv[0] には, コマンド(プログラム)自身の名前がセットされる:
argv[0] = "./my-echo"; // コマンド名(プログラム名)
なお,コマンドライン引数の内容はすべて, 文字列として取り扱われることに注意しよう. List 1 を見ればわかることだが,念のため... 人間が「数値」200 を入力したつもりでも, コンピュータにとっては「数字列」"200" である.
コマンドの引数のついでに,コマンドの戻り値についても追加説明しておく. すでに知っている通り, main( ) 関数は int 型の戻り値をもつ:
int main( ... ) { ... return (0); }
そして,プログラムの終了状態(正常終了/異常終了など)を知るために, この戻り値を利用するということだった.
ところで,main( ) 以外の関数の中から, プログラムを終了したいこともよくある. この場合には,exit( ) 関数を利用すればよい. この関数の引数は,main( ) の戻り値と同じである.
void exit(int 終了状態)
なお,関数 exit( ) のプロトタイプ宣言は, ヘッダファイル /usr/include/stdlib.h にある. このヘッダファイルでは,次のような定数マクロも定義されている:
#define EXIT_FAILURE 1 // 失敗の場合の終了状態 #define EXIT_SUCCESS 0 // 成功の場合の終了状態
これらのマクロを利用すれば, 定数 0 とか 1 を使うより, ソースコードの意味が明確になる.
以上のことから,これらを利用したソースの一般形は次のようになる:
#include <stdlib.h> void sub( ... ) { ... if ( エラー ) exit(EXIT_FAILURE); // 異常終了:main( ) へ戻らずに終了 // (main( ) へ戻って return (1) したのと同じこと) ... } int main( ... ) { ... sub(...); ... return (EXIT_SUCCESS); // 正常終了:return (0) と同 }
もちろん,必ずしも exit() を使うとは限らない. 後処理等でメイン関数に戻る必要がある場合には, 次のようになる:
... int sub(...) { ... if ( エラー ) return (1); // とりあえず main() へ戻ってから... ... return (0); } int main( ... ) { ... if (sub(...)) goto ERROR; // エラー処理へ行き... ... return (EXIT_SUCCESS); ERROR: ... // 必要な後片付けを済ませてから... return (EXIT_FAILURE); // お行儀良く異常終了 }
なお,必要な後処理の例としては, fopen() に対する fclose() 等がある.
Unix コマンドのクローンの例をもうひとつ紹介しておく. List 2 は,wc の機能限定版クローン p-wc(プチ wc)のソースである. 本物の wc コマンドでは, ファイルの行数,単語数,文字数を調べてくれるが, このクローンでは単語数だけしか調べない.
#include <stdio.h> #include <stdlib.h> // エラー処理関数(msg:エラーメッセージ) void error(char *msg) { fprintf(stderr, "エラー:%s\n", msg); fprintf(stderr, "使い方: p-wc [ファイル]\n"); exit(EXIT_FAILURE); } // 単語数カウント関数 int wc(FILE *fp) { char word[256]; int n = 0; while (fscanf(fp, "%s", word) != EOF) { n++; } return (n); } int main(int argc, char *argv[]) { FILE *fp; int n; if (argc == 1) { // 引数なしのとき fp = stdin; // 標準入力 } else if (argc == 2) { // 引数 1 個のとき fp = fopen(argv[1], "r"); // ファイル入力 if (fp == NULL) error("ファイルを開けない"); } else { error("引数の個数が変"); } n = wc(fp); printf("%d\n", n); fclose(fp); return (EXIT_SUCCESS); }
このプログラムでは, コマンドライン引数にファイル名を指定して実行すると, そのファイル内の単語数を標準出力する. 引数がなければ標準入力(stdin)から単語数を調べる. すなわち,次のような複数の使い方がある:
$ ./p-wc data.txt # argc=2,ファイル入力 $ ./p-wc < data.txt # argc=1,リダイレクトによる標準入力 $ cat data.txt | ./p-wc # argc=1,パイプによる標準入力
ここで,パイプとは,プログラム間でデータをやりとりするために, 標準出力と標準入力とを連結する仕組みである. 一般形は次の通り:
$ コマンド1 | コマンド2 | コマンド3 | ... | コマンドn
コマンド1の標準出力が コマンド2の標準入力となり, コマンド2の標準出力が コマンド3の標準入力となり, コマンド3の標準出力が...,コマンドn の標準入力となる.
実行例:
$ ./pastetable > outfile.txt エラー : 引数の個数が足りません.入力ファイルを1つ以上指定して下さい. 使い方 : pastetable 入力ファイル ... $ cat outfile.txt
# まず,適当な1列ずつデータの入力ファイルをエディタで作成しておこう. $ cat name.txt 氏名 秋元才加 板野友美 ... $ cat jpn.txt 国語 65 60 ... $ cat math.txt 数学 70 75 ... $ cat eng.txt 英語 70 70 ... # 以下,複数の列データを結合. $ ./pastetable name.txt math.txt 氏名 数学 秋元才加 70 板野友美 75 ... $ ./pastetable name.txt jpn.txt math.txt eng.txt 氏名 国語 数学 英語 秋元才加 65 70 70 板野友美 60 75 70 ...
アドバイス: