diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex index 4dc747e5be1..0b4509cea1a 100644 --- a/lib/elixir/lib/option_parser.ex +++ b/lib/elixir/lib/option_parser.ex @@ -8,6 +8,10 @@ defmodule OptionParser do @type errors :: [{String.t, String.t | nil}] @type options :: [switches: Keyword.t, strict: Keyword.t, aliases: Keyword.t] + defmodule InvalidOptionError do + defexception [:message] + end + @doc """ Parses `argv` into a keywords list. @@ -64,6 +68,9 @@ defmodule OptionParser do If a switch can't be parsed, it is returned in the invalid options list. + If you want to raise an exception for all the invalid options, please use + `parse!/2`. + The following extra "types" are supported: * `:keep` - keeps duplicated items in the list instead of overriding them. @@ -117,6 +124,36 @@ defmodule OptionParser do do_parse(argv, compile_config(opts), [], [], [], true) end + @doc """ + The same as `parse/2` but raises an `OptionParser.InvalidOptionError` + exception if any invalid options are given. + + If there weren't any errors, returns a three-element tuple as follows: + + 1. parsed options, + 2. remaining arguments, + 3. empty list. + + ## Examples + + iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer]) + ** (OptionParser.InvalidOptionError) 1 error found! Option --limit is of the wrong type, expected a integer, given "xyz" + + iex> OptionParser.parse!(["--unknown", "xyz"], strict: []) + ** (OptionParser.InvalidOptionError) 1 error found! Unknown option --unknown + + iex> OptionParser.parse!(["-l", "xyz", "-f", "bar"], strict: [limit: :integer, foo: :integer], aliases: [l: :limit, f: :foo]) + ** (OptionParser.InvalidOptionError) 2 errors found! Option -l is of the wrong type, expected a integer, given "xyz". Option -f is of the wrong type, expected a integer, given "bar" + + """ + @spec parse!(argv, options) :: {parsed, argv, errors} | no_return + def parse!(argv, opts \\ []) when is_list(argv) and is_list(opts) do + case parse(argv, opts) do + {_parsed, _argv, []} = result -> result + {_parsed, _argv, errors} -> raise InvalidOptionError, message: format_errors(errors, opts) + end + end + @doc """ Similar to `parse/2` but only parses the head of `argv`; as soon as it finds a non-switch, it stops parsing. @@ -137,6 +174,32 @@ defmodule OptionParser do do_parse(argv, compile_config(opts), [], [], [], false) end + @doc """ + The same as `parse_head/2` but raises an `OptionParser.InvalidOptionError` + exception if any invalid options are given. + + If there weren't any errors, returns a three-element tuple as follows: + + 1. parsed options, + 2. remaining arguments, + 3. empty list. + + ## Examples + + iex> OptionParser.parse_head!(["--number", "lib", "test/enum_test.exs", "--verbose"], strict: [number: :integer]) + ** (OptionParser.InvalidOptionError) 1 error found! Option --number is of the wrong type, expected a integer, given "lib" + + iex> OptionParser.parse_head!(["--verbose", "true", "--source", "lib", "test/enum_test.exs", "--unlock"], strict: [verbose: :integer, source: :integer]) + ** (OptionParser.InvalidOptionError) 2 errors found! Option --verbose is of the wrong type, expected a integer, given "true". Option --source is of the wrong type, expected a integer, given "lib" + """ + @spec parse_head!(argv, options) :: {parsed, argv, errors} | no_return + def parse_head!(argv, opts \\ []) when is_list(argv) and is_list(opts) do + case parse_head(argv, opts) do + {_parsed, _argv, []} = result -> result + {_parsed, _argv, errors} -> raise InvalidOptionError, message: format_errors(errors, opts) + end + end + defp do_parse([], _config, opts, args, invalid, _all?) do {Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)} end @@ -144,7 +207,7 @@ defmodule OptionParser do defp do_parse(argv, {aliases, switches, strict}=config, opts, args, invalid, all?) do case next(argv, aliases, switches, strict) do {:ok, option, value, rest} -> - # the option exist and it was successfully parsed + # the option exists and it was successfully parsed kinds = List.wrap Keyword.get(switches, option) new_opts = do_store_option(opts, option, value, kinds) do_parse(rest, config, new_opts, args, invalid, all?) @@ -499,4 +562,28 @@ defmodule OptionParser do defp negative_number?(arg) do match?({_, ""}, Float.parse(arg)) end + + defp format_errors(errors, opts) do + details = Enum.map_join(errors, ". ", &format_error(&1, opts)) + total = Enum.count(errors) + error = if total == 1, do: "error", else: "errors" + "#{total} #{error} found! #{details}" + end + + defp format_error({option, nil}, _) do + "Unknown option #{option}" + end + + defp format_error({option, value}, opts) do + option_key = option |> String.lstrip(?-) |> String.to_atom() + + type = + if option_alias = opts[:aliases][option_key] do + opts[:strict][option_alias] + else + opts[:strict][option_key] + end + + "Option #{option} is of the wrong type, expected a #{type}, given #{inspect value}" + end end diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs index 69cf93969c7..4096c3bb516 100644 --- a/lib/elixir/test/elixir/option_parser_test.exs +++ b/lib/elixir/test/elixir/option_parser_test.exs @@ -198,6 +198,27 @@ defmodule OptionParserTest do == {[source: "from_docs/"], [], [{"--doc", nil}]} end + test "parse!/2 raise an exception for an unknown option using strict" do + assert_raise OptionParser.InvalidOptionError, "1 error found! Unknown option --doc", fn -> + args = ["--source", "from_docs/", "--doc", "show"] + OptionParser.parse!(args, strict: [source: :string, docs: :string]) + end + end + + test "parse!/2 raise an exception when an option is of the wrong type" do + assert_raise OptionParser.InvalidOptionError, fn -> + args = ["--bad", "opt", "foo", "-o", "bad", "bar"] + OptionParser.parse!(args, switches: [bad: :integer]) + end + end + + test "parse_head!/2 raise an exception when an option is of the wrong type" do + assert_raise OptionParser.InvalidOptionError, "1 error found! Option --number is of the wrong type, expected a integer, given \"lib\"", fn -> + args = ["--number", "lib", "test/enum_test.exs"] + OptionParser.parse_head!(args, strict: [number: :integer]) + end + end + test ":switches with :strict raises" do assert_raise ArgumentError, ":switches and :strict cannot be given together", fn -> OptionParser.parse([], strict: [], switches: [])