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?
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With