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!