defmodule Optimus do alias Optimus.Arg alias Optimus.Flag alias Optimus.Option defstruct [ :name, :description, :version, :author, :about, :allow_unknown_args, :parse_double_dash, :args, :flags, :options, :subcommands, :subcommand ] defmodule ParseResult do @type arg_value :: term @type flag_value :: boolean | pos_integer @type option_value :: term | [term] @type t :: %ParseResult{ args: %{atom => arg_value}, flags: %{atom => flag_value}, options: %{atom => option_value}, unknown: [String.t] } defstruct [ args: %{}, flags: %{}, options: %{}, unknown: [] ] end defmodule OptimusConfigurationError do defexception message: "invalid optimus configuration" end @type parser_result :: {:error, String.t} | {:ok, term} @type custom_parser :: (String.t -> parser_result) @type parser :: :integer | :float | :string | custom_parser @type arg_spec_item :: {:value_name, String.t} | {:help, String.t} | {:required, boolean} | {:parser, parser} @type arg_spec :: [arg_spec_item] @type flag_spec_item :: {:short, String.t} | {:long, String.t} | {:help, String.t} | {:multiple, boolean} @type flag_spec :: [flag_spec_item] @type option_spec_item :: {:value_name, String.t} | {:short, String.t} | {:long, String.t} | {:help, String.t} | {:multiple, boolean} | {:required, boolean} | {:parser, parser} @type option_spec :: [option_spec_item] @type spec_item :: {:name, String.t} | {:name, String.t} | {:version, String.t} | {:author, String.t} | {:about, String.t} | {:allow_unknown_args, boolean} | {:parse_double_dash, boolean} | {:args, [arg_spec]} | {:flags, [flag_spec]} | {:options, [option_spec]} @type spec :: [spec_item] @type error :: String.t @opaque t :: %Optimus{} @spec new(spec) :: {:ok, t} | {:error, [error]} def new(props) do props |> set_default_name |> Optimus.Builder.build end @spec new!(spec) :: t | no_return def new!(props) do case new(props) do {:ok, optimus} -> optimus {:error, error} -> raise OptimusConfigurationError, message: "invalid optimus configuration: #{error}" end end @spec from_yaml(filename :: String.t) :: {:ok, t} | {:error, error} def from_yaml(filename) do with {:ok, spec} <- Optimus.YamlLoader.load(filename), do: new(spec) end @spec from_yaml(filename :: String.t) :: t | no_return def from_yaml!(filename) do case from_yaml(filename) do {:ok, optimus} -> optimus {:error, error} -> raise OptimusConfigurationError, message: "invalid optimus configuration: #{inspect error}" end end @type subcommand_path :: [atom] @spec parse(t, [String.t]) :: {:ok, ParseResult.t} | {:ok, subcommand_path, ParseResult.t} | {:error, [error]} | {:error, subcommand_path, [error]} | :version | :help | {:help, subcommand_path} def parse(optimus, command_line) do with :ok <- validate_command_line(command_line), :ok <- parse_specials(optimus, command_line), {sub_optimus, subcommand_path, sub_command_line} <- find_subcommand(optimus, [], command_line), {parsed, errors, unknown} <- parse_all_kinds({sub_optimus.options, sub_optimus.flags, sub_optimus.args}, %{}, sub_command_line, [], []), errors_with_unknown <- validate_unknown(sub_optimus, unknown, errors), all_errors <- validate_required(sub_optimus, parsed, errors_with_unknown), do: parse_result(sub_optimus, subcommand_path, parsed, unknown, all_errors) end @spec parse!(t, [String.t], (integer -> no_return)) :: ParseResult.t | {subcommand_path, ParseResult.t} | no_return def parse!(optimus, command_line, halt \\ &System.halt/1) do case parse(optimus, command_line) do {:ok, parse_result} -> parse_result {:ok, subcommand_path, parse_result} -> {subcommand_path, parse_result} {:error, errors} -> optimus |> Optimus.Errors.format(errors) |> put_lines halt.(1) {:error, subcommand_path, errors} -> optimus |> Optimus.Errors.format(subcommand_path, errors) |> put_lines halt.(1) :version -> optimus |> Optimus.Title.title |> put_lines halt.(0) :help -> optimus |> Optimus.Help.help([], columns()) |> put_lines halt.(0) {:help, subcommand_path} -> optimus |> Optimus.Help.help(subcommand_path, columns()) |> put_lines halt.(0) end end def help(optimus) do optimus |> Optimus.Help.help([], columns()) |> Enum.join("\n") end defp columns do case :io.columns do {:ok, width} -> width _ -> 80 end end defp put_lines(lines) do lines |> Enum.map(&IO.puts/1) end defp parse_specials(_, ["--version"]), do: :version defp parse_specials(_, ["--help"]), do: :help defp parse_specials(optimus, ["help" | subcommand]) do case find_exact_subcommand(optimus, subcommand) do [_ | _] = subcommand_path -> {:help, subcommand_path} _ -> {:error, ["invalid subcommand: #{Enum.join(subcommand, " ")}"]} end end defp parse_specials(_, _), do: :ok defp find_exact_subcommand(optimus, subcommand, subcommand_path \\ []) defp find_exact_subcommand(_, [], found_path), do: Enum.reverse(found_path) defp find_exact_subcommand(optimus, [name | rest], found_path) do case Enum.find(optimus.subcommands, &(name == &1.name)) do %Optimus{subcommand: subcommand} = sub_optimus -> find_exact_subcommand(sub_optimus, rest, [subcommand | found_path]) _ -> :error end end def fetch_subcommand(optimus, subcommand_path), do: fetch_subcommand(optimus, subcommand_path, [optimus.name]) def fetch_subcommand(optimus, [], subcommand_name), do: {optimus, Enum.reverse(subcommand_name)} def fetch_subcommand(optimus, [subcommand_id | subcommand_path], subcommand_name) do subcommand = Enum.find(optimus.subcommands, &(subcommand_id == &1.subcommand)) fetch_subcommand(subcommand, subcommand_path, [subcommand.name | subcommand_name]) end # private functions defp set_default_name(props) do if Keyword.keyword?(props) && props[:name] == nil do Keyword.put(props, :name, to_string(:escript.script_name)) else props end end defp get_arg(parsed, arg) do Map.get(parsed, {:arg, arg.name}) end defp get_flag(parsed, flag) do case flag do %Flag{multiple: true} -> Map.get(parsed, {:flag, flag.name}, 0) _ -> if Map.get(parsed, {:flag, flag.name}), do: true, else: false end end defp get_option(parsed, option) do case option do %Option{multiple: true} -> default_value = if is_nil(option.default) do [] else option.default end parsed |> Map.get({:option, option.name}, default_value) |> Enum.reverse _ -> default_value = if is_nil(option.default) do nil else option.default end case Map.get(parsed, {:option, option.name}, default_value) do [val|_] -> val _ -> default_value end end end defp validate_command_line(command_line) when is_list(command_line) do if Enum.all?(command_line, &is_binary/1) do :ok else {:error, "list of strings expected"} end end defp validate_command_line(_), do: {:error, "list of strings expected"} defp find_subcommand(optimus, path, []), do: {optimus, Enum.reverse(path), []} defp find_subcommand(optimus, path, [item | items] = command_line) do case Enum.find(optimus.subcommands, &(item == &1.name)) do %Optimus{} = sub_optimus -> find_subcommand(sub_optimus, [sub_optimus.subcommand | path], items) nil -> {optimus, Enum.reverse(path), command_line} end end @end_of_flags_and_options "--" defp parse_all_kinds(_, parsed, [], errors, unknown), do: {parsed, Enum.reverse(errors), Enum.reverse(unknown)} defp parse_all_kinds({options, flags, args} = parsers, parsed, [item | rest_items] = items, errors, unknown) do if item == @end_of_flags_and_options do parse_args(args, parsed, rest_items, errors, unknown) else case Option.try_match(options, parsed, items) do {:ok, new_parsed, new_items} -> parse_all_kinds(parsers, new_parsed, new_items, errors, unknown) {:error, error, new_items} -> parse_all_kinds(parsers, parsed, new_items, [error| errors], unknown) :skip -> case Flag.try_match(flags, parsed, items) do {:ok, new_parsed, new_items} -> parse_all_kinds(parsers, new_parsed, new_items, errors, unknown) {:error, error, new_items} -> parse_all_kinds(parsers, parsed, new_items, [error| errors], unknown) :skip -> case args do [arg | other_args] -> case Arg.parse(arg, parsed, items) do {:ok, new_parsed, new_items} -> parse_all_kinds({options, flags, other_args}, new_parsed, new_items, errors, unknown) {:error, error, new_items} -> parse_all_kinds({options, flags, other_args}, parsed, new_items, [error| errors], unknown) end [] -> parse_all_kinds(parsers, parsed, rest_items, errors, [item | unknown]) end end end end end defp parse_args([arg | args], parsed, [_ | _] = items, errors, unknown) do case Arg.parse(arg, parsed, items) do {:ok, new_parsed, new_items} -> parse_args(args, new_parsed, new_items, errors, unknown) {:error, error, new_items} -> parse_args(args, parsed, new_items, [error| errors], unknown) end end defp parse_args([], parsed, [item | rest], errors, unknown), do: parse_args([], parsed, rest, errors, [item | unknown]) defp parse_args(_, parsed, [], errors, unknown), do: {parsed, Enum.reverse(errors), Enum.reverse(unknown)} defp validate_unknown(_optimus, [], errors), do: errors defp validate_unknown(optimus, unknown, errors) do if optimus.allow_unknown_args do errors else error = "unrecognized arguments: #{unknown |> Enum.map(&inspect/1) |> Enum.join(", ")}" [error | errors] end end defp validate_required(optimus, parsed, []) do missing_required_args = optimus.args |> Enum.reject(&Map.has_key?(parsed, {:arg, &1.name})) |> Enum.filter(&(&1.required)) |> Enum.map(&Optimus.Format.format_in_error(&1)) missing_required_options = optimus.options |> Enum.reject(&Map.has_key?(parsed, {:option, &1.name})) |> Enum.filter(&(&1.required)) |> Enum.map(&Optimus.Format.format_in_error(&1)) required_arg_error = case missing_required_args do [] -> [] _ -> ["missing required arguments: #{missing_required_args |> Enum.join(", ")}"] end required_option_error = case missing_required_options do [] -> [] _ -> ["missing required options: #{missing_required_options |> Enum.join(", ")}"] end required_arg_error ++ required_option_error end defp validate_required(_optimus, _parsed, errors), do: errors defp parse_result(optimus, subcommand_path, parsed, unknown, []) do args = optimus.args |> Enum.map(fn(arg) -> {arg.name, get_arg(parsed, arg)} end) |> Enum.into(%{}) flags = optimus.flags |> Enum.map(fn(flag) -> {flag.name, get_flag(parsed, flag)} end) |> Enum.into(%{}) options = optimus.options |> Enum.map(fn(option) -> {option.name, get_option(parsed, option)} end) |> Enum.into(%{}) parse_result = %ParseResult{args: args, flags: flags, options: options, unknown: unknown} case subcommand_path do [] -> {:ok, parse_result} [_|_] -> {:ok, subcommand_path, parse_result} end end defp parse_result(_optimus, subcommand_path, _parsed, _unknown, errors) do case subcommand_path do [] -> {:error, errors} [_|_] -> {:error, subcommand_path, errors} end end end defimpl Optimus.Format, for: Optimus do def format(optimus), do: optimus.name def format_in_error(optimus), do: optimus.name def format_in_usage(optimus), do: optimus.name def help(optimus), do: optimus.about || "" end