lib/ex_cmd.ex

defmodule ExCmd do
  @moduledoc """
  ExCmd is an Elixir library to run and communicate with external
  programs with back-pressure.
  """

  @doc """
  Runs the given command with arguments and return an Enumerable to
  read command output.

  First parameter must be a list containing command with
  arguments. example: `["cat", "file.txt"]`.

  ### Optional

    * `input` - Input can be binary, iodata, `Enumerable` (list, stream)
       or a function which accepts `Collectable`.

      * Binary
        ```
        # binary
        ExCmd.stream!(~w(base64), input: "Hello Wolrd")
        ```

      * Enumerable or iodata

        ```
        # list
        ExCmd.stream!(~w(base64), input: ["hello", "world"])

        # iodata
        ExCmd.stream!(~w(cat), input: ["He", ["llo"], [' ', ?W], ['or', "ld"]])

        # stream
        ExCmd.stream!(~w(cat), input: File.stream!("log.txt", [], 65536))
        ```

      * Collectable:

        If the input in a function with arity 1, ExCmd will call that
        function with a `Collectable` as the argument. The function
        must *push* input to this collectable. Return value of the
        function is ignored.

        ```
        ExCmd.stream!(~w(cat), input: fn sink ->
          Enum.into(1..100, sink, &to_string/1)
        end)
        ```

    * `exit_timeout` - Time to wait for external program to exit.
      After writing all of the input we close the stdin stream and
      wait for external program to finish and exit. If program does
      not exit within `exit_timeout` an error will be raised and
      subsequently program will be killed in background. Defaults
      timeout is `:infinity`.

  All other options are passed to `ExCmd.Process.start_link/2`

  ## Examples

  ```
  ExCmd.stream!(~w(ffmpeg -i pipe:0 -f mp3 pipe:1), input: File.stream!("music_video.mkv", [], 65336))
  |> Stream.into(File.stream!("music.mp3"))
  |> Stream.run()
  ```

  With input as binary

  ```
  iex> ExCmd.stream!(~w(cat), input: "Hello World")
  ...> |> Enum.into("")
  "Hello World"
  ```

  ```
  iex> ExCmd.stream!(~w(base64), input: <<1, 2, 3, 4, 5>>)
  ...> |> Enum.into("")
  "AQIDBAU=\n"
  ```

  With input as list

  ```
  iex> ExCmd.stream!(~w(cat), input: ["Hello ", "World"])
  ...> |> Enum.into("")
  "Hello World"
  ```

  With input as iodata

  ```
  iex> ExCmd.stream!(~w(base64), input: [<<1, 2,>>, [3], [<<4, 5>>]])
  ...> |> Enum.into("")
  "AQIDBAU=\n"
  ```

  When program exit abnormally it will raise
  `ExCmd.Stream.AbnormalExit` error with exit_status.

  ```
  iex> try do
  ...>   ExCmd.stream!(["sh", "-c", "exit 5"]) |> Enum.to_list()
  ...> rescue
  ...>   e in ExCmd.Stream.AbnormalExit ->
  ...>    e.exit_status
  ...> end
  5
  ```
  """
  @type collectable_func() :: (Collectable.t() -> any())

  @spec stream!(nonempty_list(String.t()),
          input: Enum.t() | collectable_func(),
          exit_timeout: timeout(),
          cd: String.t(),
          env: [{String.t(), String.t()}],
          log: boolean()
        ) :: ExCmd.Stream.t()
  def stream!(cmd_with_args, opts \\ []) do
    ExCmd.Stream.__build__(cmd_with_args, opts)
  end
end