DEV Community

Tiziano
Tiziano

Posted on

Password input in Elixir

I was looking for a function to get user input without echoing on the screen, for example to get a password from the command line interface. I didn't find any function in the IO Elixir library and the erlang :io.password does not work while running a Mix Task. As suggested in this Stackoverflow answer I went to look how the hex tasks implements this feature.

Processes on the rescue

The approach seems a bit hacky at first, but quite effective. The idea is to spawn a process while the user is typing and clearing the row every millisecond. After the user has typed, the process is stopped by sending an appropriate message.

Here you can see the full code snippet, extracted from the hex repository See this pull request.

defmodule Get do
  # Password prompt that hides input by every 1ms
  # clearing the line with stderr
  def password(prompt) do
    pid = spawn_link(fn -> loop(prompt) end)
    ref = make_ref()
    value = IO.gets("#{prompt} ")

    send(pid, {:done, self(), ref})
    receive do: ({:done, ^pid, ^ref} -> :ok)

    value
  end

  defp loop(prompt) do
    receive do
      {:done, parent, ref} ->
        send(parent, {:done, self(), ref})
        IO.write(:standard_error, "\e[2K\r")
    after
      1 ->
        IO.write(:standard_error, "\e[2K\r#{prompt} ")
        loop(prompt)
    end
  end
end

First a process is spawned and the newly created process enters in a loop. Every millisecond, the following string is printed on the screen: "\e[2K\r". This is an XTerm control sequence that erases all the line (ESC [ 2K) and then brings back the cursor at the beginning of the row (\r).

After the value is read from the command line, a message is sent to the spawned process to signal the end of input. The process then ends the loop and signal back to the parent that he has been closed.

I think that in this code snippet there are some interesting tips:

  • the clear of the row happens by writing on the :standard_error and not on the :standard_input. I think this is because the :standard_input is blocking on receiving what the user is typing
  • a couple of messages are exchanged between the parent and the child process: in particular the parent waits for the child to acknowledge that he has finished. Likely this is to protect from the corner case where the last character(s) are not cleared up (the last IO.write in the :done block) and from the case where the child process clears the next prompt line (receive block in parent process).
  • make_ref is used to create a unique reference and used as a "uniqueness" flag when sending messages.

Top comments (0)