Terminfo 文字列ケーパビリティのパラメータ展開

Terminfo (5) ケーパビリティは、 様々な端末のエスケープ・シーケンスに柔軟に対応するために、 スタック計算機を組み込んであります。 それを使って、 パラメータを加工して、 望みの位置に望みの形式でパラメータを展開していきます。 そのスタック演算とパラメータ展開をやってくれる curses ライブラリの tparm 手続きを ruby で書いてみます。

例えば、 xterm の色指定ケーパビリティでは色番号の入れ換えのために条件式を使っています。 スタック演算を組み込んで、 curses ライブラリと同じ出力になるようにしておきます。

fg = "\e[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m"
ti = Terminfo.new
(0 .. 7).each {|color| p ti.tparm(fg, color) }
#=> "\e[30m"
#   "\e[34m"
#   "\e[32m"
#   "\e[36m"
#   "\e[31m"
#   "\e[35m"
#   "\e[33m"
#   "\e[37m"

スタック演算にはグローバル変数が利用可能です。 変数名は英文字一文字で、 大文字と小文字を区別します。 それぞれ 26 個なので、 計 52 個の変数領域を用意しておきます。 パラメータはゼロを除く数字一文字で、 9 個あります。 これを tparm メソッドの可変引数とします。 初期値と省略時のデフォルト値は文字ゼロにしておきました。 演算スタック stack と出力文字列 out を準備して、 ケーパビリティ文字列の先頭から順に変換していきます。

class Terminfo
  def initialize()
    @var = Array.new(52) { '0' }
  end

#@<tparm の正規表現@>

  def tparm(str, *parm)
    (0 .. 8).each {|i| parm[i] ||= '0' }
    stack = []
    out = ''
    pos = 0
    while pos < str.size
#@<   正規表現とのマッチ@>
#@<   リテラル部を出力します@>
      if ! m
        break
#@<   パラメータの値を push します@>
#@<   定数を push します@>
#@<   変数を push します@>
#@<   変数へ pop します@>
#@<   pop した値を出力します@>
      elsif m[6]
        case m[6]
#@<     パーセント記号そのものを出力します@>
#@<     文字数を求めます@>
#@<     パラメータの先頭 2 個それぞれに 1 を加えます@>
#@<     スタック先頭 2 個の四則演算をします@>
#@<     スタック先頭 2 個のビット演算をします@>
#@<     スタック先頭 2 個の比較をします@>
#@<     スタック先頭 2 個の論理演算をします@>
#@<     スタックの先頭の論理否定とビット反転をします@>
        end
#@<   条件式の中で真でないときの読み飛ばしをします@>
#@<   条件式の終わりまで読み飛ばします@>
      else
        out << m[0]
      end
    end
    out
  end

#@<C 言語の整数の真似をします@>
#@<変数の場所を求めます@>
end

tparm はパーセント記号で書式と演算子を記述します。 それ以外はリテラル文字列です。 書式は printf と同じものを使いますが、 フラグが演算子と同じになるものがあるため、 フラグをコロンでエスケープする記法になっています。

