902 lines
29 KiB
Elixir
902 lines
29 KiB
Elixir
defmodule OptionParser do
|
|
@moduledoc """
|
|
Functions for parsing command line arguments.
|
|
|
|
When calling a command, it's possible to pass command line options
|
|
to modify what the command does. In this documentation, those are
|
|
called "switches", in other situations they may be called "flags"
|
|
or simply "options". A switch can be given a value, also called an
|
|
"argument".
|
|
|
|
The main function in this module is `parse/2`, which parses a list
|
|
of command line options and arguments into a keyword list:
|
|
|
|
iex> OptionParser.parse(["--debug"], strict: [debug: :boolean])
|
|
{[debug: true], [], []}
|
|
|
|
`OptionParser` provides some conveniences out of the box,
|
|
such as aliases and automatic handling of negation switches.
|
|
|
|
The `parse_head/2` function is an alternative to `parse/2`
|
|
which stops parsing as soon as it finds a value that is not
|
|
a switch nor a value for a previous switch.
|
|
|
|
This module also provides low-level functions, such as `next/2`,
|
|
for parsing switches manually, as well as `split/1` and `to_argv/1`
|
|
for parsing from and converting switches to strings.
|
|
"""
|
|
|
|
@type argv :: [String.t()]
|
|
@type parsed :: keyword
|
|
@type errors :: [{String.t(), String.t() | nil}]
|
|
@type options :: [
|
|
switches: keyword,
|
|
strict: keyword,
|
|
aliases: keyword,
|
|
allow_nonexistent_atoms: boolean,
|
|
return_separator: boolean
|
|
]
|
|
|
|
defmodule ParseError do
|
|
defexception [:message]
|
|
end
|
|
|
|
@doc """
|
|
Parses `argv` into a keyword list.
|
|
|
|
It returns a three-element tuple with the form `{parsed, args, invalid}`, where:
|
|
|
|
* `parsed` is a keyword list of parsed switches with `{switch_name, value}`
|
|
tuples in it; `switch_name` is the atom representing the switch name while
|
|
`value` is the value for that switch parsed according to `opts` (see the
|
|
"Examples" section for more information)
|
|
* `args` is a list of the remaining arguments in `argv` as strings
|
|
* `invalid` is a list of invalid options as `{option_name, value}` where
|
|
`option_name` is the raw option and `value` is `nil` if the option wasn't
|
|
expected or the string value if the value didn't have the expected type for
|
|
the corresponding option
|
|
|
|
Elixir converts switches to underscored atoms, so `--source-path` becomes
|
|
`:source_path`. This is done to better suit Elixir conventions. However, this
|
|
means that switches can't contain underscores and switches that do contain
|
|
underscores are always returned in the list of invalid switches.
|
|
|
|
When parsing, it is common to list switches and their expected types:
|
|
|
|
iex> OptionParser.parse(["--debug"], strict: [debug: :boolean])
|
|
{[debug: true], [], []}
|
|
|
|
iex> OptionParser.parse(["--source", "lib"], strict: [source: :string])
|
|
{[source: "lib"], [], []}
|
|
|
|
iex> OptionParser.parse(
|
|
...> ["--source-path", "lib", "test/enum_test.exs", "--verbose"],
|
|
...> strict: [source_path: :string, verbose: :boolean]
|
|
...> )
|
|
{[source_path: "lib", verbose: true], ["test/enum_test.exs"], []}
|
|
|
|
We will explore the valid switches and operation modes of option parser below.
|
|
|
|
## Options
|
|
|
|
The following options are supported:
|
|
|
|
* `:switches` or `:strict` - see the "Switch definitions" section below
|
|
* `:allow_nonexistent_atoms` - see the "Parsing unknown switches" section below
|
|
* `:aliases` - see the "Aliases" section below
|
|
* `:return_separator` - see the "Return separator" section below
|
|
|
|
## Switch definitions
|
|
|
|
Switches can be specified via one of two options:
|
|
|
|
* `:strict` - defines strict switches and their types. Any switch
|
|
in `argv` that is not specified in the list is returned in the
|
|
invalid options list. This is the preferred way to parse options.
|
|
|
|
* `:switches` - defines switches and their types. This function
|
|
still attempts to parse switches that are not in this list.
|
|
|
|
Both these options accept a keyword list where the key is an atom
|
|
defining the name of the switch and value is the `type` of the
|
|
switch (see the "Types" section below for more information).
|
|
|
|
Note that you should only supply the `:switches` or the `:strict` option.
|
|
If you supply both, an `ArgumentError` exception will be raised.
|
|
|
|
### Types
|
|
|
|
Switches parsed by `OptionParser` may take zero or one arguments.
|
|
|
|
The following switches types take no arguments:
|
|
|
|
* `:boolean` - sets the value to `true` when given (see also the
|
|
"Negation switches" section below)
|
|
* `:count` - counts the number of times the switch is given
|
|
|
|
The following switches take one argument:
|
|
|
|
* `:integer` - parses the value as an integer
|
|
* `:float` - parses the value as a float
|
|
* `:string` - parses the value as a string
|
|
|
|
If a switch can't be parsed according to the given type, it is
|
|
returned in the invalid options list.
|
|
|
|
### Modifiers
|
|
|
|
Switches can be specified with modifiers, which change how
|
|
they behave. The following modifiers are supported:
|
|
|
|
* `:keep` - keeps duplicated elements instead of overriding them;
|
|
works with all types except `:count`. Specifying `switch_name: :keep`
|
|
assumes the type of `:switch_name` will be `:string`.
|
|
|
|
To use `:keep` with a type other than `:string`, use a list as the type
|
|
for the switch. For example: `[foo: [:integer, :keep]]`.
|
|
|
|
### Negation switches
|
|
|
|
In case a switch `SWITCH` is specified to have type `:boolean`, it may be
|
|
passed as `--no-SWITCH` as well which will set the option to `false`:
|
|
|
|
iex> OptionParser.parse(["--no-op", "path/to/file"], switches: [op: :boolean])
|
|
{[op: false], ["path/to/file"], []}
|
|
|
|
### Parsing unknown switches
|
|
|
|
When the `:switches` option is given, `OptionParser` will attempt to parse
|
|
unknown switches:
|
|
|
|
iex> OptionParser.parse(["--debug"], switches: [key: :string])
|
|
{[debug: true], [], []}
|
|
|
|
Even though we haven't specified `--debug` in the list of switches, it is part
|
|
of the returned options. This would also work:
|
|
|
|
iex> OptionParser.parse(["--debug", "value"], switches: [key: :string])
|
|
{[debug: "value"], [], []}
|
|
|
|
Switches followed by a value will be assigned the value, as a string. Switches
|
|
without an argument will be set automatically to `true`. Since we cannot assert
|
|
the type of the switch value, it is preferred to use the `:strict` option that
|
|
accepts only known switches and always verify their types.
|
|
|
|
If you do want to parse unknown switches, remember that Elixir converts switches
|
|
to atoms. Since atoms are not garbage-collected, OptionParser will only parse
|
|
switches that translate to atoms used by the runtime to avoid leaking atoms.
|
|
For instance, the code below will discard the `--option-parser-example` switch
|
|
because the `:option_parser_example` atom is never used anywhere:
|
|
|
|
OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
|
|
# The :option_parser_example atom is not used anywhere below
|
|
|
|
However, the code below would work as long as `:option_parser_example` atom is
|
|
used at some point later (or earlier) **in the same module**. For example:
|
|
|
|
{opts, _, _} = OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
|
|
# ... then somewhere in the same module you access it ...
|
|
opts[:option_parser_example]
|
|
|
|
In other words, Elixir will only parse options that are used by the runtime,
|
|
ignoring all others. If you would like to parse all switches, regardless if
|
|
they exist or not, you can force creation of atoms by passing
|
|
`allow_nonexistent_atoms: true` as option. Use this option with care. It is
|
|
only useful when you are building command-line applications that receive
|
|
dynamically-named arguments and must be avoided in long-running systems.
|
|
|
|
## Aliases
|
|
|
|
A set of aliases can be specified in the `:aliases` option:
|
|
|
|
iex> OptionParser.parse(["-d"], aliases: [d: :debug], strict: [debug: :boolean])
|
|
{[debug: true], [], []}
|
|
|
|
## Examples
|
|
|
|
Here are some examples of working with different types and modifiers:
|
|
|
|
iex> OptionParser.parse(["--unlock", "path/to/file"], strict: [unlock: :boolean])
|
|
{[unlock: true], ["path/to/file"], []}
|
|
|
|
iex> OptionParser.parse(
|
|
...> ["--unlock", "--limit", "0", "path/to/file"],
|
|
...> strict: [unlock: :boolean, limit: :integer]
|
|
...> )
|
|
{[unlock: true, limit: 0], ["path/to/file"], []}
|
|
|
|
iex> OptionParser.parse(["--limit", "3"], strict: [limit: :integer])
|
|
{[limit: 3], [], []}
|
|
|
|
iex> OptionParser.parse(["--limit", "xyz"], strict: [limit: :integer])
|
|
{[], [], [{"--limit", "xyz"}]}
|
|
|
|
iex> OptionParser.parse(["--verbose"], switches: [verbose: :count])
|
|
{[verbose: 1], [], []}
|
|
|
|
iex> OptionParser.parse(["-v", "-v"], aliases: [v: :verbose], strict: [verbose: :count])
|
|
{[verbose: 2], [], []}
|
|
|
|
iex> OptionParser.parse(["--unknown", "xyz"], strict: [])
|
|
{[], ["xyz"], [{"--unknown", nil}]}
|
|
|
|
iex> OptionParser.parse(
|
|
...> ["--limit", "3", "--unknown", "xyz"],
|
|
...> switches: [limit: :integer]
|
|
...> )
|
|
{[limit: 3, unknown: "xyz"], [], []}
|
|
|
|
iex> OptionParser.parse(
|
|
...> ["--unlock", "path/to/file", "--unlock", "path/to/another/file"],
|
|
...> strict: [unlock: :keep]
|
|
...> )
|
|
{[unlock: "path/to/file", unlock: "path/to/another/file"], [], []}
|
|
|
|
## Return separator
|
|
|
|
The separator `--` implies options should no longer be processed.
|
|
By default, the separator is not returned as parts of the arguments,
|
|
but that can be changed via the `:return_separator` option:
|
|
|
|
iex> OptionParser.parse(["--", "lib"], return_separator: true, strict: [])
|
|
{[], ["--", "lib"], []}
|
|
|
|
iex> OptionParser.parse(["--no-halt", "--", "lib"], return_separator: true, switches: [halt: :boolean])
|
|
{[halt: false], ["--", "lib"], []}
|
|
|
|
iex> OptionParser.parse(["script.exs", "--no-halt", "--", "foo"], return_separator: true, switches: [halt: :boolean])
|
|
{[{:halt, false}], ["script.exs", "--", "foo"], []}
|
|
|
|
"""
|
|
@spec parse(argv, options) :: {parsed, argv, errors}
|
|
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
|
do_parse(argv, build_config(opts), [], [], [], true)
|
|
end
|
|
|
|
@doc """
|
|
The same as `parse/2` but raises an `OptionParser.ParseError`
|
|
exception if any invalid options are given.
|
|
|
|
If there are no errors, returns a `{parsed, rest}` tuple where:
|
|
|
|
* `parsed` is the list of parsed switches (same as in `parse/2`)
|
|
* `rest` is the list of arguments (same as in `parse/2`)
|
|
|
|
## Examples
|
|
|
|
iex> OptionParser.parse!(["--debug", "path/to/file"], strict: [debug: :boolean])
|
|
{[debug: true], ["path/to/file"]}
|
|
|
|
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
|
|
** (OptionParser.ParseError) 1 error found!
|
|
--limit : Expected type integer, got "xyz"
|
|
|
|
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
|
|
** (OptionParser.ParseError) 1 error found!
|
|
--unknown : Unknown option
|
|
|
|
iex> OptionParser.parse!(
|
|
...> ["-l", "xyz", "-f", "bar"],
|
|
...> switches: [limit: :integer, foo: :integer],
|
|
...> aliases: [l: :limit, f: :foo]
|
|
...> )
|
|
** (OptionParser.ParseError) 2 errors found!
|
|
-l : Expected type integer, got "xyz"
|
|
-f : Expected type integer, got "bar"
|
|
|
|
"""
|
|
@spec parse!(argv, options) :: {parsed, argv}
|
|
def parse!(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
|
case parse(argv, opts) do
|
|
{parsed, args, []} -> {parsed, args}
|
|
{_, _, errors} -> raise ParseError, format_errors(errors, opts)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Similar to `parse/2` but only parses the head of `argv`;
|
|
as soon as it finds a non-switch, it stops parsing.
|
|
|
|
See `parse/2` for more information.
|
|
|
|
## Example
|
|
|
|
iex> OptionParser.parse_head(
|
|
...> ["--source", "lib", "test/enum_test.exs", "--verbose"],
|
|
...> switches: [source: :string, verbose: :boolean]
|
|
...> )
|
|
{[source: "lib"], ["test/enum_test.exs", "--verbose"], []}
|
|
|
|
iex> OptionParser.parse_head(
|
|
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
|
|
...> switches: [source: :string, verbose: :boolean, unlock: :boolean]
|
|
...> )
|
|
{[verbose: true, source: "lib"], ["test/enum_test.exs", "--unlock"], []}
|
|
|
|
"""
|
|
@spec parse_head(argv, options) :: {parsed, argv, errors}
|
|
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
|
do_parse(argv, build_config(opts), [], [], [], false)
|
|
end
|
|
|
|
@doc """
|
|
The same as `parse_head/2` but raises an `OptionParser.ParseError`
|
|
exception if any invalid options are given.
|
|
|
|
If there are no errors, returns a `{parsed, rest}` tuple where:
|
|
|
|
* `parsed` is the list of parsed switches (same as in `parse_head/2`)
|
|
* `rest` is the list of arguments (same as in `parse_head/2`)
|
|
|
|
## Examples
|
|
|
|
iex> OptionParser.parse_head!(
|
|
...> ["--source", "lib", "path/to/file", "--verbose"],
|
|
...> switches: [source: :string, verbose: :boolean]
|
|
...> )
|
|
{[source: "lib"], ["path/to/file", "--verbose"]}
|
|
|
|
iex> OptionParser.parse_head!(
|
|
...> ["--number", "lib", "test/enum_test.exs", "--verbose"],
|
|
...> strict: [number: :integer]
|
|
...> )
|
|
** (OptionParser.ParseError) 1 error found!
|
|
--number : Expected type integer, got "lib"
|
|
|
|
iex> OptionParser.parse_head!(
|
|
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
|
|
...> strict: [verbose: :integer, source: :integer]
|
|
...> )
|
|
** (OptionParser.ParseError) 2 errors found!
|
|
--verbose : Missing argument of type integer
|
|
--source : Expected type integer, got "lib"
|
|
|
|
"""
|
|
@spec parse_head!(argv, options) :: {parsed, argv}
|
|
def parse_head!(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
|
case parse_head(argv, opts) do
|
|
{parsed, args, []} -> {parsed, args}
|
|
{_, _, errors} -> raise ParseError, format_errors(errors, opts)
|
|
end
|
|
end
|
|
|
|
defp do_parse([], _config, opts, args, invalid, _all?) do
|
|
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
|
|
end
|
|
|
|
defp do_parse(argv, %{switches: switches} = config, opts, args, invalid, all?) do
|
|
case next_with_config(argv, config) do
|
|
{:ok, option, value, rest} ->
|
|
# the option exists and it was successfully parsed
|
|
kinds = List.wrap(Keyword.get(switches, option))
|
|
new_opts = store_option(opts, option, value, kinds)
|
|
do_parse(rest, config, new_opts, args, invalid, all?)
|
|
|
|
{:invalid, option, value, rest} ->
|
|
# the option exist but it has wrong value
|
|
do_parse(rest, config, opts, args, [{option, value} | invalid], all?)
|
|
|
|
{:undefined, option, _value, rest} ->
|
|
invalid = if config.strict?, do: [{option, nil} | invalid], else: invalid
|
|
do_parse(rest, config, opts, args, invalid, all?)
|
|
|
|
{:error, ["--" | rest] = remaining_args} ->
|
|
args =
|
|
if config.return_separator? do
|
|
Enum.reverse(args, remaining_args)
|
|
else
|
|
Enum.reverse(args, rest)
|
|
end
|
|
|
|
{Enum.reverse(opts), args, Enum.reverse(invalid)}
|
|
|
|
{:error, [arg | rest] = remaining_args} ->
|
|
# there is no option
|
|
if all? do
|
|
do_parse(rest, config, opts, [arg | args], invalid, all?)
|
|
else
|
|
{Enum.reverse(opts), Enum.reverse(args, remaining_args), Enum.reverse(invalid)}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Low-level function that parses one option.
|
|
|
|
It accepts the same options as `parse/2` and `parse_head/2`
|
|
as both functions are built on top of this function. This function
|
|
may return:
|
|
|
|
* `{:ok, key, value, rest}` - the option `key` with `value` was
|
|
successfully parsed
|
|
|
|
* `{:invalid, key, value, rest}` - the option `key` is invalid with `value`
|
|
(returned when the value cannot be parsed according to the switch type)
|
|
|
|
* `{:undefined, key, value, rest}` - the option `key` is undefined
|
|
(returned in strict mode when the switch is unknown or on nonexistent atoms)
|
|
|
|
* `{:error, rest}` - there are no switches at the head of the given `argv`
|
|
|
|
"""
|
|
@spec next(argv, options) ::
|
|
{:ok, key :: atom, value :: term, argv}
|
|
| {:invalid, String.t(), String.t() | nil, argv}
|
|
| {:undefined, String.t(), String.t() | nil, argv}
|
|
| {:error, argv}
|
|
|
|
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
|
next_with_config(argv, build_config(opts))
|
|
end
|
|
|
|
defp next_with_config([], _config) do
|
|
{:error, []}
|
|
end
|
|
|
|
defp next_with_config(["--" | _] = argv, _config) do
|
|
{:error, argv}
|
|
end
|
|
|
|
defp next_with_config(["-" | _] = argv, _config) do
|
|
{:error, argv}
|
|
end
|
|
|
|
defp next_with_config(["- " <> _ | _] = argv, _config) do
|
|
{:error, argv}
|
|
end
|
|
|
|
# Handles --foo or --foo=bar
|
|
defp next_with_config(["--" <> option | rest], config) do
|
|
{option, value} = split_option(option)
|
|
|
|
if String.contains?(option, ["_"]) do
|
|
{:undefined, "--" <> option, value, rest}
|
|
else
|
|
tagged = tag_option(option, config)
|
|
next_tagged(tagged, value, "--" <> option, rest, config)
|
|
end
|
|
end
|
|
|
|
# Handles -a, -abc, -abc=something, -n2
|
|
defp next_with_config(["-" <> option | rest] = argv, config) do
|
|
{option, value} = split_option(option)
|
|
original = "-" <> option
|
|
|
|
cond do
|
|
is_nil(value) and starts_with_number?(option) ->
|
|
{:error, argv}
|
|
|
|
String.contains?(option, ["-", "_"]) ->
|
|
{:undefined, original, value, rest}
|
|
|
|
String.length(option) == 1 ->
|
|
# We have a regular one-letter alias here
|
|
tagged = tag_oneletter_alias(option, config)
|
|
next_tagged(tagged, value, original, rest, config)
|
|
|
|
true ->
|
|
key = get_option_key(option, config.allow_nonexistent_atoms?)
|
|
option_key = config.aliases[key]
|
|
|
|
if key && option_key do
|
|
IO.warn("multi-letter aliases are deprecated, got: #{inspect(key)}")
|
|
next_tagged({:default, option_key}, value, original, rest, config)
|
|
else
|
|
next_with_config(expand_multiletter_alias(option, value) ++ rest, config)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp next_with_config(argv, _config) do
|
|
{:error, argv}
|
|
end
|
|
|
|
defp next_tagged(:unknown, value, original, rest, _) do
|
|
{value, _kinds, rest} = normalize_value(value, [], rest)
|
|
{:undefined, original, value, rest}
|
|
end
|
|
|
|
defp next_tagged({tag, option}, value, original, rest, %{switches: switches, strict?: strict?}) do
|
|
if strict? and not Keyword.has_key?(switches, option) do
|
|
{:undefined, original, value, rest}
|
|
else
|
|
{kinds, value} = normalize_tag(tag, option, value, switches)
|
|
{value, kinds, rest} = normalize_value(value, kinds, rest)
|
|
|
|
case validate_option(value, kinds) do
|
|
{:ok, new_value} -> {:ok, option, new_value, rest}
|
|
:invalid -> {:invalid, original, value, rest}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Receives a key-value enumerable and converts it to `t:argv/0`.
|
|
|
|
Keys must be atoms. Keys with `nil` value are discarded,
|
|
boolean values are converted to `--key` or `--no-key`
|
|
(if the value is `true` or `false`, respectively),
|
|
and all other values are converted using `to_string/1`.
|
|
|
|
It is advised to pass to `to_argv/2` the same set of `options`
|
|
given to `parse/2`. Some switches can only be reconstructed
|
|
correctly with the `:switches` information in hand.
|
|
|
|
## Examples
|
|
|
|
iex> OptionParser.to_argv(foo_bar: "baz")
|
|
["--foo-bar", "baz"]
|
|
iex> OptionParser.to_argv(bool: true, bool: false, discarded: nil)
|
|
["--bool", "--no-bool"]
|
|
|
|
Some switches will output different values based on the switches
|
|
types:
|
|
|
|
iex> OptionParser.to_argv([number: 2], switches: [])
|
|
["--number", "2"]
|
|
iex> OptionParser.to_argv([number: 2], switches: [number: :count])
|
|
["--number", "--number"]
|
|
|
|
"""
|
|
@spec to_argv(Enumerable.t(), options) :: argv
|
|
def to_argv(enum, options \\ []) do
|
|
switches = Keyword.get(options, :switches, [])
|
|
|
|
Enum.flat_map(enum, fn
|
|
{_key, nil} -> []
|
|
{key, true} -> [to_switch(key)]
|
|
{key, false} -> [to_switch(key, "--no-")]
|
|
{key, value} -> to_argv(key, value, switches)
|
|
end)
|
|
end
|
|
|
|
defp to_argv(key, value, switches) do
|
|
if switches[key] == :count do
|
|
List.duplicate(to_switch(key), value)
|
|
else
|
|
[to_switch(key), to_string(value)]
|
|
end
|
|
end
|
|
|
|
defp to_switch(key, prefix \\ "--") when is_atom(key) do
|
|
prefix <> String.replace(Atom.to_string(key), "_", "-")
|
|
end
|
|
|
|
@doc ~S"""
|
|
Splits a string into `t:argv/0` chunks.
|
|
|
|
This function splits the given `string` into a list of strings in a similar
|
|
way to many shells.
|
|
|
|
## Examples
|
|
|
|
iex> OptionParser.split("foo bar")
|
|
["foo", "bar"]
|
|
|
|
iex> OptionParser.split("foo \"bar baz\"")
|
|
["foo", "bar baz"]
|
|
|
|
"""
|
|
@spec split(String.t()) :: argv
|
|
def split(string) when is_binary(string) do
|
|
do_split(String.trim_leading(string, " "), "", [], nil)
|
|
end
|
|
|
|
# If we have an escaped quote, simply remove the escape
|
|
defp do_split(<<?\\, quote, t::binary>>, buffer, acc, quote),
|
|
do: do_split(t, <<buffer::binary, quote>>, acc, quote)
|
|
|
|
# If we have a quote and we were not in a quote, start one
|
|
defp do_split(<<quote, t::binary>>, buffer, acc, nil) when quote in [?", ?'],
|
|
do: do_split(t, buffer, acc, quote)
|
|
|
|
# If we have a quote and we were inside it, close it
|
|
defp do_split(<<quote, t::binary>>, buffer, acc, quote), do: do_split(t, buffer, acc, nil)
|
|
|
|
# If we have an escaped quote/space, simply remove the escape as long as we are not inside a quote
|
|
defp do_split(<<?\\, h, t::binary>>, buffer, acc, nil) when h in [?\s, ?', ?"],
|
|
do: do_split(t, <<buffer::binary, h>>, acc, nil)
|
|
|
|
# If we have space and we are outside of a quote, start new segment
|
|
defp do_split(<<?\s, t::binary>>, buffer, acc, nil),
|
|
do: do_split(String.trim_leading(t, " "), "", [buffer | acc], nil)
|
|
|
|
# All other characters are moved to buffer
|
|
defp do_split(<<h, t::binary>>, buffer, acc, quote) do
|
|
do_split(t, <<buffer::binary, h>>, acc, quote)
|
|
end
|
|
|
|
# Finish the string expecting a nil marker
|
|
defp do_split(<<>>, "", acc, nil), do: Enum.reverse(acc)
|
|
|
|
defp do_split(<<>>, buffer, acc, nil), do: Enum.reverse([buffer | acc])
|
|
|
|
# Otherwise raise
|
|
defp do_split(<<>>, _, _acc, marker) do
|
|
raise "argv string did not terminate properly, a #{<<marker>>} was opened but never closed"
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp build_config(opts) do
|
|
{switches, strict?} =
|
|
cond do
|
|
opts[:switches] && opts[:strict] ->
|
|
raise ArgumentError, ":switches and :strict cannot be given together"
|
|
|
|
switches = opts[:switches] ->
|
|
validate_switches(switches)
|
|
{switches, false}
|
|
|
|
strict = opts[:strict] ->
|
|
validate_switches(strict)
|
|
{strict, true}
|
|
|
|
true ->
|
|
IO.warn("not passing the :switches or :strict option to OptionParser is deprecated")
|
|
{[], false}
|
|
end
|
|
|
|
%{
|
|
aliases: opts[:aliases] || [],
|
|
allow_nonexistent_atoms?: opts[:allow_nonexistent_atoms] || false,
|
|
return_separator?: opts[:return_separator] || false,
|
|
strict?: strict?,
|
|
switches: switches
|
|
}
|
|
end
|
|
|
|
defp validate_switches(switches) do
|
|
Enum.map(switches, &validate_switch/1)
|
|
end
|
|
|
|
defp validate_switch({_name, type_or_type_and_modifiers}) do
|
|
valid = [:boolean, :count, :integer, :float, :string, :keep]
|
|
invalid = List.wrap(type_or_type_and_modifiers) -- valid
|
|
|
|
if invalid != [] do
|
|
raise ArgumentError,
|
|
"invalid switch types/modifiers: " <> Enum.map_join(invalid, ", ", &inspect/1)
|
|
end
|
|
end
|
|
|
|
defp validate_option(value, kinds) do
|
|
{invalid?, value} =
|
|
cond do
|
|
:invalid in kinds ->
|
|
{true, value}
|
|
|
|
:boolean in kinds ->
|
|
case value do
|
|
t when t in [true, "true"] -> {false, true}
|
|
f when f in [false, "false"] -> {false, false}
|
|
_ -> {true, value}
|
|
end
|
|
|
|
:count in kinds ->
|
|
case value do
|
|
nil -> {false, 1}
|
|
_ -> {true, value}
|
|
end
|
|
|
|
:integer in kinds ->
|
|
case Integer.parse(value) do
|
|
{value, ""} -> {false, value}
|
|
_ -> {true, value}
|
|
end
|
|
|
|
:float in kinds ->
|
|
case Float.parse(value) do
|
|
{value, ""} -> {false, value}
|
|
_ -> {true, value}
|
|
end
|
|
|
|
true ->
|
|
{false, value}
|
|
end
|
|
|
|
if invalid? do
|
|
:invalid
|
|
else
|
|
{:ok, value}
|
|
end
|
|
end
|
|
|
|
defp store_option(dict, option, value, kinds) do
|
|
cond do
|
|
:count in kinds ->
|
|
Keyword.update(dict, option, value, &(&1 + 1))
|
|
|
|
:keep in kinds ->
|
|
[{option, value} | dict]
|
|
|
|
true ->
|
|
[{option, value} | Keyword.delete(dict, option)]
|
|
end
|
|
end
|
|
|
|
defp tag_option("no-" <> option = original, config) do
|
|
%{switches: switches, allow_nonexistent_atoms?: allow_nonexistent_atoms?} = config
|
|
|
|
cond do
|
|
(negated = get_option_key(option, allow_nonexistent_atoms?)) &&
|
|
:boolean in List.wrap(switches[negated]) ->
|
|
{:negated, negated}
|
|
|
|
option_key = get_option_key(original, allow_nonexistent_atoms?) ->
|
|
{:default, option_key}
|
|
|
|
true ->
|
|
:unknown
|
|
end
|
|
end
|
|
|
|
defp tag_option(option, config) do
|
|
%{allow_nonexistent_atoms?: allow_nonexistent_atoms?} = config
|
|
|
|
if option_key = get_option_key(option, allow_nonexistent_atoms?) do
|
|
{:default, option_key}
|
|
else
|
|
:unknown
|
|
end
|
|
end
|
|
|
|
defp tag_oneletter_alias(alias, config) when is_binary(alias) do
|
|
%{aliases: aliases, allow_nonexistent_atoms?: allow_nonexistent_atoms?} = config
|
|
|
|
if option_key = aliases[to_existing_key(alias, allow_nonexistent_atoms?)] do
|
|
{:default, option_key}
|
|
else
|
|
:unknown
|
|
end
|
|
end
|
|
|
|
defp expand_multiletter_alias(options, value) do
|
|
{options, maybe_integer} =
|
|
options
|
|
|> String.to_charlist()
|
|
|> Enum.split_while(&(&1 not in ?0..?9))
|
|
|
|
{last, expanded} =
|
|
options
|
|
|> List.to_string()
|
|
|> String.graphemes()
|
|
|> Enum.map(&("-" <> &1))
|
|
|> List.pop_at(-1)
|
|
|
|
expanded ++
|
|
[
|
|
last <>
|
|
if(maybe_integer != [], do: "=#{maybe_integer}", else: "") <>
|
|
if(value, do: "=#{value}", else: "")
|
|
]
|
|
end
|
|
|
|
defp normalize_tag(:negated, option, value, switches) do
|
|
if value do
|
|
{[:invalid], value}
|
|
else
|
|
{List.wrap(switches[option]), false}
|
|
end
|
|
end
|
|
|
|
defp normalize_tag(:default, option, value, switches) do
|
|
{List.wrap(switches[option]), value}
|
|
end
|
|
|
|
defp normalize_value(nil, kinds, t) do
|
|
cond do
|
|
:boolean in kinds ->
|
|
{true, kinds, t}
|
|
|
|
:count in kinds ->
|
|
{nil, kinds, t}
|
|
|
|
value_in_tail?(t) ->
|
|
[h | t] = t
|
|
{h, kinds, t}
|
|
|
|
kinds == [] ->
|
|
{true, kinds, t}
|
|
|
|
true ->
|
|
{nil, [:invalid], t}
|
|
end
|
|
end
|
|
|
|
defp normalize_value(value, kinds, t) do
|
|
{value, kinds, t}
|
|
end
|
|
|
|
defp value_in_tail?(["-" | _]), do: true
|
|
defp value_in_tail?(["- " <> _ | _]), do: true
|
|
defp value_in_tail?(["-" <> arg | _]), do: starts_with_number?(arg)
|
|
defp value_in_tail?([]), do: false
|
|
defp value_in_tail?(_), do: true
|
|
|
|
defp split_option(option) do
|
|
case :binary.split(option, "=") do
|
|
[h] -> {h, nil}
|
|
[h, t] -> {h, t}
|
|
end
|
|
end
|
|
|
|
defp to_underscore(option), do: to_underscore(option, <<>>)
|
|
defp to_underscore("-" <> rest, acc), do: to_underscore(rest, acc <> "_")
|
|
defp to_underscore(<<c>> <> rest, acc), do: to_underscore(rest, <<acc::binary, c>>)
|
|
defp to_underscore(<<>>, acc), do: acc
|
|
|
|
defp get_option_key(option, allow_nonexistent_atoms?) do
|
|
option
|
|
|> to_underscore()
|
|
|> to_existing_key(allow_nonexistent_atoms?)
|
|
end
|
|
|
|
defp to_existing_key(option, true), do: String.to_atom(option)
|
|
|
|
defp to_existing_key(option, false) do
|
|
try do
|
|
String.to_existing_atom(option)
|
|
rescue
|
|
ArgumentError -> nil
|
|
end
|
|
end
|
|
|
|
defp starts_with_number?(<<char, _::binary>>) when char in ?0..?9, do: true
|
|
defp starts_with_number?(_), do: false
|
|
|
|
defp format_errors([_ | _] = errors, opts) do
|
|
types = opts[:switches] || opts[:strict]
|
|
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))
|
|
end
|
|
|
|
defp format_error({option, nil}, opts, types) do
|
|
if type = get_type(option, opts, types) do
|
|
"#{option} : Missing argument of type #{type}"
|
|
else
|
|
msg = "#{option} : Unknown option"
|
|
|
|
case did_you_mean(option, types) do
|
|
{similar, score} when score > 0.8 ->
|
|
msg <> ". Did you mean --#{similar}?"
|
|
|
|
_ ->
|
|
msg
|
|
end
|
|
end
|
|
end
|
|
|
|
defp format_error({option, value}, opts, types) do
|
|
type = get_type(option, opts, types)
|
|
"#{option} : Expected type #{type}, got #{inspect(value)}"
|
|
end
|
|
|
|
defp get_type(option, opts, types) do
|
|
allow_nonexistent_atoms? = opts[:allow_nonexistent_atoms] || false
|
|
key = option |> String.trim_leading("-") |> get_option_key(allow_nonexistent_atoms?)
|
|
|
|
if option_key = opts[:aliases][key] do
|
|
types[option_key]
|
|
else
|
|
types[key]
|
|
end
|
|
end
|
|
|
|
defp did_you_mean(option, types) do
|
|
key = option |> String.trim_leading("-") |> String.replace("-", "_")
|
|
Enum.reduce(types, {nil, 0}, &max_similar(&1, key, &2))
|
|
end
|
|
|
|
defp max_similar({source, _}, target, {_, current} = best) do
|
|
source = Atom.to_string(source)
|
|
|
|
score = String.jaro_distance(source, target)
|
|
option = String.replace(source, "_", "-")
|
|
if score < current, do: best, else: {option, score}
|
|
end
|
|
end
|