I'd like a user to input a conditional expression interactively, something like f1 == 'b' || f2 == 'd' where f1 and f2 are ruby functions. eval condition works ok. However, I'd like to prevent errors like f3 = 'c' where f3 is not defined in ruby. Ruby silently defines a local variable. I'd like to capture an error instead.
So, is there a way to prevent ruby from creating local variables or a way to capture a creation event?
Example:
#!/usr/bin/ruby
class Conditions
def f1
"a"
end
def f2
"b"
end
def evalCondition(cond)
begin
eval cond
rescue => ex
puts ex
end
end
end
def evalCondition(cond)
conds = Conditions.new
conds.evalCondition(cond)
end
puts evalCondition("f1 == 'x'")
puts evalCondition("f1 == 'a' || f2 == 'b'")
puts evalCondition("f3 = 'y'")
I'd like to catch the last one as an error.
I found a way to check if some local variables is created, but you need bind a context in the eval
class Context
def f1
return 'a'
end
def f2
return 'b'
end
def get_binding
binding
end
end
context = Context.new
bind = context.get_binding
eval("a = f1 == 'b' || f2 == 'b'", bind)
variables = eval('self.local_variables', bind)
throw "undefined variables: " + variables.join(' ') if (variables.size)
Related
Can you please tell me why it is passing nil to check method? I am getting error main.rb:5:in `check': undefined method `%' for nil:NilClass (NoMethodError)
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 1..6
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
The reason you are getting a nil in the beginning of the array is that you are manually setting the keys in the array which creates a hole since arrays are 0 indexed in Ruby:
ary = Array.new
ary[1] = "a"
ary[2] = "b"
ary[3] = "c"
# => [nil, "a", "b", "c"]
While you could salvage this code with:
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 0..5
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
A more idiomatically correct way to write this in Ruby is:
str = 5.times.map do
n = gets.chomp.to_i
if n%3 == 0
'Pop'
elsif n.even?
'Crackle'
elsif n.odd?
'Snap'
end
end.join(" ")
puts str
for String.new and Array.new are rarely used if ever used. Use blocks instead of methods unless you're planning to reuse it later. In Ruby you can use the methods from Enumerable to both iterate over and transform arrays, hashes, ranges and other types of objects so there rarely is a reason to iterate and modify an external variable like in other languages.
With for x in 0..5 you would then have
t.rb:21:in `<main>': undefined method `my_array' for "":String (NoMethodError)
because my_array is not a method that you can send to $output.
There are many ways to do the same thing in Ruby.
my_array = []
def check(n)
case
when n % 3 == 0
'Pop'
when n.even?
'Crackle'
when n.odd?
'Snap'
else 'boom !' # not necessary in this particular case
end
end
(1..6).each do | i |
print "#{i} Enter a number > "
my_array << gets.to_i
end
puts my_array.collect { |e| check(e) }.join(' ')
Execution :
$ ruby t.rb
1 Enter a number > 44
2 Enter a number > 66
3 Enter a number > 30494
4 Enter a number > 383849
5 Enter a number > 2234
6 Enter a number > 4333
Crackle Pop Crackle Snap Crackle Snap
Don't use global variables, like $output. In the ancient (imperative programming style) languages, it was a common bug to inadvertently modify a variable accessible from anywhere.
The object oriented paradigm has been invented to isolate variables (encapsulated in an
object) to make it more difficult to modify them accidentally.
You could have use an instance variable :
#output = ''
if n%3 == 0
#output << 'Pop '
but beeing defined in the special 'main' object, it is not protected against unwanted access.
chomp is not necessary before to_i, see this post
Use iterators instead of loops. for is imperative style (C, Java), which imposes you to manage
the begin and end indexes. In an object oriented language, you simply send an iterate message to a
collection object, which takes cares of the internal details.
if and case are expressions which return the last computed value. check() returns that value.
Your my_array.each { |x| check(x) } mutates the variable $output and returns no result. In a big program, a later maintenance could insert some processing that modifies $output before you use it (bug).
The functional programming paradigm (Scala, Elixir, Kotlin) tends to use immutable variables and use functions to transform data.
The new my_array.collect { |e| check(e) }.join(' ') iterates over my_array, transforms each element calling the function check(), produces a new (immutable) collection with these transformed elements, which is then transformed by the function join() to produce the final result.
You have
for x in 1..6
my_array[x] = gets.chomp.to_i
end
Which populates the array from indexes 1 through 6, all arrays begin at index 0 so, in your method
my_array.each { |x| check(x) }
The .each method will iterate through each element of the array, starting at 0, which in this case would be nil because you never assigned a value to that index, you could change your range to
for x in 0..6
my_array[x] = gets.chomp.to_i
end
And that would work, remember to use 2 dots and not 3, as
0..6
0...6
are different, the first one is inclusive, the second one is exclusive.
You can check up more about ranges here
Trying to write a program interactively which can take inputs from command line as an expression or attributes like -
irb : 3+2
Should evaluate to => 5
Attribute
irb : abc = 1
=> 1
irb : jkl(or def) = 1
=> 1
irb : abc + def
=> 2
Also the evaluation should take place once user inputs blank line.
My efforts : I created a method attr_accessor which iterates through the array of *secret passed to it, and calls define_method on each attr, creating an instance variable getter and setter for each attribute.
Part of code working :
I made a success in evaluating the expressions and returning string values.
irb : 3+2
Should evaluate to => 5
irb : True
=> True
But still stuck with evaluation of assignment to attributes and unable to dynamically store those values in my interactive irb. Below expected results are not working :
Attribute
irb : abc = 1
=> 1
irb : def = 1
=> 1
irb : abc + def
=> 2
Note - I don't want to use "require 'irb' " or " "require 'pry'". Can this be achieved with simple ruby code ?
My Solution:
class Demo
def self.attr_accessor(*secret)
secret.each do |attr|
define_method(attr) { instance_variable_get("##{attr}") }
define_method("#{attr}=") { |val| instance_variable_set("##{attr}", val) }
end
get_binding
end
def self.method_new(input)
#object = attr_accessor(input)
end
def self.method(secret)
#object = Regexp.new(/\A[\d+\-*\/=. ]+\z/).match(secret.to_s) ? eval(secret) : "Invalid expression"
get_binding
end
def self.simple_method(secret)
#object = secret
get_binding
end
def self.get_binding
binding
end
end
user_input = ''
until user_input == 'q' do
user_input = gets.chomp
if user_input =~ /^.*=.*$/
b2 = Demo.method_new(*user_input)
puts eval('#object', b2)
elsif user_input =~ /\A[\d+\-*\/=. ]+\z/
b3 = Demo.method(user_input)
puts eval('#object', b3)
else
b4 = Demo.simple_method(user_input)
puts eval('#object', b4)
end
end
Expected Result:
irb : 3+2
#note - each result evaluated after user enters blank line
Should evaluate to => 5
Attributes ---
irb : abc = 1
#note - each result evaluated after user enters blank line
=> 1
irb : def = 1
#note - each result evaluated after user enters blank line
=> 1
irb : abc + def( or jkl)
#note - each result evaluated after user enters blank line
=> 2
Actual Result : Output is "Invalid expression" for all other inputs except expressions and simple strings.
I believe, I have partly reached to the solution of above problem. Now I can store the values of attributes in a hash map. I tried accessing these values through keys and thus can easily store and display values for assignments like:
rb : x = 1
=> 1
or
rb : y = 1
But the part of code I have written for evaluating 'x + y' is trying to partition it on operator and then accessing value of each attribute.
I am doing something wrong in line of code marked with comment #faulty. Due to which I got output like
=> x y
I am unable to access key values after partitioning.
Can someone please advise on this piece of code alone ?
Solution:
class Module
def initialize(args)
args.each do |key, value|
# the instance_variable_set method dynamically creates instance variables
# with the key as the name and value as the assigned value
instance_variable_set("##{key}",value)
# define_singleton_method creates a getter method with the same name as the
# key and inside the block you define what it returns
define_singleton_method(key){ value }
#defining the setter method
define_singleton_method("#{key}=") do |val|
instance_variable_set("##{key}", val)
end
end
end
end
class Demo
#var :bar
def self.eval_binary_expr(expr)
if expr =~ /^.*=.*$/
obj = Module.new(:name => expr)
#object1 = eval(obj.name)
get_binding
else
obj = Module.new(:name => expr)
l_operand, op, r_operand = (obj.name).partition(%r{[/*+-]}) #Faulty
if op.empty?
raise ArgumentError, "Invalid operation or no operation in expression: #{expr}"
end
case op
when '/'; then #object1 = (l_operand / r_operand); get_binding
when '*'; then #object1 = (l_operand * r_operand); get_binding
when '+'; then #object1 = (l_operand + r_operand); get_binding
when '-'; then #object1 = (l_operand - r_operand); get_binding
end
end
end
def self.method(secret)
#object2 = Regexp.new(/\A[\d+\-*\/=. ]+\z/).match(secret.to_s) ? eval(secret) : "Invalid expression"
get_binding
end
def self.new_method(secret)
#object3 = secret
get_binding
end
def self.get_binding
binding
end
end
user_input = ''
until user_input == 'q' do
user_input = gets.chomp
if user_input =~ /\A[\w+\-*\/=. ]+\z/
b2 = Demo.eval_binary_expr(user_input)
puts eval('#object1', b2)
elsif user_input =~ /\A[\d+\-*\/=. ]+\z/
b3 = Demo.method(user_input)
puts eval('#object2', b3)
else
b4 = Demo.new_method(user_input)
puts eval('#object3', b4)
end
end
I am making a Ruby REPL to be used inside an application. I made code:
a = 1
b = 2
currentScope = []
Kernel.local_variables.each do |var|
currentScope << [var,Kernel.eval(var.to_s)]
end
launchREPL(currentScope)
Inside the REPL, I can execute the following code:
#a #=>1
#a+#b #=>3
Ideally I wouldn't have to write the four lines of code before I launch the REPL, and instead I would like to run them inside the launchREPL function. However this would require access to the previous scope from inside the launchREPL function.
Test1
Most notably I tried:
launchREPL(Kernel)
When I do the following:
def launchREPL(scope)
F = 0
puts scope.local_variables # => [:F]
end
it is apparent that this method is not valid.
Test2
launchREPL(Kernel.binding)
def launchREPL(scope)
Kernel.binding.local_variables #= Error: private method 'local_variables' called for #<Binding>
end
Is there any way to do what I'm trying to do?
Edit: P.S. This is currently the code inside 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
Although what you are trying to achieve is unclear and there are definitely many ways to do it properly, every ruby method might be called with Object#send approach:
def launchREPL(scope)
scope.send :local_variables #⇒ here you go
end
a = 42
launchREPL(binding).include?(:a)
#⇒ true
Sidenote: this is how your “4 lines” are usually written in ruby:
local_variables.map { |var| [var, eval(var.to_s)] }
And this is how they should be written (note Binding#local_variable_get):
local_variables.map { |var| [var, binding.local_variable_get(var)] }
The summing up:
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
This won’t fit the comment, so I would post it as an answer.
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)
This is how your function should be written (I have just make it looking as ruby code.) The above works (currently it has no break at all, but you can see as it’s calculating 4200 infinitely.)
UPDATE: OK, so I implemented your code, but now the indentation is not showing up! Any ideas what might be wrong? I modified the code so that it would attempt to pass my original test (this is only an exercise so in real life I would not be overriding the XmlDocument class) and here is the modified code:
class XmlDocument
attr_reader :indent_depth, :bool
def initialize(bool = false, indent_depth = 0)
#indent_depth = indent_depth
#bool = bool
end
def method_missing(name, *args)
indentation = ' '*indent_depth
attrs = (args[0] || {}).map { |k, v| " #{k}='#{v}'" }.join(' ')
if block_given?
puts indent_depth
opening = "#{indentation}<#{name}#{attrs}>"
contents = yield(XmlDocument.new(true,indent_depth+1))
closing = "#{indentation}</#{name}>"
bool ? opening + "\n" + contents + "\n" + closing : opening + contents + closing
else
"#{indentation}<#{name}#{attrs}/>"
end
end
end
I'm trying to get the method to pass this test:
it "indents" do
#xml = XmlDocument.new(true)
#xml.hello do
#xml.goodbye do
#xml.come_back do
#xml.ok_fine(:be => "that_way")
end
end
end.should ==
"<hello>\n" +
" <goodbye>\n" +
" <come_back>\n" +
" <ok_fine be='that_way'/>\n" +
" </come_back>\n" +
" </goodbye>\n" +
"</hello>\n"
...but I'm unsure as to where to go with my code, below. I was thinking of using a counter to keep track of how far indented we have to go. I tried some code, but then deleted it because it was getting too messy and I have a feeling that the indentation should not be too complicated to implement.
class XmlDocument
def initialize(bool = false)
#bool = bool
end
def send(tag_name)
"<#{tag_name}/>"
end
def method_missing(meth, arg={}, &block)
arbitrary_method = meth.to_s
tag_string = ''
# 1) test for block
# 2) test for arguments
# 3) test for hash
if block_given? # check for #xml.hello do; #xml.goodbye; end
if yield.class == String # base case: #xml.hello do; "yellow"; end
"<#{arbitrary_method}>#{yield}</#{arbitrary_method}>"
else # in the block we do not have a string, we may have another method
method_missing(yield)
end
elsif arg.empty? # no arguments e.g. #xml.hello
send(arbitrary_method)
else # hash as argument e.g. #xml.hello(:name => 'dolly')
send("#{arbitrary_method} #{arg.keys[0]}='#{arg.values[0]}'")
end
end
end
Your code needs a lot of work - some pointers:
Do not override the send method!
Don't call yield over and over - you don't know what side effects you might cause, not to mention a performance hit - call it once, and remember the return value.
You might want to read up on how to write a DSL (here is a blogpost on the subject), to see how it was done correctly in other places.
Ignoring the above, I will try to answer your question regarding indentation.
In a DSL use case, you might want to use a context object which holds the indentation depth as state:
class Indented
attr_reader :indent_depth
def initialize(indent_depth = 0)
#indent_depth = indent_depth
end
def method_missing(name, *args)
indentation = ' ' * indent_depth
attrs = (args[0] || {}).map { |k, v| "#{k}='#{v}'" }.join(' ')
if block_given?
"#{indentation}<#{name} #{attrs}>\n" +
yield(Indented.new(indent_depth + 1)) +
"\n#{indentation}</#{name}>"
else
"#{indentation}<#{name} #{attrs}/>"
end
end
end
xml = Indented.new
puts xml.hello do |x|
x.goodbye do |x|
x.come_back do |x|
x.ok_fine(:be => "that_way")
end
end
end
# => <hello >
# => <goodbye >
# => <come_back >
# => <ok_fine be='that_way'/>
# => </come_back>
# => </goodbye>
# => </hello>
I have the following string and want to define a method to append the letter 'd' to the end of it. One way I can do this is with the class << str idiom.
str = 'abc'
class << str
def frob
return self + "d"
end
end
Is there a way I can do this with the following syntax?
def str.frob
return str + 'd'
end
When I try to do this I get the undefined local variable or method 'str' for "abc":String (NameError)
str = 'abc'
def str.frob
p self
return self + 'd'
end
p str.frob
# >> "abc"
# >> "abcd"
str.singleton_methods # => [:frob]
str.object_id # => 79771730
You got the error because singleton_method #frob has its own local scope,where str is not legal. Thus you got the legitimate error. That method is created on the object "abc" having object_id,79771730.