defmodule Cmder do @moduledoc """ Cmder is a runner for generic commands. ## Profiles You can define multiple cmder profiles. config :cmder, tailwindcss: [ cmd: "tailwindcss", path: Path.expand("../assets/node_modules/.bin", __DIR__), args: ~w(--input=css/app.css --output=../priv/static/assets/app.css --postcss), cd: Path.expand("../assets", __DIR__) env: %{"NODE_ENV" => "development"} ], cpx: [ prevent_zombie: true, path: Path.expand("../assets/node_modules/.bin", __DIR__), args: ~w(static/**/* ../priv/static), cd: Path.expand("../assets", __DIR__) ] If no `cmd` is given the profile name is being used as command. Zombie processes can be prevented by adding 'prevent_zombie' key with a truthy value. On systems with bash available the script if [Ports|https://hexdocs.pm/elixir/Port.html#module-zombie-operating-system-processes] will be used as wrapper. """ use Application require Logger @doc false def start(_, _) do Supervisor.start_link([], strategy: :one_for_one) end @doc """ Runs the given profile with `args`. The given args will be appended to the configured args. The task output will be streamed directly to stdio. It returns the status of the underlying call. """ def run(profile, extra_args) when is_atom(profile) and is_list(extra_args) do config = profile |> config_for!() |> maybe_wrap_in_zombie() cmd = config[:cmd] args = config[:args] opts = [ cd: config[:cd] || File.cwd!(), env: config[:env] || %{}, into: IO.stream(:stdio, :line), stderr_to_stdout: true ] cmd |> bin_path(config) |> System.cmd(args ++ extra_args, opts) |> elem(1) end defp config_for!(profile) when is_atom(profile) do config = Application.get_env(:cmder, profile) || raise ArgumentError, """ unknown cmder profile. Make sure the profile is defined in your config/config.exs file, such as: config :cmder, #{profile}: [ cmd: "#{profile}" path: "/usr/bin", args: ~w(--input=css/app.css --output=../priv/static/assets/app.css --postcss), cd: Path.expand("../assets", __DIR__), env: %{"NODE_ENV" => "development"} ] """ config |> Keyword.put_new(:cmd, Atom.to_string(profile)) |> Keyword.put_new(:args, []) end defp maybe_wrap_in_zombie(config) do if System.find_executable("bash") && config[:prevent_zombie] do config |> Keyword.put(:args, [bin_path(config[:cmd], config) | config[:args]]) |> Keyword.put(:cmd, "zombie.sh") |> Keyword.put(:path, :code.priv_dir(:cmder)) else config end end defp bin_path(cmd, config) do path = config[:path] || "" Path.join(path, cmd) end end