Skip to content

Commit cb0c3d8

Browse files
author
José Valim
committed
Support :strict option on parse and improve return types
1 parent d9ece7e commit cb0c3d8

File tree

2 files changed

+103
-45
lines changed

2 files changed

+103
-45
lines changed

lib/elixir/lib/option_parser.ex

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ defmodule OptionParser do
33
This module contains functions to parse command line arguments.
44
"""
55

6+
@type argv :: [String.t]
7+
@type parsed :: Keyword.t
8+
@type errors :: Keyword.t
9+
@type options :: [switches: Keyword.t, strict: Keyword.t, aliases: Keyword.t]
10+
611
@doc """
712
Parses `argv` into a keywords list.
813
@@ -32,7 +37,18 @@ defmodule OptionParser do
3237
## Switches
3338
3439
Many times though, it is better to explicitly list the available
35-
switches and their formats. The following types are supported:
40+
switches and their formats. The switches can be specified via two
41+
different options:
42+
43+
* `:strict` - the switches are strict. Any switch that does not
44+
exist in the switch list is treated as an error;
45+
46+
* `:switches` - configure some switches. Switches that does not
47+
exist in the switch list are still attempted to be parsed;
48+
49+
Note only `:strict` or `:switches` may be given at once.
50+
51+
For each switch, the following types are supported:
3652
3753
* `:boolean` - Marks the given switch as a boolean. Boolean switches
3854
never consume the following value unless it is
@@ -41,27 +57,35 @@ defmodule OptionParser do
4157
* `:float` - Parses the switch as a float;
4258
* `:string` - Returns the switch as a string;
4359
44-
If a switch can't be parsed, the option is returned in the invalid
45-
options list (third element of the returned tuple).
60+
If a switch can't be parsed or is not specfied in the strict case,
61+
the option is returned in the invalid options list (third element
62+
of the returned tuple).
4663
4764
The following extra "types" are supported:
4865
4966
* `:keep` - Keeps duplicated items in the list instead of overriding;
5067
5168
Examples:
5269
53-
iex> OptionParser.parse(["--unlock", "path/to/file"], switches: [unlock: :boolean])
70+
iex> OptionParser.parse(["--unlock", "path/to/file"], strict: [unlock: :boolean])
5471
{[unlock: true], ["path/to/file"], []}
5572
5673
iex> OptionParser.parse(["--unlock", "--limit", "0", "path/to/file"],
57-
...> switches: [unlock: :boolean, limit: :integer])
74+
...> strict: [unlock: :boolean, limit: :integer])
5875
{[unlock: true, limit: 0], ["path/to/file"], []}
5976
60-
iex> OptionParser.parse(["--limit", "3"], switches: [limit: :integer])
77+
iex> OptionParser.parse(["--limit", "3"], strict: [limit: :integer])
6178
{[limit: 3], [], []}
6279
63-
iex> OptionParser.parse(["--limit", "yyz"], switches: [limit: :integer])
64-
{[], [], [limit: "yyz"]}
80+
iex> OptionParser.parse(["--limit", "xyz"], strict: [limit: :integer])
81+
{[], [], [limit: "xyz"]}
82+
83+
iex> OptionParser.parse(["--unknown", "xyz"], strict: [])
84+
{[], ["xyz"], [unknown: nil]}
85+
86+
iex> OptionParser.parse(["--limit", "3", "--unknown", "xyz"],
87+
...> switches: [limit: :integer])
88+
{[limit: 3, unknown: "xyz"], [], []}
6589
6690
## Negation switches
6791
@@ -85,10 +109,9 @@ defmodule OptionParser do
85109
{[debug: true], [], []}
86110
87111
"""
112+
@spec parse(argv, options) :: {parsed, argv, errors}
88113
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
89-
config =
90-
Keyword.merge(opts, [all: true, strict: false])
91-
|> compile_config()
114+
config = compile_config(opts, true)
92115
do_parse(argv, config, [], [], [])
93116
end
94117

@@ -107,14 +130,12 @@ defmodule OptionParser do
107130
{[verbose: true, source: "lib"], ["test/enum_test.exs", "--unlock"], []}
108131
109132
"""
133+
@spec parse_head(argv, options) :: {parsed, argv, errors}
110134
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
111-
config =
112-
Keyword.merge(opts, [all: false, strict: false])
113-
|> compile_config()
135+
config = compile_config(opts, false)
114136
do_parse(argv, config, [], [], [])
115137
end
116138

