DannyB
DannyB

Reputation: 14806

Evaluate a string with indexed array as values

I would like to take a string that contains positional argument markers (not named), supply it with an array (not hash) of values, and have it evaluated.

The use case as an example would be somewhat like ARGV.

For example,

# given:
string = "echo $1 ; echo $@"
values = ["hello", "world"]

# expected result:
"echo hello ; echo hello world"

The below function is the best I could come up with:

def evaluate_args(string, arguments)
  return string unless arguments.is_a? Array and !arguments.empty?

  # Create a variable that can replace $@ with all arguments, and quote
  # arguments that had "more than one word" originally
  all_arguments = arguments.map{|a| a =~ /\s/ ? "\"#{a}\"" : a}.join ' '

  # Replace all $1 - $9 with their respective argument ($1 ==> arguments[0])
  string.gsub!(/\$(\d)/) { arguments[$1.to_i - 1] }

  # Replace $@ or $* with all arguments
  string.gsub!(/\$[*|@]/, all_arguments)

  return string
end

And it seems to me like it can and should be simpler.

I was hoping to find something that is closer to the Kernel.sprintf method of doing things - like "string with %{marker}" % {marker: 'value'}

So, although this issue is almost solved for me (I think), I would love to know if there is something I missed that can make it more elegant.

Upvotes: 1

Views: 61

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110725

string = "echo $1 ; echo $@ ; echo $2 ; echo $cat"
values = ["hello", "World War II"]

vals = values.map { |s| s.include?(' ') ? "\"#{s}\"" : s }
  #=> ["hello", "\"World War II\""]
all_vals = vals.join(' ')
  #=> "hello \"World War II\"" 
string.gsub(/\$\d+|\$[@*]/) { |s| s[/\$\d/] ? vals[s[1..-1].to_i-1] : all_vals }
  #=> "echo hello ; echo hello \"World War II\" ; echo \"World War II\" ; echo $cat" $cat" 

Upvotes: 1

Jordan Running
Jordan Running

Reputation: 106077

It seems like you're trying to reproduce Bash-style variable expansion, which is an extremely complex problem. At the very least, though, you can simplify your code in two ways:

  1. Use Kernel.sprintf's built in positional argument feature. The below code does this by substituting e.g. $1 with the sprintf equivalent %1$s.
  2. Use Shellwords from the standard library to escape arguments with spaces etc.
require 'shellwords'

def evaluate_args(string, arguments)
  return string unless arguments.is_a? Array and !arguments.empty?
  tmpl = string.gsub(/\$(\d+)/, '%\1$s')
  (tmpl % arguments).gsub(/\$[*@]/, arguments.shelljoin)
end

string = "echo $1 ; echo $@"
values = ["hello", "world"]

puts evaluate_args(string, values)
# => echo hello ; echo hello world

If you didn't have the $* requirement I'd suggest just dropping the Bash-like format and just using sprintf, since it covers everything else you mentioned. Even so, you could further simplify things by using sprintf formatting for everything else:

def evaluate_args(string, arguments)
  return string unless arguments.is_a? Array and !arguments.empty?
  string.gsub('%@', arguments.shelljoin) % arguments
end

string = "echo %1$s ; echo %@"
values = ["hello", "world"]

puts evaluate_args(string, values)
# => echo hello ; echo hello world

Edit

If you want to use %{1} with sprintf you could turn the input array into a hash where the integer indexes are turned into symbol keys, e.g. ["hello", "world"] becomes { :"1" => "hello", :"2" => "world" }:

require "shellwords"

def evaluate_args(string, arguments)
  return string unless arguments.is_a? Array and !arguments.empty?
  string % {
    :* => arguments.shelljoin,
    **arguments.map.with_index {|val,idx| [ :"#{idx + 1}", val ] }.to_h
  }
end

string = "echo %{1} ; echo %{*}"
values = ["hello", "world"]

puts evaluate_args(string, values)
# => echo hello ; echo hello world

Upvotes: 2

Related Questions