連携処理1:コマンドラインインタフェース

日常のPC作業の道具として使える unix コマンドのような プログラムを作成してみよう. 単独のコマンドとして動作するだけでなく, 他のコマンドと組み合わせて協調的にも利用できるような, 柔軟性のあるプログラムにしよう.

教科書の該当範囲:第5.10節
参考書の該当範囲:なし

標準入出力

リダイレクトとパイプ

前期に学習したファイル入出力では, 前処理の fopen() とか後処理の fclose() を書く必要があり, 面倒くさかったなぁ... キーボード入力と画面出力では,前処理・後処理は不要だったのに...

いや,実は...入力ファイルと出力ファイルが1つずつの場合には, fopen() 等は必要ない. プログラムの実行時にリダイレクトを指定すれば, ファイルからの入力およびファイルへの出力を簡単に実現できる.

また,パイプを指定すれば, 復数のプログラムの入出力を連結し実行できる. この場合,中間ファイル(各プログラムの出力ファイル)を作らずに済む.

実行方法の一般型:

$  プログラム < 入力ファイル	# 入力リダイレクト:データをファイルから入力
$  プログラム > 出力ファイル	# 出力リダイレクト:実行結果をファイルへ出力
$  プログラム < 入力ファイル > 出力ファイル	# 入出力リダイレクトの同時利用
$  プログラム1 < | プログラム2 | ... | プログラムn	# パイプ:出力を次のプログラムへ入力
$  プログラム1 < 入力ファイル | プログラム2 | ... | プログラムn > 出力ファイル	# パイプとリダイレクトの同時利用
>」の代わりに「>>」を使えば, 出力ファイルの末尾へのデータの追加も可能.

なお,これらの場合, scanf( ) 関数などの入力処理では, キーボードからではなく,入力ファイルからデータを読み取ることになる. また,printf( ) 関数などの出力処理では, 端末画面ではなく,出力ファイルへデータを書き込むことになる. つまり,入力リダイレクトとキーボード入力とは同じことであり, また,出力リダイレクトと画面出力も同じことになる. そのため,これらはひと括りに 標準入力(キーボードと入力ファイル)および 標準出力(端末画面と出力ファイル)と呼ばれている.

では,次のプログラムを利用して,これらの意味を理解しよう.

io-1.c:(テキストを入力し,そのまま出力するプログラム)

#include <stdio.h>

int main(void)
{
	char	c;		// 文字
	int	n = 0;		// 文字数

//	printf("テキスト > ");	// プロンプトの表示
		// リダイレクト利用の場合,プロンプトは不要.

	while (1) {
		if (scanf("%c", &c) == EOF) break;	// 単語文字列の入力
		printf("%c", c);			// 単語文字列の表示
		n++;
	}
	if (n == 0) perror("入力がナッシング");		// エラーの表示

	return (0);
}

実行例:

$ ./io-1		# ふつーに実行
テキスト >  hello world		# てきとーなテキストをキーボード入力
hello world			# 入力したテキストが表示される
...
...
[Ctrl]+[D]

$ ./io-1 > data.txt	# 出力リダイレクトを指定して実行
hello world			# てきとーな単語をキーボード入力
...			# あれ?何も表示されないぞ
[Ctrl]+[D]

$ cat data.txt		# ファイル data.txt の内容を表示するコマンド
hello world
...				# ファイルに書き込まれてた

$ ./io-1 < data.txt	# 入力リダイレクトを指定して実行
hello world		# ファイル word.txt の内容が入力され,表示された
...

$ ./io-1 < data.txt | wc -w	# パイプを指定し,出力を wc コマンドへ連結
3			# 単語数が表示される

$ ./io-1 > data.txt	# 出力リダイレクトを指定して実行
[Ctrl]+[D]	# 何も入力せずに終了
入力がナッシング		# エラーはファイルではなく画面に出力された
プロンプトの「 > 」 (プログラムが表示したもの)と リダイレクトの「 > 」 (コマンドラインでユーザが指定するもの)との区別に注意.

特別なファイルポインタ

キーボード入力・画面出力がリダイレクトによってファイル入出力に変化したのか? いや実は,Unix では,キーボードも端末画面もファイルの一種とみなされている. 次のような特別な名前のファイルポインタがあらかじめ用意されている:

これらは,プログラム実行時にはすでに,自動的にオープンされている.

標準出力と標準エラー出力の違いを理解しよう:

$  ls			# ファイルを確認
infile.txt  outfile.txt

$  ls nofile.txt outfile.txt > ls.txt	# 存在しないファイルを ls し,出力リダイレクト
ls: nofile.txt にアクセスできません: No such file or directory
				# ↑ ls のエラー出力(出力リダイレクトされていない)
$  cat < ls.txt
outfile.txt			# ls の標準出力(出力リダイレクトされていた)

逆に言えば,これからは, 標準出力にエラーメッセージを表示してはダメだ. エラーを標準出力してしまうと:

