何日か前の「決意表明」の後、あらためて「ふつうの Linux プログラミング 第2版」を読み直すことにした。C 言語に対するコンプレックスを払拭するのだ。

過去にも書いたことがあるけど「ふつうの Linux プログラミング」は大変な名著である。僕は第1版も第2版も購入している (そして、どちらも途中で脱落した…)。今回は練習問題をすべてこなして、最後まで読み切ろうと思う。

最初の練習問題は「cat コマンドの再実装」だ。write(2)read(2) が紹介された章の問題なので、僕のコードも無駄にシステムコールを使っている。

全体のコードは次の通りだ。

// 本章で作った cat コマンドを改造して、コマンドライン引数でファイル名が渡されなかったら標準入力を読むようにしなさい。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static void do_cat(int fd, const char *s);
static void die(const char *s);

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

  if (argc == 1) {
    do_cat(STDIN_FILENO, "STDIN");
  } else {
    for (int i = 1; i < argc; i++) {
      int fd;
      fd = open(argv[i], O_RDONLY);
      do_cat(fd, argv[i]);
    }
  }

  exit(0);
}

#define BUFFER_SIZE 2048

static void do_cat(int fd, const char *path) {
  unsigned char buf[BUFFER_SIZE];

  if (fd < 0) die(path);
  for (;;) {
    int n = read(fd, buf, sizeof buf);
    if (n < 0) die(path);
    if (n == 0) break;
    if (write(STDOUT_FILENO, buf, n) < 0) die(path);
  }
  if (close(fd) < 0) die(path);
}

static void die(const char *s) {
  perror(s); 
  exit(1);
}

長いので関数ごとに見ていこう。

do_cat()

まずは do_cat() の実装についてだ。

static void do_cat(int fd, const char *path) {
  unsigned char buf[BUFFER_SIZE];

  if (fd < 0) die(path);
  for (;;) {
    int n = read(fd, buf, sizeof buf);
    if (n < 0) die(path);
    if (n == 0) break;
    if (write(STDOUT_FILENO, buf, n) < 0) die(path);
  }
  if (close(fd) < 0) die(path);
}

ファイルディスクリプタ fd が渡され、無限ループの中で read() し続ける。read()sizeof buf が返すバイト数だけ、読み取り結果を buf に詰める。sizeof buf は実質的には BUFFER_SIZE だが、buf の定義を読まずに書けるため sizeof buf とする方が好ましい。本書の言葉を借りると「より小さい範囲だけを読んで理解できるのが “よいコード”」ということだ。

read(2) は失敗時に -1 を返すので、そのときはエラー処理用の die() を呼ぶ。また、read() の結果はすぐに write() しているが、write(2) も失敗時には -1 を返す。その場合も、やはりエラー処理用の die() が呼ばれる。

ファイルを最後まで読み切ったときのみ、read(2)0 を返す。このときに無限ループから抜ける。

die()

次に die() を見てみよう。

static void die(const char *s) {
  perror(s); 
  exit(1);
}

perror(3) はライブラリ関数であり、変数 errno の値から適切なエラーメッセージをピックアップし、標準エラー出力に出力する。なお errno はシステムコールの実行に失敗したときに設定されるグローバル変数である。

公式解答

サポートページからたどれる解答例と比較すると、僕のコードの方が書籍内のコードを流用しているように思う。