ページャ

ページフォルトの話に続くと誤解されそうだが、今回はmore、pg、lessなどファイルページャの方の話。近所の図書館でO'Reillyの「UNIX Cプログラミング(Using C on the UNIX System)」を見つけた*1。今日のネタはここから。
ファイルを特定の行数でぶった切るだけの、素のページャを書いてみる。教科書的には、ioctl経由でttyドライバを制御する例になっている。本書の発行は1991年なのだが(ターゲットは4.2BSD、4.3BSD、SVR2あたり)、ioctl直叩きはいまどきのUNIXじゃ動かないだろうから*2、termiosを使って書き直してみる。やっていることはファイルを22行表示して、端末をエコーオフ、非カノニカルモード化して入力を待つ、ということの繰り返し。main関数の先頭でモードを設定し、終了前に元の設定に戻している。

#include <stdio.h>
#include <termios.h>

#define LINES 22

void
prompt()
{
  char answer;

  printf(":");
  answer = getchar();
  putchar('\n');
}

void
more(char *file)
{
  FILE *fp;
  int line;
  char linebuf[1024];

  if ((fp = fopen(file, "r")) == NULL) {
    perror(file);
    return;
  }

  for (;;) {
    line = 1;
    while (line < LINES) {
      if (fgets(linebuf, sizeof(linebuf), fp) == NULL) {
	fclose(fp);
	prompt();
	return;
      }

      fputs(linebuf, stdout);
      line++;
    }

    prompt();
  }
}

int
main(int argc, char **argv)
{
  struct termios tio, tin;

  if (argc < 2) {
    fprintf(stderr, "Usage: %s file [file...]\n", *argv);
    return 1;
  }

  tcgetattr(0, &tio);

  tin = tio;
  tin.c_lflag &= ~ECHO;
  tin.c_lflag &= ~ICANON;
  tin.c_cc[VMIN] = 1;
  tin.c_cc[VTIME] = 0;

  tcsetattr(0, TCSANOW, &tin);

  while (--argc)
    more(*++argv);

  tcsetattr(0, TCSANOW, &tio);

  return 0;
}

Plan9にはmoreやlessはなくて、p(ageinate)。でも、pってリターンキーを押さないと次ページに進まないし、rawモードになっていない雰囲気。もう少しmoreっぽく変更しようと思ったが、そもそもPlan 9のコンソールはttyとずいぶん考え方が違う。ということで、入力待ちの処理はauth/login.cのreadln関数を参考にした。コンソール(/dev/cons)をrawモードに設定するなどの制御は/dev/consctlに書き込めばよい。ちょっと冗長になるけど、実装がブラックボックスになっている感じがなくてグッド。

#include <u.h>
#include <libc.h>
#include <bio.h>

#define LINES 22

Biobuf bout;

void
prompt(void)
{
  char ch;
  int fdin, fdout, n;

  fdin = open("/dev/cons", OREAD);
  fdout = open("/dev/cons", OWRITE);
  fprint(fdout, "%s", ":");

  for (;;) {
    n = read(fdin, &ch, 1);
    if (n < 0) {
      close(fdin);
      close(fdout);
      fprint(2, "cannot read cons");
      exits("prompt");
    }
    if (ch == 0x7f)
      exits(0);
    else {
      write(fdout, "\n", 1);
      close(fdin);
      close(fdout);
      return;
    }
  }
}

void
more(char *file)
{
  Biobuf bin;
  char *s;
  int line;
  int f, n;

  f = open(file, OREAD);
  if (f < 0) {
    fprint(2, "cannot open %r\n");
    return;
  }

  Binit(&bin, f, OREAD);

  for (;;) {
    line = 1;
    while (line < LINES) {
      s = Brdline(&bin, '\n');
      if (s == 0) {
	prompt();
	return;
      }
      Bwrite(&bout, s, Blinelen(&bin));
      Bflush(&bout);
      line++;
    }

    prompt();
  }

  Bterm(&bin);
}

void
main(int argc, char **argv)
{
  int ctl;

  if (argc < 2) {
    fprint(2, "Usage: %s file [file...]\n", *argv);
    exits("no files");
  }

  ctl = open("/dev/consctl", OWRITE);
  if (ctl < 0) {
    fprint(2, "cannot set raw mode");
    exits("prompt");
  }

  write(ctl, "rawon", 5);

  Binit(&bout, 1, OWRITE);

  while (--argc)
    more(*++argv);

  Bterm(&bout);

  write(ctl, "rawoff", 6);
  close(ctl);

  exits(0);
}

以下、余談。コンソールのデフォルトモードはcookedモードで、このモードでは、プロセスのread前に、カーソルキーが入力されたらどうするとか、Escでホールドモードとかいった特定の文字に対するルール(ラインディシプリン)にしたがって入力が解釈される。一方、入力をそのまま欲しい場合はrawモードに変更する必要がある。rawモードでは、readはバッファリングされずに一文字ずつ渡される。Plan9の場合は前述したように/dev/consctlに"rawon"を書き込んでやればよい。UNIXの場合はOS依存でICANONとかCBREAKとかRAWとか、黒魔術の世界が広がっている。

ネタ元にしておいて何だが、いまさらこの本でUNIX + Cを勉強する価値はないだろうな。「ふつうのLinuxプログラミング」とか、もっと現代的な話題を扱ったよい本がいろいろ出ているし。

UNIX Cプログラミング (NUTSSHELL HANDBOOKS)

UNIX Cプログラミング (NUTSSHELL HANDBOOKS)

*1:そういえば、O'Reilly Japanができる前はアスキーから翻訳が出ていたよなぁ。

*2:BSDのioctlとSVRのtermioの例が載っている。POSIXではtermioを拡張したtermiosが標準化されている。ちなみにtcgetattr/tcsetattrは内部でioctlを呼び出していて、Linux (glibc)はTCGETS/TCSETS、BSDはTIOCGETA/TIOCGETAを指定する。