Use proper case for proper nouns (e.g., Elixir, Rebar, Git, and others) as well as acronyms (e.g., URL, HTTP, and others).
456 lines
12 KiB
Elixir
456 lines
12 KiB
Elixir
defmodule Kernel.CLI do
|
|
@moduledoc false
|
|
|
|
@blank_config %{commands: [], output: ".", compile: [],
|
|
halt: true, compiler_options: [], errors: [],
|
|
pa: [], pz: [], verbose_compile: false}
|
|
|
|
@doc """
|
|
This is the API invoked by Elixir boot process.
|
|
"""
|
|
def main(argv) do
|
|
argv = for arg <- argv, do: IO.chardata_to_string(arg)
|
|
|
|
{config, argv} = parse_argv(argv)
|
|
System.argv(argv)
|
|
|
|
run fn _ ->
|
|
errors = process_commands(config)
|
|
|
|
if errors != [] do
|
|
Enum.each(errors, &IO.puts(:stderr, &1))
|
|
System.halt(1)
|
|
end
|
|
end, config.halt
|
|
end
|
|
|
|
@doc """
|
|
Runs the given function by catching any failure
|
|
and printing them to stdout. `at_exit` hooks are
|
|
also invoked before exiting.
|
|
|
|
This function is used by Elixir's CLI and also
|
|
by escripts generated by Elixir.
|
|
"""
|
|
def run(fun, halt \\ true) do
|
|
{ok_or_shutdown, status} = exec_fun(fun, {:ok, 0})
|
|
if ok_or_shutdown == :shutdown or halt do
|
|
{_, status} = at_exit({ok_or_shutdown, status})
|
|
|
|
# Ensure Logger messages are flushed before halting
|
|
case :erlang.whereis(Logger) do
|
|
pid when is_pid(pid) -> Logger.flush()
|
|
_ -> :ok
|
|
end
|
|
|
|
System.halt(status)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def parse_argv(argv) do
|
|
parse_argv(argv, @blank_config)
|
|
end
|
|
|
|
@doc false
|
|
def process_commands(config) do
|
|
results = Enum.map(Enum.reverse(config.commands), &process_command(&1, config))
|
|
errors = for {:error, msg} <- results, do: msg
|
|
Enum.reverse(config.errors, errors)
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp at_exit(res) do
|
|
hooks = :elixir_config.get_and_put(:at_exit, [])
|
|
res = Enum.reduce(hooks, res, &exec_fun/2)
|
|
if hooks == [], do: res, else: at_exit(res)
|
|
end
|
|
|
|
defp exec_fun(fun, res) when is_function(fun, 1) and is_tuple(res) do
|
|
parent = self()
|
|
|
|
{pid, ref} =
|
|
spawn_monitor(fn ->
|
|
try do
|
|
fun.(elem(res, 1))
|
|
catch
|
|
:exit, {:shutdown, int} when is_integer(int) ->
|
|
send parent, {self, {:shutdown, int}}
|
|
exit({:shutdown, int})
|
|
:exit, reason
|
|
when reason == :normal
|
|
when reason == :shutdown
|
|
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
|
|
send parent, {self, {:shutdown, 0}}
|
|
exit(reason)
|
|
kind, reason ->
|
|
stack = System.stacktrace
|
|
print_error(kind, reason, stack)
|
|
send parent, {self, {:shutdown, 1}}
|
|
exit(to_exit(kind, reason, stack))
|
|
else
|
|
_ ->
|
|
send parent, {self, res}
|
|
end
|
|
end)
|
|
|
|
receive do
|
|
{^pid, res} ->
|
|
:erlang.demonitor(ref, [:flush])
|
|
res
|
|
{:DOWN, ^ref, _, _, other} ->
|
|
print_error({:EXIT, pid}, other, [])
|
|
{:shutdown, 1}
|
|
end
|
|
end
|
|
|
|
defp to_exit(:throw, reason, stack), do: {{:nocatch, reason}, stack}
|
|
defp to_exit(:error, reason, stack), do: {reason, stack}
|
|
defp to_exit(:exit, reason, _stack), do: reason
|
|
|
|
defp shared_option?(list, config, callback) do
|
|
case parse_shared(list, config) do
|
|
{[h | hs], _} when h == hd(list) ->
|
|
new_config = %{config | errors: ["#{h} : Unknown option" | config.errors]}
|
|
callback.(hs, new_config)
|
|
{new_list, new_config} ->
|
|
callback.(new_list, new_config)
|
|
end
|
|
end
|
|
|
|
defp print_error(kind, reason, trace) do
|
|
IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
|
|
end
|
|
|
|
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
|
|
:elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
|
|
:elixir_def, :elixir_map]
|
|
|
|
defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
|
|
prune_stacktrace(t)
|
|
end
|
|
|
|
defp prune_stacktrace([{__MODULE__, :wrapper, 1, _} | _]) do
|
|
[]
|
|
end
|
|
|
|
defp prune_stacktrace([h | t]) do
|
|
[h | prune_stacktrace(t)]
|
|
end
|
|
|
|
defp prune_stacktrace([]) do
|
|
[]
|
|
end
|
|
|
|
# Parse shared options
|
|
|
|
defp parse_shared([opt | _t], _config) when opt in ["-v", "--version"] do
|
|
if function_exported?(IEx, :started?, 0) and IEx.started? do
|
|
IO.puts "IEx " <> System.build_info[:build]
|
|
else
|
|
IO.puts :erlang.system_info(:system_version)
|
|
IO.puts "Elixir " <> System.build_info[:build]
|
|
end
|
|
|
|
System.halt 0
|
|
end
|
|
|
|
defp parse_shared(["-pa", h | t], config) do
|
|
paths = expand_code_path(h)
|
|
Enum.each(paths, &:code.add_patha/1)
|
|
parse_shared t, %{config | pa: config.pa ++ paths}
|
|
end
|
|
|
|
defp parse_shared(["-pz", h | t], config) do
|
|
paths = expand_code_path(h)
|
|
Enum.each(paths, &:code.add_pathz/1)
|
|
parse_shared t, %{config | pz: config.pz ++ paths}
|
|
end
|
|
|
|
defp parse_shared(["--app", h | t], config) do
|
|
parse_shared t, %{config | commands: [{:app, h} | config.commands]}
|
|
end
|
|
|
|
defp parse_shared(["--no-halt" | t], config) do
|
|
parse_shared t, %{config | halt: false}
|
|
end
|
|
|
|
defp parse_shared(["-e", h | t], config) do
|
|
parse_shared t, %{config | commands: [{:eval, h} | config.commands]}
|
|
end
|
|
|
|
defp parse_shared(["-r", h | t], config) do
|
|
parse_shared t, %{config | commands: [{:require, h} | config.commands]}
|
|
end
|
|
|
|
defp parse_shared(["-pr", h | t], config) do
|
|
parse_shared t, %{config | commands: [{:parallel_require, h} | config.commands]}
|
|
end
|
|
|
|
defp parse_shared([erl, _ | t], config) when erl in ["--erl", "--sname", "--name", "--cookie", "--logger-otp-reports", "--logger-sasl-reports"] do
|
|
parse_shared t, config
|
|
end
|
|
|
|
defp parse_shared([erl | t], config) when erl in ["--detached", "--hidden", "--werl"] do
|
|
parse_shared t, config
|
|
end
|
|
|
|
defp parse_shared(list, config) do
|
|
{list, config}
|
|
end
|
|
|
|
defp expand_code_path(path) do
|
|
path = Path.expand(path)
|
|
case Path.wildcard(path) do
|
|
[] -> [to_charlist(path)]
|
|
list -> Enum.map(list, &to_charlist/1)
|
|
end
|
|
end
|
|
|
|
# Process init options
|
|
|
|
defp parse_argv(["--" | t], config) do
|
|
{config, t}
|
|
end
|
|
|
|
defp parse_argv(["+elixirc" | t], config) do
|
|
parse_compiler t, config
|
|
end
|
|
|
|
defp parse_argv(["+iex" | t], config) do
|
|
parse_iex t, config
|
|
end
|
|
|
|
defp parse_argv(["-S", h | t], config) do
|
|
{%{config | commands: [{:script, h} | config.commands]}, t}
|
|
end
|
|
|
|
defp parse_argv([h | t] = list, config) do
|
|
case h do
|
|
"-" <> _ ->
|
|
shared_option? list, config, &parse_argv(&1, &2)
|
|
_ ->
|
|
if Keyword.has_key?(config.commands, :eval) do
|
|
{config, list}
|
|
else
|
|
{%{config | commands: [{:file, h} | config.commands]}, t}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp parse_argv([], config) do
|
|
{config, []}
|
|
end
|
|
|
|
# Parse compiler options
|
|
|
|
defp parse_compiler(["--" | t], config) do
|
|
{config, t}
|
|
end
|
|
|
|
defp parse_compiler(["-o", h | t], config) do
|
|
parse_compiler t, %{config | output: h}
|
|
end
|
|
|
|
defp parse_compiler(["--no-docs" | t], config) do
|
|
parse_compiler t, %{config | compiler_options: [{:docs, false} | config.compiler_options]}
|
|
end
|
|
|
|
defp parse_compiler(["--no-debug-info" | t], config) do
|
|
parse_compiler t, %{config | compiler_options: [{:debug_info, false} | config.compiler_options]}
|
|
end
|
|
|
|
defp parse_compiler(["--ignore-module-conflict" | t], config) do
|
|
parse_compiler t, %{config | compiler_options: [{:ignore_module_conflict, true} | config.compiler_options]}
|
|
end
|
|
|
|
defp parse_compiler(["--warnings-as-errors" | t], config) do
|
|
parse_compiler t, %{config | compiler_options: [{:warnings_as_errors, true} | config.compiler_options]}
|
|
end
|
|
|
|
defp parse_compiler(["--verbose" | t], config) do
|
|
parse_compiler t, %{config | verbose_compile: true}
|
|
end
|
|
|
|
defp parse_compiler([h | t] = list, config) do
|
|
case h do
|
|
"-" <> _ ->
|
|
shared_option? list, config, &parse_compiler(&1, &2)
|
|
_ ->
|
|
pattern = if File.dir?(h), do: "#{h}/**/*.ex", else: h
|
|
parse_compiler t, %{config | compile: [pattern | config.compile]}
|
|
end
|
|
end
|
|
|
|
defp parse_compiler([], config) do
|
|
{%{config | commands: [{:compile, config.compile} | config.commands]}, []}
|
|
end
|
|
|
|
# Parse IEx options
|
|
|
|
defp parse_iex(["--" | t], config) do
|
|
{config, t}
|
|
end
|
|
|
|
# This clause is here so that Kernel.CLI does not
|
|
# error out with "unknown option"
|
|
defp parse_iex(["--dot-iex", _ | t], config) do
|
|
parse_iex t, config
|
|
end
|
|
|
|
defp parse_iex([opt, _ | t], config) when opt in ["--remsh"] do
|
|
parse_iex t, config
|
|
end
|
|
|
|
defp parse_iex(["-S", h | t], config) do
|
|
{%{config | commands: [{:script, h} | config.commands]}, t}
|
|
end
|
|
|
|
defp parse_iex([h | t] = list, config) do
|
|
case h do
|
|
"-" <> _ ->
|
|
shared_option? list, config, &parse_iex(&1, &2)
|
|
_ ->
|
|
{%{config | commands: [{:file, h} | config.commands]}, t}
|
|
end
|
|
end
|
|
|
|
defp parse_iex([], config) do
|
|
{config, []}
|
|
end
|
|
|
|
# Process commands
|
|
|
|
defp process_command({:cookie, h}, _config) do
|
|
if Node.alive? do
|
|
wrapper fn -> Node.set_cookie(String.to_atom(h)) end
|
|
else
|
|
{:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"}
|
|
end
|
|
end
|
|
|
|
defp process_command({:eval, expr}, _config) when is_binary(expr) do
|
|
wrapper fn -> Code.eval_string(expr, []) end
|
|
end
|
|
|
|
defp process_command({:app, app}, _config) when is_binary(app) do
|
|
case Application.ensure_all_started(String.to_atom(app)) do
|
|
{:error, {app, reason}} ->
|
|
{:error, "--app : Could not start application #{app}: " <>
|
|
Application.format_error(reason)}
|
|
{:ok, _} ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp process_command({:script, file}, _config) when is_binary(file) do
|
|
if exec = find_elixir_executable(file) do
|
|
wrapper fn -> Code.require_file(exec) end
|
|
else
|
|
{:error, "-S : Could not find executable #{file}"}
|
|
end
|
|
end
|
|
|
|
defp process_command({:file, file}, _config) when is_binary(file) do
|
|
if File.regular?(file) do
|
|
wrapper fn -> Code.require_file(file) end
|
|
else
|
|
{:error, "No file named #{file}"}
|
|
end
|
|
end
|
|
|
|
defp process_command({:require, pattern}, _config) when is_binary(pattern) do
|
|
files = filter_patterns(pattern)
|
|
|
|
if files != [] do
|
|
wrapper fn -> Enum.map files, &Code.require_file(&1) end
|
|
else
|
|
{:error, "-r : No files matched pattern #{pattern}"}
|
|
end
|
|
end
|
|
|
|
defp process_command({:parallel_require, pattern}, _config) when is_binary(pattern) do
|
|
files = filter_patterns(pattern)
|
|
|
|
if files != [] do
|
|
wrapper fn -> Kernel.ParallelRequire.files(files) end
|
|
else
|
|
{:error, "-pr : No files matched pattern #{pattern}"}
|
|
end
|
|
end
|
|
|
|
defp process_command({:compile, patterns}, config) do
|
|
# If ensuring the dir returns an error no files will be found.
|
|
_ = :filelib.ensure_dir(:filename.join(config.output, "."))
|
|
|
|
case filter_multiple_patterns(patterns) do
|
|
{:ok, []} ->
|
|
{:error, "No files matched provided patterns"}
|
|
{:ok, files} ->
|
|
wrapper fn ->
|
|
Code.compiler_options(config.compiler_options)
|
|
opts =
|
|
if config.verbose_compile do
|
|
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 5s)")]
|
|
else
|
|
[]
|
|
end
|
|
Kernel.ParallelCompiler.files_to_path(files, config.output, opts)
|
|
end
|
|
{:missing, missing} ->
|
|
{:error, "No files matched pattern(s) #{Enum.join(missing, ",")}"}
|
|
end
|
|
end
|
|
|
|
defp filter_patterns(pattern) do
|
|
pattern
|
|
|> Path.wildcard
|
|
|> :lists.usort
|
|
|> Enum.filter(&File.regular?/1)
|
|
end
|
|
|
|
defp filter_multiple_patterns(patterns) do
|
|
matched_files = Enum.map patterns, fn(pattern) ->
|
|
case filter_patterns(pattern) do
|
|
[] -> {:missing, pattern}
|
|
files -> {:ok, files}
|
|
end
|
|
end
|
|
|
|
files = Enum.filter_map matched_files,
|
|
fn(match) -> elem(match, 0) == :ok end,
|
|
&elem(&1, 1)
|
|
|
|
missing_patterns = Enum.filter_map matched_files,
|
|
fn(match) -> elem(match, 0) == :missing end,
|
|
&elem(&1, 1)
|
|
|
|
if missing_patterns == [] do
|
|
{:ok, :lists.usort(Enum.concat(files))}
|
|
else
|
|
{:missing, :lists.usort(missing_patterns)}
|
|
end
|
|
end
|
|
|
|
defp wrapper(fun) do
|
|
_ = fun.()
|
|
:ok
|
|
end
|
|
|
|
defp find_elixir_executable(file) do
|
|
if exec = System.find_executable(file) do
|
|
# If we are on Windows, the executable is going to be
|
|
# a .bat file that must be in the same directory as
|
|
# the actual Elixir executable.
|
|
case :os.type() do
|
|
{:win32, _} ->
|
|
base = Path.rootname(exec)
|
|
if File.regular?(base), do: base, else: exec
|
|
_ ->
|
|
exec
|
|
end
|
|
end
|
|
end
|
|
end
|