しばらく前にscanfが忌避される理由についての記事で、「fscanfとsscanfは使い方や場面を間違わなければ十分に使える」と書いた。
ところで私がテキストデータを解析して処理するソフトをC言語で書く場合を考えると、基本的に読み込み処理と解析処理は自作するか便利な外部ライブラリを使うのだが、手抜きする場合だとfgets + sscanfを使う。fscanfは使ったことがない。
これは何故だろうか? ちょっと考えてみて、どうも私の書くソフトの構造が原因ではないかと気づいた。
私が書くfgets + sscanfを使うようなソフトは、基本的に個人的に使うツールだ。これらのツールは大抵の場合Unix由来のツールを含む幾つかのアプリと組み合わせて使う。各アプリはパイプで連結するし、リダイレクトも使う。このような場合、アプリに入力データを流し込むとき、標準入力を使う方法と入力ファイルを引数指定する場合の両方に対応していると都合がよい。特に標準入力から読み込めれるようにしていないと、他のツールで処理した結果をパイプで連結して流し込むことができなくて不便だ。
例を上げてみよう。Unixのcatを真似た簡単なコンソールアプリだ。引数無しか、引数が単一のダッシュ(`-')の場合は標準入力から読む。それ以外の場合は引数をファイルとして開いて読み込む。簡単にするため、本家catと違ってオプション無し。Windowsの場合は更にテキストデータのみに対応しているとする(デフォルトではWindowsの標準出力にバイナリデータを流そうとしてもうまくいかないので)。
#include <assert.h> #include <errno.h> #include <stdio.h> #include <string.h> /* 指定されたファイルをそのまま出力する。 * 将来の拡張を考慮して、出力先も引数指定可能にしておく。 */ static void write_file(FILE *in, FILE *out) { int c; assert(in != NULL && out != NULL); while ((c = fgetc(in)) != EOF) { (void) fputc(c, out); } } int main(int argc, char *argv[]) { if (argc <= 1) { write_file(stdin, stdout); } else { FILE *in; int i; for (i = 1; i < argc; ++i) { if (strcmp(argv[i], "-") == 0) { in = stdin; } else if (errno = 0, (in = fopen(argv[i], "r")) == NULL) { perror(argv[i]); continue; } else { /*EMPTY*/ } write_file(in, stdout); if (in != stdin) { (void) fclose(in); } } } return 0; }
このような構造は、多分Unixの世界ではよくあるものだと思う。というのもUNIX V7のソースを眺めていた際に似たような書き方を見つけて、それを真似ているからだ。
私がテキスト処理用のコンソールアプリを書く場合、基本的には例に上げたソースのような構造になっている。今回は関数write_fileで入力をそのまま出力しているが、実際にはwrite_fileにあたる部分の関数で「ファイルから1行読み込み、解析し、処理する」という処理をファイルの終わりまで繰り返すことになる。この時、入力データは標準入力から読み込まれるかもしれないし、ファイルから読み込まれるかもしれないので、どちらにも対応できるように実装する必要がある。標準入力からは手動で入力できてしまうという事情もあるので、悪意のある人が入力したデータや意図せず発生した変なデータにも対応できるようにするのが望ましい。
そうなるとfscanfは使えない。fscanfの第一引数にstdinを指定することは、実質的にscanfを使うのと同じで、scanfと同様の様々な問題を抱えることになるからだ。しかしfgets + sscanfならもう少し堅牢な実装が可能だ。
もっともfgets + sscanfでは可変長のデータに対応できない部分があるので、mallocを使用してメモリがある限り確実に一行読み込む関数を用意するし、解析処理も別途用意することが多いのだが。