プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第17回 プログラミングについて 『タブをスペースに変換する』
コラム
2022/09/16

第17回 プログラミングについて 『タブをスペースに変換する』

アポロレポート

『タブをスペースに変換する』

 

  今回はちょっと息抜きということで、いろいろと考えてみたのですが、テキストファイルのタブをスペースに変換するプログラムを作ってみることにしましょう。

 基本的にはフィルタ形式のプログラムとし、動作環境はDOSということにします。
使用しているコンパイラはMS-C/C++またはQuick Cです。話を簡単にするために入力から1文字ずつ読んで処理をする形式とします。まずはプログラムの基本形を作ります。

 #include <stdio.h>

 main()
 {
  char c;

  while(1){
   c=getchar();
   if(feof(stdin))break;

   putchar(c);
  }
 }

 これは標準入力から1文字を読んで標準出力に1文字を出力する簡単なプログラムです。入力が終了したか否かを調べるのは一般的にテキストファイルのときには、

 if((c=getchar())==EOF)...

または

 while((c=getchar())!=EOF){
  :
  :
 }

とするのですが、これは私の好みです。EOFは-1の値なのですが、心配症の私は文字の値に-1などというものがあるかも知れないと考えてしまうのです。

 このプログラムを次のように実行してみると(実行ファイルの名前は ts.exe としています)、TYPEコマンドと同じ結果となります。

 type text.dat | ts

 次に日本語文字の処理とカラム数をカウントする機能を付け加えます。日本語を分類する関数とヘッダーファイルは、MS-C/C++では _ismbblead と <mbctype.h>ですが、Quick Cでは iskanji と <jctype.h> ですので注意して下さい。 本来ならば #if ... #endif を使って移植性を高めるように書くのですが、ここでは前者のみを使用します。

 #include <stdio.h>
 #include <mbctype.h>

 #define TAB 8

 main()
 {
  char c;
  int position;

  position=0;

  while(1){
   c=getchar(); if(feof(stdin))break;

   if(_ismbblead(c)){
    putchar(c);
    c=getchar(); if(feof(stdin))break;
    putchar(c);

    position+=2;
   }
   else if(c=='\t'){
   }
   else{
    putchar(c);

    if(c=='\n')position=0;
    else    position++;
   }
  }
 }

 だんだんと長くなってきましたので、読みずらいかも知れませんが行数を詰めています。 _ismbblead 関数は2バイト文字の先頭か否かを調べる関数です。読み込んだ文字がタブの時の処理はまだ作っていないので、このプログラムを実行すると、タブが削除されますので注意して下さい。

 次にタブの部分を追加しましょう。

   else if(c=='\t'){
    int pos,n;

    pos=(position/TAB+1)*TAB;
    n =pos - position;

    while(n-- > 0)putchar(' ');

    position=pos;
   }

いたって簡単なのですが、ちょっとだけ説明をしましょう。

 pos=(position/TAB+1)*TAB;

は現在のタブによって移動する次の文字の1つ前のカラム数を求めています。いま3文字を読み込んだところでタブとなったとすると、 position の値が3になっていますので、

 position/TAB

は TAB が8と設定されていますので0となり、

 (position/TAB+1)*TAB

は (0+1)*8 となりますので結果は pos には8が代入されます。

 n=pos-position;

は、スペースを何個挿入かを求めています。 pos が8で position が3ですから n は5になります。そこで次のループでその個数分のスペースを代入するのです。

 while(n-- > 0)putchar(' ');

