CollectableStreamer
A Collectable that lets you process the output of System.cmd/3 and System.shell/2 line by line, while still receiving the full output and exit code when the command finishes.
Motivation
In the Elixir standard library, this behaviour is nearly achievable:
-
To get the output and the exit code, one uses the standard
System.cmd/3andSystem.shell/2functions, like this:
System.shell("ls -l")But this does not allow us to see the output line by line as the command executes.
-
To get the output while the command is run, one can use the Mix helper
Mix.Shell.cmd/3:
Mix.Shell.cmd("ls -l", [], fn x -> IO.puts(x) end)This allows us to print the output, line by line, but it returns just the exit code. We don't get the output afterwards.
-
We can get the output from
Mix.Shell.cmd/3like this:
ExUnit.CaptureIO.with_io(fn -> Mix.Shell.cmd("ls -l", [], fn x -> IO.puts(x) end) end)But we're back where we started because we don't see the output line by line!
To have both the ability to "see" the output as it happens and get the output and exit code afterwards, you can use CollectableStreamer.
Usage
streamer = CollectableStreamer.new(fn line -> IO.puts("Received: #{line}") end)
{result, exit_code} = System.cmd("rsync", ["-av", "foo@example.com:/source/", "/destination/"], into: streamer)
# result is a %CollectableStreamer{} — use to_string/1 to get the collected output
IO.puts("Exit code: #{exit_code}")
IO.puts("Full output:\n#{result}")Callback examples
The callback receives each line of output as a string. Besides printing, you can use it to:
Log progress for a long-running task:
streamer = CollectableStreamer.new(fn line ->
Logger.info("deploy: #{String.trim(line)}")
end)Send lines to another process:
streamer = CollectableStreamer.new(fn line ->
send(pid, {:output, line})
end)Parse structured output:
streamer = CollectableStreamer.new(fn line ->
case String.split(line, ",") do
[timestamp, level, message | _] ->
process_log_entry(timestamp, level, message)
_ ->
:skip
end
end)Disabling output collection
By default, all output lines are collected in memory so they are available after the command finishes. For long-running commands where you only need to process lines as they arrive, you can disable collection:
streamer = CollectableStreamer.new(fn line -> IO.puts(line) end, collect: false)
{_result, exit_code} = System.shell("tail -f /var/log/syslog", into: streamer)Installation
The package can be installed by adding collectable_streamer to your list of
dependencies in mix.exs:
def deps do
[
{:collectable_streamer, "~> 0.2.1"}
]
end