連携処理2(テキスト処理)

文字列処理のための標準ライブラリ関数の仕組を理解しよう. そして,プログラミング言語処理系などで必要となるテキスト処理 (テキストからトークンへの分解,トークンの解釈・変換,等) の基礎部分を作成してみよう.

今回は「連携」と云う程のものではありませんが, 前回のコマンドライン引数(文字列 argv[*])との関連が深いものなので, このカテゴリに分類しました.
教科書の該当範囲:第9.2.3項,第10.2.2項,第10.2.3項

ライブラリ関数の仕様については, オンラインマニュアル(man コマンド)も活用しよう.


文字列の入出力と変換

文字列の入出力

文字入出力の標準ライブラリ関数:(stdio.h

文字列入出力の標準ライブラリ関数:(stdio.h

では,fgets()fputs() のクローンを作成し, それらの仕組みを理解しよう. (最も基本的な入出力関数 fgetc()fputc() については, そのまま利用する.)

ソース lio.c

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

/*
行文字列を入力する標準ライブラリ関数 fgets() のクローン
buf:行文字列のバッファ配列
n:バッファサイズ
fp:入力ファイルのポインタ
*/
char *myfgets(char *buf, int n, FILE *fp)
{
	int	i;	// 文字数のカウンタ
	int	c;	// 文字が入力されるよ(EOF対応のためintね)
	char	*s;	// buf 内の文字へのポインタ

	s = buf;
	n--;			// 終端記号の余地を空けておく
	for (i = 0; i < n; i++) {	// 最大 n 文字まで...
		c = fgetc(fp);		// 文字を入力
		if (c == EOF) break;

		*s = (char)c;		// 文字を buf に書き込む
		s++;

		if (c == '\n') break;	// 改行文字(書込済)で入力を終了
	}
	if (s == buf) return (NULL);	// 何も入力していない場合,関数を終了
	*s = '\0';		// 終端記号を書き込む
	return (buf);
}

/*
行文字列を出力する標準ライブラリ関数 fputs() のクローン
s:行文字列のポインタ
fp:出力ファイルのポインタ
return:正常終了では 0,異常終了では EOF
*/
int myfputs(char *s, FILE *fp)
{
	while (*s != '\0') {
		if (fputc((int)*s, fp) == EOF) return (EOF);
		s++;
	}
	return (0);
}

#define	BUFLEN	256

int main(void)
{
	char	buf[BUFLEN];

	while (1) {
//		if (fgets(buf, BUFLEN, stdin) == NULL) break;
//		fputs(buf, stdout);
//		↑ ライブラリ関数を利用する場合
//		↓ クローン関数を利用する場合
		if (myfgets(buf, BUFLEN, stdin) == NULL) break;
		myfputs(buf, stdout);
	}
	return (EXIT_SUCCESS);
}
$ ./lio < lio.c | less

ライブラリ関数とクローン関数とが同じ結果となるか確認しよう.

ソースコードのコメント記号 // を付け替えたり, 呼び出す関数の名前に my を付けたり外したりして, 実験を繰り返すんですよ.
数字列の数値化

数字列(数字の文字列)を数値化したり, その逆も必要な場合がある.

文字種を検査する標準ライブラリ関数:(ctype.h

数字列(数字の文字列)を数値化する標準ライブラリ関数:(stdlib.h

標準ライブラリ関数 atoi() のクローンを作成し, その仕組みを理解しよう.

ソース atoi.c

#include <stdio.h>
#include <stdlib.h>	// atoi(), etc.
#include <ctype.h>	// isspace(), isdigit(), etc.

/*
数字列を整数値へ変換する関数 atoi() のクローン
str:数字列
return:整数値
*/
int myatoi(char *str)
{
	int	val;	// 数値の絶対値
	int	sgn;	// 数値の正負符号 +1 or -1
	char	*s;	// 数字列内の数字へのポインタ

	// 先頭の空白を除去
	s = str;
	while (isspace(*s)) s++;
		// 反復対象の命令が1個だけなのでブロック化{ }は省略可

	// 正負符号を取得...符号なしの場合もあるよ
	sgn = +1;		// デフォルトで正としておく
	if (*s == '-') {
		sgn = -1; s++;
	} else if (*s == '+') {		// このelseの必要性...わかるかな?
		s++;
	}
		// 二択('-'/'+')と見せかけて実は三択ですよ('-'/'+'/なし)わかるかな?
		// 見かけも三択として if...else if...else と書いてもOKだけど長たらしいよね?

	// 絶対値を算出
	val = 0;
	while (isdigit(*s)) {	// 先頭(上位)から1文字(1桁)ずつ...
		val = val*10 + (int)(*s - '0');
			// 直前までの数値化結果を10倍(1桁上げ)
			// 下位の桁の数字(ASCIIコード)を数値化し加算
		s++;
	}

	// 変換結果の整数値を返す
	return (val*sgn);
}

#define	BUFLEN	256

int main(void)
{
	char	buf[BUFLEN];

	while (1) {
		printf("行文字列 > ");
		if (fgets(buf, BUFLEN, stdin) == NULL) break;
		printf("atoi()   : %d\n", atoi(buf));	// ライブラリ関数
		printf("myatoi() : %d\n", myatoi(buf));		// クローン関数
		printf("\n");
	}
	printf("終了\n");
	return (EXIT_SUCCESS);
}
$ ./atoi
行文字列 > +123
atoi()   : 123
myatoi() : 123

行文字列 > -45
atoi()   : -45
myatoi() : -45

行文字列 > 67ab8
atoi()   : 67
myatoi() : 67

...
行文字列 > [Ctrl]+[D]
終了

文字列の分解

空白区切によるトークンの抽出

行文字列からトークン (token;字句単位...要するに単語のような部分文字列)を取り出してみよう.

トークン間の区切としては,空白などの区切文字 (delimiter)を使う場合と,文字種の変化部分を使う場合とがある. まずは簡単に,空白区切によるトークン抽出関数を作成してみよう.

例:"var = var + 123 ;""var""=""var""+""123"";"

このような処理は, 関数 scanf("%s %s", ...) とか, コマンドライン引数 argv の取得などで 内部的に利用されている.
トークン抽出用の標準ライブラリ関数 strtok() もある. 調査しておこう. ただし,元の文字列の内容を書き換えてしまう等, 少々クセのあるものなので,今回は利用しないでおく. (だけど,手早く済ませたい場合には便利な手段ですよ.)

ソース tok-1.c

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>	// strcmp()

/*
文字列から単語トークンを1個だけ抽出する関数
(要するに sscanf(str, "%s", tok) の拡張版)
str : 処理対象の行文字列
tok : トークンの文字列バッファ
return : 次に処理すべき文字のアドレス
*/
char *sgetword(char *str, char *tok, int n)
{
	char	*s;	// 行文字列内の文字へのポインタ
	char	*t;	// トークン内の文字へのポインタ
	int	i;	// 文字番号のカウンタ

	s = str;	// s = &str[0]; と同意
	t = tok;	// t = &tok[0]; と同意

	while (isspace(*s)) s++;	// 先頭の空白文字を無視

	n--;		// 終端記号の分だけコピー可能な文字数を減
	for (i = 0; i < n; i++) {
		if (*s == '\0') break;
		if (isspace(*s)) break;
		*t = *s; t++;	// or ... tok[i] = *s;
		s++;
	}
	if (t == tok) return (NULL);	// トークンなしの場合,エラー
	*t = '\0';	// or ... tok[i] = '\0';	// トークンを終端		

	return (s);
}

#define	BUFLEN	256

int main(void)
{
	char	line[BUFLEN];	// 行文字列のバッファ配列
	char	tok[BUFLEN];	// トークン文字列のバッファ配列
	char	*expr;		// line 内の次のトークンへのポインタ

	while (1) {
		printf("数式 > ");
		if (fgets(line, BUFLEN, stdin) == NULL) break;

		expr = line;
		while (1) {
			expr = sgetword(expr, tok, BUFLEN);
				// tok を抽出,expr を更新
			if (expr == NULL) break;
			if (strcmp(tok, "quit") == 0) goto END;
			printf("[%s]\n", tok);
		}
	}
END:
	printf("終了\n");
	return (EXIT_SUCCESS);
}
$ ./tok-1
数式 > 123 + 45	# 空白区切の文字列なら...
[123]		# ...分解される
[+]
[45]
数式 > 123+45	# 空白区切なしだと...
[123+45]	# ...分解されない
...
数式 > quit

このようにテキストをトークンに分解できれば, 数式計算やスクリプトの処理に応用できそうだ. しかし,ユーザ目線では,空白を省略できないのは不便だ...

例えば,Cコンパイラでは, C言語のソースコード=文字列を入力としている訳ですが, 文字列内のトークン間の空白の字数は任意ですよ. 知ってた? トークン間の区切が明らかな場合には, 空白を付けても,付けなくても,いくつ付けても良い.
具体的に:int x = 0; でも int x=0 ; でもOK だよね? ただし,intx=0; だと, 最初のトークンが intx になってしまうので,当然 NG.
文字種別のトークンの抽出

区切文字だけに頼らずにトークンを抽出する方法として, 文字種の変化部分を区切とみなしてみよう. (もちろん空白区切も併用できるように.)

例:"var=var+123;""var""=""var""+""123"";"

とりあえず簡単に, 数字と数字以外とだけを区別してみると...

ソース tok-2.c:(tok-1.c を元に改造)

...

/*
文字列から数字トークンまたは数字以外トークンを1個だけ抽出する関数
数字トークン:数字だけを含む文字列
数字以外トークン:数字以外・空白以外だけを含む文字列
(sgetword() の改良版)
str : 処理対象の行文字列
tok : トークンの文字列バッファ
return : 次に処理すべき文字のアドレス
*/
// char * sgetword(...)
char *sgettok(char *str, char *tok, int n)
{
	char	*s;
	char	*t;
	int	i;
	int	d0, d1;	// 直前と現在の数字フラグ(数字ならゼロ以外)

	s = str;
	t = tok;

	while (isspace(*s)) s++;

	d0 = isdigit(*s);		// 先頭の文字の数字フラグ
	n--;
	for (i = 0; i < n; i++) {
		if (*s == '\0') break;
		if (isspace(*s)) break;

		d1 = isdigit(*s);	// 現在の文字の数字フラグを設定
		if (d1 != d0) break;	// 文字種が変わった場合,トークンを終端

		*t = *s; t++;
		s++;

		d0 = d1;		// 直前の文字の数字フラグを更新
			// 次の文字へ進むと,現在の文字が直前の文字となるので
	}
	if (t == tok) return (NULL);
	*t = '\0';

	return (s);
}

int main(void)
{
	...
	while (1) {
		...
		while (1) {
//			expr = sgetword(expr, tok, BUFLEN);
			expr = sgettok(expr, tok, BUFLEN);
			...
		}
	}
	...
}
$ ./tok-2
数式 > 123 + 45
[123]		# 空白区切は tok-1 と同様
[+]
[45]
数式 > 123+45
[123]		# 空白なしでも分解できた
[+]
[45]
...

これで数式計算にも便利に使えそうだ.


練習問題

数式計算プログラム(電卓)を作成せよ.

今回作成した関数 sgettok() と標準ライブラリ関数 atoi() 等を利用し, 整数の四則演算だけからなる数式(文字列)を標準入力すると, その式の値を算出するプログラム calc.c を作成せよ.

$ ./calc
数式 > 123
123
数式 > 123+45
168
数式 > 1*2-3
-1
数式 > 1+2*3
9	# これは,本来なら 1+(2*3)=7 のハズだが...
	# 簡単のため演算子の優先順位を考慮せず...
	# (1+2)*3=9 として良い.

数式 > -1
式が変	# 最初のトークンは数字のハズなのでエラー.
	# ...でもこれはOKとして結果を -1 にできる方が良い.

数式 > 1++2
式が変	# 演算子は1文字のハズなのでエラー.
	# ...これも結果を 3 にできる方が良い.
...
要するに,計算言語インタプリタ bc の劣化版クローンです.

簡単のための仮定・制限:

なお,数字列のバッファ配列が必要となる場合, int型の整数の表現可能範囲(±20億程度) が最大 10桁であることを利用して, バッファサイズを決め打ちしてよい.