defmodule Pinchflat.Utils.CliUtils do @moduledoc """ Utility methods for working with CLI executables """ require Logger alias Pinchflat.Utils.StringUtils @doc """ Wraps a command in a shell script that will terminate the command if stdin is closed. Useful for stopping commands if the job runner is cancelled. Delegates to `System.cmd/3` and any options/output are passed through. Custom options can be passed in. Custom options: - logging_arg_override: if set, the passed value will be logged in place of the actual arguments passed to the command Returns {binary(), integer()} """ def wrap_cmd(command, args, passthrough_opts \\ [], opts \\ []) do wrapper_command = Path.join(:code.priv_dir(:pinchflat), "cmd_wrapper.sh") actual_command = [command] ++ args command_opts = set_command_opts() ++ passthrough_opts logging_arg_override = Keyword.get(opts, :logging_arg_override, Enum.join(args, " ")) Logger.info("[command_wrapper]: #{command} called with: #{logging_arg_override}") {output, status} = System.cmd(wrapper_command, actual_command, command_opts) log_cmd_result(command, logging_arg_override, status, output) {output, status} end @doc """ Parses a list of command options into a list of strings suitable for passing to `System.cmd/3`. We want to satisfy the following behaviours: 1. If the key is an atom, convert it to a string and convert it to kebab case (for convenience) 2. If the key is a string, assume we want it as-is and don't convert it 3. If the key is accompanied by a value, append the value to the list 4. If the key is not accompanied by a value, assume it's a flag and PREpend it to the list Returns [binary()] """ def parse_options(command_opts) do command_opts |> List.wrap() |> Enum.reduce([], &parse_option/2) end defp parse_option({k, v}, acc) when is_atom(k) do stringified_key = StringUtils.to_kebab_case(Atom.to_string(k)) parse_option({"--#{stringified_key}", v}, acc) end defp parse_option({k, v}, acc) when is_binary(k) do acc ++ [k, to_string(v)] end defp parse_option(arg, acc) when is_atom(arg) do stringified_arg = arg |> Atom.to_string() |> StringUtils.to_kebab_case() parse_option("--#{stringified_arg}", acc) end defp parse_option(arg, acc) when is_binary(arg) do acc ++ [arg] end defp log_cmd_result(command, logging_arg_override, status, output) do log_message = "[command_wrapper]: #{command} called with: #{logging_arg_override} exited: #{status} with: #{output}" log_level = if status == 0, do: :debug, else: :error Logger.log(log_level, log_message) end defp set_command_opts do # This resolves an issue where yt-dlp would attempt to write to a read-only directory # if you scanned a new video with `--windows-filenames` enabled. Hopefully can be removed # in the future. [ cd: Application.get_env(:pinchflat, :tmpfile_directory) ] end end