iex(1)> XOpts.parse([])
{:ok, %{switches: %{}, keywords: %{}, positionals: [], errors: []}}
iex(2)> XOpts.parse(~W[alpha beta gamma])
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[alpha beta gamma], errors: []}}
iex(3)> XOpts.parse(~W[:verbose alpha level: 42 beta gamma])
{:ok, %{switches: %{verbose: true}, keywords: %{level: "42"}, positionals: ~W[alpha beta gamma], errors: []}}
Posix is widely used and although it is ugly (beauty lies you know in whose eyes), we can accept it by default
iex(4)> XOpts.parse(~W[--verbose alpha --level 42 beta gamma])
{:ok, %{switches: %{verbose: true}, keywords: %{level: "42"}, positionals: ~W[alpha beta gamma], errors: []}}
iex(0)> configuration = %{
...(0)> allowed_keywords: %{
...(0)> alpha2: {:string, default: "fr"}
...(0)> },
...(0)> required_keywords: %{
...(0)> value: {:int, min: 1} # could be written as: value: :positive_int
...(0)> }
If we do not want to parse posix switches or keywords we can disable them
iex(5)> XOpts.parse(~W[--verbose alpha --level 42 beta gamma], posix: false)
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[--verbose alpha --level 42 beta gamma], errors: []}}
For fairness we can also disable keyword style arguments:
iex(6)> XOpts.parse(~W[:verbose alpha level: 42 beta gamma], keyword_style: false)
{:ok, %{switches: %{}, keywords: %{}, positionals: ~W[:verbose alpha level: 42 beta gamma], errors: []}}
As we have seen in the examples configuration can be passed as keyword arguments, however for more complex configuration that might become tedious and we therefore will pass in a map.
iex(7)> configuration = %{
...(7)> allowed_keywords: %{
...(7)> count: :int,
...(7)> message: :string }}
...(7)> XOpts.parse(~W[hello count: 42])
{:ok, %{switches: %{}, keywords: %{count: 42}, positionals: ~W[hello], errors: []}}
Did you notice the type conversion of the int
parameter?
Of course you did!
Now the user is alerted of misspelled or badly typed arguments:
iex(8)> configuration = %{
...(8)> allowed_keywords: %{
...(8)> count: :int,
...(8)> message: :string }}
...(8)> XOpts.parse(~W[hello cont: 42])
{:error,
%{switches: %{},
keywords: %{},
positionals: ~W[hello],
errors: [{:forbidden, keyword: "cont", value: 42}]}}
She will also be alerted of badly typed arguments
iex(9)> configuration = %{
...(9)> allowed_keywords: %{
...(9)> count: :int,
...(9)> message: :string }}
...(9)> XOpts.parse(~W[hello cont: alpha])
{:error,
%{switches: %{},
keywords: %{},
positionals: ~W[hello],
errors: [{:invalid_type, keyword: "cont", value: "alpha", requested: :int}]}}
Sometimes keyword arguments need to be present
iex(10)> configuration = %{
...(10)> required_keywords: %{
...(10)> n: :non_negative_int }}
...(10)> XOpts.parse(~W[n: 2])
{:ok, %{switches: %{}, keywords: %{n: 2}, positionals: ~W[], errors: []}}
and when they are not
iex(11)> configuration = %{
...(11)> required_keywords: %{
...(11)> n: :non_negative_int }}
...(11)> XOpts.parse(~W[n: 2])
{:ok,
%{switches: %{}, keywords: %{n: 2}, positionals: ~W[], errors: [{:missing, keyword: "n"}]}}
or violate a constraint
iex(12)> configuration = %{
...(12)> required_keywords: %{
...(12)> n: :non_negative_int }}
...(12)> XOpts.parse(~W[n: 2])
{:error,
%{switches: %{},
keywords: %{n: 2},
positionals: [],
errors: [{:constraint_violation, keyword: "n", value: -1, range: [0]}]}}
iex(13)> configuration = %{
...(13)> allowed_keywords: %{
...(13)> lang: ~r(A [[:alnum:]]{2} z)x},
...(13)> required_keywords: %{
...(13)> base: :any},
...(13)> allowed_switches: []}
...(13)> input = ~W[ --verbose lang: frr ]
...(13)> XOpts.parse(input, configuration)
{:error,
%{
switches: %{},
keywords: %{},
positionals: [],
errors: [
{:forbidden, switch: "verbose"},
{:missing, keyword: "base"},
{:constraint_violation, keyword: "lang", value: "frr", constraint: ~r(A [[:alnum:]]{2} z)x}]}}
Which was, of course, completely unnecessary
iex(14)> configuration = %{
...(14)> allowed_keywords: %{
...(14)> lang: ~r(A [[:alnum:]] {2} z)x},
...(14)> required_keywords: %{base: :any},
...(14)> allowed_switches: []}
...(14)> input = ~W[ lang: fr --base zero cinq ]
...(14)> XOpts.parse(input, configuration)
{:ok,
%{
switches: %{},
keywords: %{base: "zero", lang: "fr"},
positionals: ~W[cinq],
errors: []}}
Allowed keyword arguments as well as positionals can have default values
iex(15)> configuration = %{
...(15)> allowed_keywords: %{
...(15)> n: [:int, default: 42] }}
...(15)> XOpts.parse([], configuration)
{:ok,
%{switches: %{},
keywords: %{n: 42},
poistionals: [],
errors: []}}
which, of course can be overridden:
iex(16)> configuration = %{
...(16)> allowed_keywords: %{
...(16)> n: [:int, default: 42] }}
...(16)> XOpts.parse(~W[n: 11], configuration)
{:ok,
%{switches: %{},
keywords: %{n: 11},
poistionals: [],
errors: []}}
The first possibility do assure the Order Of Things (TM: Dominon) is just to assure that keyword arguments and switches come before poistional arguments.
This can be accomplished with the strict: true
parameter
iex(17)> XOpts.parse(~W[hello :world], strict: true)
{:error,
%{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: [
{:ordered_argument_error, world: "after positional"}
]}}
or simply by calling the parse_strictly/2
function
iex(18)> XOpts.parse_strictly(~W[hello :world])
{:error,
%{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: [
{:ordered_argument_error, world: "after positional"}
]}}
Of course the End Of Keywords separator ::
or --
avoid this
iex(19)> XOpts.parse_strictly(~W[hello :: :world])
{:ok,
%{switches: %{}, keywords: %{}, positionals: ~W[hello :world], errors: []}}
It might be necessary to request and or restrict the number of positional parameters
A first example requiring at least two
iex(20)> configuration = %{
...(20)> nof_postionals: [2]}
...(20)> XOpts.parse(~W[a], configuration)
{:error,
%{switches: %{}, keywords: %{}, poistionals: ~W[a], errors: [
{:missing_positional, needed: 2, present: 1}
]}
}
Of course Chuck Norris 5th lemma holds: Enough is enough
iex(21)> configuration = %{
...(21)> nof_postionals: [2]}
...(21)> XOpts.parse(~W[a b], configuration)
{:ok,
%{switches: %{}, keywords: %{}, poistionals: ~W[a b], errors: []}}
If we want to restrict the number of positionals it is done with the second number in this list:
iex(22)> configuration = %{
...(22)> nof_postionals: [1, 2]}
...(22)> XOpts.parse(~W[a b c], configuration)
{:error,
%{switches: %{}, keywords: %{}, poistionals: ~W[a b c], errors: [
{:spurious_positional, allowed: 2, present: 3}
]}
}
We can also constrain positional parameters
iex(23)> configuration = %{
...(23)> positional_constraints: [:int, ~r{AA}]} # Stupid example but well
...(23)> XOpts.parse(~W[ab Bc], configuration)
{:error,
%{switches: %{}, keywords: %{}, poistionals: ~W[ab Bc],
errors: [
{:constraint_violation, positional: 1, value: "ab", constraint: :int},
{:constraint_violation, positional: 2, value: "Bc", constraint: ~r{AA}},
]}}
and use nil for unconstrained positionals between constrained ones:
iex(24)> configuration = %{
...(24)> positional_constraints: [:int, nil, ~r{AA}]} # Stupid example but well
...(24)> XOpts.parse(~W[ab de Bc], configuration)
{:error,
%{switches: %{}, keywords: %{}, poistionals: ~W[ab de Bc],
errors: [
{:constraint_violation, positional: 1, value: "ab", constraint: :int},
{:constraint_violation, positional: 3, value: "Bc", constraint: ~r{AA}},
]}}
Also note that defining a constraint for a positional parameter does not make it required:
iex(25)> configuration = %{
...(25)> positional_constraints: [:int, nil, ~r{AA}]} # Stupid example but well
...(25)> XOpts.parse(~W[42 de], configuration)
{:ok,
%{switches: %{}, keywords: %{}, poistionals: [42, "de"], errors: []}}
Constraints, like Regex
or :string
are just checked and if they succeed the
value is assigned to the keyword or positional argument as is. :string
always succeeds BTW.
However there are other builtin types that will, if the check succeeds, coherce the string into a different form.
We have already seen an example for that, but there can be constraints added as follows
For simplicity we will use the imported form for doctests from now on, obviously parse/1
is imported from
XOpts
iex(26)> parse(~W[42], positional_constraints: [[:int, max: 40]])
{:error,
%{switches: %{}, keywords: %{}, positionals: [42], errors: [
{:constraint_violation_error, positional: 1, value: 42, max: 40}
]}}
Of course min
can also be used.
The parser will also not allow impossible constraints as shown in the next example
iex(27)> parse([], allowed_keywords: %{n: [:int, min: 10, max: 5]})
{:illegal_config, [{:empty_range, keyword: :n, min: 10, max: 5}]}
And defaults need to be in range too:
iex(28)> parse([], allowed_keywords: %{n: [:int, min: 10, default: 0]})
{:illegal_config, [{:illegal_default, keyword: :n, min: 10, default: 0}]}
If we use min and max we can just pass a range
iex(29)> parse(~W[n: 40], required_keywords: %{n: 41..50})
{:error,
%{switches: %{}, keywords: %{n: 40}, positionals: [], errors: [
{:constraint_violation_error, keyword: :n, value: 40, min: 41, max: 50}
]}}
Oh and let us prove that respecting the requirements yields the results we want, too:
iex(30)> parse(~W[n: 42], required_keywords: %{n: 41..50})
{:ok,
%{switches: %{}, keywords: %{n: 42}, positionals: [], errors: []}}
Some ranges, even open ones, are predefined, as, e.g.
iex(31)> parse(~W[n: -1], allowed_keywords: %{n: :non_negative_int})
{:error,
%{switches: %{}, keywords: %{n: -1}, positionals: [], errors: [
{:constraint_violation_error, keyword: :n, value: -1, min: 0}
]}}
or
iex(32)> parse(~W[n: 0], allowed_keywords: %{n: :positive_int})
{:error,
%{switches: %{}, keywords: %{n: 0}, positionals: [], errors: [
{:constraint_violation_error, keyword: :n, value: 0, min: 1}
]}}
If available in Hex, the package can be installed
by adding xopts
to your list of dependencies in mix.exs
:
def deps do
[
{:xopts, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/xopt.