第41回
仕様設計からコーディングまで~タブ/スペース変換プログラムを作る(1)

ソースコードを書く

ここまでのことを踏まえて、ソースを書いていきましょう。タブから複数のスペースに変換する関数と、それを呼び出すmain関数を定義します。

タブ→スペース変換関数を作る

タブからスペースの変換関数を考えてみましょう。

関数名を“tb2sp”とし、書式は以下のようにします。
void  tb2sp(char *src, char *dest, int tab);

引数は以下の通りです。
char *src  : タブコードを含む変換前の文字列
char *dest : タブをスペースに置き換えた変換後の文字列
int   tab  : タブストップ幅

関数のおおよその動作は、以下のようになります。

(1) 文字列srcの終端まで以下を繰り返す

(2) 1文字を読み取る

(3) タブコードかどうかを判断

タブコードなら
  → スペースの数を求める
    destにスペースを挿入

タブコードでなければ
  → *destに*srcを1文字代入

(4) (2)に戻る

これを元に、漢字コードに対応した処理を付け足したものがリスト1です。ソース中では、タブコードは0x09、スペースは0x20と表記しています。

リスト1:タブコードをスペースに変換する関数“tb2sp”のソース
void    tb2sp(char *src, char *dest, int tab)
{
  int  i, j;    /* ループカウンタ */
  int  step;    /* 必要なスペースの数を格納 */

  for (i = 0; *src != '\0'; i++) { /* 行の終わりまで */
    if (*src == 0x09) {	/* タブコードだったとき */
      /* 漢字コードならさらに1桁進める */
      if ((i > 0) && (iskanji(*(src-1)))) {
        *dest++ = *src++;
      }
      else {    /* タブ->スペース変換 */
        step = tab - (i % tab);
        /* stepに次のタブストップまでの文字数を代入 */
        for (j = 0; j < step; j++) {
          *dest++ = 0x20;    /* destにstep回スペースを挿入 */
        }
        src++;
        i += (j - 1);
      }
    }
    else {    /* タブでなければ普通にコピーする */
      *dest++ = *src++;
    }
  }
  *dest = '\0'; /* 終端のNULL */
}

main関数を作る

main関数では、ファイルから1行ずつ読み込んでは、それを先の関数tb2spに引数として渡す……という形にします。

構造を簡潔にするため、ファイルは標準入力から読み込んで、処理結果は標準出力に送り出すことにしましょう。プログラム名は“tb2sp.exe”とし、
tb2sp <タブストップ幅>
で起動させることにしましょう。

フィルタなので、入力ファイルを“abc.c”、出力ファイルを“xyz.txt”、タブストップ幅を8とする場合は以下のようなコマンドラインになります。
tb2sp 8 < abc.c >xyz.txt

記号定数の定義

まず、タブストップ幅の最小/最大値やスペースとタブコードなどのマジックナンバーを記号定数として、リスト2のように定義しておきます。

読み込んで処理する文字列は、最大1024バイトとしておきます。これを超える文字は読み込まれません。

やや少ないような気もしますが、変換元のファイルはプログラムのソース(主にC)を前提としているため、あまり長くしてもメモリを無駄に消費するだけと考えました。もちろん、もっと長くしても構いません。

このバッファのサイズを“BUFSIZE”という名前で定義しているので、たとえば2048バイト確保したいなら
#define BUFSIZE 2048
のように書き直すだけで済みます。

リスト2:記号定数の定義
#define     BUFSIZE         1024    /* 文字列保存用バッファのサイズ */
#define	    MIN_TAB         1       /* タブストップ幅の最小値 */
#define	    MAX_TAB         16      /*    〃    最大値 */
#define	    DEFAULT_TAB     8       /*    〃    標準値 */
#define     _SPACE          0x20    /* スペースコード */
#define     _TAB            0x09    /* タブコード */

main関数の処理

main関数では、以下のような処理を行います。

(1) コマンドラインオプションをチェック
指定されていなければエラーで終了

(2) コマンドラインオプションのタブ幅(文字列)を整数(int型)に変換
最小値(4)以下なら4に、最大値(16)以上なら16に調整する

(3) 標準入力から1行読み込んで関数tb2spに渡す

(4) 変換結果を標準エラー出力へ送る

(5) whileループで(3)~(4)をファイル終端まで繰り返す

この動作をソースにしたものがリスト3です。番号の位置を手がかりに、処理をたどってみてください。

リスト3:タブコードをスペースに変換するプログラムのmain関数のソース
void    main(int argc, char *argv[])
{
  char  rbuf[BUFSIZE + 1]; /* 読み込みバッファ */
  char  wbuf[BUFSIZE + 1]; /* 書き出しバッファ */
  int   n;

  /* コマンドラインオプション(タブ幅)をチェック */
  if (argv[1] == NULL) { --------------------------------------- (1)
    puts("TABCONV : タブストップ幅(1-16)を指定してください.");
    exit(-1);
  }

  n = atoi(argv[1]);/* オプション文字を数値に直す */
  /* 最小値以上/最大値以下に調整 */
  if (n < MIN_TAB) --------------------------------------------- (2)
    n = DEFAULT_TAB;
  else if (n > MAX_TAB)
    n = MAX_TAB;

  fprintf(stderr, "タブストップ間隔 : %d\n", n);
  /* 標準入力から読み込んでtb2sp関数に渡す */
  while (fgets(rbuf, BUFSIZE, stdin) != NULL) { ---------------- (5)
    tb2sp(rbuf, wbuf, n); -------------------------------------- (3)
      fputs(wbuf, stdout); ------------------------------------- (4)
  }
  fprintf(stderr, "\a\t---- 変換終了しました!!\n");
}

エラーメッセージについて

リスト3では、エラーメッセージなどの表示にprintfではなくfprintfを使いました。

printf関数は文字列の標準出力を行います。フィルタプログラムでは、標準出力をファイルなどに切り替えて使うのが普通です。そのため、printf関数で出力するメッセージはリダイレクト先のファイルなどに送られてしまいます。

しかし、ユーザーに状況を知らせるエラーメッセージなどは、処理結果の出力がどこに切り替えられてもディスプレイに表示されなければなりません。そのためには、出力が常にディスプレイに送られる「標準エラー出力」に出力する必要があるのです。

fprintf関数は、文字列表示の動作はprintf関数と同じですが、出力先を指定できるようになっています。これを利用して、フィルタプログラムで本来の処理結果以外のメッセージを表示させる場合には、fprintf関数で標準エラー出力“stderr”に出力させるようにしておきます。


これでソースは出来上がりました。ただ、先のリスト1(tb2sp関数の定義)ではリスト2の記号定数を使っていなかったため、0x09を_TABに、0x20を_SPACEに書き換える必要があります。

それらの処理を施した最終的なソースの全体は、サンプルの“tb2sp.c”に保存してありますので、参考にしてください。

また、tb2sp.cの一部を抜き出して字下げのスペースをタブに置き換えた“test.txt”というファイルも用意しました(タブストップ幅を4桁に設定して字下げ位置を調整しています)。

tb2sp n < test.txt > xxxx.txt
のようにして、実行形式ファイルの動作を試すことができます(nにはタブストップ幅を示す任意の値が入ります)。

【サンプルファイル実行の注意点】
お使いのOSによっては日本語の部分が文字化けする可能性がありますが、再コンパイルすることで回避できます。