63389


Fluentd の [filter plugin](https://docs.fluentd.org/plugin-development/api-
plugin-filter) の中で正規表現を用いた文字列置換を行おうとしています。  
Rubyのバージョンは [td-agent v3.5.1](https://www.fluentd.org/download)パッケージに組み込まれている
2.4.9 を利用しています。

* * *
[code]

    def hash(str)
      "####{str}###"
    end
    
    patterns = [
      [Regexp.compile("(hoge)", true), lambda{|x|x}],
      [Regexp.compile("(piyo)", true), Proc.new{$1.downcase}],
      [Regexp.compile("(huga)", true), lambda{|_|"#{hash($1)}-hashed"}],
    ]
    
    text = "HoGePiYoHuGa"
    
    patterns.each do |p,r|
      text = text.gsub(p, &r)
    end
    
    p text
    
[/code]

上記のコードを、クラスを用いて実装したいと考え、次のように実装しました。  
(最終的には `Fluent::Plugin::Filter` 継承クラスで実現するこを考えています)

[code]

    class MyClass
      def self.hash(str)
        "####{str}###"
      end
    
      @@patterns = [
        [Regexp.compile("(hoge)", true), lambda{|x|x}],
        [Regexp.compile("(piyo)", true), Proc.new{$1.downcase}],
        [Regexp.compile("(huga)", true), lambda{|_|"#{self.hash($1)}-hashed"}],
      ]
    
      def filter(text)
        @@patterns.each do |p,r|
          text = text.gsub(p, &r)
        end
        return text
      end
    end
    
    my_obj = MyClass.new
    text = "HoGePiYoHuGa"
    res = my_obj.filter(text)
    
    p res
    
[/code]

このとき、 `Proc.new` や `lambda` の中で参照しようとしている `$1` が `nil`
になっているようで最初のコードと同じ結果を得られませんでした。

クラス変数を用いて実現する場合、正しくはどう記述すべきでしょうか。  
(あるいは、クラス変数を用いようとしているのは正規表現を事前コンパイルしておきたいからなのですが、他に正しいやり方があるでしょうか。)


CC BY-SA 4.0
2020-02-26T05:44:26.990
63365
2020-02-26T18:29:43.993
2808
question
ruby fluentd
Ruby の Proc.new/lambda 内 で正規表現 $1 参照


次のように書くしかないと思われます。

[code]

      @@patterns = [
        [Regexp.compile("hoge", true), lambda{|x|x}],
        [Regexp.compile("piyo", true), Proc.new{|x| x.downcase}],
        [Regexp.compile("huga", true), lambda{|x|"#{self.hash(x)}-hashed"}],
      ]
    
[/code]

まず、次のことを理解しておく必要があります。

  1. Procやlambdaは作成された場所のコンテキスト(binding)を保持しており、呼び出されたときはそのコンテキストで動作します。つまり、class直下で作成されれば、class直下で実行されたように動くと言うことです。(この動作は他言語でクロージャーと呼ばれるものです)
  2. 特殊変数は変数によってグローバルスコープのものやローカルスコープのもの、そしてスレッドローカルかどうかが違います。`$1`等はローカルスコープでスレッドローカルです(つまり、ローカル変数のようなもの)。参考:[$1 (Ruby リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/Kernel/v/1.html)

どういうことかというと、クラス変数に保存されているProc達はクラス直下のコンテキストで動作すると言うことです。このコンテキストはメイン(トップレベル)やメソッド内のローカル変数は見えません。そして、`$1`のようなローカルスコープの特殊変数も見えないと言うこと(クラス直下のコンテキストは独立して`$1`等を管理しているとも言える)です。書き換える前のコードはどちらもメイン(トップレベル)のコンテキストであるため、うまくいっていたとも言えます。

これを回避するには`$1`を使わない方法を取るしかありません。ブロック引数でマッチされた文字列全体が渡されますので、それを利用するほかありません。部分マッチを使いたくなった場合は、もう一回正規表現でマッチさせるぐらいしか方法は無いと思われます。


CC BY-SA 4.0
2020-02-26T15:06:24.207
63386
2020-02-26T15:06:24.207
7347
63365
answer


`$1` ~ `$n`(`$~`) を lambda や Proc の local variable として設定するという方法もあります。

[code]

    def filter(text)
      @@patterns.each do |p, r|
        text = text.gsub(p) {
          r.binding.tap do |b|
            b.local_variable_set(:_, $~)
            b.eval("$~=_")
          end
          r.($~)
        }
      end
      return text
    end
    
[/code]


CC BY-SA 4.0
2020-02-26T18:29:43.993
63389
2020-02-26T18:29:43.993
63365
answer