Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically Alias Method that uses $& Global Variable

I'm trying to alias a method that use's Ruby's special $& (returns last regex match). I can do this manually and it works:

original = String.instance_method(:sub)
String.send(:define_method, :sub) do |*args, &block|
  puts "called"
  original.bind(self).call(*args, &block)
end
"foo".sub(/f/) { $&.upcase }
  called
  # => "Foo"

However if I try to write a method that does this for me, it fails:

def programatic_alias(klass, method_name)
  original = klass.instance_method(method_name)
  klass.send(:define_method, method_name) do |*args, &block|
    puts "called"
    original.bind(self).call(*args, &block)
  end
end

programatic_alias(String, :sub)
"foo".sub(/f/) { $&.upcase }
  called
  NoMethodError: undefined method `upcase' for nil:NilClass
  called
  called
  called
    from (irb):19:in `block in irb_binding'

It looks like the global state is being affected by the scope of the programatic_alias method, but I'm not sure if that's what's going on. The questions is this: how can I programmatically alias String#sub so that it still works with Ruby's special global variables?

like image 255
Schneems Avatar asked May 28 '15 17:05

Schneems


1 Answers

As far as I know, you can't do this. The docs say

These global variables are thread-local and method-local variables.

If you dig into the ruby source, accessing $& calls last_match_getter which gets its data from rb_backref_get, which calls vm_svar_get which (skipping over a few more internal methods) gets the current control frame and reads the data from there. None of this data is exposed to the ruby api - there's no way to propagate this data from one frame to the one you want to access it in.

In your second example the call to the original method is happening inside your programatic_alias method, and so $& is being set in that scope. For the same reason

'foo'.try(:sub, /f/) {$&.upcase}

won't work either.

Your first example half works because the place where sub is called and the place where $& is referenced (inside the block) are in the same method scope (in this case the ruby top level). Change it to:

original = String.instance_method(:sub)
String.send(:define_method, :sub) do |*args, &block|
  puts "called"
  original.bind(self).call(*args, &block)
end

def x
  "foo".sub(/f/) { $&.upcase }
end

x()

and $& is no longer defined in your block (if you catch the exception thrown by x you can see that $& is being set at the top level)

like image 51
Frederick Cheung Avatar answered Oct 21 '22 12:10

Frederick Cheung