117-
118139
defp do_parse([], _config, opts, args, invalid) do
119140
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
120141
end
@@ -127,11 +148,11 @@ defmodule OptionParser do
127148
new_opts = do_store_option(opts, option, value, kinds)
128149
do_parse(rest, config, new_opts, args, invalid)
129150

130-
{:error, {:value, option, value}, rest} ->
151+
{:invalid, option, value, rest} ->
131152
# the option exist but it has wrong value
132153
do_parse(rest, config, opts, args, [{option, value}|invalid])
133154

134-
{:error, {:undefined, option, value}, rest} ->
155+
{:undefined, option, value, rest} ->
135156
# the option does not exist (for strict cases)
136157
do_parse(rest, config, opts, args, [{option, value}|invalid])
137158

@@ -148,12 +169,32 @@ defmodule OptionParser do
148169
end
149170
end
150171

151-
152172
@doc """
153173
Low-level function that parses one option.
174+
175+
It accepts the same options as `parse/2` and `parse_head/2`
176+
as both functions are built on top of next. This function
177+
may return:
178+
179+
* `{:ok, key, value, rest}` - the option `key` with `value` was successfully parsed
180+
181+
* `{:invalid, key, value, rest}` - the option `key` is invalid with `value`
182+
(returned when the switch type does not match the one given via the command line)
183+
184+
* `{:undefined, key, value, rest}` - the option `key` is undefined
185+
(returned on strict cases and the switch is unknown)
186+
187+
* `{:error, rest}` - there are no switches at the top of the given argv
154188
"""
189+
190+
@spec next(argv, options) ::
191+
{:ok, key :: atom, value :: term, argv} |
192+
{:invalid, key :: atom, value :: term, argv} |
193+
{:undefined, key :: atom, value :: term, argv} |
194+
{:error, argv}
195+
155196
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
156-
{aliases, switches, strict, _} = compile_config(opts)
197+
{aliases, switches, strict, _} = compile_config(opts, true)
157198
next(argv, aliases, switches, strict)
158199
end
159200

@@ -171,13 +212,13 @@ defmodule OptionParser do
171212

172213
if strict and not option_defined?(opt, switches) do
173214
{_, opt_name} = opt
174-
{:error, {:undefined, opt_name, value}, rest}
215+
{:undefined, opt_name, value, rest}
175216
else
176217
{opt_name, kinds, value} = normalize_option(opt, value, switches)
177218
{value, kinds, rest} = normalize_value(value, kinds, rest, strict)
178219
case validate_option(opt_name, value, kinds) do
179220
{:ok, new_value} -> {:ok, opt_name, new_value, rest}
180-
:invalid -> {:error, {:value, opt_name, value}, rest}
221+
:invalid -> {:invalid, opt_name, value, rest}
181222
end
182223
end
183224
end
@@ -188,11 +229,18 @@ defmodule OptionParser do
188229

189230
## Helpers
190231

191-
defp compile_config(opts) do
192-
aliases = opts[:aliases] || []
193-
switches = opts[:switches] || []
194-
strict = opts[:strict] || false
195-
all = opts[:all] || false
232+
defp compile_config(opts, all) do
233+
aliases = opts[:aliases] || []
234+
235+
{switches, strict} = cond do
236+
s = opts[:switches] ->
237+
{s, false}
238+
s = opts[:strict] ->
239+
{s, true}
240+
true ->
241+
{[], false}
242+
end
243+
196244
{aliases, switches, strict, all}
197245
end
198246

lib/elixir/test/elixir/option_parser_test.exs

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,6 @@ defmodule OptionParserTest do
130130
== {[no_docs: true], ["foo"], []}
131131
end
132132

