edit-cxx11 その 2 grammar_type

sam (1) テキスト・エディタを真似して、編集コマンドの構文解析部と評価実行部を分離しています。解析部は grammar_type で、評価実行部は editor_type です。

grammar_type は、行番号を求めるアドレス式の解析と、各コマンドごとのオプション・パラメータを解析して、 command_type 構造体のメンバに書き込みます。 naddr をゼロにして、アドレス式が省略されていることを表します。addr1 と addr2 がコマンド文字の前に記述した行範囲を表す 2 つの式で、 comma は区切り文字がコンマかセミコロンかを表します。addr3 は m コマンドの移動先か t コマンドのコピー先のアドレス式です。 pattern は、 s コマンドのパターン、 param は s コマンドの置換テンプレート、 e コマンド等のファイル名に使います。a コマンド等のヒアドキュメントのテキストも param メンバに格納しますが、それは評価実行部がおこないます。

// addr_type type
enum {LINENUM, LINEOFFSET, DOT, DOLLAR, MARK, PATTERN};

// node of address expression vectors
struct addr_type {
    int type;
    int disp;    // type=LINENUM のときは絶対行番号、それ以外はオフセット
    std::wstring pattern;
};

// parsed editor command
struct command_type {
    int naddr;
    std::vector<addr_type> addr1;
    int comma;
    std::vector<addr_type> addr2;
    int command;
    std::vector<addr_type> addr3;
    std::wstring pattern;
    std::wstring param;  // aci: doc, s: replacement, efrw: file
    bool gflag;
    bool pflag;
};

行番号式は sam (1) のサブセットになっています。取り除いたのは、バッファ選択項とカラム項です。行に関係する項はすべて有効です。アドレス式は、冬に書いたものが元になっていて、基本的な点は変わっていません。

ed (1) のアドレス式の解析器
ed (1) のアドレス式のモックアップ評価器

コマンドのオプション・パラメータの解析をおこなう get メンバ関数は、テーブルを使って、コマンドごとのパラメータの取り込みをおこないます。このテーブルを使う方式は sam (1) が使っているテクニックです。テーブルの項目がゼロでないならば、パラメータをセットするという意味の条件式が並んでいます。

bool
grammar_type::get (std::wstring::const_iterator& s, command_type& ct)
{
    static const struct {
        int cmd, a1, a2, a3, re, pr, g, p;
    } tbl[21] = {
        {'a',  2, 2, 0, 0, 0, 0, 0}, // meanings of values at a1 a2 a3:
        {'c',  2, 2, 0, 0, 0, 0, 0}, //   see setaddrdefault ()
        {'d',  2, 2, 0, 0, 0, 0, 1},
        {'e',  0, 0, 0, 0, 2, 0, 0},
        {'f',  0, 0, 0, 0, 2, 0, 0},
        {'g',  1, 4, 0, 1, 0, 0, 0},
        {'i',  2, 2, 0, 0, 0, 0, 0},
        {'j',  2, 3, 0, 0, 1, 0, 1},
        {'k',  2, 2, 0, 0, 0, 0, 0},
        {'m',  2, 2, 2, 0, 0, 0, 1},
        {'n',  2, 2, 0, 0, 0, 0, 0},
        {'p',  2, 2, 0, 0, 0, 0, 0},
        {'q',  0, 0, 0, 0, 0, 0, 0},
        {'r',  1, 4, 0, 0, 2, 0, 0},
        {'s',  2, 2, 0, 1, 3, 1, 1},
        {'t',  2, 2, 2, 0, 0, 0, 1},
        {'u',  0, 0, 0, 0, 0, 0, 0},
        {'v',  1, 4, 0, 1, 0, 0, 0},
        {'w',  1, 4, 0, 0, 2, 0, 0},
        {'=',  1, 4, 0, 0, 0, 0, 1},
        {'\n', 3, 3, 0, 0, 0, 0, 0}};

    ct.pflag = false;
    ct.gflag = false;
    ct.param.clear ();
    if (! getlist (s, ct))
        return false;
    s = skipspace (s);
    int idx = -1;
    for (std::size_t i = 0; i < sizeof (tbl) / sizeof (tbl[0]); ++i)
        if (*s == tbl[i].cmd) {
            idx = i;
            break;
        }
    if (idx < 0)
        return false;
    ct.command = *s;
    if (0 == ct.naddr) {
        setaddrdefault (ct.addr1, tbl[idx].a1);
        setaddrdefault (ct.addr2, tbl[idx].a2);
    }
    else if (tbl[idx].a1 == 0)
        return false;
    if ('\n' != *s)
        s = skipspace (s + 1);
    if (tbl[idx].a3 && ! getaddr3 (s, ct))
        return false;
    if (tbl[idx].re && ! getre (s, ct))
        return false;
    if (1 == tbl[idx].pr && ! getparam (s, ct))
        return false;
    if (2 == tbl[idx].pr && ! getfile (s, ct))
        return false;
    if (3 == tbl[idx].pr && ! getreplacement (s, ct))
        return false;
    if (tbl[idx].g  && ! getgflag (s, ct))
        return false;
    if (tbl[idx].p  && ! getpflag (s, ct))
        return false;
    if ('g' == ct.command || 'v' == ct.command)
        return true;
    s = skipspace (s);
    return '\n' == *s;
}

