第49回
ユーティリティを作る~文字列置換プログラム

main関数を作る

最後に、先に作った2つの関数を呼び出して全体の処理を行うmain関数を組み上げます。

変数の宣言

コマンドラインパラメータを取得するため、main関数は以下のように引数付きで定義します。
int main(int argc, char *argv[])

まず、読み込んだ1行を保存するための文字列を宣言します。1行が1024バイトまでという前提です。
  char buf[1024 + 1];

続いて、入力ファイルと出力ファイルのために、2個のFILE型ポインタを宣言します。
  FILE *fp1, *fp2;

パラメータのチェック

コマンドラインに指定されるパラメータは、最低でも3個必要です。そこで、引数argcの値(プログラム名を含む、コマンドライン・パラメータの数)が4未満なら、使用方法を表示して終了させます。

fputs関数は出力先を指定できるputs関数です。fprintfでは出力先を先に指定しましたが、fputs関数では出力する文字列の次に出力先を指定します。

if (argc < 4) { -------- パラメータの数が足りない場合
  fputs( --------------- エラーメッセージを表示して終了する
    "usage : strchg <old str> <new str> <org file> [<new file>]¥n",
     stderr);
  exit(1);
}

このパラメータのチェックは非常に重要です。パラメータを入力せずに実行されたプログラムのargv[1]には、予期せぬ値が入っています。もしパラメータが足りないまま処理を続けると、存在しないパラメータ文字列をポインタに受け取って処理することになり、プログラムの実行時に思わぬエラーが発生します。

このような事態を引き起こさないよう、パラメータを使用するプログラムでは、開始時に必ずパラメータのチェックを行うようにします。

ファイルのオープン

入力ファイルはargv[3]に保存されています。openfile関数を呼び出して、これを"r"モード(読み取りモード)でオープンします。オープン時にエラーが発生した場合にはopenfile関数内でエラーメッセージが表示され、NULLが帰ってきます。その場合は、そのままexit(1)としてプログラムを終了させます。

if ((fp1 = openfile(argv[3], "r")) == NULL)
  exit(1);

出力ファイルはargv[4]に保存されています。openfile関数を使い、これを"w"モード(書き込みモード)でオープンします。ただし、argv[4]は省略可能としているので、指定されていない場合があります。そこで、まずargv[4]がNULL(パラメータの終端)でないかどうかを調べ、パラメータの終端ならargv[4]の代わりに標準出力"stdout"を指定します。標準出力はリダイレクトできます。

stdoutやstderrなど入出力先を示す識別名はFILE構造体へのポインタなので、fopen関数で返されるポインタと同様に扱えます。

if (argv[4] != NULL) {
                ↓出力ファイルが指定してあればそのままオープンする
  if ((fp2 = openfile(argv[4], "w")) == NULL) 
    exit(1);
}
else ------------ 出力ファイルが未指定なら出力先を標準出力にする
  fp2 = stdout;

文字列の置換

argv[1]は旧文字列、argv[2]は新文字列へのポインタです。これと、最初に宣言したバッファを使い、whileループで1行ずつstrchg関数に送って処理します。

ファイルからの読み込みには、fgets関数を使います。書式は以下のようになっています。
char * fgets(char * buf, size_t size, FILE *fp);

fpで示すファイルから1行読み込んでbufに保存し、bufを返します。sizeで示すバイト数以上は読み込まれません。

バッファの内容を出力ファイルに書き込む処理では、fputs関数を使っています。出力先はfp2です。fputs関数は書き込みに失敗するとEOFを返すので、そのときにはエラーメッセージを表示させます。

while (fgets(buf, 1024, fp1) != NULL) {
  strchg(buf, argv[1], argv[2]);
  if (fputs(buf, fp2) == EOF) {
      fprintf(stderr, 
        "Error : can not write file ¥'%s¥'.¥n", argv[4]);
      exit(1);
  }
}

プログラムの完成

これでmain関数が出来上がります(リスト3)。

さらに、1行を保存する文字列バッファのサイズ1024と、パラメータの最小値である4という2つのマジックナンバー(直値)に記号定数を定義し、それに応じて書き直したソースがリスト4です。

#define   ARGMIN    4
#define   BUFSIZE   1024

