· elixir

Notes on Elixir: Templating with EEx

EEx is to Elixir what Jinja is to Python. It enables the use of Elixir code inside strings and files. It’s used extensively in the Phoenix framework for HTML templates, and recently I’ve started to use it to replace my Python scripts for generating Cisco config files.

Like most templating engines EEx has various tags it uses for marking up your template:

These tags, in the context of a template are evaluated by one of 4 functions, or 2 macros:

So, examples;

iex> string = "Hello, <%= name %>!"
"Hello, <%= name %>!"
iex> EEx.eval_string string, [name: "World"]
"Hello, World!"

Here, string is bound to a simple template, with a variable name inside the tag. We then evaluate the string, passing in a keyword list of arguments, and hey presto, in our output the name variable has been replaced! Not so different from string interpolation really. Except we have access to all of Elixir inside the tag;

iex> template = "<%= if n > 100 do %> \
...> <%= n %> is greater than 100. \
...> <% else %> \
...> <%= n %> is lower than 100. \
...> <% end %>"
"<%= if n > 100 do %>n is greater than 100.<% else %>n is lower than 100.<% end %>"
iex> EEx.eval_string template, [n: 10]
" 10 is lower than 100. "
iex> EEx.eval_string template, [n: 101]
" 101 is greater than 100. "

Notice the <% %> tags for the else & end of the if statement. The content of these tags is evaluated, but never returned to the template. We’ll get a compiler warning if we include an unused variable in there, or an error if we have an unbound variable, otherwise any results just won’t appear in our final output;

iex> string = "Hello, <% name %>!"
"Hello, <% name %>!"
iex> EEx.eval_string string, [name: "World"]
"Hello, !"
nofile:1: warning: variable name in code block has no effect as it is never returned (remove the variable or assign it to _ to avoid warnings)
iex> EEx.eval_string string
** (CompileError) nofile:1: undefined function name/0
    (elixir) expanding macro: Kernel.<>/2
             nofile:1: (file)
iex> string = "Hello, <% 2 + 2 %>!"
iex> EEx.eval_string string
"Hello, !"
iex> string = "Hello, <%= 2 + 2 %>!"
iex> EEx.eval_string string
"Hello, 4!"

The quotation tag is interesting, it’s almost like a raw string indicator, telling EEx to not evaluate the tag. I’ve not had cause to use it yet, but I can see it might be useful for some kind of two-stage templating;

iex> string = "Hello, <%% 2 + 2 %>!"
"Hello, <%% 2 + 2 %>!"
iex> EEx.eval_string string
"Hello, <% 2 + 2 %>!"
iex> string = "Hello, <%%= 2 + 2 %>!"
"Hello, <%%= 2 + 2 %>!"
iex> output = EEx.eval_string string
"Hello, <%= 2 + 2 %>!"
iex> EEx.eval_string output
"Hello, 4!"

When first evaluated the quotation tag becomes a regular tag, which can then be evaluated again if necessary.

So, we’ve seen eval_string/2 in the previous examples. eval_file/2 is much the same but takes a file as it’s template rather than a string;

iex> File.write("example.txt", "Hello, <%= name %>!")
:ok
iex(52)> EEx.eval_file "example.txt", [name: "World"]
"Hello, World!"
iex(53)> File.read("example.txt")
{:ok, "Hello, <%= name %>!"}

Note that the original template file has not been changed, EEx has just created a new output from the combination of the template and the given variable bindings. Also, the template file here was just a .txt file rather than a .eex file. The template file isn’t required to be a .eex file, but convention is to combine the two extensions, for instance .txt.eex or .html.eex, as this ‘preserves its intent as a template, and also denotes what it’s outputting’, and helps with syntax highlighting too. (thanks asonge & ciastek)

The compile_string/2 & compile_file/2 functions are fairly straightforward, generating AST. I’ve had to put this in a gist, here, as it screws with Jekyll’s liquid tags, jeez!

Alhough I’ve had no use for them myself, they are actually used within the eval_string/3 & eval_file/3 functions.

