Automating interactive Elixir evaluation via stdin

Suraj N. Kurapati

  1. Problem
    1. Approach
      1. Solution

        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!