Skip to content

Commit 451b8eb

Browse files
AndrewDrygaJosé Valim
authored andcommitted
Implement allow_inexistent_atoms for OptionParser (#5709)
Signed-off-by: José Valim <[email protected]>
1 parent 23742f1 commit 451b8eb

File tree

2 files changed

+50
-31
lines changed

2 files changed

+50
-31
lines changed

lib/elixir/lib/option_parser.ex

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ defmodule OptionParser do
5353
5454
* `:switches` or `:strict` - see the "Switch definitions" section below
5555
* `:aliases` - see the "Aliases" section below
56+
* `:allow_nonexistent_atoms` - see the "Parsing undefined switches" section below
5657
5758
## Switch definitions
5859
@@ -112,6 +113,15 @@ defmodule OptionParser do
112113
iex> OptionParser.parse(["--no-op", "path/to/file"], switches: [op: :boolean])
113114
{[op: false], ["path/to/file"], []}
114115
116+
### Parsing undefined switches
117+
118+
By default, only arguments that have defined atom representation will be parsed.
119+
This happens because creating atoms at runtime is considered to be unsafe,
120+
but you can still force creation of atoms by passing `allow_nonexistent_atoms: true`
121+
to the list of function options.
122+
123+
This is useful when you are building command-line applications that receive dynamically-named arguments.
124+
115125
## Aliases
116126
117127
A set of aliases can be specified in the `:aliases` option:
@@ -251,8 +261,8 @@ defmodule OptionParser do
251261
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
252262
end
253263

254-
defp do_parse(argv, {aliases, switches, strict?} = config, opts, args, invalid, all?) do
255-
case next(argv, aliases, switches, strict?) do
264+
defp do_parse(argv, {aliases, switches, strict?, allow_nonexistent_atoms?} = config, opts, args, invalid, all?) do
265+
case next(argv, aliases, switches, strict?, allow_nonexistent_atoms?) do
256266
{:ok, option, value, rest} ->
257267
# the option exists and it was successfully parsed
258268
kinds = List.wrap Keyword.get(switches, option)
@@ -307,35 +317,35 @@ defmodule OptionParser do
307317
{:error, argv}
308318

309319
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
310-
{aliases, switches, strict?} = compile_config(opts)
311-
next(argv, aliases, switches, strict?)
320+
{aliases, switches, strict?, allow_nonexistent_atoms?} = compile_config(opts)
321+
next(argv, aliases, switches, strict?, allow_nonexistent_atoms?)
312322
end
313323

314-
defp next([], _aliases, _switches, _strict?) do
324+
defp next([], _aliases, _switches, _strict?, _allow_nonexistent_atoms?) do
315325
{:error, []}
316326
end
317327

318-
defp next(["--" | _] = argv, _aliases, _switches, _strict?) do
328+
defp next(["--" | _] = argv, _aliases, _switches, _strict?, _allow_nonexistent_atoms?) do
319329
{:error, argv}
320330
end
321331

322-
defp next(["-" | _] = argv, _aliases, _switches, _strict?) do
332+
defp next(["-" | _] = argv, _aliases, _switches, _strict?, _allow_nonexistent_atoms?) do
323333
{:error, argv}
324334
end
325335

326-
defp next(["- " <> _ | _] = argv, _aliases, _switches, _strict?) do
336+
defp next(["- " <> _ | _] = argv, _aliases, _switches, _strict?, _allow_nonexistent_atoms?) do
327337
{:error, argv}
328338
end
329339

330340
# Handles --foo or --foo=bar
331-
defp next(["--" <> option | rest], _aliases, switches, strict?) do
341+
defp next(["--" <> option | rest], _aliases, switches, strict?, allow_nonexistent_atoms?) do
332342
{option, value} = split_option(option)
333-
tagged = tag_option(option, switches)
334-
do_next(tagged, value, "--" <> option, rest, switches, strict?)
343+
tagged = tag_option(option, switches, allow_nonexistent_atoms?)
344+
do_next(tagged, value, "--" <> option, rest, switches, strict?, allow_nonexistent_atoms?)
335345
end
336346

337347
# Handles -a, -abc, -abc=something
338-
defp next(["-" <> option | rest] = argv, aliases, switches, strict?) do
348+
defp next(["-" <> option | rest] = argv, aliases, switches, strict?, allow_nonexistent_atoms?) do
339349
{option, value} = split_option(option)
340350
original = "-" <> option
341351

@@ -345,26 +355,26 @@ defmodule OptionParser do
345355
String.contains?(option, ["-", "_"]) ->
346356
{:undefined, original, value, rest}
347357
String.length(option) > 1 ->
348-
key = get_option_key(option)
358+
key = get_option_key(option, allow_nonexistent_atoms?)
349359
option_key = aliases[key]
350360
if key && option_key do
351361
IO.warn "multi-letter aliases are deprecated, got: #{inspect(key)}"
352-
do_next({:default, option_key}, value, original, rest, switches, strict?)
362+
do_next({:default, option_key}, value, original, rest, switches, strict?, allow_nonexistent_atoms?)
353363
else
354-
next(expand_multiletter_alias(option, value) ++ rest, aliases, switches, strict?)
364+
next(expand_multiletter_alias(option, value) ++ rest, aliases, switches, strict?, allow_nonexistent_atoms?)
355365
end
356366
true ->
357367
# We have a regular one-letter alias here
358-
tagged = tag_oneletter_alias(option, aliases)
359-
do_next(tagged, value, original, rest, switches, strict?)
368+
tagged = tag_oneletter_alias(option, aliases, allow_nonexistent_atoms?)
369+
do_next(tagged, value, original, rest, switches, strict?, allow_nonexistent_atoms?)
360370
end
361371
end
362372

363-
defp next(argv, _aliases, _switches, _strict?) do
373+
defp next(argv, _aliases, _switches, _strict?, _allow_nonexistent_atoms?) do
364374
{:error, argv}
365375
end
366376

367-
defp do_next(tagged, value, original, rest, switches, strict?) do
377+
defp do_next(tagged, value, original, rest, switches, strict?, allow_nonexistent_atoms?) do
368378
if strict? and not option_defined?(tagged, switches) do
369379
{:undefined, original, value, rest}
370380
else
@@ -489,6 +499,7 @@ defmodule OptionParser do
489499

490500
defp compile_config(opts) do
491501
aliases = opts[:aliases] || []
502+
allow_nonexistent_atoms? = opts[:allow_nonexistent_atoms] || false
492503

493504
{switches, strict?} = cond do
494505
opts[:switches] && opts[:strict] ->
@@ -501,7 +512,7 @@ defmodule OptionParser do
501512
{[], false}
502513
end
503514

504-
{aliases, switches, strict?}
515+
{aliases, switches, strict?, allow_nonexistent_atoms?}
505516
end
506517

507518
defp validate_option(value, kinds) do
@@ -552,27 +563,27 @@ defmodule OptionParser do
552563
end
553564
end
554565

555-
defp tag_option("no-" <> option = original, switches) do
566+
defp tag_option("no-" <> option = original, switches, allow_nonexistent_atoms?) do
556567
cond do
557-
(negated = get_option_key(option)) && :boolean in List.wrap(switches[negated]) ->
568+
(negated = get_option_key(option, allow_nonexistent_atoms?)) && :boolean in List.wrap(switches[negated]) ->
558569
{:negated, negated}
559-
option_key = get_option_key(original) ->
570+
option_key = get_option_key(original, allow_nonexistent_atoms?) ->
560571
{:default, option_key}
561572
true ->
562573
:unknown
563574
end
564575
end
565576

566-
defp tag_option(option, _switches) do
567-
if option_key = get_option_key(option) do
577+
defp tag_option(option, _switches, allow_nonexistent_atoms?) do
578+
if option_key = get_option_key(option, allow_nonexistent_atoms?) do
568579
{:default, option_key}
569580
else
570581
:unknown
571582
end
572583
end
573584

574-
defp tag_oneletter_alias(alias, aliases) when is_binary(alias) do
575-
if option_key = aliases[to_existing_key(alias)] do
585+
defp tag_oneletter_alias(alias, aliases, allow_nonexistent_atoms?) when is_binary(alias) do
586+
if option_key = aliases[to_existing_key(alias, allow_nonexistent_atoms?)] do
576587
{:default, option_key}
577588
else
578589
:unknown
@@ -662,13 +673,15 @@ defmodule OptionParser do
662673
defp to_underscore(<<>>, acc),
663674
do: acc
664675

665-
def get_option_key(option) do
676+
def get_option_key(option, allow_nonexistent_atoms?) do
666677
if string = to_underscore(option) do
667-
to_existing_key(string)
678+
to_existing_key(string, allow_nonexistent_atoms?)
668679
end
669680
end
670681

671-
defp to_existing_key(option) do
682+
defp to_existing_key(option, true),
683+
do: String.to_atom(option)
684+
defp to_existing_key(option, false) do
672685
try do
673686
String.to_existing_atom(option)
674687
rescue
@@ -702,7 +715,8 @@ defmodule OptionParser do
702715
end
703716

704717
defp get_type(option, opts, types) do
705-
key = option |> String.trim_leading("-") |> get_option_key()
718+
allow_nonexistent_atoms? = opts[:allow_nonexistent_atoms] || false
719+
key = option |> String.trim_leading("-") |> get_option_key(allow_nonexistent_atoms?)
706720

707721
if option_key = opts[:aliases][key] do
708722
types[option_key]

lib/elixir/test/elixir/option_parser_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ defmodule OptionParserTest do
256256
== {[foo: true, boo: "-"], ["-"], []}
257257
end
258258

259+
test "allow nonexistent atoms" do
260+
assert OptionParser.parse(["--option-key-creates-atom"], allow_nonexistent_atoms: true) ==
261+
{[{String.to_atom("option_key_creates_atom"), true}], [], []}
262+
end
263+
259264
test "correctly handles negative integers" do
260265
assert OptionParser.parse(["arg1", "-43"])
261266
== {[], ["arg1", "-43"], []}

0 commit comments

Comments
 (0)