Elixir is known for being a language made for building distributed applications that scale, are massively concurrent, and have self-healing properties. All of these adjectives paint Elixir in a grandiose light. And for good reasons!
But is Elixir also a language that can be used for the more mundane tasks of this world like scripting? I think the answer is a definite yes.
To see this, let’s take a look at all the ways we can write scripts using Elixir. We’ll build using the same example, going from simple to more complex solutions.
Defining the script
Our script will simply create a new markdown file with today’s date so we can
write our To Do list. The structure of the directories will be YYYY/MM/DD.md
,
so we will nest each day under a month and each month under a year.
# Get today's date
date = Date.utc_today()
year = Integer.to_string(date.year)
month = Integer.to_string(date.month)
day = Integer.to_string(date.day)
# Generate the month's full path with YYYY/MM format
month_path =
File.cwd!()
|> Path.join(year)
|> Path.join(month)
# Create the month and year directories
File.mkdir_p(month_path)
# Generate the filename with today's date
filename = Path.join(month_path, day) <> ".md"
# Check existence so we don't override a file
unless File.exists?(filename) do
# include sample header
header = """
---
date: #{Date.to_string(date)}
---
To Do
=====
- What do you need to accomplish today?
"""
# write to the file
File.open(filename, [:write], fn file ->
IO.write(file, header)
end)
end
# Print out confirmation message
final_message = """
> Created #{Path.relative_to_cwd(filename)}
"""
IO.puts(final_message)
Excellent! That is the full extent of our script. Let’s now see how we can run it.
elixir todo.exs
The first and perhaps simplest way to run an elixir script is just to run
elixir name_of_file.exs
from the shell. So let’s do that.
- Save the above code in a file named
todo.exs
- Run
elixir todo.exs
- You should see the following message (with the date you’re running this script instead of the date I ran it):
$ elixir todo.exs
> Created 2018/9/28.md
If we inspect the file that it created, we can confirm that the template is there,
---
date: 2018-09-28
---
To Do
=====
- What do you need to accomplish today?
bin/todo
A second way to run the script is to make it into an executable. This is perhaps an extension of the one above, but I include it as a separate step because I think it could prove useful in some projects.
Follow these steps:
- Move
todo.exs
under abin/
directory (not required but nice) - Mark it as executable with chmod:
chmod +x bin/todo.exs
- Add a shebang
#! /usr/bin/env elixir
at the top of your file - Rename it to
bin/todo
And run bin/todo
!
If you’re unfamiliar with using chmod
and shebang
to turn a file into an
executable, take a look at the notes in this let’s build a CLI video.
When would I use these?
Our todo.exs
and bin/todo
scripts are examples of standalone scripts. They
are useful when you want to run a task that is self-contained. bin/todo
has
the benefit that the user of the script need not know that the script is running
Elixir (though it still needs to have Elixir installed). It’s just a script like
any other one you may find in your bin/
folder.
mix run todo.exs
Now if you’re working with Elixir, it is very likely you are already working in
a mix project. If that is the case, mix
allows you to run arbitrary scripts
via mix run [name of file]
.
To test this, let’s go ahead and create a new project called tasker and run our script there.
Follow these steps:
mix new tasker
,cd
into thetasker
directory,- make a
scripts/
directory, - copy the
todo.exs
file from the first section intoscripts/todo.exs
Now run mix run scripts/todo.exs
!
When would I use mix?
The benefit of running this script via mix
(as opposed to the previous two
options) is that the script is part of your project, so it has access to all the
code you have defined in the project.
Let’s bring that point home by extracting most of the logic to a TodoBuilder
module:
# lib/todo_builder.ex
defmodule TodoBuilder do
def run(date) do
year = Integer.to_string(date.year)
month = Integer.to_string(date.month)
day = Integer.to_string(date.day)
month_path =
File.cwd!()
|> Path.join(year)
|> Path.join(month)
File.mkdir_p(month_path)
filename = Path.join(month_path, day) <> ".md"
unless File.exists?(filename) do
header = """
---
date: #{Date.to_string(date)}
---
To Do
=====
- What do you need to accomplish today?
"""
File.open(filename, [:write], fn file ->
IO.write(file, header)
end)
end
{:ok, filename}
end
end
# scripts/todo.exs
{:ok, filename} =
Date.utc_today()
|> TodoBuilder.run()
final_message = """
> Created #{Path.relative_to_cwd(filename)}
"""
IO.puts(final_message)
Mix run in the wild
If you’re interested in seeing this “in the wild”, Phoenix seeds data in their
applications by running mix run priv/repo/seeds.exs
.
mix tasker.todo
Having it as a script that can run with our project code is nice. But sometimes
we want to make it a more explicit part of our project. Turning our script into
a mix task can do just that. This is especially true if we expect external
parties to use our project since mix
tasks have the extra benefit of
documentation!
Let’s change our script into a mix
task. In the tasker
project,
- Create a
lib/mix/tasks/
directory - Create a
todo.ex
task - Move the code we have in
scripts/todo.exs
into that file - Add
use Mix.Task
at the top of the module - Add a
@shortdoc
and@moduledoc
with descriptions of what the task does
# lib/mix/tasks/todo.ex
defmodule Mix.Tasks.Todo do
use Mix.Task
@shortdoc "Creates a new todo file with today's date"
@moduledoc """
Creates a new todo file with today's date
## Example
mix todo
"""
def run(_args) do
{:ok, filename} =
Date.utc_today()
|> TodoBuilder.run()
final_message = """
> Created #{Path.relative_to_cwd(filename)}
"""
Mix.shell().info(final_message)
end
end
Now run mix todo
and voila!
But that’s not all. Note the use of @shortdoc
and @moduledoc
. The
documentation that we added in those two module attributes makes our script
especially friendly to other users.
If you check mix help
, you’ll see that our task is listed right after mix
test
, and it uses @shortdoc
for its documentation.
Now check out mix help todo
. It uses your @moduledoc for documentation!
Mix task in the wild
If you’re interested on how this gets used in the wild, take a look at how
Phoenix and Ecto use mix
tasks for commonly performed actions and for
their generators. Things such as mix phx.new
, mix phx.server
, and mix
ecto.migrate
are all mix
tasks!
./tasker escript
Wow, it’s been a long road but we’re finally down to the last way we can script in Elixir (that I know of).
One of the built-in mix
tasks that comes with mix
is mix escript.build. It
packages your project and dependencies into a binary that is executable. It even
embeds Elixir as part of the script, so it can be used so long as a machine has
Erlang/OTP without requiring Elixir to be present.
Let’s get to it. We have to first update our mix.exs
file to define an entry
point for the escript.build
task,
# mix.exs
def project do
[
app: :tasker,
# other options
#
# add this line below
escript: escript()
]
end
# add the entry point
defp escript do
[main_module: Tasker.TodoCLI]
end
Now let’s create Tasker.TodoCLI
and put our code there,
# lib/tasker/todo_cli.ex
defmodule Tasker.TodoCLI do
def main(_args) do
{:ok, filename} =
Date.utc_today()
|> TodoBuilder.run()
final_message = """
> Created #{Path.relative_to_cwd(filename)}
"""
IO.puts(final_message)
end
end
Now we can simply run mix escript.build
to build the executable. And you
should see that you have a new executable called tasker
in the main directory.
Run it with ./tasker
and see the magic happen!
When would I use escript?
Much like a mix
task, an escript
has the ability to use the rest of your
project’s codebase. But it has that ability because the project and its
dependencies are compiled and packaged in. That means you can use it outside
of your mix
project. And since Elixir is embedded, all you need is to have
Erlang/OTP installed. So it makes our little script into an executable that is
easily shareable with others!
To test that, feel free to move the ./tasker
script out of your mix
project into another directory and run it. You’ll see it does its work just
fine!
Anything else?
We covered a lot of ground, and we saw that Elixir has great tooling for creating scripts. But I would be remiss if I didn’t include a mention of the OptionParser module. It’s a little module that helps parse command line options from something like this,
--flag true --option arg1
into a keyword list like this,
[flag: true, option: "arg1"]
So if you’re writing Elixir scripts, it’s sure to come in handy!