Batch connect erb template `eval` strange behavior

I’m trying not to repeat myself in all my different batch connect apps. So I created /var/www/ood/apps/common/attributes.yml and I import that file into all the form.yml:

attributes:
<%= File.read('/var/www/ood/apps/common/attributes.yml') %>

This works well.

I have logic in submit.yml that checks if a field is blank and changes that field to a default value. So I created /var/www/ood/apps/common/form_default_values.rb:

def default(x, default_value)
    return x.blank? ? default_value : x
end

partition = default(_partition, "cpu-preempt")
cpus_per_task = default(cpus_per_task, 2)
time_limit = default(time_limit, "1:00:00")
mem = "#{default(mem_gigs, 8)}GB"
gpus = default(gpus, 0)

What I thought should work but didn’t was this:

---
batch_connect:
<%
eval(File.read('/var/www/ood/apps/common/form_default_values.rb'))
%>
script:
  native:
  - --partition=<%=partition%>
  - --cpus-per-task=<%=cpus_per_task%>
  - --time=<%=time_limit%>
  - --mem=<%=mem%>
  - --gpus=<%=gpus%>

All those variables which are defined in form_default_values.rb come out as empty strings. I assume that they’re undefined and that <%=undefined%> just comes out as nothing.

What does work for some reason is this:

---
batch_connect:
<%
eval(File.read('/var/www/ood/apps/common/form_default_values.rb'))
partition=partition
cpus_per_task=cpus_per_task
time_limit=time_limit
mem=mem
gpus=gpus
%>

script:
  native:
  - --partition=<%=partition%>
  - --cpus-per-task=<%=cpus_per_task%>
  - --time=<%=time_limit%>
  - --mem=<%=mem%>
  - --gpus=<%=gpus%>

By adding the pointless instruction foobar=foobar, I can now reference foobar a few lines later. Am I crazy? Can anyone else reproduce this?

I also tried adding binding as a second positional argument to eval, same result.

The use of eval here isn’t providing the context of the file, just the result of running it - eval isn’t supposed to be used the way you’re using it (and probably shouldn’t be used at all in production code - it’s a security risk). eval is out of scope here, so ERB can’t see those variables without the references you made.

You could use a module or .yml.erb file to define these values - either option would probably work. Is there a reason you chose to use eval? That seems like it’s over-complicating matters a bit when what you’re doing is more or less the same thing as pulling in a hash of values.

I’m pretty sure this solution does what you’re looking for:

# /var/www/ood/apps/common/defaults.rb
module CommonDefaults
  def self.default_values(input_values)
    {
      partition: input_values.fetch(:partition, "cpu-preempt"),
      cpus_per_task: input_values.fetch(:cpus_per_task, 2),
      # ... other defaults ...
    }
  end
end
# file.yml
<%
  require '/var/www/ood/apps/common/defaults'
  values = CommonDefaults.default_values(local_values)
%>
script:
  native:
  - --partition=<%= values[:partition] %>
  - --cpus-per-task=<%= values[:cpus_per_task] %>
  - ...

Edit - You can also add initializers to Rails to load the file for you so the require isn’t needed.