0

As practice, I am trying to write a small Elixir script that will take in user input from the terminal and write it to a file. My goal is to be able to accept multiple lines of user input from the terminal at once (followed by some terminator in this current iteration).

My current and relatively naive approach does work as far as this initial mission is concerned: I have a recursive get_line/2 function that will take a line of input, concatenate it to all input previously taken in, and then it will call itself. If the line of input it takes is the designated 'terminator' (":done\n" in this case) then it will write all the concatenated input into a file.

So this does work, but I wanted to know if there was a better way, as this is a considerably more complicated program vs one that reads a file into the terminal, which was just two lines or so really.

The code:

# Mission: find a way to accept multiple lines of input from the
# terminal at once, and write the input into an md file 'my_file.md'.

# Naive approach: read line-by-line in a loop until a terminator is
# reached.

defmodule Term2file do
  def get_input() do
    IO.gets("> ")
    |> get_line()
  end

  defp get_line(line, data \\ "") do
    case line do
      ":done\n" ->
        File.write!("./my_file.md", data, [:append])
      _ ->
        new_data = data <> line
        get_line(IO.gets("> "), new_data)
    end
  end
end

Term2file.get_input()

In an ideal world I would be able to accept input in a similar fashion to Discord (or some another message service), where newlines can be entered using shift-enter and using enter normally would 'complete' the message- allowing the entire input to be edited before entry, even across newlines. I think this might be unreasonable however, and feels like it would require a lot of "fighting" the nature of the terminal itself, but perhaps my intuition is wrong.

Regardless, I would like to know if I've over-complicated this solution, and if there is a simpler, or more straightforward/idiomatic approach that I missed.

Samuel Ludwig
  • 111
  • 1
  • 7
  • Check this: https://stackoverflow.com/questions/39805210/get-console-user-input-as-typed-char-by-char – Aleksei Matiushkin May 19 '19 at 18:41
  • 1
    Hmm, interesting, I didn't find this when looking before. Going off of the answers linked, it doesn't really look like there is a simple solution (in reference to the char-by-char approach). @AlekseiMatiushkin – Samuel Ludwig May 19 '19 at 19:06

1 Answers1

4

You can use streams to traverse the I/O device and write to the file:

defmodule Term2file do
  def get_input() do
    IO.stream(:stdio, :line)
    |> Stream.take_while(& &1 != ":done\n")
    |> Enum.into(File.stream!("./my_file.md", [:append]))
  end
end

Term2file.get_input()

This means you will write to the file as lines are entered. You also don't need to worry about managing the resources. The only downside is that you can't have a custom prompt (a PR that adds this an option though would be appreciated).

In an ideal world I would be able to accept input in a similar fashion to Discord (or some another message service), where newlines can be entered using shift-enter and using enter normally would 'complete' the message

AFAIK, this is not possible since shift-enter and enter are not distinct in any way to a terminal? But you can use Ctrl+D to send a EOF instruction, causing the IO.stream to terminate. This just works in the snippet above but you can also change your program to handle the Ctrl+D output accordingly.

José Valim
  • 50,409
  • 12
  • 130
  • 115
  • 1
    The man himself! Thank you very much, this is certainly a bit more concise, I had tried using `IO.stream` in a previous ill-fated attempt, but I hadn't known about the `Stream.take_while` function at the time. – Samuel Ludwig May 19 '19 at 20:04