アドレス式が省略されたときは、コマンドごとのデフォルトのアドレス式を setaddrdefault メンバ関数で設定します。1 は行番号 1、2 は現点 (.)、 3 は現点の次の行 (.+1)、 4 は末尾行 ($) です。

bool
grammar_type::setaddrdefault (std::vector<addr_type>& addr, int const kind)
{
    if (1 == kind)  // 1
        addr.push_back ({LINENUM, 1, L""});
    if (2 == kind || 3 == kind) // . || .+1
        addr.push_back ({DOT, 0, L""});
    if (3 == kind)  // .+1
        addr.push_back ({LINEOFFSET, 1, L""});
    if (4 == kind)  // $
        addr.push_back ({DOLLAR, 0, L""});
    return true;
}

アドレス式のパターンと s コマンドのパターンは // のように空にできます。空にしたときは、前の空でないパターンを流用します。 アドレス式のパターンは / と / で囲まなければなりませんが、s コマンドのパターン区切りに / 以外の他の記号を使ってもかまいません。すべての記号が使えるわけではなく、実用面を考えつつ利用できる文字に制限をかけています。 skippattern は、入れ子の括弧とバックスラッシュのクォートを追跡してパターンの終わりまでスキャン・ポインタを進めるいつものやつです。

bool
grammar_type::getre (std::wstring::const_iterator& s, command_type& ct)
{
    static const std::wstring refirst (L"%|:{}<>?/");
    s = skipspace (s);
    if (refirst.find (*s) == std::wstring::npos)
        return false;
    std::wstring::const_iterator s1 = s;
    if ((s = skippattern (s1)) == s1)
        return false;
    if (s - s1 > 2)
        lastpattern.assign (s1 + 1, s - 1);
    ct.pattern = lastpattern;
    return true;
}

s コマンドの置換テンプレートに、s/pat/new/ のような伝統的な記法に加えて、波括弧とアングルでは Perl 風の記法 s{pat}{new} も使えます。

bool
grammar_type::getreplacement (std::wstring::const_iterator& s, command_type& ct)
{
    static const std::wstring refirst (L"%|:{}<>?/");
    if ('}' == s[-1] || '>' == s[-1])
        s = skipspace (s);
    else
        --s;
    if (refirst.find (*s) == std::wstring::npos)
        return false;
    std::wstring::const_iterator s1 = s;
    if ((s = skippattern (s1)) == s1)
        return false;
    ct.param.assign (s1 + 1, s - 1);
    return true;
}

j コマンドのセパレータも s コマンドの置換テンプレートと同じ記法を使えるようにしています。省略すると空文字列を使います。

bool
grammar_type::getparam (std::wstring::const_iterator& s, command_type& ct)
{
    ct.param.clear ();
    s = skipspace (s);
    static const std::wstring paramfirst (L"%|:{}<>?/\"'");
    if (paramfirst.find (*s) == std::wstring::npos)
        return true;
    std::wstring::const_iterator s1 = s;
    if ((s = skippattern (s1)) != s1)
        ct.param.assign (s1 + 1, s - 1);
    return true;
}

ファイル名に空白を使えないようにしています。この点は将来変更するかもしれません。

bool
grammar_type::getfile (std::wstring::const_iterator& s, command_type& ct)
{
    s = skipspace (s);
    std::wstring::const_iterator s1 = s;
    while (' ' < *s && 0x7f != *s)
        ++s;
    ct.param.assign (s1, s);
    return true;
}