Ruby - Evaluate escaped interpolated sections in a string - ruby

I feel like this should be obvious but I'm having a mental block. Using Ruby, let's say that I have a string which might or might not contain some escaped string-interpolation segments, like this:
s1 = "Hello world"
=> "Hello world"
s2 = "Hello \#{foo}, hello \#{bar}"
=> "Hello \#{foo}, hello \#{bar}"
#note that the string-interpolation (#{}) segments above are escaped, so they're not interpolated
#- that is, it doesn't try to look up what foo and bar are, it's just a dumb string.
I want to "render out" these strings effectively, like so:
foo = "chunky"
=> "chunky"
bar = "bacon"
=> "bacon"
myfunc(s1)
=> "Hello world"
myfunc(s2)
=> "Hello chunky, hello bacon"
This seems like it should be fundamental but i just can't see it...
EDIT - i've found one way to do this which is to escape the quotes in the original string too, then it can be eval'd: feels kind of clunky though.
>> s1 = "\"Hello world\""
=> "\"Hello world\""
>> s2 = "\"Hello \#{foo}, hello \#{bar}\""
=> "\"Hello \#{foo}, hello \#{bar}\""
>> foo = "chunky"
=> "chunky"
>> bar = "bacon"
=> "bacon"
>> eval s1
=> "Hello world"
>> eval s2
=> "Hello chunky, hello bacon"
It also requires normal strings to have their quotes escaped too which isn't ideal.

You're "doing it wrong". There is a standard practice for this: Use sprintf instead of eval.
Rather than defining the string like this:
s2 = 'Hello #{foo}, hello #{bar}'
(Note: Using single quotation marks rather than double quotation marks is an easier/simpler way of defining the un-evaluated string.)
Instead, define it like this:
s2 = 'Hello %{foo}, hello %{bar}'
And then to interpolate the parameters, use sprintf:
sprintf(s2, { foo: "Chunky", bar: "Bacon"})
Or equivalently:
s2 % { foo: "Chunky", bar: "Bacon"}

if you take the simple answer:
eval "\"#{s2}\""
which encloses the s2 string in double quotes and then runs it through eval to evaluate the string, we can then look to proceduralise it, to make further coding look prettier, and easier to debug:
defining the objects as a def, this has a draw-back that it only evaluates global variables, as you only pass the string into the procedure:
# this one depends on global variables ...
def myfunc(x)
eval "\"#{x}\""
end
myfunc(s2)
defining the object as a lambda (Ruby Blacks and Lambdas), this has the advantage of running "in place", so local variables should be evaluated:
# should use local variables
expand= ->(x) { eval "\"#{x}\"" }
expand.(s2)
# or
expand[s2]
# or in long form:
expand.call(s2)

Related

String interpolation in a large JSON string

I am attempting to replace a variable in a large search string I will be using to call Elastic Search, but I can't seem to get the variable into the string. Below is what I have attempted, using string interpolation to try and add my_var into the search string at 3 different places.
my_var = "foo"
search_string = '{"query":{"bool":{"must":[{"nested":{"path":"some_status", "query":{"bool":{"must":{"terms":{"status.code":["ACTIVE"]}}}}}}, {"bool":{"should":[{"nested":{"path":"acc", "query":{"bool":{"must":[{"match_phrase":{"acc.name.text":"{#my_var}"}}]}}}}, {"bool":{"must":[{"match_phrase":{"name.stuff":"{#my_var}"}}]}}, {"nested":{"path":"trade_names", "query":{"bool":{"must":[{"match_phrase":{"some_names.some_name.stuff":"{#my_var}"}}]}}}}]}}]}}}'
JSON.parse(search_string)
What am I doing wrong here?
First of all, it's #{my_var}, not {#my_var}.
Beside that typo, string interpolation needs "...", it is disabled in '...':
foo = 123
str = "foo = #{foo}" # <- turns #{foo} into 123
#=> "foo = 123"
str = 'foo = #{foo}' # <- keeps #{foo} literally
#=> "foo = \#{foo}"
Since your string contains many literal double quote characters, using "..." would add a lot of escape characters.
Apart from quotes, you could use the percent string %Q(...):
str = %Q({"foo":"#{foo}"})
#=> "{\"foo\":\"123\"}"
or a heredoc: (using JSON as the delimiter might enable syntax highlighting in your editor)
str = <<-JSON.chomp
{"foo":"#{foo}"}
JSON
#=> "{\"foo\":\"123\"}"
Another option is to construct a Ruby hash and turn that into JSON:
require 'json'
hash = { 'foo' => foo }
str = hash.to_json
#=> "{\"foo\":123}"
The JSON library also handles escaping for you.

Ruby single and double quotes

