引き続き「ふつうの Linux プログラミング 第2版」を読んでいく。6章の練習問題に次のようなものがある。

タブ文字 ('\t') を 「\t」という2文字、改行を「'$' + 改行」の2文字として置き換えながら出力する cat コマンドを書きなさい。

次のようなコードを書いてみた。

// P.132 の練習問題

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

#define BUFFER_SIZE 2048

static void do_cat(FILE *f);

int main(int argc, char *argv[]) {
  if (argc == 1) {
    do_cat(stdin);
  } else {
    for (int i = 1; i < argc; i++) {
      FILE *f;

      f = fopen(argv[i], "r");
      if (!f) {
        perror(argv[i]);
        exit(1);
      }

      do_cat(f);
      fclose(f);
    }
  }

  exit(0);
}

static void do_cat(FILE *f) {
  int c;
  while ((c = fgetc(f)) != EOF) {
    switch (c) {
      case '\t':
        putchar('\\');
        putchar('t');
      break;
      case '\n':
        putchar('$');
        putchar(c);
      break;
      default:
        putchar(c);
    }
  }
}

前回の疑似 cat と比較して新しい要素といえば FILE 型と、それに付随する fopen(3)fclose(3) である。また putchar(3) も前回は使用していない。それぞれについて短く説明しよう。

まず fopen() はファイルパスからストリームを生成する関数である。返り値の型は FILE* になる。fclose() は逆にストリームを閉じる関数だ。fclose() を忘れないようにするため、fopen()fclose() は同じ関数内で実行することが望ましい。

また FILE 型は typedef で定義されており、ファイルディスクリプタと、システムコールを実行する頻度を減らすための I/O のバッファリング機能を持つ。

なお putchar(c)putc(c, stdout) と同じ意味である。出力先のストリームが stdout であるなら短く書けるというだけの話だ。

公式解答

サポートページからたどれる解答例を眺めてみると、僕のコードとそれほど違いがないように思える。

僕が putchar() を2回コールしていたのに対して、fputs("...", stdout) している点と、この関数コールが EOF を返すときに exit() するところが主な差分である。

本書の中に fgets(3) は第3引数のストリームから 1行読んで 第1引数のバッファ (char *buf) に格納するが fputs(3) は必ずしも「行」を出力するとは限らない、とある。したがって fputs("\\t", stdout) は改行を出力しない。なるほど。