I came up with a solution based on set_trace_func
. I can't resist the restrictions Charlie points out , but I believe that what I wrote should work more or less, as you described:
#!/usr/bin/ruby def hash_from_binding(bin) h = Hash.new bin.eval("local_variables").each do |i| v = bin.eval(i) v && h[i]=bin.eval(i) end bin.eval("instance_variables").each do |i| v = bin.eval(i) v && h[i]=bin.eval(i) end h end $old_binding = hash_from_binding(binding) $new_binding = hash_from_binding(binding) set_trace_func lambda {|event, file, line, id, bin, classname| $old_binding = $new_binding $new_binding = hash_from_binding(bin) diff = $new_binding.reject {|k, v| $old_binding[k] == $new_binding[k]} printf("%d:\n", line) # $old_binding.each do |k,v| # printf("%8s: %s\n", k, v) # end # $new_binding.each do |k,v| # printf("%8s: %s\n", k, v) # end diff.each do |k,v| printf("%8s: %s\n", k, v) end } a = "hello" b = "world" c = "there" d = nil e = false @a = "HELLO" @b = "WORLD" A="Hello" B="World" def foo foo_a = "foo" @foo_b = "foo" end foo
hash_from_binding(bin)
will turn the Binding
object into a Hash
. You can remove the instance_variables
part if you do not want to. You can remove the local_variables
part if you don't want to. The complication of v && h[i]=bin.eval(i)
is due to the oddness in Binding objects - even though the trace function has not yet βfigured outβ the whole content, the Binding
object passed to the trace function knows about all the variables that will be identified in the area. It is not comfortable. This at least filters out variables that are not assigned a value. Because of this, it also filters out variables assigned to nil
or false
. You may be happy with the reject
action in the trace function to filter for you.
The set_trace_func
API set_trace_func
called by the trace method for each source string that is parsed. (This can be a serious limitation to different runtimes.) Therefore, I wrote a trace function that will compare the old binding object with the new binding object and report changed variable definitions. You can also provide new variable definitions, but this will skip cases like:
a = 1 a = 2
One funny consequence is that binding messages change dramatically on function calls, as new variables come to life and old variables are removed from the environment. This may be too confusing for the output, but perhaps the event
parameter may be useful in determining whether to print new variable values. (Since calling a function can change the values ββof variables in the "return" code area, printing them seems safe.)
When the tool starts by itself, it displays the following:
$ ./binding.rb 38: 39: a: hello 40: b: world 41: c: there 42: 43: 44: @a: HELLO 45: @b: WORLD 46: 48: 48: 48: 53: @a: HELLO @b: WORLD 48: 49: 50: foo_a: foo 50: @foo_b: foo $
This is the most difficult piece of Ruby code I ran this tool on, so it can break something non-trivial.