Fork me on GitHub

2011/07/28


このエントリーをはてなブックマークに追加
最近earthquakeという、rubyで書かれていて端末上で動作するtwitterクライアントをWindowsで動かそうと色々弄ってます。作者のjugyoさんにコミットビットも貰ってwork-on-windowsブランチで作業してます。earthquake側の修正はだいたいイケてるはずなんですが、問題はreadlineという行編集ライブラリで問題が発生。
ちゃんと書くと、C言語で書かれたreadlineをwrapしているreadlineモジュールじゃなくて、rubyinstallerに標準添付されたPure RubyScriptなモジュール。中身はUNIXのコードとWindowsのコードが入り乱れていて、rbreadline.rbなんか8686行もある大作。

まぁPure RubyScriptでCの真似事をしようってんだからこうなるよね...って感じ。
ただバイト長とキャラクタ数と、文字幅の扱いが間違ってて、Windows-31JなDBCSなんかではちゃんと動かない。パッチ書いて「問題があるんだよ。気付いて!」ってつもりでpull request送ったらいきなりIssue trackerでcloseされてカチンと来たので「その態度はいかがなもんかと思う」的なコメントをした。そしたら「まずテストを書け」との事だったので書いた。

しかしながら、上記の間違いを直すとしてreadlineの正しい動きをテストするって難しい。関数単体なら出来るけどUIのテストはどの言語でも苦しむ。おまけにreadlineはreadline()メソッドを呼び出してる最中は、テスターが止まってしまう。終えるにはユーザの入力が必要。
じゃぁユーザの入力を横取りしてやんよ!って事で、rl_get_char()をぶんどる事にした。
module RbReadline
  #...

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end
こんな感じにRbReadline.rl_get_char()をMix-inで上書きしてやる。こうすれば、外からキー入力を差し替えられる。
RbReadline.rl_get_char = [""""""""""]
buf = Readline.readline("")
これさえ出来れば、文字入力させて最後のカーソル位置が正しい位置にいる事を確認出来る。カーソルの位置はGetConsoleScreenBufferInfo()で得られるのでWin32API使ってゴリゴリ取った。また、最終的な入力結果が文字化けしていない事を確認する為にReadConsoleOutputCharacter()も使った。
全体のコードは以下の様になった。
# encoding: CP932
require 'test/unit'
require 'rb-readline'
require 'Win32API'

module RbReadline
  @GetStdHandle = Win32API.new("kernel32","GetStdHandle",['L'],'L')
  @hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE)
  @GetConsoleScreenBufferInfo = Win32API.new("kernel32","GetConsoleScreenBufferInfo",['L','P'],'L')
  @ReadConsoleOutputCharacter = Win32API.new("kernel32","ReadConsoleOutputCharacter",['L','P','L','L','P'],'I')

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  def last_xy
    @last_xy
  end

  def last_xy=(val)
    @last_xy = val
  end

  def xy
    csbi = 0.chr * 24
    @GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi)
    [csbi[4,2].unpack('s*').first, csbi[6,4].unpack('s*').first]
  end

  def get_line(l)
    line = 0.chr * 80
    length = 80
    coord = l << 16
    num_read = ' ' * 4
    @ReadConsoleOutputCharacter.Call(@hConsoleHandle,line,length,coord,num_read)
    line.force_encoding("Windows-31J")
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end

class TestReadline < Test::Unit::TestCase

  def setup
    Readline::HISTORY << "世界".force_encoding("ISO-8859-1")
    Readline::HISTORY << "abc".force_encoding("ISO-8859-1")
    Readline::HISTORY << "bcdef".force_encoding("ISO-8859-1")
    RbReadline.rl_get_char = []
    RbReadline.last_xy = RbReadline.xy
    puts
  end

  def test_cursor_position_normal
    a = RbReadline.xy
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 2, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_mix
    a = RbReadline.xy
    buf = Readline.readline("$$$")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_single_width
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""b""c""d""e"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_double_width
    a = RbReadline.xy
    RbReadline.rl_get_char = [""""""""""]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 12, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_previous_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_next_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340P"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_history_include_multibyte
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 6, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_into
    a = RbReadline.xy
    RbReadline.rl_get_char = ["""""""\340K""\340K""a"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
    assert_equal "$あaいう"RbReadline.get_line(b[1])
  end
end
僕がpull requestしたpatchもtest_cursor_position_insert_intoはNG出るのでこれを直していこうと思う。
Posted at 02:42 in ソフトウェア::lang::ruby
Tagged as: rb-readline, readline, ruby
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip

2009/03/11


このエントリーをはてなブックマークに追加
readline使ってコマンドライン提供、curlで通信、結果をjson-cでパースまで作った。燃え尽きた。
追記
shebangから使えるようにした。
#!/usr/bin/dansh
以下コード
// for MSVC: cl -I.. /Tp dansh.cpp curl.lib readline.lib ..\Release\json.lib
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <io.h>
namespace json {
#include "json.h"
}
#define READLINE_STATIC
#include <readline/readline.h>
#include <curl/curl.h>
#define API_URL "http://api.dan.co.jp/perleval.cgi?c=callback&s="

typedef struct {
    char* data;     // response data from server
    size_t size;    // response size of data
} MEMFILE;

MEMFILE*
memfopen() {
    MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
    mf->data = NULL;
    mf->size = 0;
    return mf;
}

void
memfclose(MEMFILE* mf) {
    if (mf->data) free(mf->data);
    free(mf);
}

size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
    MEMFILE* mf = (MEMFILE*) stream;
    int block = size * nmemb;
    if (!mf->data)
        mf->data = (char*) malloc(block);
    else
        mf->data = (char*) realloc(mf->data, mf->size + block);
    if (mf->data) {
        memcpy(mf->data + mf->size, ptr, block);
        mf->size += block;
    }
    return block;
}

char*
memfstrdup(MEMFILE* mf) {
    char* buf = (char*) malloc(mf->size + 1);
    memcpy(buf, mf->data, mf->size);
    buf[mf->size] = 0;
    return buf;
}

char*
url_encode_alloc(const char* str, int force_encode) {
    const char* hex = "0123456789abcdef";

    char* buf = NULL;
    unsigned char* pbuf = NULL;
    int len = 0;

    if (!str) return NULL;
    len = strlen(str)*3;
    buf = (char*) malloc(len+1);
    memset(buf, 0, len+1);
    pbuf = (unsigned char*)buf;
    while(*str) {
        unsigned char c = (unsigned char)*str;
        if (c == ' ')
            *pbuf++ = '+';
        else if (c & 0x80 || force_encode) {
            *pbuf++ = '%';
            *pbuf++ = hex[c >> 4];
            *pbuf++ = hex[c & 0x0f];
        } else
            *pbuf++ = c;
        str++;
    }
    return buf;
}

void do_dan(const char* line) {
    char* source = url_encode_alloc(line, TRUE);
    char* url = (char*) malloc(strlen(API_URL) + strlen(source) + 1);
    strcpy(url, API_URL);
    strcat(url, source);

    MEMFILE* mf = memfopen();
    CURL* curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
    CURLcode res = curl_easy_perform(curl);
    free(url);
    if (res == CURLE_OK) {
        char* data = memfstrdup(mf);
        memfclose(mf);

        // remove callback function
        char* ptr;
        ptr = strrchr(data, ')');
        if (ptr) *ptr = 0;
        ptr = strchr(data, '(');
        if (ptr) *ptr++ = 0;
        else ptr = data;

        json::json_object *obj = json::json_tokener_parse(ptr);
        if (!is_error(obj)) {
            json::json_object *result = json::json_object_object_get(obj, "result");
            if (!is_error(result)) {
                printf("%s\n", json::json_object_to_json_string(result));
                json_object_put(result);
            }
        }
        free(data);
    }
    curl_easy_cleanup(curl);
}

int main(int argc, char **argv)
{
    char* line = NULL;

    //json::mc_set_debug(1);
    if (argc == 2) {
        FILE* fp = fopen(argv[1], "rb");
        if (!fp) {
            perror("can't open file");
            exit(-1);
        }
        char buf[BUFSIZ];
        while (fgets(buf, sizeof(buf), fp)) {
            if (!line) {
                if (strncmp(buf, "#!", 2))
                    line = strdup(buf);
            } else {
                line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
                strcat(line, buf);
            }
        }
        fclose(fp);
        do_dan(line);
        free(line);
    }
    if (!isatty(fileno(stdin))) {
        char buf[BUFSIZ];
        while (fgets(buf, sizeof(buf), stdin)) {
            if (!line) {
                if (strncmp(buf, "#!", 2))
                    line = strdup(buf);
            } else {
                line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
                strcat(line, buf);
            }
        }
        do_dan(line);
        free(line);
    } else {
        while (line = readline("dan> ")) {
            do_dan(line);
            free(line);
        }
    }
    return 0;
}

参考文献: 404 Blog Not Found:Ajax - perlを実行するAPI
Posted at 14:01 in ソフトウェア::lang::c
Tagged as: c, curl, json, readline
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip