Skip to content

Commit 2417ca0

Browse files
committed
Enhance OptionParser.ParseError with available options display
Example output: Expected one of: --count INTEGER (alias: -c) --debug, --no-debug (alias: -d) --files STRING (alias: -f) (may be given more than once) --verbose, --no-verbose (alias: -v) Prompt ====== When we raise ParseError, include all of the options we could potentially accept, alongside their types and aliases. For example, the switches `[foo: :string, bar: :integer]` and `aliases: [b: :bar]`, the error message should say: Expected one of: --foo STRING --bar INTEGER (alias: -b) Furthermore, for types that are :keep (which default to string), you should add: --bar INTEGER (alias: -b) (may be given more than once) And boolean ones accept no arguments, so they should be written as: --baz, --no-baz Sort all of them alphabetically.
1 parent c3437c3 commit 2417ca0

File tree

2 files changed

+146
-18
lines changed

2 files changed

+146
-18
lines changed

lib/elixir/lib/option_parser.ex

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,11 @@ defmodule OptionParser do
282282
283283
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
284284
** (OptionParser.ParseError) 1 error found!
285-
--limit : Expected type integer, got "xyz"
285+
--limit : Expected type integer, got "xyz"...
286286
287287
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
288288
** (OptionParser.ParseError) 1 error found!
289-
--unknown : Unknown option
289+
--unknown : Unknown option...
290290
291291
iex> OptionParser.parse!(
292292
...> ["-l", "xyz", "-f", "bar"],
@@ -295,7 +295,7 @@ defmodule OptionParser do
295295
...> )
296296
** (OptionParser.ParseError) 2 errors found!
297297
-l : Expected type integer, got "xyz"
298-
-f : Expected type integer, got "bar"
298+
-f : Expected type integer, got "bar"...
299299
300300
"""
301301
@spec parse!(argv, options) :: {parsed, argv}
@@ -354,15 +354,15 @@ defmodule OptionParser do
354354
...> strict: [number: :integer]
355355
...> )
356356
** (OptionParser.ParseError) 1 error found!
357-
--number : Expected type integer, got "lib"
357+
--number : Expected type integer, got "lib"...
358358
359359
iex> OptionParser.parse_head!(
360360
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
361361
...> strict: [verbose: :integer, source: :integer]
362362
...> )
363363
** (OptionParser.ParseError) 2 errors found!
364364
--verbose : Missing argument of type integer
365-
--source : Expected type integer, got "lib"
365+
--source : Expected type integer, got "lib"...
366366
367367
"""
368368
@spec parse_head!(argv, options) :: {parsed, argv}
@@ -863,15 +863,20 @@ defmodule OptionParser do
863863
error_count = length(errors)
864864
error = if error_count == 1, do: "error", else: "errors"
865865

866-
"#{error_count} #{error} found!\n" <>
867-
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
866+
slogan =
867+
"#{error_count} #{error} found!\n" <>
868+
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
869+
870+
case format_available_options(opts, types) do
871+
"" -> slogan
872+
available_options -> slogan <> "\n\n#{available_options}"
873+
end
868874
end
869875

870876
defp format_error({option, nil}, opts, types) do
871877
if type = get_type(option, opts, types) do
872878
if String.contains?(option, "_") do
873879
msg = "#{option} : Unknown option"
874-
875880
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
876881
else
877882
"#{option} : Missing argument of type #{type}"
@@ -917,4 +922,59 @@ defmodule OptionParser do
917922
option = String.replace(source, "_", "-")
918923
if score < current, do: best, else: {option, score}
919924
end
925+
926+
defp format_available_options(opts, switches) do
927+
reverse_aliases =
928+
opts
929+
|> Keyword.get(:aliases, [])
930+
|> Enum.reduce(%{}, fn {alias, target}, acc ->
931+
Map.update(acc, target, [alias], &[alias | &1])
932+
end)
933+
934+
formatted_options =
935+
switches
936+
|> Enum.sort()
937+
|> Enum.map(fn {name, types} ->
938+
types = List.wrap(types)
939+
option_name = String.replace(Atom.to_string(name), "_", "-")
940+
941+
case types |> List.delete(:keep) |> List.first(:string) do
942+
:boolean ->
943+
base = "--#{option_name}, --no-#{option_name}"
944+
add_aliases(base, name, reverse_aliases)
945+
946+
type ->
947+
base = "--#{option_name} #{String.upcase(Atom.to_string(type))}"
948+
base = add_aliases(base, name, reverse_aliases)
949+
950+
if :keep in types do
951+
base <> " (may be given more than once)"
952+
else
953+
base
954+
end
955+
end
956+
end)
957+
958+
if formatted_options == [] do
959+
""
960+
else
961+
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
962+
end
963+
end
964+
965+
defp add_aliases(base, name, reverse_aliases) do
966+
case Map.get(reverse_aliases, name, []) do
967+
[] ->
968+
base
969+
970+
alias_list ->
971+
alias_str =
972+
alias_list
973+
|> Enum.sort()
974+
|> Enum.map(&("-" <> Atom.to_string(&1)))
975+
|> Enum.join(", ")
976+
977+
base <> " (alias: #{alias_str})"
978+
end
979+
end
920980
end

