Current section
Files
Jump to
Current section
Files
lib/helpful_options.ex
defmodule HelpfulOptions do
@moduledoc """
A project-specific command-line option parser
"""
require Logger
@help_left_column_width 29
@doc ~S"""
Parses command arguments, returns an error if
required arguments are not supplied and sets the logging level
iex> HelpfulOptions.run(["--foo", "hi"], switches: [foo: %{type: :string}])
{:ok, %{foo: "hi"}, []}
iex> HelpfulOptions.run(["-f", "hi"], switches: [foo: %{type: :string}], aliases: [f: :foo])
{:ok, %{foo: "hi"}, []}
iex> HelpfulOptions.run(["--bar", "hi"], switches: [foo: %{type: :string}])
{:error, "Unexpected parameters supplied: [\"--bar\"]"}
iex> HelpfulOptions.run(["non-switch"], remaining: 1)
{:ok, %{}, ["non-switch"]}
iex> HelpfulOptions.run(["pizza"])
{:error, "You supplied unexpected non-switch arguments [\"pizza\"]"}
iex> HelpfulOptions.run(["first", "second"], remaining: 2..3)
{:ok, %{}, ["first", "second"]}
iex> HelpfulOptions.run(["first"], remaining: 2)
{:error, "Supply 2 non-switch arguments"}
iex> HelpfulOptions.run(["first"], remaining: 2..3)
{:error, "Supply 2..3 non-switch arguments"}
iex> HelpfulOptions.help([foo: %{type: :boolean}])
"Options:\n --foo Optional parameter"
iex> HelpfulOptions.help([foo: %{type: :string}])
"Options:\n --foo=FOO Optional parameter"
iex> HelpfulOptions.help([foo: %{type: :string, required: true}])
"Options:\n --foo=FOO Required"
iex> HelpfulOptions.help([foo: %{type: :boolean, description: "Frobnicate"}])
"Options:\n --foo Frobnicate"
iex> HelpfulOptions.help([foo: %{type: :string, description: "Frobnicate"}])
"Options:\n --foo=FOO Frobnicate"
iex> HelpfulOptions.help([foo: %{type: :string, description: "Frobnicate", required: true}])
"Options:\n --foo=FOO Frobnicate. Required"
"""
def run(args, options \\ []) do
with {:ok, opts} <- options_map(options),
{:ok, named, remaining} <- parse(args, opts),
{:ok} <- check_required(named, opts),
{:ok} <- check_remaining(remaining, opts),
{:ok} <- setup_logger(named),
{:ok, filtered} <- remove_logging(named) do
{:ok, filtered, remaining}
else
{:error, message} ->
{:error, message}
end
end
def help(switches) do
items =
switches
|> Enum.map(&switch_help/1)
|> Enum.join("\n")
"Options:\n#{items}"
end
defp switch_help({name, options}) do
left_column = if options.type == :string do
parameter = name |> Atom.to_string() |> String.upcase()
" --#{name}=#{parameter} "
else
" --#{name} "
end
extra = @help_left_column_width - String.length(left_column)
padding = if extra > 0 do
String.duplicate(" ", extra)
else
""
end
right_column = if options[:description], do: [options.description], else: []
right_column = if options[:required], do: ["Required" | right_column], else: right_column
right_column = if length(right_column) == 0, do: ["Optional parameter"], else: right_column
right_column = right_column |> Enum.reverse() |> Enum.join(". ")
"#{left_column}#{padding}#{right_column}"
end
defp options_map(options), do: {:ok, Enum.into(options, %{})}
defp parse(args, opts) do
aliases = (opts[:aliases] || []) ++ [q: :quiet, v: :verbose]
switches = Keyword.merge((opts[:switches] || []), [quiet: %{type: :boolean}, verbose: %{type: :count}])
switches_keyword = switches_keyword(switches)
case OptionParser.parse(args, aliases: aliases, strict: switches_keyword) do
{named_list, remaining, []} ->
named = Enum.into(named_list, %{})
{:ok, named, remaining}
{_, _, invalid} ->
keys = Enum.map(invalid, fn {key, _value} -> key end)
{:error, "Unexpected parameters supplied: #{inspect(keys)}"}
end
end
defp switches_keyword(switches) do
Enum.map(switches, fn {name, %{type: type}} -> {name, type} end)
end
defp check_required(named, opts) do
missing =
(opts[:switches] || [])
|> Enum.map(fn
{name, %{required: true}} ->
if !Map.has_key?(named, name), do: name
_ ->
nil
end)
|> Enum.filter(&(&1))
if length(missing) == 0 do
{:ok}
else
{:error, "Please supply the following parameters: #{inspect(missing)}"}
end
end
defp check_remaining(remaining, %{remaining: %Range{} = range}) do
if length(remaining) in range do
{:ok}
else
{:error, "Supply #{inspect(range)} non-switch arguments"}
end
end
defp check_remaining(remaining, %{remaining: count})
when length(remaining) == count, do: {:ok}
defp check_remaining(_remaining, %{remaining: count}) do
{:error, "Supply #{count} non-switch arguments"}
end
defp check_remaining([], _options), do: {:ok}
defp check_remaining(remaining, _opts) do
{:error, "You supplied unexpected non-switch arguments #{inspect(remaining)}"}
end
defp setup_logger(named) do
verbose = Map.get(named, :verbose, 1)
quiet = Map.get(named, :quiet, false)
level = if quiet do
0
else
1 + verbose
end
level_atom =
case level do
0 -> :none
1 -> :info
_ -> :debug
end
Logger.configure([level: level_atom])
Logger.debug("Logger level set to #{level_atom}")
{:ok}
end
defp remove_logging(named) do
{
:ok,
named
|> Map.delete(:quiet)
|> Map.delete(:verbose)
}
end
end