I needed to read from a named pipe in an Elixir program, I made a mistake and I learned a few thing. Follow me.
First create the named pipe:
$ mkfifo "pipe"
In Erlang you read from it like this
$ erl 1> Fifo = open_port("pipe", [eof]). 2> receive 2> {Fifo, {data, Data}} -> 2> io:format("Got some data: ~p~n", [Data]) 2> end.
You can check that it works by executing echo hi > pipe
from another shell.
So I expected to able to write this in Elixir
$ iex iex(1)> fifo = Port.open("pipe", [:eof])
However this is what I get
** (ArgumentError) argument error :erlang.open_port({"pipe"}, [:eof])
If you can see my noob mistake in the call to Port.open
don’t tell anybody yet, read on.
After trying many permutations of the arguments and read twice both http://erlang.org/doc/man/erlang.html#open_port-2 and http://elixir-lang.org/docs/stable/elixir/Port.html I gave up. I resolved to write it as an Erlang module and call it from my Elixir project. It was pretty easy:
1) Create an erlang
directory in the main directory of the project. The name is not magical, you can name it as you wish.
2) Add this line into the project
options of mix.exs
erlc_paths: ["erlang"],
That’s the directory you created.
3) Create an erlang/namedpipe.erl
file with this code
-module(namedpipe). -export([read/1]). read(Pipe) -> Fifo = open_port(Pipe, [eof]), receive {Fifo, {data, Data}} -> Data end.
See how it can almost map 1 to 1 to Elixir. Variables are capitalized, symbols are lowercased and there are statement terminators (comma and full stop). All functions are private to the module except the explicitly exported ones.
4) Run mix
and see that it compiles the Erlang file. Great!
But now
$ iex iex(1)> :namedpipe.read("pipe") ** (ArgumentError) argument error :erlang.open_port("pipe", [:eof]) erlang/namedpipe.erl:5: :namedpipe.read/1
Oh oh, what’s going on? I finally realized that it’s because of the wrong quote character! Single quotes are needed when passing strings to Erlang. Single quotes in Elixir are character lists, which is what Erlang needs. Double quotes are UTF-8 encoded binary data, which Erlang doesn’t understand.
So this works:
$ iex iex(1)> :namedpipe.read('pipe')
Now go to another shell and run echo hi > pipe
and confirm that it works for you too.
But wait, I did use double quotes in my first failed Elixir attempt. So did I do all of this for nothing? Embarrassingly, yes. This works:
$ iex iex(1)> fifo = Port.open('pipe', [:eof]) iex(2)> receive do ...(2)> {fifo, {:data, data}} -> ...(2)> IO.puts("Got some data: #{data}") ...(2)> end
At least I learned how to embed Erlang code in an Elixir project and to care about single and double quotes.
Finally, you can embed that code in an Elixir module and call it both with a single quoted char list or a double quoted binary. Write this into lib/namedpipe.ex
defmodule NamedPipe do def read(pipe) when is_binary(pipe) do read(String.to_char_list(pipe)) end def read(pipe) do fifo = Port.open(pipe, [:eof]) receive do {fifo, {:data, data}} -> data end end end
It uses guards to decide which version of the read
function to call. Now
$ iex -S mix iex(1)> NamedPipe.read("pipe") 'hi\n' iex(2)> NamedPipe.read('pipe') 'hi\n'
Success!