Elixir script, what can and can't be done

shmink

Tom Nicklin

Posted on July 29, 2022

Elixir script, what can and can't be done

Outside of mix.exs I haven't engaged with .exs files much. I imagine that might be the same for some of you. However, I ran into a situation recently where I needed to.

I wanted my elixir project in a docker container, so that when running it would go straight into the terminal awaiting user input; collect the input line by line and return a list of all of those lines when an empty line is given. Creating an elixir script as the ENTRYPOINT for the program felt like the right thing to do. However, I struggled to find any substantial documentation and/or examples to understand the limitations of what can and cannot be done with elixir script.

After some trial and error, I found some bits that worked and started iterating from there until I had something I could use for my project. What follows are examples of where the code does the same thing but in different formats and layouts. Side note, for code snippets from the terminal, I've named the file start.exs

Also, we get to have a look at IO.stream (docs) for anyone else that might have ran into the same issue as me.

You can define functions!

In hindsight (which is always 20/20) this makes sense but for whatever reason it didn't occur to me that you can do this. The only difference here compared to a normal elixir module is that you call the function at the end of the file.

defmodule Lines do
  def run(lines \\ []) do
    case IO.read(:stdio, :line) do
      "\n" ->
        # done reading lines!
        lines
        |> Enum.reverse()
        |> Enum.map(&String.replace(&1, "\n", ""))
        |> IO.inspect()

      line ->
        run([line | lines])
    end
  end
end

Lines.run()
Enter fullscreen mode Exit fullscreen mode
$ elixir start.exs
hi
there

["hi", "there"]
Enter fullscreen mode Exit fullscreen mode

No thanks, I prefer my functions anonymous.

Here's another approach, if for whatever reason you didn't want to define modules or functions.

IO.stream(:stdio, :line)
|> Enum.reduce_while([], fn line, lines ->
  case line do
    "\n" -> {:halt, lines |> Enum.reverse()}
    line -> {:cont, [line | lines]}
  end
end)
|> Enum.map(&String.replace(&1, "\n", ""))
|> IO.inspect()
Enter fullscreen mode Exit fullscreen mode
$ elixir start.exs
hi
there

["hi", "there"]
Enter fullscreen mode Exit fullscreen mode

Time for my final form

IO.stream(:stdio, :line)
|> Enum.take_while(&(&1 != "\n"))
|> Enum.map(&String.replace(&1, "\n", ""))
|> IO.inspect()
Enter fullscreen mode Exit fullscreen mode
$ elixir start.exs
hi
there

["hi", "there"]
Enter fullscreen mode Exit fullscreen mode

Conclusion

The point of this article, wasn't so much the code but I think I struggled to get my head around scoping issues after only ever dealing with traditional elixir code. Hopefully, if you're in a similar situation, you can now see that there is more flexibility than you might have originally thought.

In regards to IO.stream docs. I made a PR to make some amendments.

💖 💪 🙅 🚩
shmink
Tom Nicklin

Posted on July 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related