diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex index c68619f6736..1094692a7c9 100644 --- a/lib/elixir/lib/option_parser.ex +++ b/lib/elixir/lib/option_parser.ex @@ -282,11 +282,11 @@ defmodule OptionParser do iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer]) ** (OptionParser.ParseError) 1 error found! - --limit : Expected type integer, got "xyz" + --limit : Expected type integer, got "xyz"... iex> OptionParser.parse!(["--unknown", "xyz"], strict: []) ** (OptionParser.ParseError) 1 error found! - --unknown : Unknown option + --unknown : Unknown option... iex> OptionParser.parse!( ...> ["-l", "xyz", "-f", "bar"], @@ -295,7 +295,7 @@ defmodule OptionParser do ...> ) ** (OptionParser.ParseError) 2 errors found! -l : Expected type integer, got "xyz" - -f : Expected type integer, got "bar" + -f : Expected type integer, got "bar"... """ @spec parse!(argv, options) :: {parsed, argv} @@ -354,7 +354,7 @@ defmodule OptionParser do ...> strict: [number: :integer] ...> ) ** (OptionParser.ParseError) 1 error found! - --number : Expected type integer, got "lib" + --number : Expected type integer, got "lib"... iex> OptionParser.parse_head!( ...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"], @@ -362,7 +362,7 @@ defmodule OptionParser do ...> ) ** (OptionParser.ParseError) 2 errors found! --verbose : Missing argument of type integer - --source : Expected type integer, got "lib" + --source : Expected type integer, got "lib"... """ @spec parse_head!(argv, options) :: {parsed, argv} @@ -863,15 +863,20 @@ defmodule OptionParser do error_count = length(errors) error = if error_count == 1, do: "error", else: "errors" - "#{error_count} #{error} found!\n" <> - Enum.map_join(errors, "\n", &format_error(&1, opts, types)) + slogan = + "#{error_count} #{error} found!\n" <> + Enum.map_join(errors, "\n", &format_error(&1, opts, types)) + + case format_available_options(opts, types) do + "" -> slogan + available_options -> slogan <> "\n\n#{available_options}" + end end defp format_error({option, nil}, opts, types) do if type = get_type(option, opts, types) do if String.contains?(option, "_") do msg = "#{option} : Unknown option" - msg <> ". Did you mean #{String.replace(option, "_", "-")}?" else "#{option} : Missing argument of type #{type}" @@ -917,4 +922,57 @@ defmodule OptionParser do option = String.replace(source, "_", "-") if score < current, do: best, else: {option, score} end + + defp format_available_options(opts, switches) do + reverse_aliases = + opts + |> Keyword.get(:aliases, []) + |> Enum.reduce(%{}, fn {alias, target}, acc -> + Map.update(acc, target, [alias], &[alias | &1]) + end) + + formatted_options = + switches + |> Enum.sort() + |> Enum.map(fn {name, types} -> + types = List.wrap(types) + + case types |> List.delete(:keep) |> List.first(:string) do + :boolean -> + base = "#{to_switch(name)}, #{to_switch(name, "--no-")}" + add_aliases(base, name, reverse_aliases) + + type -> + base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}" + base = add_aliases(base, name, reverse_aliases) + + if :keep in types do + base <> " (may be given more than once)" + else + base + end + end + end) + + if formatted_options == [] do + "" + else + "Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1)) + end + end + + defp add_aliases(base, name, reverse_aliases) do + case Map.get(reverse_aliases, name, []) do + [] -> + base + + alias_list -> + alias_str = + alias_list + |> Enum.sort() + |> Enum.map_join(", ", &("-" <> Atom.to_string(&1))) + + base <> " (alias: #{alias_str})" + end + end end diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs index 89b46968b46..9abb3a50b16 100644 --- a/lib/elixir/test/elixir/option_parser_test.exs +++ b/lib/elixir/test/elixir/option_parser_test.exs @@ -100,21 +100,46 @@ defmodule OptionParserTest do end test "parse!/2 raises an exception for an unknown option using strict" do - msg = "1 error found!\n--doc-bar : Unknown option. Did you mean --docs-bar?" + msg = + """ + 1 error found! + --doc-bar : Unknown option. Did you mean --docs-bar? + + Supported options: + --docs-bar STRING + --source STRING\ + """ assert_raise OptionParser.ParseError, msg, fn -> argv = ["--source", "from_docs/", "--doc-bar", "show"] OptionParser.parse!(argv, strict: [source: :string, docs_bar: :string]) end - assert_raise OptionParser.ParseError, "1 error found!\n--foo : Unknown option", fn -> - argv = ["--source", "from_docs/", "--foo", "show"] - OptionParser.parse!(argv, strict: [source: :string, docs: :string]) - end + assert_raise OptionParser.ParseError, + """ + 1 error found! + --foo : Unknown option + + Supported options: + --docs STRING + --source STRING\ + """, + fn -> + argv = ["--source", "from_docs/", "--foo", "show"] + OptionParser.parse!(argv, strict: [source: :string, docs: :string]) + end end test "parse!/2 raises an exception for an unknown option using strict when it is only off by underscores" do - msg = "1 error found!\n--docs_bar : Unknown option. Did you mean --docs-bar?" + msg = + """ + 1 error found! + --docs_bar : Unknown option. Did you mean --docs-bar? + + Supported options: + --docs-bar STRING + --source STRING\ + """ assert_raise OptionParser.ParseError, msg, fn -> argv = ["--source", "from_docs/", "--docs_bar", "show"] @@ -123,14 +148,57 @@ defmodule OptionParserTest do end test "parse!/2 raises an exception when an option is of the wrong type" do - assert_raise OptionParser.ParseError, fn -> - argv = ["--bad", "opt", "foo", "-o", "bad", "bar"] - OptionParser.parse!(argv, switches: [bad: :integer]) + assert_raise OptionParser.ParseError, + """ + 1 error found! + --bad : Expected type integer, got "opt" + + Supported options: + --bad INTEGER\ + """, + fn -> + argv = ["--bad", "opt", "foo", "-o", "bad", "bar"] + OptionParser.parse!(argv, switches: [bad: :integer]) + end + end + + test "parse!/2 lists all supported options and aliases" do + expected_suggestion = + """ + 1 error found! + --verbos : Unknown option. Did you mean --verbose? + + Supported options: + --count INTEGER (alias: -c) + --debug, --no-debug (alias: -d) + --files STRING (alias: -f) (may be given more than once) + --name STRING (alias: -n) + --verbose, --no-verbose (alias: -v)\ + """ + + assert_raise OptionParser.ParseError, expected_suggestion, fn -> + OptionParser.parse!(["--verbos"], + strict: [ + name: :string, + count: :integer, + verbose: :boolean, + debug: :boolean, + files: :keep + ], + aliases: [n: :name, c: :count, v: :verbose, d: :debug, f: :files] + ) end end test "parse_head!/2 raises an exception when an option is of the wrong type" do - message = "1 error found!\n--number : Expected type integer, got \"lib\"" + message = + """ + 1 error found! + --number : Expected type integer, got "lib" + + Supported options: + --number INTEGER\ + """ assert_raise OptionParser.ParseError, message, fn -> argv = ["--number", "lib", "test/enum_test.exs"] diff --git a/lib/mix/test/mix/task_test.exs b/lib/mix/test/mix/task_test.exs index d0e7315aa54..3cdb9a1e299 100644 --- a/lib/mix/test/mix/task_test.exs +++ b/lib/mix/test/mix/task_test.exs @@ -43,7 +43,7 @@ defmodule Mix.TaskTest do end test "run/2 converts OptionParser.ParseError into Mix errors" do - message = "Could not invoke task \"hello\": 1 error found!\n--unknown : Unknown option" + message = ~r"Could not invoke task \"hello\": 1 error found!\n--unknown : Unknown option" assert_raise Mix.Error, message, fn -> Mix.Task.run("hello", ["--parser", "--unknown"]) @@ -52,7 +52,7 @@ defmodule Mix.TaskTest do Mix.Task.clear() message = - "Could not invoke task \"hello\": 1 error found!\n--int : Expected type integer, got \"foo\"" + ~r"Could not invoke task \"hello\": 1 error found!\n--int : Expected type integer, got \"foo\"" assert_raise Mix.Error, message, fn -> Mix.Task.run("hello", ["--parser", "--int", "foo"])