07 月 26 日(火)3-4h

コマンドインタフェース

今回は,シェルとプログラムとの間のインタフェースについて理解しよう. そして,既存の Unix コマンドと組み合わせて協調的に利用できるような, 柔軟性のあるプログラムを作成してみよう.

要するに,Unix コマンドを自作するんだ.

シェル

Unix の文字端末にログインする(端末を開く)と, コマンドの入力を受け付けてくれる. これは,日常的すぎて気付きづらいことだが,実は, Cのプログラムで文字列の入力を処理しているのと 同じ状態になっている.

つまり,ログイン直後から動いている特別なプログラムがあり, それがコマンドの入力・実行の処理をしてくれているわけだ. このようなプログラムは,シェルと呼ばれている.

シェルのソースコード断片:

char cmd[256], arg[256];

printf("[prompt]$ ");		// プロンプト表示
scanf("%s %s", cmd, arg);	// コマンド入力
...
このコードはイメージです. 実在のシェルのソースとは限りません.

シェルの実行例:

[prompt]$ mkdir C/0726
...

なお,シェルのコマンドには, 内部コマンドと外部コマンドの2種類がある:

ところで,外部コマンドでは, シェルプログラムから他のプログラムを呼び出していることになる. また,実は,シェルは端末ウィンドウとは別のプログラムであり, 端末プログラムがシェルプログラムを呼び出していることにもなる. つまり,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 コマンドのクローンでもある.

List 1. echo のクローン my-echo.c
#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);
}

コマンドライン引数をいくつか与えて実行してみよう:

$  my-echo  no pokemon here	# echo クローン
no pokemon here

$  echo  no pokemon here	# 本来の echo
no pokemon here

この実行結果と List 1 の内容を比較してみれば, 各変数の値が次のようにセットされていることが容易に理解できるハズだ:

int argc = 4;		// 引数の個数(コマンド名も勘定に含む)
char *argv[4];
argv[1] = "no";		// 1 番目の引数
argv[2] = "pokemon";	// 2 番目の引数
argv[3] = "here";	// 3 番目の引数
List 1 では, 反復カウンタ i の初期値が 0 ではなく,1 であることに注意.

ちなみに,argv[0] には, コマンド(プログラム)自身の名前がセットされる:

argv[0] = "./my-echo";	// コマンド名(プログラム名)
これを確認するには,List 1 の for 文の初期値を i = 1 から i = 0 へ書き換えてみればよい.

また,List 1 を見ればわかることだが,念のため... コマンドライン引数の内容はすべて, 文字列として取り扱われることに注意しよう. 例えば,人間がコマンドライン引数に「数値」123 を指定したつもりでも, このプログラムにとっては「数字列」"123" となる.

もし,引数を数値として利用したいのなら, 以前紹介した関数 atoi( )atof( ) による変換が必要になる.

コマンドの戻り値

コマンドの引数のついでに,コマンドの戻り値についても追加説明しておく. すでに知っている通り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() を使うとは限らない. 後処理(あとしょり;postprocess)等でメイン関数に戻る必要がある場合には, 次のようにする:

...
int sub(...)
{
	...
	if ( エラー ) return (1);	// とりあえず main() へ戻ってから...
	...
	return (0);
}

int main( ... )
{
	...
	if (sub(...)) goto ERROR;	// エラー処理へ行き...
	...
	return (EXIT_SUCCESS);
ERROR:
	...				// 必要な後片付けを済ませてから...
	return (EXIT_FAILURE);		// お行儀良く異常終了
}

なお,必要な後処理の例としては, fopen() に対する fclose() 等がある.


例:wc クローン

Unix コマンドのクローンの例をもうひとつ紹介しておく. List 2 は,wc の機能限定版クローン p-wc(プチwc or パチもんwc)のソースである. 本物の wc コマンドでは, ファイルの行数,単語数,文字数を調べてくれるが, このクローンでは単語数だけしか調べない.

List 2. wc の劣化版クローン p-wc.c
#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);
}
重要:引数の内容 argv[i] を使う場合, 必ず事前に,引数の個数 argc をチェックし, その引数の存在を確認すること. たとえば,コマンドライン引数が 1 個しか指定されていない場合 (argc = 2 の場合), argv[0 または 1] は存在するが, argv[2 以上] は存在しない. 存在しないハズの引数(ゴミ)にアクセスすると,おかしなことになってしまう.

このプログラムでは, コマンドライン引数にファイル名を指定して実行すると, そのファイル内の単語数を標準出力する. 引数がなければ標準入力(stdin)から単語数を調べる. すなわち,次のような複数の使い方がある:

$  ./p-wc data.txt		# argc=2,ファイル入力

$  ./p-wc < data.txt		# argc=1,リダイレクトによる標準入力

$  cat data.txt | ./p-wc	# argc=1,パイプによる標準入力
ここで,data.txt としては,適当なテキストファイルを使ってください. ソースファイルでもOK.

ここで,パイプとは,プログラム間でデータをやりとりするために, 標準出力と標準入力とを連結する仕組みである. 一般形は次の通り:

$  コマンド1 | コマンド2 | コマンド3 | ... | コマンドn

コマンド1の標準出力が コマンド2の標準入力となり, コマンド2の標準出力が コマンド3の標準入力となり, コマンド3の標準出力が...,コマンドn の標準入力となる.


本日の課題

前回の練習問題 のプログラム pastetable を元にして, 複数のファイルを結合し,任意の列数の表を処理できるように改良せよ.

実行例:

ヒント: 複数のファイルから入力するので, ファイルポインタの配列 FILE *fp[256] を使う必要があるだろう. (要素数については定数 256 の決め打ちでよい.) そして, for (i = 1; i < argc; i++) { ... }

アドバイス:

レポート提出 注意事項: 以下の点についても厳しくチェックする:


(c) 2016, yanagawa@kushiro-ct.ac.jp