この初版は Lua 5.0 向けに書かれています。後のバージョンにも概ね通用しますが、いくつかの違いがあります。
第4版は Lua 5.3 を対象としており、Amazon や他の書店で入手できます。
本書を購入することで、Lua プロジェクトの支援にもなります。


20.3 – キャプチャ

キャプチャ機構により、パターンは、対象文字列の一部でパターンの一部に一致する部分を抽出し、後で利用することができます。キャプチャするパターンの部分を括弧で囲むことで、キャプチャを指定します。

string.find にキャプチャを指定すると、呼び出しの結果としてキャプチャされた値が追加で返されます。この機能の典型的な使い方は、文字列を部分に分割することです。

    pair = "name = Anna"
    _, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
    print(key, value)  --> name  Anna
パターン '%a+' は、空でない文字列を指定します。パターン '%s*' は、空の可能性のあるスペースのシーケンスを指定します。したがって、上記の例では、パターン全体は、文字列の後にスペースのシーケンスが続き、その後に `=´ が続き、再びスペースと別の文字列が続きます。両方の文字列のパターンは括弧で囲まれているため、一致が発生した場合にキャプチャされます。 find 関数は、常に最初に一致が発生したインデックス(前の例ではダミー変数 _ に格納されています)を返し、次にパターンマッチング中に作成されたキャプチャを返します。以下は同様の例です。
    date = "17/7/1990"
    _, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
    print(d, m, y)  --> 17  7  1990

パターン自体でキャプチャを使用することもできます。パターンでは、d が 1 桁の数字である '%d' のような項目は、d 番目のキャプチャのコピーとのみ一致します。典型的な使用例として、文字列内で単一引用符または二重引用符で囲まれた部分文字列を見つけたいとします。 '["'].-["']' のようなパターン、つまり引用符の後に何かが続き、別の引用符が続くパターンを試すことができます。ただし、"it's all right" のような文字列では問題が発生します。この問題を解決するには、最初の引用符をキャプチャして、2 番目の引用符を指定するために使用します。

    s = [[then he said: "it's all right"!]]
    a, b, c, quotedPart = string.find(s, "([\"'])(.-)%1")
    print(quotedPart)   --> it's all right
    print(c)            --> "
最初のキャプチャは引用符文字自体であり、2 番目のキャプチャは引用符の内容( '.-' に一致する部分文字列)です。

キャプチャされた値の 3 番目の使い方は、gsub の置換文字列です。パターンと同様に、置換文字列には '%d' のような項目が含まれている場合があります。これらの項目は、置換が行われるときに対応するキャプチャに変更されます。(ちなみに、これらの変更のため、置換文字列の `%´ は "%%" としてエスケープする必要があります。)例として、次のコマンドは、文字列のすべての文字をコピーの間にハイフンを入れて複製します。

    print(string.gsub("hello Lua!", "(%a)", "%1-%1"))
      -->  h-he-el-ll-lo-o L-Lu-ua-a!
これは隣接する文字を入れ替えます。
    print(string.gsub("hello Lua", "(.)(.)", "%2%1"))
      -->  ehll ouLa

より有用な例として、次のような LaTeX スタイルで記述されたコマンドを含む文字列を取得するプリミティブなフォーマットコンバーターを作成してみましょう。

    \command{some text}
そしてそれらを XML スタイルのフォーマットに変更します。
    <command>some text</command>
この仕様では、次の行がジョブを実行します。
    s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")
たとえば、s が文字列の場合
    the \quote{task} is to \em{change} that.
その gsub 呼び出しはそれを次のように変更します。
    the <quote>task</quote> is to <em>change</em> that.
もう1つの有用な例は、文字列をトリミングする方法です。
    function trim (s)
      return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
    end
パターン形式の賢明な使用に注意してください。2 つのアンカー(`^´ と `$´)は、文字列全体を取得することを保証します。 '.-' はできるだけ展開しようとしないため、2 つのパターン '%s*' は両端のすべてのスペースに一致します。また、gsub は 2 つの値を返すため、追加の結果(カウント)を破棄するために追加の括弧を使用していることにも注意してください。

キャプチャされた値の最後の使い方は、おそらく最も強力です。置換文字列の代わりに、関数と

    function expand (s)
      s = string.gsub(s, "$(%w+)", function (n)
            return _G[n]
          end)
      return s
    end
    
    name = "Lua"; status = "great"
    print(expand("$name is $status, isn't it?"))
      --> Lua is great, isn't it?
指定された変数に文字列値があるかどうかがわからない場合は、それらの値に tostring を適用できます。
    function expand (s)
      return (string.gsub(s, "$(%w+)", function (n)
                return tostring(_G[n])
              end))
    end
    
    print(expand("print = $print; a = $a"))
      --> print = function: 0x8050ce0; a = nil