I've recently been coding in Ruby and have come from Python, where single and double quotes made no difference to how the code worked as far as I know.
I moved to Ruby to see how it worked, and to investigate the similarities between Ruby and Python.
I was using single-quoted strings once and noticed this:
hello = 'hello'
x = '#{hello} world!'
puts x
It returned '#{hello} world!' rather than 'hello world!'.
After noticing this I tried double quotes and the problem was fixed. Now I'm not sure why that is.
Do single and double quotes change this or is it because of my editor (Sublime text 3)? I'm also using Ruby version 2.0 if it works differently in previous versions.
In Ruby, double quotes are interpolated, meaning the code in #{} is evaluated as Ruby. Single quotes are treated as literals (meaning the code isn't evaluated).
var = "hello"
"#{var} world" #=> "hello world"
'#{var} world' #=> "#{var} world"
For some extra-special magic, Ruby also offers another way to create strings:
%Q() # behaves like double quotes
%q() # behaves like single quotes
For example:
%Q(#{var} world) #=> "hello world"
%q(#{var} world) #=> "#{var} world"
You should read the Literals section of the official Ruby documentation.
It is very concise, so you need to read carefully. But it explains the difference between double-quoted and single-quoted strings, and how they are equivalent to %Q/.../ and %q/.../ respectively.
If you enclose Ruby string in single qoutes, you can't use interpolation. That's how Ruby works.
Single-quoted strings don't process escape sequence \ and they don't do string interpolation.
For a better understanding, take a look at String concatenation vs. interpolation
To answer your question, you have to use "" when you want to do string interpolation:
name = 'world'
puts "Hello #{name}" # => "Hello world"
Using escape sequence:
puts 'Hello\nworld' # => "Hello\nworld"
puts "Hello\nworld" # => "Hello
world"
Ruby supports single-quoted string, for many uses like as follow:
>> 'foo'
=> "foo"
>> 'foo' + 'bar'
=> "foobar"
In above example, those two types of strings are identical. We can use double quote in place of single quote and we will get same output like above example.
As you face problem, while using interpolation in single quoted string because Ruby do not interpolate into single-quoted string. I am taking one example for more understanding:
>> '#{foo} bar'
=> "\#{foo} bar"
Here you can see that return values using double-quoted strings, which requires backslash to escape special characters such as #.
Single quoted string often useful because they are truly literal.
In the string interpolation concept, the essential difference between using single or double quotes is that double quotes allow for escape sequences while single quotes do not.
Let's take an example:
name = "Mike"
puts "Hello #{name} \n How are you?"
The above ruby code with string interpolation will interpolate the variable called name which is written inside brackets with its original value which is Mike. And it will also print the string How are you? in a separate line since we already placed an escape sequence there.
Output:
Hello Mike
How are you?
If you do the same with single quotes, it will treat the entire string as a text and it will print as it is including the escape sequence as well.
name = Mike'
puts 'Hello #{name} \n How are you'?
Output:
Hello #{name} \n How are you?

Why does capturing named groups in Ruby result in "undefined local variable or method" errors?

I am having trouble with named captures in regular expressions in Ruby 2.0. I have a string variable and an interpolated regular expression:
str = "hello world"
re = /\w+/
/(?<greeting>#{re})/ =~ str
greeting
It raises the following exception:
prova.rb:4:in <main>': undefined local variable or methodgreeting' for main:Object (NameError)
shell returned 1
However, the interpolated expression works without named captures. For example:
/(#{re})/ =~ str
$1
# => "hello"
Named Captures Must Use Literals
You are encountering some limitations of Ruby's regular expression library. The Regexp#=~ method limits named captures as follows:
The assignment does not occur if the regexp is not a literal.
A regexp interpolation, #{}, also disables the assignment.
The assignment does not occur if the regexp is placed on the right hand side.
You'll need to decide whether you want named captures or interpolation in your regular expressions. You currently cannot have both.
Assign the result of #match; this will be accessible as a hash that allows you to look up your named capture groups:
> matches = "hello world".match(/(?<greeting>\w+)/)
=> #<MatchData "hello" greeting:"hello">
> matches[:greeting]
=> "hello"
Alternately, give #match a block, which will receive the match results:
> "hello world".match(/(?<greeting>\w+)/) {|matches| matches[:greeting] }
=> "hello"
As an addendum to both answers in order to make it crystal clear:
str = "hello world"
# => "hello world"
re = /\w+/
# => /\w+/
re2 = /(?<greeting>#{re})/
# => /(?<greeting>(?-mix:\w+))/
md = re2.match str
# => #<MatchData "hello" greeting:"hello">
md[:greeting]
# => "hello"
Interpolation is fine with named captures, just use the MatchData object, most easily returned via match.

resolve #{var} in string

I have loaded a string with #{variable} references in it. How would I resolve those variables in the string like puts does?
name="jim"
str="Hi #{name}"
puts str
Instead of puts, I would like to have the result available to pass as a parameter or save into a variable.
you could eval it
name = "Patrick"
s = 'hello, #{name}'
s # => "hello, \#{name}"
# wrap the string in double quotes, making it a valid interpolatable ruby string
eval "\"#{s}\"" # => "hello, Patrick"
puts doesn't resolve the variables. The Ruby parser does when it creates the string. if you passed str to any other method, it would be the same as passing 'Hi jim', since the interpolation is already done.
String has a format option that appears as %. It can be used to pass arguments into a predefined string much like interpolation does.
message = "Hello, %s"
for_patrick = message % "Patrick" #=> "Hello, Patrick"
for_jessie = message % "Jessie" #=> "Hello, Jessie"
messages = "Hello, %s and %s"
for_p_and_j = messages % ["Patrick", "Jessie"] #=> "Hello, Patrick and Jessie"
It may not look "Rubyish" but I believe it is the functionality you are looking for.
So, if you have a string coming in from somewhere that contains these placeholders, you can then pass in values as arguments as so:
method_that_gets_hello_message % "Patrick"
This will also allow you to only accept values you are expecting.
message = "I can count to %d"
message % "eleven" #=> ArgumentError: invalid value for Integer()
There's a list on Wikipedia for possible placeholders for printf() that should also work in Ruby.
The eval seems to be the only solution for this particular task. But we can avoid this dirty-unsafe-dishonourable eval if we modify the task a bit: we can resolve not local, but instance variable without eval using instance_variable_get:
#name = "Patrick"
#id = 2 # Test that number is ok
#a_b = "oooo" # Test that our regex can eat underscores
s = 'hello, #{name} !!#{id} ??#{a_b}'
s.gsub(/#\{(\w+)\}/) { instance_variable_get '#'+$1 }
=> "hello, Patrick !!2 ??oooo"
In this case you even can use any other characters instead of #{} (for example, %name% etc), by only modifying the regex a bit.
But of course, all this smells.
It sounds like you want the basis for a template system, which Ruby does easily if you use String's gsub or sub methods.
replacements = { '%greeting%' => 'Hello', '%name%' => 'Jim' }
pattern = Regexp.union(replacements.keys)
'%greeting% %name%!'.gsub(pattern, replacements)
=> "Hello Jim!"
You could just as easily define the key as:
replacements = { '#{name}' => 'Jim' }
and use Ruby's normal string interpolation #{...} but I'd recommend not reusing that. Instead use something unique.
The advantage to this is the target => replacement map can easily be put into a YAML file, or a database table, and then you can swap them out with other languages, or different user information. The sky is the limit.
The benefit to this also, is there is no evaluation involved, it's only string substitution. With a bit of creative use you can actually implement macros:
macros = { '%salutation%' => '%greeting% %name%' }
replacements = { '%greeting%' => 'Hello', '%name%' => 'Jim' }
macro_pattern, replacement_pattern = [macros, replacements].map{ |h| Regexp.union(h.keys) }
'%salutation%!'.gsub(macro_pattern, macros).gsub(replacement_pattern, replacements)
=> "Hello Jim!"

String interpolation when not using a string literal

I have a Ruby script that used string interpolation to build error messages.
p "#{vName} is not a defined variable" => 'xxx is not a defined variable'
Another programmer came through and attempted to externalize the string literals to a separate configuration file. Of course, he doesn't get the substitution.
p err_string_from_config => '#{vName} is not a defined variable'
I've looked around, but couldn't come up with anything better than converting to sprintf strings and using printf.
Does anybody know how to get the #{} substitution to work on strings that are not double quote literals within the Ruby script?
Actually Ruby has functionality very similar to John's Python example:
$ irb
>> greeting = 'hello %s, my name is %s!'
>> interpolated = greeting % ['Mike', 'John']
=> "hello Mike, my name is John!"
>>
This is also useful if your argument is an array constant. If you must use #{} style interpolation you could use eval:
>> greeting = 'hi #{name}' # notice name is not defined yet
>> name = "mike"
>> eval '"' + greeting + '"'
The eval approach is going to be much slower than using % style interpolation, so it's a trade-off.
I suggest that you look at Liquid templating language which provides more powerful features (e.g. you can reference parameters by name).
Previous example would look like:
greeting = Liquid::Template.parse("hello {{your_name}}, my name is {{my_name}}!")
interpolated = greeting.render('your_name' => 'Mike', 'my_name' => 'John')
# => "hello Mike, my name is John!"
Here's how I do it, just for the record. A bit clearer imo.
gief = '#{later} please'
later = "later"
puts eval(%Q["#{gief}"])
# => later please
But honestly, that is such a hack. Unless you have a really good reason to use a string, use a proc instead. I always try to use plain ruby instead of evaling strings.
gief = Proc.new {|l| "#{l} please" }
later = "later"
puts gief.call(later)
# => later please

Resources