defmodule ExCheck.Command do @moduledoc false def run(command, opts \\ []) do command |> async(opts) |> await() end def async([exec | args], opts) do stream_fn = parse_stream_option(opts) cd = Keyword.get(opts, :cd, ".") exec_path = resolve_exec_path(exec, cd) env = opts |> Keyword.get(:env, %{}) |> Enum.map(fn {n, v} -> {String.to_charlist(n), String.to_charlist(v)} end) spawn_opts = [ :stream, :binary, :exit_status, :hide, :use_stdio, :stderr_to_stdout, args: args, cd: cd, env: env ] Task.async(fn -> start_time = DateTime.utc_now() port = Port.open({:spawn_executable, exec_path}, spawn_opts) handle_port(port, stream_fn, "", opts[:silenced], start_time) end) end def unsilence(task = %Task{pid: pid}) do send(pid, :unsilence) task end def await(task, timeout \\ :infinity) do {output, status, stream_fn, silenced, duration} = Task.await(task, timeout) if silenced, do: stream_fn.(output) {output, status, duration} end @ansi_code_regex ~r/(\x1b\[[0-9;]*m)/ defp parse_stream_option(opts) do case Keyword.get(opts, :stream) do true -> if Keyword.get(opts, :tint) && IO.ANSI.enabled?() do fn output -> output |> String.replace(@ansi_code_regex, "\\1" <> IO.ANSI.faint()) |> IO.write() end else &IO.write/1 end falsy when falsy in [nil, false] -> fn _ -> nil end func when is_function(func) -> func end end defp resolve_exec_path(exec, cd) do cond do Path.type(exec) == :absolute -> exec File.exists?(Path.join(cd, exec)) -> Path.join(cd, exec) |> Path.expand() path_to_exec = System.find_executable(exec) -> path_to_exec true -> raise("executable not found: #{exec}") end end defp handle_port(port, stream_fn, output, silenced, start_time) do receive do {^port, {:data, data}} -> data = if output == "", do: String.replace(data, ~r/^\s*/, ""), else: data unless silenced, do: stream_fn.(data) handle_port(port, stream_fn, output <> data, silenced, start_time) {^port, {:exit_status, status}} -> duration = DateTime.diff(DateTime.utc_now(), start_time) {output, status, stream_fn, silenced, duration} :unsilence -> stream_fn.(output) handle_port(port, stream_fn, output, false, start_time) end end end