そして現在のカラム数を position=pos; で設定します。

 さて、基本的にはこれでも使えるのですが、言ってしまえばこの状態では急場しのぎ程度のプログラムで、標準入力や標準出力以外に直接ファイルをアクセスしたり、タブストップの位置を任意に設定できるようしないと汎用性がでてきません。そこで、このプログラムに大改修をくわえましょう。

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

 int tab_stop=8;

 char in_fname[_MAX_PATH]="標準入力";
 char out_fname[_MAX_PATH]="標準出力";

 FILE *in_fp=stdin ;
 FILE *out_fp=stdout;

 /*================================ Main routine =============================
 * Main routine.
 */
 main()
 {
  char c;
  int position;
  int ret_sts;
  int sts;

  ret_sts=0;

  position=0;

  while(1){
   if(!get_char(&c,&ret_sts))break;

   if(_ismbblead(c)){
    if(!put_char( c,&ret_sts))break;
    if(!get_char(&c,&ret_sts))break;
    if(!put_char( c,&ret_sts))break;

    position+=2;
   }
   else if(c=='\t'){
    int pos,n;

    pos=(position/tab_stop+1)*tab_stop;
    n =pos - position;

    while(n-- > 0){
     if(!put_char(' ',&ret_sts))break;
    }

    if(n>=0)break;

    position=pos;
   }
   else{
    if(!put_char(c,&ret_sts))break;

    if(c=='\n')position=0;
    else    position++;
   }
  }

  fclose( in_fp);
  fclose(out_fp);

  return ret_sts;
 }
 /*================================== get_char ===============================
 * Get character.
 */
 get_char(c,ret_sts)
 char *c;
 int *ret_sts;
 {
  *c=fgetc(in_fp);
  if(feof(in_fp))return 0;

  if(ferror(in_fp)){
   fprintf(stderr,"--- データの読み込み時にエラーが発生しました ---\n");
   perror(in_fname);

   *ret_sts=1;
   return 0;
  }

  return 1;
 }
 /*================================= put_char ================================
 * Put character.
 */
 put_char(c,ret_sts)
 char c;
 int *ret_sts;
 {
  fputc(c,out_fp);
  if(ferror(out_fp)){
   fprintf(stderr,"--- データの書き込み時にエラーが発生しました ---\n");
   perror(out_fname);

   *ret_sts=1;
   return 0;
  }

  return 1;
 }

 getchar と putchar は fgetc と fputc に改め、 入力と出力を専用に行なう関数にしました。そしてこのプログラムの終了時に呼びだし側に状態を返すための仕組も盛り込みました。正常終了したときには0を、異常終了したときには1を返すようにします。ファイル名などの変数をグローバル変数(関数外で定義)したのは、このプログラムは結果として小さいプログラムですので、極力プログラムを簡単にするためです。

 目的とする機能を満たすためのプログラムの起動時のオプションの指定方法を、

 ts [-t tab_stop] [-i input_file_name] [-o output_file_name]

 または、

 ts [/t tab_stop] [/i input_file_name] [/o output_file_name]

 ヘルプを表示するときには、

 ts -?

 または、

 ts /?

としましょう。

 -t はタブストップの位置を指定するオプションとし、tab_stop はその数です。
   このオプションが指定されなかったときにはタブストップを8とします。

 -i は入力ファイル名を指定するオプションとし、input_file_name はそのファイル名とします。 -i オプションが指定されなかったときには標準入力から読み込むこととします。

 -o は出力ファイルを指定するオプションとし、 output_file_name はそのファイル名とします。 -o オプションが指定されなかったときには標準出力に出力することとします。

 呼び出し側からオプションの受け取るために main 関数の導入部分と終了部分を次のように改修します。

 char in_fname[_MAX_PATH]="";
 char out_fname[_MAX_PATH]="";

 FILE *in_fp=NULL;
 FILE *out_fp=NULL;

 is_display_help=0;

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

   if(!analize_argment(argc,argv))return 1;

   if(is_display_help){
    display_help();
    return 0;
   }

   if(!open_io_stream())return 1;

   while(1){
    :
    :
   }

   close_io_stream();
   :
   :
 }

