Given the following two pieces of code:
def hello(z)
"hello".gsub(/(o)/, &z)
end
z = proc {|m| p $1}
hello(z)
# prints: nil
def hello
z = proc {|m| p $1}
"hello".gsub(/(o)/, &z)
end
hello
# prints: "o"
Why are the outputs of these two pieces of code different? Is there a way to pass a block to gsub
from outside of the method definition so that the variables $1
, $2
would be evaluated in the same way as if the block was given inside the method definition?
The two versions are different because the
$1
variable is thread-local and method-local. In the first example,$1
only exists in the block outside thehello
method. In the second example,$1
exists inside thehello
method.Note that
gsub
passes the match string into the block, soz = proc { |m| pp m }
will only work as long as your regular expression only contains the whole match. As soon as your regular expression contains anything other than the reference you want, you're out of luck.For example,
"hello".gsub(/l(o)/) { |m| m }
=>hello
, because the whole match string was passed to the block.Whereas,
"hello".gsub(/l(o)/) { |m| $1 }
=>helo
, because thel
that was matched is discarded by the block, all we are interested in is the capturedo
.My solution is to
match
the regular expression, then pass theMatchData
object into the block:A proc in ruby has lexical scope. This means that when it finds a variable that is not defined, it is resolved within the context the proc was defined, not called. This explains the behavior of your code.
You can see the block is defined before the regexp, and this can cause confusion. The problem involves a magic ruby variable, and it works quite differently than other variables. Citing @JörgWMittag
If you are really up to find exactly how the
$1
and$2
variables work, I assume the only "documentation" you will find is rubyspec, that is a spec for ruby done the hard way by the Rubinus folks. Have a nice hacking, but be prepared for the pain.You can achieve what you want with this following modification (but I bet you already know that)
I'm not aware of a way to change the scope of a proc on the fly. But would you really want to do this?
Things like
$1
,$2
acts like LOCAL VARIABLES, despite its leading$
. You can try the code below to prove this:Your problem is because the proc
z
is defined outside the methodhello
, soz
accesses the$1
in the context ofmain
, butgsub
sets the$1
in the context of methodhello
.Here is a workaround (Ruby 2). The given Proc
z
behaves exactly as the block given toString#gsub
.The background is explained in detail in this answer to the question "How to pass Regexp.last_match to a block in Ruby" (posted in 2018).