1 Introduction
In this post, we are going to explore several approaches to Elixir scripting. Some are simpler; and some are more complex. Some do not use the Elixir build tool Mix; and some do.
More information -- Here is another blog with more information about scripting under Elixir -- https://webonrails.com/2015/11/03/creating-command-line-programs-in-elixir/
2 Scripting without Mix
As described in the Elixir docs at https://elixir-lang.org/getting-started/introduction.html#running-scripts, running a script under Elixir can be as simple as the following:
Write the script:
# test01.exs IO.puts("Hello")
Run the script:
$ elixir test01.exs
And, under Linux/UNIX, if we want a "shebang" or hashbang:
#!/usr/bin/env elixir # test01.exs IO.puts("Hello")
And we change the access/mode of the file to executable, then, we can run it as follows:
$ ./test01.exs
There are several additional capabilities that we'd like our script to have:
- Access to command line arguments
- The ability to use external modules
2.1 Access to command line arguments
System.argv/0 gives us access to any command line arguments.
And, OptionParser.parse helps us process the arguments so that we can capture both the command line options and any additional, positional arguments.
Here is an example:
#!/usr/bin/env elixir # test01.exs defmodule Test13.CLI do @moduledoc """ synopsis: Prints args, possibly multiple times, possibly upper cased. usage: $ test01 {options} arg1 arg2 ... options: --upcase Convert args to upper case. --count=n Print n times. """ def main([help_opt]) when help_opt == "-h" or help_opt == "--help" do IO.puts(@moduledoc) end def main(args) do {opts, cmd_and_args, errors} = parse_args(args) case errors do [] -> process_args(opts, cmd_and_args) _ -> IO.puts("Bad option:") IO.inspect(errors) IO.puts(@moduledoc) end end defp parse_args(args) do {opts, cmd_and_args, errors} = args |> OptionParser.parse(strict: [upcase: :boolean, help: :boolean, count: :integer]) {opts, cmd_and_args, errors} end defp process_args(opts, args) do count = Keyword.get(opts, :count, 1) convertfn = if Keyword.has_key?(opts, :upcase) do fn (arg) -> String.upcase(arg) end else fn (arg) -> arg end end Stream.iterate(0, &(&1 + 1)) |> Stream.take(count) |> Enum.each(fn (idx) -> if idx > 0 do IO.puts("-----------------") end Stream.with_index(args) |> Enum.each(fn ({arg, index}) -> arg1 = convertfn.(arg) IO.puts("arg #{index + 1}. #{arg1}") end) end) end end Test13.CLI.main(System.argv)
Notes:
- Notice that at the bottom of this script, we call the function that starts it, and we pass the (un-parsed command line arguments).
2.2 The ability to use external modules
In order to use an external module and call a function in it, we'll have to do the following:
- Get the source code for the module.
- Compile it.
- Access it from our Elixir script.
Retrieving and compiling -- I use Mix, the Elixir build tool. So, as an example, it's easy for me to include the following in my mix.exs file:
defp deps do [ {:jason, ">0.0.0"} ] end
I can then compile it and build the ebin files with:
$ mix deps.get $ mix deps.compile
Next we need to enable Elixir (actually the underlying Erlang system) to find these modules. We can do that in one of the following ways:
Use the "-pa" or "-pz" command line option to elixir:
$ elixir -pz /path/to/my/ebin test01.exs arg1 arg2 ...
In my case, the /path/to/my/ebin is a path to a sub-directory of the _build directory under my Mix project. Note that (on Linux) I can use a symbolic link to make it easier to access that directory.
Set the ERL_LIBS environment variable to include the needed ebin directory. Example:
export ERL_LIBS=/path/to/my/ebin
Specify locations (directories) from which to load compiled code in your Erlang configuration file: ~/.erlang. For example:
code:add_pathsz([ "/home/yourname/a1/Erlang/Elixir/Test/test21/_build/dev/lib/sweet_xml/ebin/", "/home/yourname/a1/Erlang/Elixir/Test/test21/_build/dev/lib/jason/ebin/", "/home/yourname/a1/Erlang/Elixir/Test/test21/_build/dev/lib/test21/ebin/" ]).
For information about the use of command line flags "-pa" and "-pz" and for information about ERL_LIBS, see: http://erlang.org/doc/man/erl.html. Note that when you use environment variable ERL_LIBS, subdirectories are also searched. For infomation about code:add_pathsz/1 and other functions in the code module for this purpose see https://erlang.org/doc/man/code.html.
If you decide to use the ERL_LIBS environment variable, then, on Linux, you can set it with something like the following:
# replace $ export ERL_LIBS=/path/to/my/libs # append $ export ERL_LIBS=$ERL_LIBS:/path/to/my/libs
Or, for one time use, set the environment variable and run your script as follows:
$ ERL_LIBS=/the/path/to/my/libs elixir my_script.exs
In our script, we need to make the module available using the require directive. Here is an example:
#!/usr/bin/env elixir require Jason elixir_data = [11, 22, 33, 44] {:ok, jason_data} = Jason.encode(elixir_data) IO.write("jason_data: ") IO.inspect(jason_data) {:ok, elixir_data2} = Jason.decode(jason_data) IO.write("elixir_data2: ") IO.inspect(elixir_data2)
3 Scripting with Mix
Another strategy is to use Mix to build or generate your script for you. This provides the following features:
Elixir is embedded in the "script".
Because Elixir is embedded into the script, you will not need Elixir on a machine on which you run the script. However, you will need Erlang.
Because Elixir is embedded into the script, the script is quite large. It's greater than a megabyte on my system.
The resulting script is actually an Erlang script and is run with escript rather than elixir. In fact, a hash-bang line is inserted at the top of this file, so on Linux systems, you can run it with something like:
$ ./my_script arg1 arg2 ...
For more help with this do: $ mix help escript.build which inside your Mix project directory.
Create a Mix project -- If you do not already have one, create a project with Mix. Example:
$ mix new test14
Configuration -- Add an escript clause to the project definition in your mix.exs file. Example:
def project do [ app: :test14, version: "0.1.0", elixir: "~> 1.9-rc", start_permanent: Mix.env() == :prod, deps: deps(), escript: [ main_module: Test14.CLI, comment: "A sample escript", ] ] end
Write the code --
- Create an additional file in the ./lib directory of your Mix project.
- In that file, define the module that you specified as the main_module in mix.exs.
- In that module, define a main function. This function will receive one argument, specifically, the list of command line arguments.
Here is an example:
defmodule Test14.CLI do @moduledoc """ synopsis: Prints args, possibly multiple times. usage: $ test10 {options} arg1 arg2 ... options: --verbose Add more info. --count=n Print n times. """ def main([]) do IO.puts(@moduledoc) end def main([help_opt]) when help_opt == "-h" or help_opt == "--help" do IO.puts(@moduledoc) end def main(args) do {opts, positional_args, errors} = args |> parse_args case errors do [] -> process_args(opts, positional_args) show_jason(positional_args) _ -> IO.puts("Bad option:") IO.inspect(errors) IO.puts(@moduledoc) end end defp show_jason(args) do {:ok, content} = Jason.encode(args) IO.puts("json content: #{content}") end defp parse_args(args) do {opts, cmd_and_args, errors} = args |> OptionParser.parse(strict: [verbose: :boolean, count: :integer]) {opts, cmd_and_args, errors} end defp process_args(opts, args) do count = Keyword.get(opts, :count, 1) printfn = if not(Keyword.has_key?(opts, :verbose)) do fn (arg) -> IO.puts(arg) end else fn (arg) -> IO.write("Message: ") IO.puts(arg) end end Stream.iterate(0, &(&1 + 1)) |> Stream.take(count) |> Enum.each(fn (_counter) -> Enum.with_index(args) |> Enum.each(fn ({arg, idx}) -> printfn.("#{idx}. #{arg}") end) end) end end
Use Mix to generate the script -- You can build it with the following:
$ mix escript.build
Run the script -- In our case, examples would be the following:
$ ./test14 --count=4 --verbose aaa bbb ccc $ escript ./test14 --count=2 alpha beta