argc には呼びだし側つまりOS側から受け取る引数の数が渡されます。 argv には引数の文字列のポインタの配列が渡されます。変数名には特に制限はありませんが、この名称を使うのが慣習化されているみたいです。amalize_argment はOSから渡された引数を解析して各種の値の設定を行なう関数、 display_help は指定されたオプションが -? または /? のみのときにヘルプを表示する関数、 open_io_stream は入出力をオープンする関数とします。

 そこで analize_argment を次のように作成しました。

 /*============================ analize_argment ==============================
 * Analize argment.
 */
 analize_argment(argc,argv)
 int  argc;
 char *argv[];
 {
  #define KIND_NO   0x00
  #define KIND_TAB  0x01
  #define KIND_IN   0x02
  #define KIND_OUT  0x04
  #define KIND_HELP  0x08

  int narg;
  char *arg;

  int kind,specified;

  specified=0x00;
  kind   =KIND_NO;

  for(narg=1;narg<argc;narg++){
   if(argv[narg][0]=='-' || argv[narg][0]=='/'){
    if(kind!=KIND_NO)goto missing_parameter_error;

    arg=argv[narg]+1;

       if(strcmp(arg,"t")==0 || strcmp(arg,"T")==0)kind=KIND_TAB ;
    else if(strcmp(arg,"i")==0 || strcmp(arg,"I")==0)kind=KIND_IN ;
    else if(strcmp(arg,"o")==0 || strcmp(arg,"O")==0)kind=KIND_OUT ;
    else if(strcmp(arg,"?")==0           )kind=KIND_HELP;
    else{
     fprintf(stderr,
      "--- 不明なオプション( %s )があります ---\n",argv[narg]);
      return 0;
    }

    if(specified&kind){
     fprintf(stderr,
      "--- オプション( %s )が重複して指定されています ---\n",
      argv[narg]);
      return 0;
    }

    specified|=kind;

    if(kind==KIND_HELP)kind=KIND_NO;
   }
   else if(kind==KIND_NO){
    fprintf(stderr,
     "--- 不明なパラメータ( %s )があります ---\n",argv[narg]);
    return 0;
   }
   else if(kind==KIND_TAB){
    tab_stop=atoi(argv[narg]);
    if(tab_stop<=0){
     fprintf(stderr,
      "--- タブストップの値( %s )に誤りがあります ---\n",argv[narg]);
     return 0;
    }

    kind=KIND_NO;
   }
   else if(kind==KIND_IN){
    strcpy(in_fname,argv[narg]);
    kind=KIND_NO;
   }
   else if(kind==KIND_OUT){
    strcpy(out_fname,argv[narg]);
    kind=KIND_NO;
   }
  }

  if(kind!=KIND_NO)goto missing_parameter_error;

  if(specified==KIND_HELP)is_display_help=1;

  return 1;
 /*
 * " Error handling "
 */
 missing_parameter_error:
  if(kind==KIND_TAB){
   fprintf(stderr,
   "--- オプション( %s )の後にタブストップの値が指定されていません ---\n",
    argv[narg-1]);
  }
  else if(kind==KIND_IN){
   fprintf(stderr,
    "--- オプション( %s )の後に入力ファイル名が指定されていません ---\n",
    argv[narg-1]);
  }
  else if(kind==KIND_OUT){
   fprintf(stderr,
    "--- オプション( %s )の後に出力ファイル名が指定されていません ---\n",
    argv[narg-1]);
  }

  return 0;

 #undef KIND_NO
 #undef KIND_TAB
 #undef KIND_IN
 #undef KIND_OUT
 #undef KIND_HELP
 }

 さらに open_io_stream と close_io_stream は次のようにしました。

 /*============================ open_io_stream ===============================
 * Open i/o stream.
 */
 open_io_stream()
 {
  if(strcmp(in_fname,"")==0){
   strcpy(in_fname,"標準入力");
   in_fp=stdin;
  }
  else{
   in_fp=fopen(in_fname,"r");
   if(in_fp==NULL){
    fprintf(stderr,
     "--- 入力ファイルのオープン時ねエラーが発生しました ---\n");
    perror(in_fname);
    return 0;
   }
  }

  if(strcmp(out_fname,"")==0){
   strcpy(out_fname,"標準出力");
   out_fp=stdout;
  }
  else{
   out_fp=fopen(out_fname,"w");
   if(out_fp==NULL){
    fprintf(stderr,
     "--- 出力ファイルのオープン時ねエラーが発生しました ---\n");
    perror(out_fname);
    close_io_stream();
    return 0;
   }
  }

  return 1;
 }

 /*============================= close_io_stream =============================
 * Close i/o stream.
 */
 close_io_stream()
 {
  if( in_fp!=NULL && in_fp!=stdin )fclose( in_fp);
  if(out_fp!=NULL && out_fp!=stdout)fclose(out_fp);

  return 1;
 }

 display_help はここでは省力します。

 以上でほとんど満足できる程度になりました。完璧ではない理由は

 (1) -i と -o オプションで入力と出力のファイルが指定されたときに、出力ファイルが入力ファイルと同一のものであったときの処理。

 (2) -o オプションで指定された出力ファイルが既に存在していたときに無条件に上書きしてもいいのか。

