Automating interactive Elixir evaluation via stdin
Problem
Newcomers to the Elixir language begin their
official instruction using the iex
tool,
which places the user in Elixir’s interactive mode.
Inside this mode, all of the Elixir commands the user types in are short-lived,
disappearing entirely (or only partially, if history is enabled) when the user quits the
iex
session.
$ iex
Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1]
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 40 + 2
42
iex(2)> "hello" <> " world"
"hello world"
iex(3)> ^C
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
a
To avoid repeating the manual labor of retyping all of their Elixir commands (or
reinserting them from saved history) when starting a new iex
session, the clever user naturally attempts to store all of their Elixir commands
in a file and to then run that file with iex
. This way, each line in the file
would be treated as if the user had manually typed it all in. :-) Problem
solved, right?
$ cat commands.txt
40 + 2
"hello" <> " world"
$ iex < commands.txt
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 42
iex(2)> "hello world"
iex(3)> ^C
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
Alas, iex
merely prints the results of the clever user’s Elixir commands. :-(
Whereas this whole method of interaction would be far more useful if iex
had
also printed each of the user’s Elixir commands along with their result, right?
$ iex-eval-stdin < commands.txt
Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1]
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 40 + 2
42
iex(2)> "hello" <> " world"
"hello world"
That would make it easier to learn Elixir, if both cause and effect were shown.
Approach
Since iex
is an interactive tool, it naturally expects to talk to the user’s
terminal device instead of the plain-text standard input and output streams.
For example, syntax highlighting the user’s Elixir command results only makes
sense when iex
is talking to a terminal device; not to the plain-text stdout.
Thus, I needed to create a fake terminal device for iex
to talk to, through
which I would send each of the user’s Elixir commands, read from stdin, to iex
.
Luckily, the Expect tool allows us to perform
exactly this kind of automation.
To start off, I wrote a Bourne shell function that generates an Expect script
from the contents of the user’s file (provided on stdin), taking care to escape
each line of input so that Expect would pass them through as-is to iex
:
iex_eval_stdin() {
{
echo spawn iex "$@"
echo 'expect "iex(*)> "'
while read -r line; do
echo "$line" | sed 's/["$\[\\]/\\&/g; s/^/send -- "/; s/$/\\r"/'
echo 'expect "???(*)> "'
done
} | expect -
echo -n -e '\033[2K\r' # clear the very last prompt
}
Once this prototype was working, I translated it into Expect to avoid escaping.
Solution
Below is my solution iex-eval-stdin
script, which is also available on
GitHub.
#!/usr/bin/expect -f
#
# Usage: iex-eval-stdin -- [ARGUMENTS_FOR_IEX...]
# Usage: iex-eval-stdin < YOUR_ELIXIR_SCRIPT_FILE
# Usage: echo YOUR_ELIXIR_SCRIPT | iex-eval-stdin
#
# Runs each line from the standard input stream using iex(1), as if
# each line were interactively typed into iex(1) in the first place.
#
# Written in 2018 by Suraj N. Kurapati <https://github.com/sunaku>
# and documented at <https://sunaku.github.io/iex-eval-stdin.html>
set prompt {\n(iex|\.\.\.)(\(\d+\)|\(.+@.+\)\d+)> $}
eval spawn -noecho iex $argv
while {[gets stdin line] >= 0} {
expect -re $prompt
send -- "$line\r"
}
expect -re $prompt ;# await last result
puts "\033\[1K" ;# clear last prompt
To install this script, copy/paste it into a file or download it from GitHub. Then, mark it as executable so that you can run it just by typing in its name:
$ chmod +x ./iex-eval-stdin
Now, you can run it: (if you don’t like typing the ./
, move it into your $PATH
)
$ echo '2 = 1 + 1' | ./iex-eval-stdin
Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1]
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 2 = 1 + 1
2
$ ./iex-eval-stdin < THE_FILESYSTEM_PATH_OF_YOUR_OWN_ELIXIR_SCRIPT_GOES_HERE
If you get an error saying that Expect couldn’t be found, try installing it:
$ sudo apt-get install expect
That’s all. :-) Enjoy learning Elixir!