What is the cause of the runtime error in this String.replace which is in an Enum.reduce loop?

What is the syntax error in this code?

Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> String.replace(sslCmd, “{{”<> x.key <> “}}”, x.value) end)

** (ArgumentError) argument error
    (stdlib) binary.erl:275: :binary.replace/4
    (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/code.ex:370: Code.require_file/2

It is part of this code below which is based on the continuation of the templating project in this question - Help with choosing data structures for template project. The code is also in a gist at github

  structList = ProcessList.parse_list(params)  
  IO.puts sslCmd
    
   Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> IO.puts " #{x.key} is #{x.value} " end) 

   # The following command fails     
   Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> String.replace(sslCmd, "{{"<> x.key <> "}}", x.value) end)

The idea is to repeated apply the search and replace over parameters in the template.

What I expect is going wrong here, is that there exists an element in structList whose :key is nil (or anything else that is not a binary string), which means that the <> operator will fail.

I highly recommend you to use Elixir’s string interpolation, because this will automatically convert non-string values into their string representations. Use "{{#{x.key}}}" instead.

1 Like

Checking the source it turns out the replacement needs to a be a binary. What function converts a string to a binary? Isn’t that rather weird?

  @spec replace(t, pattern | Regex.t, t, Keyword.t) :: t
  def replace(subject, pattern, replacement, options \\ []) when is_binary(replacement) do
    if Regex.regex?(pattern) do
      Regex.replace(pattern, subject, replacement, global: options[:global])
    else
      opts = translate_replace_options(options)
      :binary.replace(subject, pattern, replacement, opts)
    end
  end

Strings are binaries. No need to convert. But maybe you do not have a string but anything else?

Check this out - this doesn’t work

first = %{:key => "ca_cert_subj_org_unit",:value => "IT Department"}
IO.puts first.key
IO.puts String.replace("{{ca_cert_subj_org_unit}} - la la la", '{{#{first.key}}}', first.value) 
^ this causes a syntax error

Check this out - this work

first = %{:key => "ca_cert_subj_org_unit",:value => "IT Department"}
IO.puts first.key
IO.puts String.replace("{{ca_cert_subj_org_unit}} - la la la",to_string( '{{#{first.key}}}'), first.value) 
^ this works

It turns out the first option requires double quotes instead of single quotes so the search pattern should be "{{#{first.key}}}" instead of '{{#{first.key}}}'

'foo' is not a binary-string, it is a charlist.

You can try i 'foo' and i "foo" in iex and figure out the differences.

Since the string theory in elixir is much deeper that only to distinguish between charlist- and binary-strings, I’d suggest to give @JEG2 and @nathanl (I do hope this is the right one to mention :wink: a watch:

http://confreaks.tv/videos/elixirconf2016-string-theory

1 Like

I should have posted the code rather than link it. The problem was the single quoting of the subject of the search, rather than the search values. Here it is in its entirety.

I’m attempting to call String.replace in this Elixir code which gets its values from a list of structs, but it just results in a runtime error.

The string parameters to the String.replace function are all printed out and it all seems normal. Why is this happening?


Here’s the line responsible for the error:

Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> String.replace(sslCmd, "{{#{x.key}}}", x.value) end)
# Runtime error here


** (ArgumentError) argument error
    (stdlib) binary.erl:275: :binary.replace/4
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/code.ex:363: Code.require_file/2

This is the full code:

defmodule ParamStruct do                                                                                                                         
  defstruct key: "", value: "", default: "", description: "description of parameter", label: "label on web form", required: false, order: 99     
end                                                                                                                                              
                                                                                                                                                 
defmodule TemplateStruct do                                                                                                                      
  defstruct key: "must be unique", name: "descriptive name", code: "", executable: false, destination: "", delete_after: false,                  
  perms: "644"                                                                                                                                   
end                                                                                                                                              
                                                                                                                                                 
defmodule ProcessList do                                                                                                                         
  def parse_list([]), do: []                                                                                                                     
                                                                                                                                                 
  def parse_list([%{"key" => ky,"value" => val,"default" => dft, "description" => desc,"label" => lbl} | tail]) do
    [%ParamStruct{key: ky, value: val, description: desc, label: lbl, default: dft } | parse_list(tail) ]
  end                                                                                                                                            
                                                                                                                                                 
  def create_recommend_list(%{"itemScores" => score_list})  do                                                                                   
    parse_list(score_list)                                                                                                                       
  end                                                                                                                                            
end   
    
params = [                                                                                                                                     
%{"key" => "ca_cert_subj_state","value" => "Greater London","default" => "Greater London","description" => "Region","label" => "State/County"},                
  %{"key" => "key-file","value" => "cacert_001","default" => "cacert_001","description" => "","label" => "Key File (without password)"},                   
  %{"key" => "key-file-pass","value" => "cacert_pass_001","default" => "cacert_pass_001","description" => "","label" => "Key File (with password)"},            
  %{"key" => "ca_cert_email","value" => "admin@domain.net","default" => "admin@domain.net","description" => "","label" => "Email"},                              
  %{"key" => "ca_cert_subj_common_name","value" => "Elixir User","default" => "domain.net","description" => "","label" => "Common Name"},                   
  %{"key" => "ca_cert_subj_country","value" => "UK","default" => "UK","description" => "Country","label" => "Country"},                            
  %{"key" => "ca_cert_subj_location","value" => "Manchester","default" => "Westchester","description" => "","label" => "Location"},                        
  %{"key" => "ca_cert_subj_organization","value" => "Elixir Programs Forum","default" => "Big Company","description" => "","label" => "Organisation"},                
  %{"key" => "ca_cert_subj_org_unit","value" => "IT Department","default" => "Infosystems and Communications","description" => "","label" => "Organisational Unit"}                                                                                                                                            
  ]          

sslCmd = '''
openssl req -x509 -new -nodes -sha256 \
 -key {{key-file-pass}}.key \
 -days 3650 \
 -out {{key-file-pass}}.pem \
 -subj "\
/C={{ca_cert_subj_country}}\
/ST={{ca_cert_subj_state}}\
/L={{ca_cert_subj_location}}\
/O={{ca_cert_subj_organization}}\
/OU={{ca_cert_subj_org_unit}}\
/CN={{ca_cert_subj_common_name}}\
/emailAddress={{ca_cert_email}}\

'''
structList = ProcessList.parse_list(params)  
#IO.inspect ProcessList.parse_list(params)
# [first | _ ] = ProcessList.parse_list(params)
# IO.puts " #{first.key} is #{first.value} "

# IO.inspect first
IO.puts sslCmd
IO.puts "list of keys and values"
IO.puts "======================="
Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> IO.puts " #{x.key} is #{x.value} " end) 

Enum.reduce(structList, sslCmd, fn(x, sslCmd) -> String.replace(sslCmd, "{{#{x.key}}}", x.value) end)
# Runtime error here

** (ArgumentError) argument error
    (stdlib) binary.erl:275: :binary.replace/4
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/code.ex:363: Code.require_file/2

OK, the culprit is in sslCmd! You are assigning a multiline charlist. Replace ''' for opening and closing with """ and it should work.

1 Like