File: shell.ex

package info (click to toggle)
erlang-hex 2.0.6-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,204 kB
  • sloc: erlang: 2,950; sh: 203; makefile: 10
file content (127 lines) | stat: -rw-r--r-- 2,997 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
defmodule Hex.Shell do
  @moduledoc false

  def info(output) do
    validate_output!(output)
    Mix.shell().info(output)
  end

  def warn(output) do
    validate_output!(output)
    Mix.shell().info([IO.ANSI.yellow(), output, IO.ANSI.reset()])
  end

  def error(output) do
    validate_output!(output)
    Mix.shell().error(output)
  end

  def debug(output) do
    validate_output!(output)

    if Mix.debug?() do
      info(output)
    end
  end

  def yes?(output) do
    validate_output!(output)
    Mix.shell().yes?(output)
  end

  def prompt(output) do
    validate_output!(output)
    Mix.shell().prompt(output)
  end

  def format(output, emit? \\ Hex.Shell.ansi_enabled?()) do
    IO.ANSI.format(output, emit?)
  end

  def cmd(command, options \\ [], callback) when is_function(callback, 1) do
    callback =
      if Keyword.get(options, :quiet, false) do
        fn x -> x end
      else
        callback
      end

    env = validate_env(Keyword.get(options, :env, []))

    args =
      if Keyword.get(options, :stderr_to_stdout, true) do
        [:stderr_to_stdout]
      else
        []
      end

    opts = [:stream, :binary, :exit_status, :hide, :use_stdio, {:env, env} | args]
    port = Port.open({:spawn, shell_command(command)}, opts)
    port_read(port, callback)
  end

  defp port_read(port, callback) do
    receive do
      {^port, {:data, data}} ->
        _ = callback.(data)
        port_read(port, callback)

      {^port, {:exit_status, status}} ->
        status
    end
  end

  # Finding shell command logic from :os.cmd in OTP
  # https://github.com/erlang/otp/blob/8deb96fb1d017307e22d2ab88968b9ef9f1b71d0/lib/kernel/src/os.erl#L184
  defp shell_command(command) do
    case :os.type() do
      {:unix, _} ->
        command =
          command
          |> String.replace("\"", "\\\"")
          |> String.to_charlist()

        ~c"sh -c \"" ++ command ++ ~c"\""

      {:win32, osname} ->
        command = ~c"\"" ++ String.to_charlist(command) ++ ~c"\""

        case {System.get_env("COMSPEC"), osname} do
          {nil, :windows} -> ~c"command.com /s /c " ++ command
          {nil, _} -> ~c"cmd /s /c " ++ command
          {cmd, _} -> ~c"#{cmd} /s /c " ++ command
        end
    end
  end

  defp validate_env(enum) do
    Enum.map(enum, fn
      {k, nil} ->
        {String.to_charlist(k), false}

      {k, v} ->
        {String.to_charlist(k), String.to_charlist(v)}

      other ->
        raise ArgumentError, "invalid environment key-value #{inspect(other)}"
    end)
  end

  if Mix.env() == :test do
    defp validate_output!(output) do
      formatted_output = output |> IO.ANSI.format_fragment(true) |> IO.chardata_to_string()

      unless String.printable?(formatted_output) do
        raise ArgumentError, "string not printable"
      end
    end
  else
    defp validate_output!(_output), do: :ok
  end

  if Mix.env() == :test do
    def ansi_enabled?(), do: false
  else
    def ansi_enabled?(), do: IO.ANSI.enabled?()
  end
end