133-
test "parses more than one key/value options" do
134-
assert OptionParser.parse(["--source", "from_docs/", "--docs", "show"])
135-
== {[source: "from_docs/", docs: "show"], [], []}
136-
end
137-
138133
test "overrides options by default" do
139134
assert OptionParser.parse(["--require", "foo", "--require", "bar", "baz"])
140135
== {[require: "bar"], ["baz"], []}
@@ -172,8 +167,23 @@ defmodule OptionParserTest do
172167
== {[source: "from_docs/", verbose: true], ["test/enum_test.exs"], []}
173168
end
174169

170+
test "parses more than one key/value options" do
171+
assert OptionParser.parse(["--source", "from_docs/", "--docs", "show"])
172+
== {[source: "from_docs/", docs: "show"], [], []}
173+
end
174+
175+
test "parses more than one key/value options using strict" do
176+
assert OptionParser.parse(["--source", "from_docs/", "--docs", "show"],
177+
strict: [source: :string, docs: :string])
178+
== {[source: "from_docs/", docs: "show"], [], []}
179+
180+
assert OptionParser.parse(["--source", "from_docs/", "--doc", "show"],
181+
strict: [source: :string, docs: :string])
182+
== {[source: "from_docs/"], ["show"], [doc: nil]}
183+
end
184+
175185
test "next strict: good options" do
176-
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
186+
config = [strict: [str: :string, int: :integer, bool: :boolean]]
177187
assert OptionParser.next(["--str", "hello", "..."], config)
178188
== {:ok, :str, "hello", ["..."]}
179189
assert OptionParser.next(["--int=13", "..."], config)
@@ -189,36 +199,36 @@ defmodule OptionParserTest do
189199
end
190200

191201
test "next strict: unknown options" do
192-
config = [switches: [bool: :boolean], strict: true]
202+
config = [strict: [bool: :boolean]]
193203
assert OptionParser.next(["--str", "13", "..."], config)
194-
== {:error, {:undefined, :str, nil}, ["13", "..."]}
204+
== {:undefined, :str, nil, ["13", "..."]}
195205
assert OptionParser.next(["--int=hello", "..."], config)
196-
== {:error, {:undefined, :int, "hello"}, ["..."]}
206+
== {:undefined, :int, "hello", ["..."]}
197207
assert OptionParser.next(["--no-bool=other", "..."], config)
198-
== {:error, {:undefined, :no_bool, "other"}, ["..."]}
208+
== {:undefined, :no_bool, "other", ["..."]}
199209
end
200210

201211
test "next strict: bad type" do
202-
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
212+
config = [strict: [str: :string, int: :integer, bool: :boolean]]
203213
assert OptionParser.next(["--str", "13", "..."], config)
204214
== {:ok, :str, "13", ["..."]}
205215
assert OptionParser.next(["--int=hello", "..."], config)
206-
== {:error, {:value, :int, "hello"}, ["..."]}
216+
== {:invalid, :int, "hello", ["..."]}
207217
assert OptionParser.next(["--int", "hello", "..."], config)
208-
== {:error, {:value, :int, "hello"}, ["..."]}
218+
== {:invalid, :int, "hello", ["..."]}
209219
assert OptionParser.next(["--bool=other", "..."], config)
210-
== {:error, {:value, :bool, "other"}, ["..."]}
220+
== {:invalid, :bool, "other", ["..."]}
211221
end
212222

213223
test "next strict: missing value" do
214-
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
224+
config = [strict: [str: :string, int: :integer, bool: :boolean]]
215225
assert OptionParser.next(["--str"], config)
216-
== {:error, {:value, :str, nil}, []}
226+
== {:invalid, :str, nil, []}
217227
assert OptionParser.next(["--int"], config)
218-
== {:error, {:value, :int, nil}, []}
228+
== {:invalid, :int, nil, []}
219229
assert OptionParser.next(["--bool=", "..."], config)
220-
== {:error, {:value, :bool, ""}, ["..."]}
230+
== {:invalid, :bool, "", ["..."]}
221231
assert OptionParser.next(["--no-bool=", "..."], config)
222-
== {:error, {:undefined, :no_bool, ""}, ["..."]}
232+
== {:undefined, :no_bool, "", ["..."]}
223233
end
224234
end

0 commit comments

Comments
 (0)