などの処理がないことです。パイプやリダイレクションを使って起動したときにも同じことになりますので処理の中には含めませんでした。

 ちょっと長いのですが、最後にTSの全文を載せておきます。

 #define version_number "v1.0"

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

 int tab_stop=8;

 char in_fname[_MAX_PATH]="";
 char out_fname[_MAX_PATH]="";

 FILE *in_fp=NULL;
 FILE *out_fp=NULL;

 int  is_display_help=0;

 /*================================ Main routine =============================
 * Main routine.
 */
 main(argc,argv)
 int  argc;
 char *argv[];
 {
  char c;
  int position;
  int ret_sts;
  int sts;
 /*
 * " Analize argment "
 */
  if(!analize_argment(argc,argv))return 1;
 /*
 * " Display help "
 */
  if(is_display_help){
   display_help();
   return 0;
  }
 /*
 * " Open i/o stream "
 */
  if(!open_io_stream())return 1;
 /*
 * " Convert "
 */
  ret_sts=0;
  position=0;

  while(1){
   if(!get_char(&c,&ret_sts))break;

   if(_ismbblead(c)){
    if(!put_char( c,&ret_sts))break;
    if(!get_char(&c,&ret_sts))break;
    if(!put_char( c,&ret_sts))break;

    position+=2;
   }
   else if(c=='\t'){
    int pos,n;

    pos=(position/tab_stop+1)*tab_stop;
    n =pos - position;

    while(n-- > 0){
     if(!put_char(' ',&ret_sts))break;
    }

    if(n>=0)break;

    position=pos;
   }
   else{
    if(!put_char(c,&ret_sts))break;

    if(c=='\n')position=0;
    else    position++;
   }
  }

  close_io_stream();

  return ret_sts;
 }
 /*================================== get_char ===============================
 * Get character.
 */
 get_char(c,ret_sts)
 char *c;
 int *ret_sts;
 {
  *c=fgetc(in_fp);
  if(feof(in_fp))return 0;

  if(ferror(in_fp)){
   fprintf(stderr,"--- データの読み込み時にエラーが発生しました ---\n");
   perror(in_fname);

   *ret_sts=1;
   return 0;
  }

  return 1;
 }
 /*================================= put_char ================================
 * Put character.
 */
 put_char(c,ret_sts)
 char c;
 int *ret_sts;
 {
  fputc(c,out_fp);
  if(ferror(out_fp)){
  fprintf(stderr,"--- データの書き込み時にエラーが発生しました ---\n");
  perror(out_fname);

  *ret_sts=1;
  return 0;
  }

  return 1;
 }
 /*============================ analize_argment ==============================
 * Analize argment.
 */
 analize_argment(argc,argv)
 int  argc;
 char *argv[];
 {
 #define KIND_NO  0x00
 #define KIND_TAB 0x01
 #define KIND_IN  0x02
 #define KIND_OUT 0x04
 #define KIND_HELP 0x08

  int narg;
  char *arg;

  int kind,specified;

  specified=0x00;
  kind   =KIND_NO;

  for(narg=1;narg<argc;narg++){
   if(argv[narg][0]=='-' || argv[narg][0]=='/'){
    if(kind!=KIND_NO)goto missing_parameter_error;

    arg=argv[narg]+1;

       if(strcmp(arg,"t")==0 || strcmp(arg,"T")==0)kind=KIND_TAB ;
    else if(strcmp(arg,"i")==0 || strcmp(arg,"I")==0)kind=KIND_IN ;
    else if(strcmp(arg,"o")==0 || strcmp(arg,"O")==0)kind=KIND_OUT ;
    else if(strcmp(arg,"?")==0           )kind=KIND_HELP;
    else{
     fprintf(stderr,
      "--- 不明なオプション( %s )があります ---\n",argv[narg]);
      return 0;
    }

    if(specified&kind){
     fprintf(stderr,
      "--- オプション( %s )が重複して指定されています ---\n",
      argv[narg]);
      return 0;
    }

    specified|=kind;

    if(kind==KIND_HELP)kind=KIND_NO;
   }
   else if(kind==KIND_NO){
    fprintf(stderr,
     "--- 不明なパラメータ( %s )があります ---\n",argv[narg]);
    return 0;
   }
   else if(kind==KIND_TAB){
    tab_stop=atoi(argv[narg]);
    if(tab_stop<=0){
     fprintf(stderr,
      "--- タブストップの値( %s )に誤りがあります ---\n",argv[narg]);
     return 0;
    }

    kind=KIND_NO;
   }
   else if(kind==KIND_IN){
    strcpy(in_fname,argv[narg]);
    kind=KIND_NO;
   }
   else if(kind==KIND_OUT){
    strcpy(out_fname,argv[narg]);
    kind=KIND_NO;
   }
  }

  if(kind!=KIND_NO)goto missing_parameter_error;

  if(specified==KIND_HELP)is_display_help=1;

  return 1;
 /*
 * " Error handling "
 */
 missing_parameter_error:
  if(kind==KIND_TAB){
   fprintf(stderr,
   "--- オプション( %s )の後にタブストップの値が指定されていません ---\n",
    argv[narg-1]);
  }
  else if(kind==KIND_IN){
   fprintf(stderr,
   "--- オプション( %s )の後に入力ファイル名が指定されていません ---\n",
    argv[narg-1]);
  }
  else if(kind==KIND_OUT){
   fprintf(stderr,
   "--- オプション( %s )の後に出力ファイル名が指定されていません ---\n",
    argv[narg-1]);
  }

  return 0;

 #undef KIND_NO
 #undef KIND_TAB
 #undef KIND_IN
 #undef KIND_OUT
 #undef KIND_HELP
 }
 /*============================= close_io_stream =============================
 * Close i/o stream.
 */
 close_io_stream()
 {
  if( in_fp!=NULL && in_fp!=stdin )fclose( in_fp);
  if(out_fp!=NULL && out_fp!=stdout)fclose(out_fp);

  return 1;
 }
 /*============================ open_io_stream ===============================
 * Open i/o stream.
 */
 open_io_stream()
 {
  if(strcmp(in_fname,"")==0){
   strcpy(in_fname,"標準入力");
   in_fp=stdin;
  }
  else{
   in_fp=fopen(in_fname,"r");
   if(in_fp==NULL){
    fprintf(stderr,
     "--- 入力ファイルのオープン時ねエラーが発生しました ---\n");
    perror(in_fname);
    return 0;
   }
  }

  if(strcmp(out_fname,"")==0){
   strcpy(out_fname,"標準出力");
   out_fp=stdout;
  }
  else{
   out_fp=fopen(out_fname,"w");
   if(out_fp==NULL){
    fprintf(stderr,
     "--- 出力ファイルのオープン時ねエラーが発生しました ---\n");
    perror(out_fname);
    close_io_stream();
    return 0;
   }
  }

  return 1;
 }
 /*============================== display_help ===============================
 * Display help.
 */
 display_help()
 {
 #define p printf

 p("TS --- テキストファイルのタブをスペースへの変換(Version %s)。\n",
                             version_number);
 p("\n");
 p(" 使用方法:\n");
 p("\n");
 p("  TS [-t tab_stop] [-i input_file_name] [-o output_file_name]\n");
 p("\n");
 p("  ∴オプション名はハイフォンまたはスラッシュのいずれかを先行させ、\n");
 p("   オプション名を指定します。オプション名の文字ケースは無視されま\n");
 p("   す。\n");
 p("\n");
 p("   -t タブストップの位置を指定するオプションです。本オプションの後\n");
 p("     にタブストップの値を指定します。本オプションが指定されなかっ\n");
 p("     たときには8がディフォルトとなります。\n");
 p("\n");
 p("   -i 入力ファイルを指定するオプションです。本オプションの後に入力\n");
 p("     ファイル名を指定します。本オプションが指定されなかったときに\n");
 p("     は標準入力が入力となります。\n");
 p("\n");
 p("   -o 出力ファイルを指定するオプションです。本オプションの後に出力\n");
 p("     ファイル名を指定します。本オプションが指定されなかったときに\n");
 p("     は標準出力が出力となります。\n");
 p("\n");
 p(" 使用例:\n");
 p("\n");
 p("  TYPE SOURCE.DAT | TS -T 4 > OUTPUT.DAT\n");
 p("\n");
 p("      これはタブストップを4とし、標準入力から読み込み、標準出力\n");
 p("     に出力します。実際には SOURCE.DAT を変換し、 OUTPUT.DAT に出\n");
 p("     力しています。\n");
 p("\n");
 p("  TS -I SOURCE.DAT -O OUTPUT.DAT\n");
 p("\n");
 p("      これは入力ファイルを SOURCE.DAT、出力ファイルを OUTPUT.DAT\n");
 p("     とし、タブストップの値はディフォルトの8を使用します。\n");

  return 1;
 }

 今回はだらだらと長くなってしまいました。プログラムの例はあまりむずかしくないので簡単に理解できるかと思います。今回の例を通じて、ちょっとしたプログラム開発の流れをくみ取っていただくことを目的としました。開発の最初の段階は簡単に記述し、目的の機能が実現できたところで肉付けを行なうようにしています。多分通常のプログラムの開発でも最初からバッチリと決めることはほとんどないでしょう。おおきなプログラムを大人数で分担して開発するときには、処理の流れや各ルーチンのインターフェイスなどを細かく決定してからすすめるのですが、やはり各ルーチン単位の開発は順次肉付けを行なうことになります。

 それではまた次回。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?