获取上一个作用域的本地变量

3

我正在制作一个Ruby REPL,用于内部应用程序。下面是代码:

a = 1
b = 2
currentScope = []
Kernel.local_variables.each do |var|
    currentScope << [var,Kernel.eval(var.to_s)]
end
launchREPL(currentScope)

在 REPL 内部,我可以执行以下代码:

@a     #=>1
@a+@b  #=>3

理想情况下,在启动REPL之前,我不需要编写这四行代码,而是希望在launchREPL函数内部运行它们。但是,这将需要从launchREPL函数内部访问先前的作用域。

测试1

特别值得注意的是,我尝试过:

launchREPL(Kernel)

当我执行以下操作时:
def launchREPL(scope)
    F = 0
    puts scope.local_variables # => [:F]
end

显然,这种方法是无效的。

测试2

launchREPL(Kernel.binding)

def launchREPL(scope)
    Kernel.binding.local_variables #= Error: private method 'local_variables' called for #<Binding>
end

有没有办法做我正在尝试做的事情?


编辑:附上目前在launchREPL中的代码:

def launchREPL(scope=nil,winName="Ruby REPL")
    # ICM RB file Begin:
    puts "\"Starting REPL...\""
    __b = binding   #Evaluating in a binding, keeps track of local variables
    __s = ""


    ###############################################################################
    # SEND INSTANCE VARIABLES TO REPL
    ###############################################################################
    #
    #How to prepare scope
    #   currentScope = []
    #   Kernel.local_variables.each do |var|
    #       currentScope << [var,Kernel.eval(var.to_s)]
    #   end
    #   launchREPL(currentScope)

    if scope != nil
        scope.each do |varDef|
            __b.instance_variable_set "@#{varDef[0].to_s}" , varDef[1]
            __b.eval("@#{varDef[0].to_s} = __b.instance_variable_get(:@#{varDef[0].to_s})")
        end
    end

    # to get instance variables: __b.instance_variable_get(__b.instance_variables[0])
    # or better:                 __b.instance_variable_get(:@pipe1)
    #
    ###############################################################################

    bStartup = true
    while bStartup || __s != ""
        # If startup required skip evaluation step
        if !bStartup

            #Evaluate command
            begin
                __ret = __s + "\n>" + __b.eval(__s).to_s
            rescue 
                __ret = __s + "\n> Error: " + $!.to_s
            end
            puts __ret
        else
            #REPL is already running
            bStartup = false
        end

        #Read user input & print previous output
        __s = WSApplication.input_box(__ret,winName,"")
        __s == nil ? __s = "" : nil
    end
end

我已经发布了launchREPL的修正版本,作为另一个答案。我仍然不建议使用它。这个实现是有漏洞的,可能不够健壮 - Aleksei Matiushkin
2个回答

3

虽然您尝试实现什么还不清楚,且肯定有许多正确的方法可以达成目标,但是每个ruby方法都可以使用Object#send方法进行调用:

def launchREPL(scope)
  scope.send :local_variables #⇒ here you go
end

a = 42
launchREPL(binding).include?(:a)
#⇒ true
旁注:这通常是用Ruby编写“4行代码”的方式:
local_variables.map { |var| [var, eval(var.to_s)] }

以下是正确写法(注意使用Binding#local_variable_get方法):

local_variables.map { |var| [var, binding.local_variable_get(var)] }

总结如下:
def launchREPL(scope)
  vars = scope.send(:local_variables).map do |var|
           [var, scope.local_variable_get(var)]
         end
  # some other code
end
a = 42
launchREPL(binding).to_h[:a]
#⇒ 42

仍然不太清楚。scope.local_variable_get(var)(在我上一个示例的第三行)正是这样做的:它launchREPL函数内部访问局部变量 - Aleksei Matiushkin
如果我在“总结”中运行您的最后一段代码,我会得到一个错误:class = NoMethodError; message = undefined method local_variable_get' for #<Binding:0x0000002ed2b918>; U:/Macros/9_Ruby/REPL/TestREPL - Lib.rb:3:in block in launchREPL';U:/Macros/9_Ruby/REPL/TestREPL - Lib.rb:2:in map';U:/Macros/9_Ruby/REPL/TestREPL - Lib.rb:2:in launchREPL';U:/Macros/9_Ruby/REPL/TestREPL - Lib.rb:8:in `<top (required)>'。 - Sancarn
scope.send(:local_variables).map { |var| [var, eval(var.to_s)] } 必须是 1.9.3 的工作解决方案。 - Aleksei Matiushkin
puts 命令会打印出映射的值,也就是所有绑定。我不知道为什么你要在那里使用 puts。再次强调:你做错了,特别是考虑到引起的问题。这是很危险的,有很多陷阱,一般情况下不应该使用这种技术。这是完全错误的方式。此外,不清楚你想在哪里返回 a - Aleksei Matiushkin
这很好。快速问题 - 为什么更喜欢使用Binding#local_variable_get而不是eval - Ryan Lue
显示剩余8条评论

0

这段内容太长放不下在评论里,所以我会把它作为一个回答发表。

def launchREPL(scope = nil, winName = "Ruby REPL")
  puts '"Starting REPL..."'

  scope.eval('local_variables').each do |var|
    instance_variable_set "@#{var}", scope.eval(var.to_s)
  end if scope

  s = ""
  loop do
    ret = begin
            "#{s}\n> #{eval(s)}"
          rescue => e
            "#{s}\n> Error: #{e.message}"
          end
    puts ret
    # s = WSApplication.input_box(ret, winName, "")
    # break if s.empty?
    s = "100 * @a" # remove this line and uncomment 2 above
  end
end

a = 42
launchREPL(binding)

以下是你的函数应该如何编写(我只是让它看起来像Ruby代码而已)。上面的代码可以正常工作(当前未包含任何 break,但你可以看到它会无限地计算出 4200)。


谢谢Mudasobwa。这个很好用!我明白你所说的“它不够健壮”,但是,当涉及到调试代码时,拥有一个Ruby REPL总比没有一个好。不过,我需要进行一些修改,因为当前变量没有被存储。(这就是我使用绑定__b的原因) - Sancarn
同时,scope = nil 也应该被替换为 scope = binding - Sancarn
Ruby自带irb,此外还有很棒的pry。顺便说一下,当我说你做错了时,我的意思是在这个世界上已经有激光技术和我们正在征服火星的情况下,重新发明轮子是不可接受的。 - Aleksei Matiushkin
是的,可惜我没有能用的外壳...这正是为什么我要自己制作的原因。我甚至不能写到HTML上去创建一个漂亮的界面...我曾尝试设置事件,比如键盘事件,但你甚至不能做那样的事情。一旦解析器完成读取脚本,它就会退出进程。公司有一个外部应用程序,可以在UI之外运行Ruby应用程序(这将允许我使用irb),然而,我还必须花费£8666购买它...所以现在,这已经足够好了。 - Sancarn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接