メモの日々


2008年03月19日(水) [長年日記]

  • 今日の行数:2544
  • このプロジェクトはもうほとんどおしまい。

[dev][unix] The GNU Readline Libraryを使った

Readlineライブラリを使ってみたのでメモ。使用したReadlineのバージョンは5.2。

単純なサンプル

次のプログラムで、

  • 行の読み取り
  • 読み取った行の履歴を保存(C-pや↑で参照可能)
  • C-b, C-f, C-a, C-e などのお馴染の編集操作
  • ~/.inputrc ファイルによるカスタマイズ

などが可能になる。

#include <stdio.h>
#include <stdlib.h>
#include <readline/readline.h>
#include <readline/history.h>

int main() {
    char* line;
    while (line = readline("> ")) { /* 行の読み取り */
        printf("line is '%s'\n", line);
        if (strlen(line) > 0) {
            add_history(line); /* 履歴を保存 */
        }
        free(line);
    }
    printf("\nexit\n");
    return 0;
}

C-cで行のクリアがされるようにしたい

上のプログラムだと、C-cを入力するとプログラムが終了してしまった。C-c入力時は入力行を捨てて次の入力待ちになるようにしたい。

ドキュメントの 2.5 Readline Signal Handling にある機能を使えば制御できそうなんだけれど、うまくいかない。正しい使い方がよくわからん。

自分でシグナルハンドラを書いて、次のようにしたら一応実現できた。 Readlineの機能はrl_event_hook, rl_done, rl_delete_text() を利用している。

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

#include <signal.h>
#include <unistd.h>

#include <readline/readline.h>
#include <readline/history.h>

static volatile sig_atomic_t signal_handled = 0;

static void handle_signal(int signo) {
    signal_handled = 1;
}

static void set_signal_handler() {
    struct sigaction action;
    action.sa_handler = handle_signal;
    action.sa_flags = 0;
    if (sigemptyset(&action.sa_mask)) {
        perror("sigemptyset");
        exit(1);
    }
    if (sigaction(SIGINT, &action, NULL)) {
        perror("sigaction");
        exit(1);
    }
}

/* readline()内から定期的に呼ばれる関数 */
static int check_state() {
    if (signal_handled) {
        signal_handled = 0;

        /* 入力中のテキストを破棄 */
        rl_delete_text(0, rl_end);

        /* readline()をreturnさせる */
        rl_done = 1;
    }
    return 0;
}

int main() {
    set_signal_handler();

    /* check_state()が定期的に呼ばれるように設定 */
    rl_event_hook = check_state;

    char* line;
    while (line = readline("> ")) {
        printf("line is '%s'\n", line);
        if (strlen(line) > 0) {
            add_history(line);
        }
        free(line);
    }
    printf("\nexit\n");
    return 0;
}

C-cでreadline()がNULLを返すようにしたい

C-c入力時に行のクリアではなくプログラムが先に進むようにしたい場合はどうするのか。(最初のプログラムでC-cを入力すると最後の「exit」が出力されないが、これが出力されるようにしたい。)

上の2番目のプログラムのcheck_state()の内容を次のように変えると実現できた。

/* readline()内から定期的に呼ばれる関数 */
static int check_state() {
    if (signal_handled) {
        /* こうしないとこの関数がどんどん呼ばれてしまうみたい */
        rl_event_hook = 0;

        /* これを呼ばないとプログラム終了後の端末がエコーバックしない */
        /* 標準入力closeの前に実行しないとダメ */
        rl_deprep_terminal();

        /* 標準入力をcloseするとreadline()がNULLを返すみたい */
        close(STDIN_FILENO);
    }
    return 0;
}

コメントにも書いたけど、

  • rl_event_hook をクリアしないと無限ループしてしまってダメ。
  • rl_deprep_terminal() を呼ばないとC-cでのプログラム終了後に端末がエコーバックしなくなる。プログラム実行前のsttyの実行結果が「speed 38400 baud; line = 0;」だったのが、プログラム終了後は「lnext = <undef>; min = 1; time = 0; -icrnl -icanon -echo」になった。ちなみに、このように端末がおかしくなった場合は resetコマンド で元に戻せる。
  • rl_deprep_terminal()の代わりにrl_cleanup_after_signal()でもいいみたい。

標準入力をcloseするのは強引かもしれない。変数signal_handledを作っているのだから、main内のループにてこの変数が1ならbreakするようにする方がいいかも。そうすればrl_deprep_terminal()の呼び出しなども不要だ。

ReadlineはGPL

便利なReadlineだけれどライセンスはGPLなんだな。利用するプログラムをGPLコンパチにできないときは使えない。

やること

  • クレジットカード
  • ダンボール
  • 振替納税手続き
  • シュレッダー