defmodule CollectableStreamer do @moduledoc ~S""" A module for accessing command output line by line. ## Example iex> import ExUnit.CaptureIO iex> fun = fn line -> IO.write("Received line: #{line}") end iex> streamer = CollectableStreamer.new(fun) iex> capture_io(fn -> System.cmd("echo", ["Hello, World!"], into: streamer) end) "Received line: Hello, World!\n" """ @behaviour Collectable defstruct [:fun, collect: true, buffer: []] @doc """ Creates a new CollectableStreamer. ## Options * `:collect` - If set to `false`, the streamer will not collect all lines into a buffer, this may be useful for long-running commands where you want to process lines on the fly without consuming memory. Default is `true`. """ def new(fun, opts \\ []) when is_function(fun, 1) do collect = Keyword.get(opts, :collect, true) %__MODULE__{fun: fun, collect: collect} end def into(streamer) do { streamer, fn streamer, {:cont, item} -> streamer.fun.(item) if streamer.collect do buffer = [item | streamer.buffer] %{streamer | buffer: buffer} else streamer end streamer, :done -> if streamer.collect do buffer = Enum.reverse(streamer.buffer) %{streamer | buffer: buffer} else streamer end _streamer, :halt -> :ok end } end defimpl Collectable do def into(streamer) do CollectableStreamer.into(streamer) end end defimpl String.Chars do def to_string(%CollectableStreamer{collect: true, buffer: buffer}) do IO.chardata_to_string(buffer) end def to_string(%CollectableStreamer{}) do "to_string(#{__MODULE__}): Collecting is disabled. No output available." end end end