lib/owl/system.ex

defmodule Owl.System do
  @moduledoc """
  An alternative to some `System` functions.
  """
  require Logger

  @doc """
  A wrapper around `System.cmd/3` which additionally logs executed `command` and `args`.

  If URL is found in logged message, then password in it is masked with asterisks.

  ## Examples

      > Owl.System.cmd("echo", ["test"])
      # 10:25:34.252 [debug] $ echo test
      {"test\\n", 0}

      > Owl.System.cmd("psql", ["postgresql://postgres:postgres@127.0.0.1:5432", "-tAc", "SELECT 1;"])
      # 10:25:50.947 [debug] $ psql postgresql://postgres:********@127.0.0.1:5432 -tAc 'SELECT 1;'
      {"1\\n", 0}

  """
  @spec cmd(binary(), [binary()], keyword()) ::
          {Collectable.t(), exit_status :: non_neg_integer()}
  def cmd(command, args, opts \\ []) do
    log_shell_command(command, args)
    System.cmd(command, args, opts)
  end

  defp log_shell_command(command, args) do
    command =
      case args do
        [] ->
          command

        args ->
          args =
            Enum.map_join(args, " ", fn arg ->
              if String.contains?(arg, [" ", ";"]) do
                "'" <> String.replace(arg, "'", "'\\''") <> "'"
              else
                arg
              end
            end)

          "#{command} #{args}"
      end

    command = sanitize_passwords_in_urls(command)

    Logger.debug("$ #{command}")
  end

  defp sanitize_passwords_in_urls(text) do
    Regex.replace(~r/\w+:\/\/[^ ]+/, text, fn value ->
      uri = URI.parse(value)

      case uri.userinfo do
        nil ->
          value

        userinfo ->
          [username, _password] = String.split(userinfo, ":")
          to_string(%{uri | userinfo: username <> ":********"})
      end
    end)
  end
end