Last, but by no means least, are the function_from_file/5 & function_from_string/5 macros. These basically do what they sound like they do; create a function from a file or string template for use elsewhere. I’ll illustrate this with an example.

Let’s say we have a very large number of devices that we need to generate individual configurations for, in my experience this would be for Cisco switches and routers. We create a template, “base_example.conf.eex”:

hostname <%= hostname %>
no logging console
username <%= user_name %> secret <%= user_password %>
aaa new-model
aaa authentication login default local
clock timezone GMT 0 0
clock summer-time BST recurring last Sun Mar 1:00 last Sun Oct 1:00
ip name-server <%= name_server %>
no ip http server
no ip http secure-server
ntp server <%= ntp_server %>
ntp update-calendar
!
<%= for [id, name] <- vlans do %>
vlan <%= id %>
  name <%= name %>
<% end %>
!
interface 0
ip address <%= ip_address %>
no shutdown

This is a very basic template, with some tags expecting variables. There is also a list comprehension towards the end, this expects vlans to be a list of 2 element lists, that it will then map over, creating as many vlan/name pairs as necessary. This is really useful for situations where you might have a variable amount of input data, saving you from having to adjust the template each time according to, in this case, the number of vlans.

So, I’ve saved this template in a templates directory. Let’s dip into iex and see what we can do:

iex> defmodule Render do
...>   require EEx
...>   EEx.function_from_file(:def, :base,
...>     Path.expand("./templates/base_example.conf.eex"),
...>     [:hostname, :ip_address, :name_server, :ntp_server,
...>      :user_name, :user_password, :vlans])
...> end
{:module, Render,
 <<70, 79, 82, 49, 0, 0, 14, 184, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 26, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>,
 {:base, 7}}

Here, we start a new module called Render, require EEx, and then use the function_from_file/5 macro, passing it the type of function we want (def or defp), the name of the function, the template we’re using, and our arguments.

We now have a function we can use to render our template:

iex> Render.base(
...>   "ABC20",
...>   "192.168.1.20",
...>   "8.8.8.8",
...>   "uk.pool.ntp.org",
...>   "admin",
...>   "password",
...>   [["999", "NATIVE"], ["100", "VOICE"], ["101", "DATA 1"], ["102", "DATA 2"], ["103", "DATA 3"]]
...> )
"hostname ABC20
no logging console
username admin secret password
aaa new-model
aaa authentication login default local
clock timezone GMT 0 0
clock summer-time BST recurring last Sun Mar 1:00 last Sun Oct 1:00
ip name-server 8.8.8.8
no ip http server
no ip http secure-server
ntp server uk.pool.ntp.org
ntp update-calendar
!
vlan 999
  name NATIVE

vlan 100
  name VOICE

vlan 101
  name DATA 1

vlan 102
  name DATA 2

vlan 103
  name DATA 3
!
interface 0
ip address 192.168.1.20
no shutdown"

We pass our device variables in to our new function, notice the list of lists for vlans, and out pops our rendered template, including all our vlans, lovely. Now, obviously, this isn’t particularly practical when dealing with thousands of devices. But what you have is a function you can use as part of a module that can read data from a csv file, I’ve found ex_csv to be pretty good for this, and map over it, applying your new rendering function, and then writing the results to a new file.

But there’s more. Passing in that list of data is pretty cumbersome, it’d be nice if we could just define a single variable that represents a map, and let the template grab what it needs from that. First, we need to change our template to look like this:

hostname <%= @hostname %>
no logging console
username <%= @user_name %> secret <%= @user_password %>
aaa new-model
aaa authentication login default local
clock timezone GMT 0 0
clock summer-time BST recurring last Sun Mar 1:00 last Sun Oct 1:00
ip name-server <%= @name_server %>
no ip http server
no ip http secure-server
ntp server <%= @ntp_server %>
ntp update-calendar
!
<%= for [id, name] <- @vlans do %>
vlan <%= id %>
  name <%= name %>
<% end %>
!
interface 0
ip address <%= @ip_address %>
no shutdown

We’re now using EEx’s @ macro, which enables us to grab data out of a map or keyword list.