#@<tparm の正規表現@>=
  YYTPARM = %r{
    %(?: p([1-9])
     |   \{(\d+)\}
     |   '(.)'
     |   g([A-Za-z])
     |   P([A-Za-z])
     |   ([%il\-+*\/m&|^=><AO!~?;])
     |   (t)
     |   (e)
     |   (c | (?:[:]?[-+# ])?(?:\d+(?:[.]\d+)?)?[doxXs])
     )?}mx
  YYTPARM_T = %r/%[?e;]/
  YYTPARM_E = %r/%[?;]/

スキャン・ポインタ pos 以降で正規表現にマッチする場所を探します。 pos とパーセントの間はリテラルなので、 場所を記録します。 マッチしなかったときは、 pos 以降すべてがリテラルになっています。 そして、 スキャン・ポインタを次の探索開始位置に動かしておきます。

#@<   正規表現とのマッチ@>=
      m = YYTPARM.match(str, pos)
      literal_start = pos
      literal_end = m&.begin(0) || str.size
      pos = m&.end(0) || str.size

Terminfo の tic コンパイラがすべてのエスケープ記法を文字へ変換してくれてますので、 リテラル部はケーパビリティに含まれているそのままを出力に流し込みます。

#@<   リテラル部を出力します@>=
      if literal_start < literal_end
        out << str[literal_start ... literal_end]
      end
      m or break

パラメータの値をスタックへ push します。 パラメータは数字の 1 から 9 で指定してあります。

#@<   パラメータの値を push します@>=
      elsif m[1]  # %p[1-9]
        stack.push parm[m[1].ord - '1'.ord]

定数をスタックへ push します。 定数は文字と数値の 2 種類で指定可能です。 文字はシングル・クォートで囲み、 数値は波括弧で囲みます。 文字の場合、 ord メソッドで C 言語の文字定数に対応する値へ変換しておきます。

#@<   定数を push します@>=
      elsif m[2]  # %{\d+}
        stack.push m[2].to_i
      elsif m[3]  # %'.'
        stack.push m[3].ord

変数の値をスタックへ push するか、 逆に変数へ pop します。 変数は配列の先頭側 26 個に大文字名の値を、 後ろ側 26 個に小文字の値を格納することにしておきます。

#@<   変数を push します@>=
      elsif m[4]  # %g[A-Za-z]
        stack.push @var[tparm_var_addr(m[4])]

#@<   変数へ pop します@>=
      elsif m[5]  # %P[A-Za-z]
        @var[tparm_var_addr(m[5])] = stack.pop

#@<変数の場所を求めます@>=
  def tparm_var_addr(x)
    x.ord - (x.upcase == x ? 'A'.ord : 'a'.ord)
  end

書式指定出力の対象は、 スタックの先頭です。 pop したものを出力します。 書式は printf と同じなのですが、 演算子と区別をするためにコロンでエスケープできるようになっています。 邪魔なので、 コロンを削除してから書式文字列に使います。 さらに、 パーセント 2 連でパーセントそのものを出力する記法も printf と同じです。

#@<   pop した値を出力します@>=
      elsif m[9]  # %c | %(:?[-+# ])?(\d+(\.\d+)?)?[doxXs]
        fmt = m[0]
        fmt[1, 1] = '' if ':' == fmt[1]
        if 's' == fmt[-1]
          out << fmt % [stack.pop.to_s]
        else
          out << fmt % [int(stack.pop)]
        end

#@<     パーセント記号そのものを出力します@>=
        when '%'  # %%
          out << '%'

パラメータやスタックの値を使うとき、 整数扱いするものは、 int メソッドで C 言語の整数の流儀に合わせて変換をします。 数値オブジェクトなら to_i、 文字列オブジェクトなら ord で整数にします。 それ以外のオブジェクトは例外を発生することにしておきます。

#@<C 言語の整数の真似をします@>=
  def int(x)
    case x
    when Numeric then x.to_i
    when String then x.ord
    else raise 'invalid type'
    end
  end

小文字のエルで、 スタックの先頭の値の文字数を求めます。 ANSI 端末では、 ファンクション・キーへ文字列を設定するとき等に利用しています。

#@<     文字数を求めます@>=
        when 'l'
          stack[-1] = stack[-1].to_s.size

小文字のアイで、 パラメータの先頭 2 個それぞれに 1 を加えます。 これは、 カーソル移動エスケープ・シーケンス用です。 curses では行と列はゼロから始まるものとして扱いますが、 ANSI 端末では 1 から始めます。 そのための値の修正をおこないます。

#@<     パラメータの先頭 2 個それぞれに 1 を加えます@>=
        when 'i'
          parm[0] = int(parm[0]) + 1
          parm[1] = int(parm[1]) + 1

スタック先頭 2 個の四則演算をします。 スタックには左辺・右辺の順に push していくものとします。 剰余演算子は m です。 ただし、 除算と剰余の結果は、 正同士のときは良いのですが、 それ以外の負が左辺または右辺にある場合を手抜きしており、 C 言語とは異なる値になるのを補正していません。

#@<     スタック先頭 2 個の四則演算をします@>=
        # %p1%{2}%-   で parm[0] - 2 を求めます。
        when '+'
          stack[-2] = int(stack[-2]) + int(stack[-1]); stack.pop
        when '-'
          stack[-2] = int(stack[-2]) - int(stack[-1]); stack.pop
        when '*'
          stack[-2] = int(stack[-2]) * int(stack[-1]); stack.pop
        when '/'
          stack[-2] = int(stack[-2]) / int(stack[-1]); stack.pop
        when 'm'
          stack[-2] = int(stack[-2]) % int(stack[-1]); stack.pop

スタック先頭 2 個のビット演算をします。

#@<     スタック先頭 2 個のビット演算をします@>=
        when '&'
          stack[-2] = int(stack[-2]) & int(stack[-1]); stack.pop
        when '|'
          stack[-2] = int(stack[-2]) | int(stack[-1]); stack.pop
        when '^'
          stack[-2] = int(stack[-2]) ^ int(stack[-1]); stack.pop

スタックの先頭 2 個の比較をします。 比較結果は C 言語を真似してゼロか 1 の数値で論理値を表すようにします。

#@<     スタック先頭 2 個の比較をします@>=
        when '='
          stack[-2] = int(stack[-2]) == int(stack[-1]) ? 1 : 0; stack.pop
        when '<'
          stack[-2] = int(stack[-2]) < int(stack[-1]) ? 1 : 0; stack.pop
        when '>'
          stack[-2] = int(stack[-2]) > int(stack[-1]) ? 1 : 0; stack.pop

スタックの先頭 2 個の論理演算をします。 C 言語を真似するため、 ゼロのときを負扱いします。

#@<     スタック先頭 2 個の論理演算をします@>=
        when 'A'
          stack[-2] = (int(stack[-2]) != 0 && int(stack[-1]) != 0) ? 1 : 0
          stack.pop
        when 'O'
          stack[-2] = (int(stack[-2]) != 0 || int(stack[-1]) != 0) ? 1 : 0
          stack.pop

スタックの先頭の論理否定とビット反転をします。

#@<     スタックの先頭の論理否定とビット反転をします@>=
        when '!'
          stack[-1] = int(stack[-1]) != 0 ? 0 : 1
        when '~'
          stack[-1] = ~ int(stack[-1])

条件式は、 %?%; で挟みます。 この間に elsif 連鎖のようにいくつでも条件と展開式の組を並べることができます。 条件と展開式の間に %t を挟み、 この演算子が条件分岐を生じます。 スタック先頭がゼロでないときに %t に続く展開式へ、 ゼロのときは、 同じレベルの %e%; までを読み飛ばします。 条件式は入れ子にすることができるので、 入れ子レベルをカウンタで追跡していきます。

#@<   条件式の中で真でないときの読み飛ばしをします@>=
      elsif m[7]  # %t
        if int(stack.pop) == 0
          level = 1
          while pos < str.size && level > 0
            m = YYTPARM_T.match(str, pos) or break
            pos = m.end(0)
            if '%;' == m[0]
              level -= 1
            elsif '%e' == m[0] && level == 1
              level -= 1
            else
              level += 1
            end
          end
        end

%e は、 真のときの展開の終わりを表しているので、 今度は同じレベルの %; までを読み飛ばします。 読み飛ばしのとき、 条件式の入れ子を考慮します。

#@<   条件式の終わりまで読み飛ばします@>=
      elsif m[8]  # %e
        level = 1
        while pos < str.size && level > 0
          m = YYTPARM_E.match(str, pos) or break
          pos = m.end(0)
          if '%;' == m[0]
            level -= 1
          else
            level += 1
          end
        end