Tuesday, October 12, 2010

Console Applications in Erlang

This tutorial is brought to you by ErlangCamp 2010 - Chicago, October 23 and 24 - already at 95% capacity! It's gonna be totally sweet.
Erlang is probably not the first language you'd think of for building console applications. Here's a typical "Hello World" application in Erlang:
-module(hello).

-export([hello/0]).

hello() ->
io:format("Hello World!~n").
After compiling the module, you'd run it as an application like this:
$ erl -run hello hello -run init stop -noshell
Hello World!
Whoa, that's a lot of work just to print a simple message to standard output!

Here's the same thing in Python:
#!/usr/bin/python
print "Hello World!"
And using it:
python hello.py
Now that's more like it! It's no surprise that script languages like Python and Perl are used extensively to build console applications.

So why bother using Erlang for this sort of thing? Erlang's core strength is handling extreme concurrency problems and building long running, fault tolerant applications. Surely you'd be better off sticking to Python, Perl, or even bash!

Actually, that thinking is basically correct. If you're competent in a scripting language, you probably want to start there. But consider Erlang for these reasons:
  • You're using Erlang for other applications and want to avoid introducing another runtime dependency for your console apps
  • You're a True Believer in functional languages and want to extend the goodness to your scripts
  • You need to communicate with Erlang nodes or work with Erlang persisted terms (e.g. config files)
  • You want to brag to your chums that you're an Erlang hipster and have entered the ranks of the cool kids!
Pretty powerful arguments. The good news is that there's no reason to forego Erlang just because it's reputed strengths lie elsewhere.

Improving Erlang's Hello World

Enough strategy - let's fix that fugly "Hello World" app! Create a file named "hello" that looks like this:
#!/usr/bin/env escript
main(_) ->
io:format("Hello World!~n").
Not as drop-dead simple as the Python version, but pretty close.

Let's run it:
$ escript hello
Hello World!
If you want to execute it directly, change its permission:
$ chmod 755 hello
$ ./hello
Hello World!
Nice!

The secret here is escript, which is installed with the standard Erlang distribution. If you can run erl, you can run escript. By using the shebang as the first line of the script, we turned this simple file into an bona fide executable Erlang application!

At this point, we have all we need to write Erlang console applications. If you want to automate something on a system and have a hankering to use some Erlang, this is how you'd do it.

Super Charging Erlang's Hello World

Let's take things further and carve out a full fledged console application famework. It's simple!

Grab the latest getopt Erlang source from github:
http://github.com/jcomellas/getopt
This is a terrific module that lets you parse command line arguments into validated Erlang terms using the nearly ubiquitous getopt convention.

What's getopt? If you've ever run an application from a command line, you've probably already seen the convention. Here's a quick summary:
  • Command line options are differentiated from command line arguments
  • By convention, options are always, well, optional
  • Arguments may be optional but are frequently required
  • Options are designated by either a leading single-dash "-" (short form) or a double-dash "--" (long form)
  • Short form options are always a single character
  • Options may have values, which follow the option name and a space (or alternatively an equals sign "=" for long forms)
Ah heck, it's probably easier to just look at an example. Here's classic getopt:
$ man ls
Now that's the sort of high quality interface we want for our Erlang console apps!

Let's tweak our "Hello World" app with a some new features:
  • Support for a custom message
  • Align the message - left, right, or center - within a particular number of spaces
  • Print help/usage info if we ask for it
This is what our help screen should look like after we're done:
$ ./hello --help
Usage: hello [-a ] [-w ] [-h ] [message]

-a, --align alignment: left (default) | right | center
-w, --width width used by alignment (79)
-h, --help display this help and exit
message message to print (Hello World!)

Prints a message to standard output, aligning it with a
particular number of spaces (width).
"Too much work" you say! Fear not - with the getopt module, it's really simple.

First, we need a specification that getopt will use to parse command line argument. We'll add the following Erlang macro to the hello script:
-define(SPEC,
[{align, $a, "align", atom,
"alignment: left (default) | right | center"},
{width, $w, "width", {integer, 79},
"width used by alignment (79)"},
{help, $h, "help", boolean,
"display this help and exit"}]).
You can read more about specifications in the getopt module documentation. The spec tells getopt what to expect in terms of arguments. This is used for both parsing the arguments and for printing usage documentation.