We’ll update our Render.base function, like so:

iex> defmodule Render do
...>   require EEx
...>   EEx.function_from_file(:def, :base,
...>     Path.expand("./templates/base_example.conf.eex"),
...>     [:assigns])
...> end
iex: warning: redefining module Render
{:module, Render,
 <<70, 79, 82, 49, 0, 0, 15, 8, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 151, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>,
 {:base, 1}}

and create a map of our data:

iex> device = %{hostname: "ABC20",
...>   ip_address: "192.168.1.20",
...>   name_server: "8.8.8.8",
...>   ntp_server: "uk.pool.ntp.org",
...>   user_name: "admin",
...>   user_password: "password",
...>   vlans: [["999", "NATIVE"], ["100", "VOICE"], ["101", "DATA 1"],
...>    ["102", "DATA 2"], ["103", "DATA 3"]]}
%{hostname: "ABC20", ip_address: "192.168.1.20", name_server: "8.8.8.8",
  ntp_server: "uk.pool.ntp.org", user_name: "admin", user_password: "password",
  vlans: [["999", "NATIVE"], ["100", "VOICE"], ["101", "DATA 1"],
   ["102", "DATA 2"], ["103", "DATA 3"]]}

Now, let’s see if it works…

iex> Render.base(device)
"hostname ABC20
no logging console
username admin secret password
aaa new-model
aaa authentication login default local
clock timezone GMT 0 0
clock summer-time BST recurring last Sun Mar 1:00 last Sun Oct 1:00
ip name-server 8.8.8.8
no ip http server
no ip http secure-server
ntp server uk.pool.ntp.org
ntp update-calendar
!
vlan 999
  name NATIVE

vlan 100
  name VOICE

vlan 101
  name DATA 1

vlan 102
  name DATA 2

vlan 103
  name DATA 3
  !
interface 0
ip address 192.168.1.20
no shutdown"

Ahh, that’s much easier, and cleaner, super. One more thing before I go, I really liked with Python’s Jinja how you were able to split up your templates, much like Phoenix does. We don’t have access to Phoenix’s render function, but we can mimic it.

We’ll start by pulling out the vlans section into it’s own template, vlans.conf.eex, replacing it with this:

<%= if Map.has_key?(assigns, :vlans) do %>
  <%= vlans(@vlans) %>
<% end %>

There’s a couple of things going on here. First, we’re going to check the map of data to see if it has a vlans key. We do this, so that if we have a device that doesn’t have any vlans it’ll just skip this, rather than break the program. If we do have a vlans key, we’re going to pass it to a new function called vlans, that will be in the Render module.

Let’s have a look at it:

iex> defmodule Render do
...>   require EEx
...>
...>   EEx.function_from_file(:def, :base,
...>                          Path.expand("./templates/base_example.conf.eex"),
...>                          [:assigns])
...>
...>   EEx.function_from_file(:def, :vlans,
...>                          Path.expand("./templates/vlans.conf.eex"),
...>                          [:vlans])
...> end
iex:15: warning: redefining module Render
{:module, Render,
 <<70, 79, 82, 49, 0, 0, 17, 236, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 202, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>,
 {:vlans, 1}}

We redefine our Render module, and call our Render.base function again:

iex(16)> Render.base(device)
"hostname ABC20
no logging console
username admin secret password
aaa new-model
aaa authentication login default local
clock timezone GMT 0 0
clock summer-time BST recurring last Sun Mar 1:00 last Sun Oct 1:00
ip name-server 8.8.8.8
no ip http server
no ip http secure-server
ntp server uk.pool.ntp.org
ntp update-calendar
!

vlan 999
  name NATIVE

vlan 100
  name VOICE

vlan 101
  name DATA 1

vlan 102
  name DATA 2

vlan 103
  name DATA 3
  !
interface 0
ip address 192.168.1.20
no shutdown"

And, once again, we easily render our template. So, this enables us to build out a hierarchy of templates to cover all possibilities of data input we may have. Which is pretty cool if you ask me. It’s certainly going to make my job easier.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket