[C] 対話型コマンドでユーザ入力サポート

2017年8月28日月曜日

C editline linux readline unix

bashやsqlite3のような対話形式のコマンドでは上下の矢印キーでコマンドの履歴をたどれたり、tabキーでファイル名を補完できたりする。
自分で作った対話型コマンドでも、こんな機能が使えたらいいよね。

readline

こんな時は GNU readline を使うことが多いかもしれない。

https://cnswww.cns.cwru.edu/php/chet/readline/rltop.html

でも、GNU readlineはライセンスがLGPLではなく、GPLなので、ライブラリとしてリンクしてしまうと使用しているアプリケーション側もGPLにしないと、配布できなくなってしまう。
このため、オープンソースのツールでも、ビルド時にGNU readlineを使うかどうかがオプションになっているものもある。

Editline

なので、GPLを避けたい場合はreadline互換のEditlineを使うという選択肢がある。
こちらは元々NetBSDで開発されていたもので、BSDライセンスで配布されている。

Editline Library (libedit)
http://thrysoee.dk/editline/

ブログ執筆時点では以下が最新なので、ダウンロードして、展開する。
libedit-20170329-3.1.tar.gz

$ tar zxf libedit-20170329-3.1.tar.gz
$ cd libedit-20170329-3.1
$ ./configurre
$ make

で、srcディレクトリの中にlibedit.laができる。
make install は任意で。

Linuxのディストリビューションによってはeditlineライブラリがパッケージで提供されているものもあるので、そちらを利用してもいい。

対話型コマンド作成

では、Editlineを使って、ユーザ入力をサポートする対話型コマンドを作ってみよう。
基本的にはreadline.hをincludeして、readline関数で入力内容を読み込めばいい。
あとはreadlineがやってくれる。

入力文字列をすべて大文字に変換して出力する例はこんな感じになる。

editline_test.c
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <editline/readline.h>

int main(void) {
    if (isatty(fileno(stdin))) {
        char *line;
        while ((line = readline("> "))) {
            if (!strcmp(line, "quit")) {
                free(line);
                break;
            }

            add_history(line);

            for (size_t i = 0; i < strlen(line); i++) {
                line[i] = toupper(line[i]);
            }
            printf("%s\n", line);
            free(line);
        }
    }

    return 0;
}

~/src/libedit-20170329-3.1/ にeditlineが展開されているとすると、こんな感じでビルドする。

$ gcc -D_POSIX_C_SOURCE -std=c99 -Wall -I ~/src/libedit-20170329-3.1/src -L ~/src/libedit-20170329-3.1/src -ledit -o editline_ editline_.c

これだけで、入力時にファイル名補完やctrl-aでのカーソル移動ができるようになる。
readline関数で読み込んだバッファを使い終わったら、freeで解放することを忘れないように。

入力の履歴機能が使いたければ、add_history関数で履歴リストに追加しよう。
これによって、すべての履歴を追加したり、重複するものは追加しなかったりを自分で判断して、履歴リストに追加できる。

リダイレクトされている時は?

readlineの機能は便利だけど、パイプやリダイレクトによって標準入力が端末以外につながっている時には逆にじゃまになる場合もある。

こういう場合は[unix] 標準出力の接続先によって、動作を変えるでやったようにisatty関数で標準入力が端末かどうかを判定して、処理を変える。
上記の例では、標準入力が端末じゃない場合は何もしないで終了しているけど、実際には一気にバッファリングして処理するとかの動作にしてしまうといい。