Skip to content

Commit d9ece7e

Browse files
author
José Valim
committed
Merge pull request #2290 from alco/option-parser-next
Tentative implementation of the `next` function
2 parents 24a2566 + 142ebc3 commit d9ece7e

File tree

2 files changed

+188
-66
lines changed

2 files changed

+188
-66
lines changed

lib/elixir/lib/option_parser.ex

Lines changed: 136 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ defmodule OptionParser do
8686
8787
"""
8888
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
89-
parse(argv, opts, true)
89+
config =
90+
Keyword.merge(opts, [all: true, strict: false])
91+
|> compile_config()
92+
do_parse(argv, config, [], [], [])
9093
end
9194

9295
@doc """
@@ -105,74 +108,122 @@ defmodule OptionParser do
105108
106109
"""
107110
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
108-
parse(argv, opts, false)
111+
config =
112+
Keyword.merge(opts, [all: false, strict: false])
113+
|> compile_config()
114+
do_parse(argv, config, [], [], [])
109115
end
110116

111-
## Helpers
112117

113-
defp parse(argv, opts, bool) do
114-
aliases = opts[:aliases] || []
115-
switches = opts[:switches] || []
116-
parse(argv, aliases, switches, bool)
118+
defp do_parse([], _config, opts, args, invalid) do
119+
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
117120
end
118121

119-
defp parse(argv, aliases, switches, all) do
120-
parse(argv, aliases, switches, [], [], [], all)
122+
defp do_parse(argv, {aliases, switches, strict, all}=config, opts, args, invalid) do
123+
case next(argv, aliases, switches, strict) do
124+
{:ok, option, value, rest} ->
125+
# the option exist and it was successfully parsed
126+
kinds = List.wrap Keyword.get(switches, option)
127+
new_opts = do_store_option(opts, option, value, kinds)
128+
do_parse(rest, config, new_opts, args, invalid)
129+
130+
{:error, {:value, option, value}, rest} ->
131+
# the option exist but it has wrong value
132+
do_parse(rest, config, opts, args, [{option, value}|invalid])
133+
134+
{:error, {:undefined, option, value}, rest} ->
135+
# the option does not exist (for strict cases)
136+
do_parse(rest, config, opts, args, [{option, value}|invalid])
137+
138+
{:error, ["--"|rest]} ->
139+
{Enum.reverse(opts), Enum.reverse(args, rest), Enum.reverse(invalid)}
140+
141+
{:error, [arg|rest]=remaining_args} ->
142+
# there is no option
143+
if all do
144+
do_parse(rest, config, opts, [arg|args], invalid)
145+
else
146+
{Enum.reverse(opts), Enum.reverse(args, remaining_args), Enum.reverse(invalid)}
147+
end
148+
end
121149
end
122150

123-
defp parse(["--"|t], _aliases, _switches, dict, args, invalid, _all) do
124-
{Enum.reverse(dict), Enum.reverse(args, t), Enum.reverse(invalid)}
151+
152+
@doc """
153+
Low-level function that parses one option.
154+
"""
155+
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
156+
{aliases, switches, strict, _} = compile_config(opts)
157+
next(argv, aliases, switches, strict)
125158
end
126159

127-
defp parse(["-" <> option|t], aliases, switches, dict, args, invalid, all) do
128-
{option, value} = split_option(option)
129-
{option, kinds, value} = normalize_option(option, value, switches, aliases)
130-
{value, kinds, t} = normalize_value(value, kinds, t)
131-
{dict, invalid} = store_option(dict, invalid, option, value, kinds)
132-
parse(t, aliases, switches, dict, args, invalid, all)
160+
defp next([], _aliases, _switches, _strict) do
161+
{:error, []}
133162
end
134163

135-
defp parse([h|t], aliases, switches, dict, args, invalid, true) do
136-
parse(t, aliases, switches, dict, [h|args], invalid, true)
164+
defp next(["--"|_]=argv, _aliases, _switches, _strict) do
165+
{:error, argv}
137166
end
138167

139-
defp parse([], _, _switches, dict, args, invalid, true) do
140-
{Enum.reverse(dict), Enum.reverse(args), Enum.reverse(invalid)}
168+
defp next(["-" <> option|rest], aliases, switches, strict) do
169+
{option, value} = split_option(option)
170+
opt = tag_option(option, value, switches, aliases)
171+
172+
if strict and not option_defined?(opt, switches) do
173+
{_, opt_name} = opt
174+
{:error, {:undefined, opt_name, value}, rest}
175+
else
176+
{opt_name, kinds, value} = normalize_option(opt, value, switches)
177+
{value, kinds, rest} = normalize_value(value, kinds, rest, strict)
178+
case validate_option(opt_name, value, kinds) do
179+
{:ok, new_value} -> {:ok, opt_name, new_value, rest}
180+
:invalid -> {:error, {:value, opt_name, value}, rest}
181+
end
182+
end
141183
end
142184

143-
defp parse(value, _, _switches, dict, _args, invalid, false) do
144-
{Enum.reverse(dict), value, Enum.reverse(invalid)}
185+
defp next(argv, _aliases, _switches, _strict) do
186+
{:error, argv}
145187
end
146188

147-
defp store_option(dict, invalid, option, value, kinds) do
148-
{invalid_option, value} =
149-
cond do
150-
:invalid in kinds ->
151-
{option, value}
152-
:boolean in kinds ->
153-
case value do
154-
t when t in [true, "true"] -> {nil, true}
155-
f when f in [false, "false"] -> {nil, false}
156-
_ -> {option, value}
157-
end
158-
:integer in kinds ->
159-
case Integer.parse(value) do
160-
{value, ""} -> {nil, value}
161-
_ -> {option, value}
162-
end
163-
:float in kinds ->
164-
case Float.parse(value) do
165-
{value, ""} -> {nil, value}
166-
_ -> {option, value}
167-
end
168-
true ->
169-
{nil, value}
170-
end
189+
## Helpers
171190

172-
if invalid_option do
173-
{dict, [{option, value}|invalid]}
191+
defp compile_config(opts) do
192+
aliases = opts[:aliases] || []
193+
switches = opts[:switches] || []
194+
strict = opts[:strict] || false
195+
all = opts[:all] || false
196+
{aliases, switches, strict, all}
197+
end
198+
199+
defp validate_option(option, value, kinds) do
200+
{invalid_opt, value} = cond do
201+
:invalid in kinds ->
202+
{option, value}
203+
:boolean in kinds ->
204+
case value do
205+
t when t in [true, "true"] -> {nil, true}
206+
f when f in [false, "false"] -> {nil, false}
207+
_ -> {option, value}
208+
end
209+
:integer in kinds ->
210+
case Integer.parse(value) do
211+
{value, ""} -> {nil, value}
212+
_ -> {option, value}
213+
end
214+
:float in kinds ->
215+
case Float.parse(value) do
216+
{value, ""} -> {nil, value}
217+
_ -> {option, value}
218+
end
219+
true ->
220+
{nil, value}
221+
end
222+
223+
if invalid_opt do
224+
:invalid
174225
else
175-
{do_store_option(dict, option, value, kinds), invalid}
226+
{:ok, value}
176227
end
177228
end
178229

@@ -185,19 +236,35 @@ defmodule OptionParser do
185236
end
186237
end
187238

188-
defp normalize_option(<<?-, option :: binary>>, value, switches, _aliases) do
189-
normalize_option(get_negated(option, switches), value, switches)
239+
defp tag_option(<<?-, option :: binary>>, value, switches, _aliases) do
240+
get_negated(option, value, switches)
190241
end
191242

192-
defp normalize_option(option, value, switches, aliases) do
193-
option = get_option(option)
194-
if alias = aliases[option] do
195-
normalize_option({:default, alias}, value, switches)
243+
defp tag_option(option, _value, _switches, aliases) when is_binary(option) do
244+
opt = get_option(option)
245+
if alias = aliases[opt] do
246+
{:default, alias}
196247
else
197-
{option, [:invalid], value}
248+
{:unknown, opt}
198249
end
199250
end
200251

252+
defp option_defined?({:unknown, _option}, _switches) do
253+
false
254+
end
255+
256+
defp option_defined?({:negated, option}, switches) do
257+
Keyword.has_key?(switches, option)
258+
end
259+
260+
defp option_defined?({:default, option}, switches) do
261+
Keyword.has_key?(switches, option)
262+
end
263+
264+
defp normalize_option({:unknown, option}, value, _switches) do
265+
{option, [:invalid], value}
266+
end
267+
201268
defp normalize_option({:negated, option}, nil, switches) do
202269
kinds = List.wrap(switches[option])
203270

@@ -219,21 +286,22 @@ defmodule OptionParser do
219286
{option, List.wrap(switches[option]), value}
220287
end
221288

222-
defp normalize_value(nil, kinds, t) do
289+
defp normalize_value(nil, kinds, t, strict) do
290+
null = if strict, do: nil, else: true
223291
cond do
224292
:boolean in kinds ->
225293
{true, kinds, t}
226294
value_in_tail?(t) ->
227295
[h|t] = t
228296
{h, kinds, t}
229297
kinds == [] ->
230-
{true, kinds, t}
298+
{null, kinds, t}
231299
true ->
232-
{true, [:invalid], t}
300+
{null, [:invalid], t}
233301
end
234302
end
235303

236-
defp normalize_value(value, kinds, t) do
304+
defp normalize_value(value, kinds, t, _) do
237305
{value, kinds, t}
238306
end
239307

@@ -256,13 +324,17 @@ defmodule OptionParser do
256324
option |> to_underscore |> String.to_atom
257325
end
258326

259-
defp get_negated("no-" <> rest = option, switches) do
327+
defp get_negated("no-" <> rest = option, value, switches) do
260328
negated = get_option(rest)
261-
option = if Keyword.has_key?(switches, negated), do: negated, else: get_option(option)
329+
option = if Keyword.has_key?(switches, negated) and value == nil do
330+
negated
331+
else
332+
get_option(option)
333+
end
262334
{:negated, option}
263335
end
264336

265-
defp get_negated(rest, _switches) do
337+
defp get_negated(rest, _value, _switches) do
266338
{:default, get_option(rest)}
267339
end
268340
end

lib/elixir/test/elixir/option_parser_test.exs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ defmodule OptionParserTest do
6767
assert OptionParser.parse(["--no-docs", "foo"], switches: [docs: :boolean])
6868
== {[docs: false], ["foo"], []}
6969
assert OptionParser.parse(["--no-docs=foo", "bar"], switches: [docs: :boolean])
70-
== {[], ["bar"], [docs: "foo"]}
70+
== {[], ["bar"], [no_docs: "foo"]}
7171
assert OptionParser.parse(["--no-docs=", "bar"], switches: [docs: :boolean])
72-
== {[], ["bar"], [docs: ""]}
72+
== {[], ["bar"], [no_docs: ""]}
7373
end
7474

7575
test "does not set unparsed booleans" do
@@ -171,4 +171,54 @@ defmodule OptionParserTest do
171171
assert OptionParser.parse(args)
172172
== {[source: "from_docs/", verbose: true], ["test/enum_test.exs"], []}
173173
end
174+
175+
test "next strict: good options" do
176+
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
177+
assert OptionParser.next(["--str", "hello", "..."], config)
178+
== {:ok, :str, "hello", ["..."]}
179+
assert OptionParser.next(["--int=13", "..."], config)
180+
== {:ok, :int, 13, ["..."]}
181+
assert OptionParser.next(["--bool=false", "..."], config)
182+
== {:ok, :bool, false, ["..."]}
183+
assert OptionParser.next(["--no-bool", "..."], config)
184+
== {:ok, :bool, false, ["..."]}
185+
assert OptionParser.next(["--bool", "..."], config)
186+
== {:ok, :bool, true, ["..."]}
187+
assert OptionParser.next(["..."], config)
188+
== {:error, ["..."]}
189+
end
190+
191+
test "next strict: unknown options" do
192+
config = [switches: [bool: :boolean], strict: true]
193+
assert OptionParser.next(["--str", "13", "..."], config)
194+
== {:error, {:undefined, :str, nil}, ["13", "..."]}
195+
assert OptionParser.next(["--int=hello", "..."], config)
196+
== {:error, {:undefined, :int, "hello"}, ["..."]}
197+
assert OptionParser.next(["--no-bool=other", "..."], config)
198+
== {:error, {:undefined, :no_bool, "other"}, ["..."]}
199+
end
200+
201+
test "next strict: bad type" do
202+
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
203+
assert OptionParser.next(["--str", "13", "..."], config)
204+
== {:ok, :str, "13", ["..."]}
205+
assert OptionParser.next(["--int=hello", "..."], config)
206+
== {:error, {:value, :int, "hello"}, ["..."]}
207+
assert OptionParser.next(["--int", "hello", "..."], config)
208+
== {:error, {:value, :int, "hello"}, ["..."]}
209+
assert OptionParser.next(["--bool=other", "..."], config)
210+
== {:error, {:value, :bool, "other"}, ["..."]}
211+
end
212+
213+
test "next strict: missing value" do
214+
config = [switches: [str: :string, int: :integer, bool: :boolean], strict: true]
215+
assert OptionParser.next(["--str"], config)
216+
== {:error, {:value, :str, nil}, []}
217+
assert OptionParser.next(["--int"], config)
218+
== {:error, {:value, :int, nil}, []}
219+
assert OptionParser.next(["--bool=", "..."], config)
220+
== {:error, {:value, :bool, ""}, ["..."]}
221+
assert OptionParser.next(["--no-bool=", "..."], config)
222+
== {:error, {:undefined, :no_bool, ""}, ["..."]}
223+
end
174224
end

0 commit comments

Comments
 (0)