より強力な例では、loadstring を使用して、ドル記号が前に付いた角かっこで囲まれたテキストに記述した式全体を評価します。

    s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]"
    
    print((string.gsub(s, "$(%b[])", function (x)
             x = "return " .. string.sub(x, 2, -2)
             local f = loadstring(x)
             return f()
           end)))
      -->  sin(3) = 0.1411200080598672; 2^5 = 32
最初の一致は文字列 "$[math.sin(3)]" で、対応するキャプチャは "[math.sin(3)]" です。 string.sub の呼び出しは、キャプチャされた文字列から角かっこを削除するため、実行のためにロードされる文字列は "return math.sin(3)" になります。 "$[2^5]" の一致についても同様です。

多くの場合、結果の文字列に興味を持たずに、文字列を反復処理するためだけの string.gsub のようなものが欲しくなります。たとえば、次のコードを使用して、文字列の単語をテーブルに収集できます。

    words = {}
    string.gsub(s, "(%a+)", function (w)
      table.insert(words, w)
    end)
s が文字列 "hello hi, again!" であった場合、そのコマンドの後、word テーブルは次のようになります。
    {"hello", "hi", "again"}
string.gfind 関数は、そのコードを記述するためのより簡単な方法を提供します。
    words = {}
    for w in string.gfind(s, "(%a)") do
      table.insert(words, w)
    end
gfind 関数は、汎用 for ループに完全に適合します。文字列内のパターンのすべての出現を反復処理する関数を返します。

そのコードをもう少し簡略化できます。明示的なキャプチャなしでパターンを使用して gfind を呼び出すと、関数はパターン全体をキャプチャします。したがって、前の例は次のように書き直すことができます。

    words = {}
    for w in string.gfind(s, "%a") do
      table.insert(words, w)
    end

次の例では、URL エンコーディングを使用します。これは、HTTP が URL でパラメータを送信するために使用するエンコーディングです。このエンコーディングは、特殊文字(`=´、`&´、`+´ など)を "%XX" としてエンコードします。ここで、XX は文字の 16 進数表現です。次に、スペースを `+´ に変更します。たとえば、文字列 "a+b = c""a%2Bb+%3D+c" としてエンコードします。最後に、各パラメータ名とパラメータ値を間に `=´ を付けて書き込み、すべての名前に対して`&`を追加します。たとえば、値

    name = "al";  query = "a+b = c"; q="yes or no"
は次のようにエンコードされます。
    name=al&query=a%2Bb+%3D+c&q=yes+or+no
ここで、この URL をデコードして、対応する名前でインデックス付けされたテーブルに各値を格納したいとします。次の関数は、基本的なデコードを実行します。
    function unescape (s)
      s = string.gsub(s, "+", " ")
      s = string.gsub(s, "%%(%x%x)", function (h)
            return string.char(tonumber(h, 16))
          end)
      return s
    end
最初のステートメントは、文字列内の各 `+´ をスペースに変更します。2 番目の gsub は、`%´ が前に付いたすべての 2 桁の 16 進数を照合し、匿名関数を呼び出します。その関数は、16 進数を数値(基数 16 の tonumber)に変換し、対応する文字(string.char)を返します。たとえば、
    print(unescape("a%2Bb+%3D+c"))  --> a+b = c

name=value のペアをデコードするには、gfind を使用します。名前と値の両方に `&´ または `=´ を含めることができないため、パターン '[^&=]+' でそれらを照合できます。

    cgi = {}
    function decode (s)
      for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do
        name = unescape(name)
        value = unescape(value)
        cgi[name] = value
      end
    end
gfind の呼び出しは、name=value 形式のすべてのペアに一致し、各ペアについて、イテレータは対応するキャプチャ(一致文字列の括弧でマークされている)を name および value の値として返します。ループ本体は、単に両方の文字列で unescape を呼び出し、ペアを cgi テーブルに格納します。

対応するエンコーディングも簡単に記述できます。最初に、escape 関数を記述します。この関数は、すべての特殊文字を `%´ とその後に 16 進数で表した文字の ASCII コード(format オプション "%02X" は、パディングに 0 を使用して 2 桁の 16 進数を作成します)としてエンコードし、次にスペースを `+´ に変更します。

    function escape (s)
      s = string.gsub(s, "([&=+%c])", function (c)
            return string.format("%%%02X", string.byte(c))
          end)
      s = string.gsub(s, " ", "+")
      return s
    end
encode 関数は、エンコードされるテーブルを走査し、結果の文字列を作成します。
    function encode (t)
      local s = ""
      for k,v in pairs(t) do
        s = s .. "&" .. escape(k) .. "=" .. escape(v)
      end
      return string.sub(s, 2)     -- remove first `&'
    end
    
    t = {name = "al",  query = "a+b = c", q="yes or no"}
    print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al