Starting a simple CLI project in Erlang with ZX

Yesterday I wrote a post about a new tooling suite for developers and users that makes dealing with Erlang more familiar to people from other languages. Using the tool for packaging and deployment/launch makes writing and deploying end-user programs in Erlang non-mysterious as well, which is a great benefit as Erlang provides a wonderful paradigm for making use of modern overwhelmingly multi-core client systems.

It is still in beta, but works well for my projects, so I’ll leave a quick tutorial here that shows the basic flow of writing a simple CLI utility in Erlang using ZX.

In this example we’ll make a program that accepts two arguments: a path to a file with JSON in it and a path to a file where the data should be written back after being converted to Erlang terms.

To start a project we do zx create project and follow the prompts.
(The snippet below excludes the full output for brevity, but you can view the entire creation prompt log here: zx_cli_creation.txt.)

ceverett@okonomiyaki:~/vcs$ zx create project

### --snip snip--
### Prompts for project meta
### --snip snip--

Writing app file: ebin/termifier.app
Project otpr-termifier-0.1.0 initialized.
ceverett@okonomiyaki:~/vcs$ 

After the project is created we see a new directory in front of us called “termifier” (or whatever the project is named). We can execute this now just to make sure everything is going as expected:

ceverett@okonomiyaki:~/vcs$ ls
termifier
ceverett@okonomiyaki:~/vcs$ zx rundir termifier
Recompile: src/termifier
Hello, World! Args: []
ceverett@okonomiyaki:~/vcs$ zx rundir termifier foo bar baz
Hello, World! Args: ["foo","bar","baz"]

Ah! So we already have something that builds and runs, very similar to how an escript works, except that using ZX we can easily add dependencies from Zomp package realms and package and execute this program on any system in the world that has ZX on it via Zomp ourselves.

…not that we have any reason to deploy a “Hello, World!” program to the wider public, of course.

Notice here that the first time we run it we see a message Recompile: src/termifier. That means the module termifier is being compiled and cached. On subsequent runs this step is not necessary unless the source file has changed (the compiler detects this on its own).

Next lets search Zomp for the tag “json” to see if there are any packages that list it as a tag, and if there are any let’s get a description so maybe we can find a website or docs for it:

ceverett@okonomiyaki:~/vcs$ zx search json
otpr-zj-1.0.5
ceverett@okonomiyaki:~/vcs$ zx describe otpr-zj
Package : otpr-zj-1.0.5
Name    : zj
Type    : lib
Desc    : A tiny JSON encoder/decoder in pure Erlang.
Author  : Craig Everett zxq9@zxq9.com
Web     : https://zxq9.com/projects/zj/docs/
Repo    : https://gitlab.com/zxq9/zj
Tags    : ["json","json encoder","json decoder"]

Ah. Checking the website it is clear we can use this to decode JSON by simply calling zj:decode(JSON). Easy. So let’s add it to the project as a dependency and invoke it in src/termifier.erl:

ceverett@okonomiyaki:~/vcs$ cd termifier
ceverett@okonomiyaki:~/vcs/termifier$ zx set dep otpr-zj-1.0.5
Adding dep otpr-zj-1.0.5
ceverett@okonomiyaki:~/vcs/termifier$ cd src
ceverett@okonomiyaki:~/vcs/termifier/src$ vim termifier.erl

Inside termifier.erl we can see the templated code for start/1:

start(ArgV) ->
    ok = io:format("Hello, World! Args: ~tp~n", [ArgV]),
    zx:silent_stop().

Lets change it so it does what we want (note here I’m going a bit further and writing a function write_terms/2 based on an older post of mine — this performs the inverse procedure of file:consult/1):

start([InPath, OutPath]) ->
    {ok, Bin} = file:read_file(InPath),
    {ok, Terms} = zj:decode(Bin),
    ok = write_terms(OutPath, Terms),
    zx:silent_stop();
start(_) ->
    ok = io:format("ERROR: Two arguments are required."),
    zx:silent_stop().


write_terms(Path, Terms) when is_list(Terms) ->
    Format = fun(Term) -> io_lib:format("~tp.~n", [Term]) end,
    Text = lists:map(Format, Terms),
    file:write_file(Path, Text);
write_terms(Path, Term) ->
    write_terms(Path, [Term]).

Note that we are calling zj:decode/1 in the body of start/1 above, knowing that ZX will find it for us and configure the environment at execution time. And now let’s give it a go!

ceverett@okonomiyaki:~/vcs$ zx rundir termifier example.json example.eterms
Recompile: src/zj
Recompile: src/termifier
ceverett@okonomiyaki:~/vcs$ cat example.json 
{
    "fruit": "Apple",
    "size": "Large",
    "color": "Red"
}
ceverett@okonomiyaki:~/vcs$ cat example.eterms 
{"color" => "Red","fruit" => "Apple","size" => "Large"}.

From here I could run zx package termifier, submit it to a realm (either the default public realm, or a new one I can create and host myself by doing zx create realm and then zx run zomp), and anyone could then run the command zx run termifier [in path] [out path] and ZX will take care of finding and building the necessary packages and launching the program.

That’s all there is to it. ZX’s template for CLI applications is quite minimal (as you can see) and is more similar to an escript than a more traditional OTP-style, supervised Erlang application. ZX has templates, however, for full-blown OTP applications, GUI code (structured also in the OTP paradigm), minimalist CLI programs like we see above, pure library code, and escripts (sometimes escript is exactly the tool you need!).

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.