In our case, we have three options: one specifying the alignment, one for the width, and another for printing the program help. Each option has:
  • A name, used to identify parsed values
  • A short form (char) and long form (string) of the option
  • A type and, optionally, a default value for the option
  • Help text
With our spec in hand, let's modify the script's main function:
main(Args) ->
case getopt:parse(?SPEC, Args) of
{ok, {Opts, MsgParts}} ->
maybe_help(Opts),
Msg = case MsgParts of
[] -> "Hello World!";
_ -> string:join(MsgParts, " ")
end,
Align = proplists:get_value(align, Opts),
Width = proplists:get_value(width, Opts),
io:format("~s~n", [format(Msg, Align, Width)]);
{error, _} -> usage(1)
end.
There are several functions that we still need to define, but the core application logic is all there.

The function first parses the arguments passed on the command line. getopt:parse/2 returns {ok, {Options, Arguments}} if the command line args comply with the specification. Otherwise, it returns {error, Reason}. In main/1, we print a message given validated input or display the program usage if there are problems.

Once the arguments are parsed, getting the user input is a simple matter of reading from Options (a propery list - see Erlang's proplists module for details) and from Arguments (a list of non-option arguments). Values provided by the user are converted to the expected type and missing values are filled in with default values from the spec.

Next, let's define usage/1.
usage(Exit) ->
getopt:usage(
?SPEC, "hello", "[message]",
[{"message", "message to print (Hello World!)"}]),
case Exit of
0 ->
io:format("Prints a message to standard output, "
"aligning it with a particular number "
"of\nspaces (width).\n");
_ -> ok
end,
erlang:halt(Exit).
Here we use getopt:usage/4 to print the expected usage of the program to standard output. The 4-arity variant lets us specify additional help text for the usage. We also print detailed help text if the application is exiting normally (exit code is 0). If the application is exiting abnormally (e.g. the input from the user is invalid), we just display the usage. Finally, we terminate the application using erlang:halt/1.

Our next function is maybe_help/1:
maybe_help(Opts) ->
case proplists:get_bool(help, Opts) of
true -> usage(0);
false -> ok
end.
This function checks for the help option and calls usage/1 if it was specified.

Here's how we format the message:
format(_, _, Width) when Width < 0 -> error("invalid width");
format(Msg, undefined, Width) -> string:left(Msg, Width);
format(Msg, left, Width) -> string:left(Msg, Width);
format(Msg, right, Width) -> string:right(Msg, Width);
format(Msg, center, Width) -> string:centre(Msg, Width);
format(_, _, _) -> error("invalid align option").
This is a dense bit of code, but it's very simple. It formats a message using one of the alignment functions in the string module. If there are problems with the input, it uses error/1 to complain:
error(Msg) ->
io:format("ERROR: ~s~n", [Msg]),
erlang:halt(1).
Pretty straight forward - print a message and exit with a non-zero value, indicating that an error occurred.

That's it! We have a strangely sophisticated "Hello World" application - and it's written in Erlang! Who'd have thunk?

Here's the complete hello source.

Let's try it out.
$ ./hello --align=center --width=40 You looking at me?
You looking at me?
Worked as advertised! You're encouraged to try it our for yourself. Can you handle the power??

Feel free to this script as a template for your own console applications.

Recap

We started with the user interface. It's a good idea to build your console applications around "usage" documentation. We kept the required inputs to a minimum (zero actually) relying on defaults to fill in values the user doesn't care about. The getopt scheme works perfectly for this.

We always handle the --help option when provided (i.e. maybe_help/1) by printing the full usage, including any detailed documentation, and exiting.

We leveraged the goodness of functional decomposition and Erlang's pattern matching to write clean and maintainable code.

One finally point: escript applications have access to all of Erlang's core modules and any user defined modules that are in the Erlang path. You're also free to dynamically modify the path to link to your custom modules (see the code module). Take full advantage of this by building the core of your application as compiled Erlang modules and use escript code to call into them.

Now, using your new powers, go out, write and deploy Erlang console applications throughout the world - may our jobs as Erlang developers be duly secured!

No comments: