I started playing around with different ways of extending the Erlang shell. The reasons, as if a reason were needed to start hacking around, were as follows:

  • When writing his book, Joe Armstrong received a lot of frustrated comments about why it wasn’t possible to declare a module directly in the Erlang shell
  • Having watched Richard Carlsson’s presentation on the array module at EUC 07, I thought I’d try to implement a way to clean up some of the output from the shell.
  • John Hughes commented that it would be really nice (for QuickCheck users) to be able to use macros in the shell.

Understanding the shell

The Erlang shell is line-oriented and expression-oriented. That last bit may warrant some explanation.

Erlang has two levels of grammar: forms and expressions. A form is a module-level construct, and is either a module attribute (e.g. ‘-module(m).’, ‘-record(r, {a,b}).’, etc.) or a function declaration. Expressions are one level underneath: the arguments of a function, and the function body, consist of expressions.

Macros are neither forms nor expressions. They are pre-processor directives, and are understood only by the Erlang pre-processor, epp. Once the form or expression reaches the parser, macros have already been replaced by their corresponding tokens. A macro definition does not have to be syntactically complete on its own, so we may not even be able to trick the parser into understanding it.

This all makes it a bit tricky to support record- and macro definitions in the shell. Recall that neither are expressions, and the shell only deals with expressions.

Still, the shell (sort of) understands records already. It provides a set of built-in functions for importing, defining and listing records:


rd(R,D) -- define a record
rf() -- remove all record information
rf(R) -- remove record information about R
rl() -- display all record information
rl(R) -- display record information about R
rp(Term) -- display Term using the shell's record information
rr(File) -- read record information from File (wildcards allowed)
rr(F,R) -- read selected record information from file(s)
rr(F,R,O) -- read selected record information with options

A quirk that can be particularly annoying at times is that, when inserting a multi-line command, the shell repeats the prompt at each continuation line:


3> F = fun(X) ->
3>           X + 2
3>     end.
#Fun<erl_eval .6.72228031>

This makes it difficult to copy a command from the shell, and reuse it later.

Extended pretty-printing

The Erlang pretty-printer uses a few tricks to try to make the output a bit easier on the eye. It tries to print lists of bytes as text strings, and, if records have been defined in the shell, formats records using record syntax. It also tries to indent tagged tuples for better readability.


5> {tag, "element 2 of the tuple", "element 3 of the tuple", "element 4 of the tuple"}.
{tag,"element 2 of the tuple",
     "element 3 of the tuple",
     "element 4 of the tuple"}
6>
rd(r, {a,b}).
r
7> #r{a = 17, b = [x]}.
#r{a = 17,b = [x]}

This is done using an undocumented function called io_lib_pretty:print(Term, Column, LineLength, Depth, RecDefFun). The fun RecDefFun is called when the pretty-printer finds a tagged record, in order to determine whether it is a known record.

My extension

I made the function io_lib_pretty:print/5 a bit more generic, so that it could be provided a function that recognizes any type of data structure and describes a compact representation. I also extended the shell with functions to load such funs (output filters):


fa(Tag, Fun) - registers a filter fun
fl() - lists filter funs
fr(Tag) - removes a filter fun

To illustrate, let’s look at the dict module. It is a fine module, implementing a functional hash table with O(1) access. However, when pretty-printed, a dict data structure looks pretty ugly:


1> dict:from_list([{N,a} || N < - lists:seq(1,5)]).
{dict,5,
     16,
     16,
     8,
     80,
     48,
     {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
     {{[],
       [[3|a]],
       [],
       [],
       [],
       [],
       [[2|a]],
       [[5|a]],
       [],
       [],
       [],
       [[1|a]],
       [[4|a]],
       [],
       [],
       []}}}

An output filter that recognizes a dict structure would look like this:


fun(#dict{}=D) ->
       {custom,dict,true,dict:dict_to_list(D)};
   (_) ->
       no
end

And using the new shell functionality, we would see this:


Eshell V5.5.4 (abort with ^G)
1>
rr(code:which(dict)).
[dict]
2>
fa(dict,fun(#dict{}=D) -> {custom,dict,true,dict:dict_to_list(D)}; (_) -> no end).
true
3>
dict:from_list([{N,a} || N <- lists:seq(1,5)]).
<|dict:[{3,a},{2,a},{5,a},{1,a},{4,a}]|>

The shell will still apply the record pretty printing filter, if none of the user-provided filters match, but the user-provided filters are tried first, in alphabetical order (by tag). The following filter might be used to clean up the output from xmerl:


4> rr(code:which(xmerl)).
[xmerl_event,
...,
xmlText]
7>
fa(xml,fun(#xmlElement{}=E) -> {custom,xml,true,xmerl_lib:simplify_element(E)}; (_) -> no end).
true
8>
xmerl_scan:file("/home/uwiger/dev/flex2/samples/restaurant/build.xml").
{<|xml:{project,[{name,"restaurant"},{default,"war"},{basedir,"."}],
                ["\n\t\t\n\t",
                 {target,[{name,"compile"}],
                         ["\n\t\t",
                          {javac,[{srcdir,"WEB-INF/src"},
                                  {destdir,"WEB-INF/classes"},
                                  {includes,"**/*.java"}],
                                 []},
                          "\n\t"]},
                 "\n\t\n\t",
                 {target,[{name,"war"},
                          {depends,"compile"},
                          {description,"Creates a WAR for running the web services"}],
                         ["\n\t\t",
                          {jar,[{destfile,"restaurant.war"},
                                {basedir,"${basedir}"},
                                {includes,"WEB-INF/**, recentReviews.jsp"}],
                               []},
                          "\n\t"]},
                 "\n\n"]}|>,
 []}

For reference, the build.xml file looks like this:


<project name="restaurant" default="war" basedir=".">
  <target name="compile">
    <javac srcdir="WEB-INF/src" destdir="WEB-INF/classes" includes="**/*.java"/>
  </target>
  <target name="war" depends="compile" description="Creates a WAR for running the web services">
    <jar destfile="restaurant.war" basedir="${basedir}" includes="WEB-INF/**, recentReviews.jsp"/>
  </target>
</project>

Next article will describe how to enter module declarations, macros and alternative syntax in the Erlang shell: