# 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:
1. To get the output and the exit code, one uses the standard `System.cmd/3` and `System.shell/2` functions, like this:
```elixir
System.shell("ls -l")
```
But this does not allow us to see the output line by line as the command executes.
2. To get the output while the command is run, one can use the Mix helper `Mix.Shell.cmd/3`:
```elixir
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.
3. We **can** get the output from `Mix.Shell.cmd/3` like this:
```elixir
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
```elixir
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:**
```elixir
streamer = CollectableStreamer.new(fn line ->
Logger.info("deploy: #{String.trim(line)}")
end)
```
**Send lines to another process:**
```elixir
streamer = CollectableStreamer.new(fn line ->
send(pid, {:output, line})
end)
```
**Parse structured output:**
```elixir
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:
```elixir
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`:
```elixir
def deps do
[
{:collectable_streamer, "~> 0.2.1"}
]
end
```