改行が入力されるのを待つことなくタイプされたキーを受けとる方法

今までやりたくてもやれなかったけど、最近やり方を知ったのでここに書いておく。
日本語が不自由なのでこんなタイトルになってしまったけど、要するに「何かのキーがタイプされるまでブロックし、タイプされた瞬間にブロックを解く」ってカンジね。
Press any key to continue 的な。


答えは termios にあった。

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <sys/select.h>

int main(int argc, char *argv[])
{
  int c;
  struct termios old_tio, tio;
  fd_set fds;

  puts("Which is your sex?");
  puts("[m] Male");
  puts("[f] Female");
  printf("Your answer: ");
  fflush(stdout);  // これがないと、キータイプを待っているときに "Your answer: " が表示されない

  tcgetattr(0, &old_tio);
  memcpy(&tio, &old_tio, sizeof(struct termios));
  tio.c_lflag &= ~ICANON;  // これがミソ。c_lflag の ICANON を落としてやる
  tcsetattr(0, TCSANOW, &tio);
  
  FD_ZERO(&fds);
  FD_SET(0, &fds);
  select(1, &fds, NULL, NULL, NULL);

  c = fgetc(stdin);
  tcsetattr(0, TCSANOW, &old_tio);  // 元の状態に戻す
  putchar('\n');

  switch (c) {
    case 'M':
    case 'm':
      puts("OK, you're male");
      break;
    case 'F':
    case 'f':
      puts("OK, you're female");
      break;
    default:
      puts("???");
  }
  return 0;
}

インタラクティブに処理をするときに、選択肢から処理を選ばせる、という場面って意外とあると思う。
そんなときに、一文字入力した後にいちいちリターンキーを押すのって面倒だよね。*1
その面倒さを取り除く方法として、こういうやり方があるよ、という話でした。


参考

  • man 4 termios
  • man 3 tcsetattr

*1:そんなことを面倒くさがるのは俺だけ?rm -i とか cp -i とか mv -i とかで、y って二回タイプするのが俺は面倒なんだよ!