リスト3:main関数
int main(int argc, char *argv[])
{
  char buf[1024 + 1];
  FILE *fp1, *fp2;

  /* パラメータのチェック */
  if (argc < 4) {
    fputs(
      "usage : strchg <old str> <new str> <org file> [<new file>]¥n",
      stderr);
    exit(1);
  }

  /* ファイルのオープン */
  if ((fp1 = fopen(argv[3], "r")) == NULL)
    exit(1);
  if (argv[4] != NULL) { /* 出力ファイルが指定してあるとき */
    if ((fp2 = fopen(argv[4], "w")) == NULL)
        exit(1);
  }
  else                   /* 未指定なら標準出力へ */
    fp2 = stdout;
  while (fgets(buf, BUFSIZE, fp1) != NULL) {
    strchg(buf, argv[1], argv[2]);
    if (fputs(buf, fp2) == EOF) {
        fprintf(stderr, 
          "Error : can not write file ¥'%s¥'.¥n", argv[4]);
        exit(1);
    }
  }
  return (0);
}

リスト4:記号定数を定義して書き直したstrchg.c
/* ----------------------------------------------------------- *
 *  strchg.c   文字列の置換                                    *
 *  Usage : strchg <old str> <new str> <org file> [<new file>] *
 *  org file の中の old str を new str に置き換え new fileに   *
 *  出力する。                                                 *
 * ----------------------------------------------------------- */

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

#define     ARGMIN      4        /* パラメータの最小値 */
#define     BUFSIZE     1024     /* 文字列バッファのサイズ */

/* ----------------------------------------------------------- *
 * strchg   buf 内の文字列からstr1を探し、見つかれば str2 に   *
 *          置き換える。                                       *
 * ----------------------------------------------------------- */
void strchg(char *buf, const char *str1, const char *str2)
{
  char tmp[BUFSIZE + 1];  /* 作業用文字列 */
  char *p;

  while ((p = strstr(buf, str1)) != NULL) {
      /* 見つからなくなるまで繰り返す
              pは旧文字列の先頭を指している */
    *p = '¥0'; /* 元の文字列を旧文字列の直前で区切って */
    p += strlen(str1);	/* ポインタを旧文字列の次の文字へ */
    strcpy(tmp, p);		/* 旧文字列から後を保存 */
    strcat(buf, str2);	/* 新文字列をその後につなぎ */
    strcat(buf, tmp);	/* さらに残りをつなぐ */
  }
}

/*------------------------------------------------------------ *
 * openfile     ファイルをオープンし、エラーの場合はメッセージ *
 *              を表示する                                     *
 *------------------------------------------------------------ */
FILE * openfile(char *name, char *mode)
{
  FILE *fp;

  if ((fp = fopen(name, mode)) == NULL)
      fprintf(stderr, "Can not open ¥'%s¥'.¥n", name);
  return (fp);
}

/*------------------------------------------------------------ *
 *                           main                              *
 *------------------------------------------------------------ */
int main(int argc, char *argv[])
{
  char buf[BUFSIZE + 1];
  FILE *fp1, *fp2;

  /* パラメータのチェック */
  if (argc < ARGMIN) {
    fputs(
      "usage : strchg <old str> <new str> <org file> [<new file>]¥n",
      stderr);
    exit(1);
  }

  /* ファイルのオープン */
  if ((fp1 = openfile(argv[3], "r")) == NULL)
    exit(1);
  if (argv[4] != NULL) { /* 出力ファイルが指定してあるとき */
    if ((fp2 = openfile(argv[4], "w")) == NULL)
        exit(1);
  }
  else                   /* 未指定なら標準出力へ */
    fp2 = stdout;

  while (fgets(buf, BUFSIZE, fp1) != NULL) {
    strchg(buf, argv[1], argv[2]);
    if (fputs(buf, fp2) == EOF) {
        fprintf(stderr, 
          "Error : can not write file ¥'%s¥'.¥n", argv[4]);
        exit(1);
    }
  }
  return (0);
}


文字列検索を発展させて、文字列の置換プログラムを作る過程を紹介しました。重要なことはプログラムを作ることそのものではなく、作る過程を考えることです。

プログラミングの基本事項を理解しただけでは、プログラムは作れません。

実現したいプログラムを小さな処理に分解し、さらにその小さな処理を実現するための手順を考えていくことで、頭の中にイメージしたプログラムが現実のものとなります。

目標とする最終形態に向けて既存の機能を組み合わせていく過程には、論理パズルを解くような趣(おもむき)があります。