lib/elixir/test/elixir/option_parser_test.exs

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,46 @@ defmodule OptionParserTest do
100100
end
101101

102102
test "parse!/2 raises an exception for an unknown option using strict" do
103-
msg = "1 error found!\n--doc-bar : Unknown option. Did you mean --docs-bar?"
103+
msg =
104+
"""
105+
1 error found!
106+
--doc-bar : Unknown option. Did you mean --docs-bar?
107+
108+
Supported options:
109+
--docs-bar STRING
110+
--source STRING\
111+
"""
104112

105113
assert_raise OptionParser.ParseError, msg, fn ->
106114
argv = ["--source", "from_docs/", "--doc-bar", "show"]
107115
OptionParser.parse!(argv, strict: [source: :string, docs_bar: :string])
108116
end
109117

110-
assert_raise OptionParser.ParseError, "1 error found!\n--foo : Unknown option", fn ->
111-
argv = ["--source", "from_docs/", "--foo", "show"]
112-
OptionParser.parse!(argv, strict: [source: :string, docs: :string])
113-
end
118+
assert_raise OptionParser.ParseError,
119+
"""
120+
1 error found!
121+
--foo : Unknown option
122+
123+
Supported options:
124+
--docs STRING
125+
--source STRING\
126+
""",
127+
fn ->
128+
argv = ["--source", "from_docs/", "--foo", "show"]
129+
OptionParser.parse!(argv, strict: [source: :string, docs: :string])
130+
end
114131
end
115132

116133
test "parse!/2 raises an exception for an unknown option using strict when it is only off by underscores" do
117-
msg = "1 error found!\n--docs_bar : Unknown option. Did you mean --docs-bar?"
134+
msg =
135+
"""
136+
1 error found!
137+
--docs_bar : Unknown option. Did you mean --docs-bar?
138+
139+
Supported options:
140+
--docs-bar STRING
141+
--source STRING\
142+
"""
118143

119144
assert_raise OptionParser.ParseError, msg, fn ->
120145
argv = ["--source", "from_docs/", "--docs_bar", "show"]
@@ -123,14 +148,57 @@ defmodule OptionParserTest do
123148
end
124149

125150
test "parse!/2 raises an exception when an option is of the wrong type" do
126-
assert_raise OptionParser.ParseError, fn ->
127-
argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
128-
OptionParser.parse!(argv, switches: [bad: :integer])
151+
assert_raise OptionParser.ParseError,
152+
"""
153+
1 error found!
154+
--bad : Expected type integer, got "opt"
155+
156+
Supported options:
157+
--bad INTEGER\
158+
""",
159+
fn ->
160+
argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
161+
OptionParser.parse!(argv, switches: [bad: :integer])
162+
end
163+
end
164+
165+
test "parse!/2 lists all supported options and aliases" do
166+
expected_suggestion =
167+
"""
168+
1 error found!
169+
--verbos : Unknown option. Did you mean --verbose?
170+
171+
Supported options:
172+
--count INTEGER (alias: -c)
173+
--debug, --no-debug (alias: -d)
174+
--files STRING (alias: -f) (may be given more than once)
175+
--name STRING (alias: -n)
176+
--verbose, --no-verbose (alias: -v)\
177+
"""
178+
179+
assert_raise OptionParser.ParseError, expected_suggestion, fn ->
180+
OptionParser.parse!(["--verbos"],
181+
strict: [
182+
name: :string,
183+
count: :integer,
184+
verbose: :boolean,
185+
debug: :boolean,
186+
files: :keep
187+
],
188+
aliases: [n: :name, c: :count, v: :verbose, d: :debug, f: :files]
189+
)
129190
end
130191
end
131192

132193
test "parse_head!/2 raises an exception when an option is of the wrong type" do
133-
message = "1 error found!\n--number : Expected type integer, got \"lib\""
194+
message =
195+
"""
196+
1 error found!
197+
--number : Expected type integer, got "lib"
198+
199+
Supported options:
200+
--number INTEGER\
201+
"""
134202

135203
assert_raise OptionParser.ParseError, message, fn ->
136204
argv = ["--number", "lib", "test/enum_test.exs"]

0 commit comments

Comments
 (0)