面倒を避けるために, 標準出力とエラー出力とを使い分けること.

これらのファイルポインタを使って, io-1.c を書き換えてみよう.

io-2.c

#include <stdio.h>

int main(void)
{
	char	c;		// 文字
	int	n = 0;		// 文字数

//	printf("テキスト > ");	// プロンプトの表示

	while (1) {
		if (fscanf(stdin, "%c", &c) == EOF) break;	// 標準入力...scanf() と同じ
		fprintf(stdout, "%c", c);			// 標準出力...printf() と同じ
		n++;		// 文字数をカウント
	}
	if (n == 0) fprintf(stderr, "入力がナッシング\n");	// 標準エラー出力...perror() の同類
	
	return (0);
}

実行結果は変わりません.

補足

標準入力・標準出力はバッファリング (buffering,一時的に蓄積)される. このため,たとえば,出力関数を実行してから実際に表示されるまでには, ある程度の時間がかかる. また,その間にプログラムが異常終了してしまうと, バッファの内容は表示されなくなってしまう.

「時間がかかる」といっても,人間が体感できるほどの時間ではない. 「表示処理が完了する前に次の処理が実行される場合がある」という意味.

一方,標準エラー出力は直接に表示されるので, 実行すれば即座に必ず表示される. したがって,たとえば,デバッグ用のテスト出力には, prinff(...) ではなく, fprintf(stderr, ... ) の方が確実.

上級者向け: バッファリングの効果については, 改行記号「\n」を出力せずに大量のデータを出力しておいて, 途中でわざとセグメントエラー等で異常終了させてみる, などとすると確かめられるかもしれない.

Unix コマンドの作成

コマンドライン引数

多くの unix コマンドでは, 実行時にコマンドライン引数を与えることによって, その動作をコントロール(切り替えたり,調整したり)できる.

たとえば,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)に代入される.

では,コマンドライン引数の簡単な利用例として, echo コマンドのクローンを作成してみよう.

echo-1.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);
}

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

$  ./echo-1  no pokemon here	# echo クローン
no pokemon here

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

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

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

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

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

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

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

コマンドの戻り値

コマンドの引数のついでに,コマンドの戻り値についても追加説明しておく. すでに知っている通り, main( ) 関数は int 型の戻り値をもつ:

int main( ... )
{
	...
	return (0);
}

そして,プログラムの終了状態(正常終了/異常終了など)を知るために, この戻り値を利用するということだった.

$ echo $?

ところで,main( ) 以外の関数の中から, プログラムを終了したいこともよくある. この場合には,exit( ) 関数を利用すればよい. この関数の引数は,main( ) の戻り値と同じである.

void exit(int 終了状態)

なお,関数 exit( ) のプロトタイプ宣言は, ヘッダファイル /usr/include/stdlib.h にある. また,このヘッダファイルでは,次のような定数マクロも定義されている:

#define  EXIT_FAILURE  1	// 失敗の場合の終了状態
#define  EXIT_SUCCESS  0	// 成功の場合の終了状態

これらのマクロを利用すれば, 定数 0 とか 1 を使うより, ソースコードの意味が明確になる.

echo-1.c を改良してみよう.

echo-2.c

#include <stdio.h>
#include <stdlib.h>		// exit, EXIT_FAILURE, EXIT_SUCCESS

// エラーメッセージを表示しプログラムを終了する関数
void fatal(char *msg)
{
	fprintf(stderr, "echo: %s\n", msg);
	exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
	int	i;

	if (argc < 2) fatal("引数が不足.何か指定して");	// エラー処理
		// 本来の echo コマンドでは,こんなエラーにはなりませんが...

	for (i = 1; i < argc; i++) {
		printf("%s ", argv[i]);
	}
	printf("\n");

	return (EXIT_SUCCESS);
}

練習問題:cat コマンドのクローン

基本プログラムを元にして,機能を追加せよ.

基本プログラム

cat.c:

#include <stdio.h>
#include <stdlib.h>

void cat(FILE *fp)
{
	char	c;

	while (1) {
		if (fscanf(fp, "%c", &c) == EOF) return;	// fp から入力
		printf("%c", c);		// 標準出力
	}
}

void fatal(char *msg, char *op)
{
	fprintf(stderr, "%s:%s\n", msg, op);
	exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
	FILE	*fp;	// 入力ファイルのポインタ
	int	i;	// 入力ファイルの番号

	for (i = 1; i < argc; i++) {
		fp = fopen(argv[i], "r");
		if (fp == NULL) fatal("fopen()失敗", argv[i]);

		cat(fp);
		fclose(fp);
	}
	return (EXIT_SUCCESS);
}

とりあえず,この基本形では, 複数のテキストファイルを連結して標準出力できる. 標準入力などの機能はない.

追加機能

なお,追加したすべての機能を使い分け可能とすること.


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