View Source CliOptions (CliOptions v0.1.2)
An opinionated command line arguments parser.
Features
The main features of CliOptions
are:
- Schema based validation of command line arguments.
- Support for common options types and custom validations.
- Strict validation by default, an error is returned if an invalid. argument is passed.
- Supports required options, default values, aliases, mutually
exclusive options and much more (check
CliOptions.Schema
for more details). - Auto-generated docs for the supported arguments.
Usage
The main function of this module is parse/2
which parses a list
of command line options and arguments into a tuple of parsed options,
positional arguments and extra options (anything passed after the --
separator).
iex> schema = [
...> name: [
...> type: :string,
...> required: true
...> ],
...> num: [
...> type: :integer,
...> default: 4,
...> short: "n"
...> ]
...> ]
...>
...> CliOptions.parse(["--name", "foo"], schema)
{:ok, {[name: "foo", num: 4], [], []}}
iex> # with positional arguments
...> CliOptions.parse(["--name", "foo", "-n", "2", "foo.ex"], schema)
{:ok, {[name: "foo", num: 2], ["foo.ex"], []}}
iex> # with positional arguments and extra options
...> CliOptions.parse(["--name", "foo", "-n", "2", "file.txt", "--", "-n", "1"], schema)
{:ok, {[name: "foo", num: 2], ["file.txt"], ["-n", "1"]}}
iex> # with invalid options
...> CliOptions.parse(["--user-name", "foo"], schema)
{:error, "invalid option \"user-name\""}
Strict validation
Notice that
CliOptions
adheres to a strict options validation approach. This means that an error will be returned in any of the following cases:
- An invalid option is provided
- An option is provided more times than expected
- The option's type is not valid
- A required option is not provided
iex> schema = [ ...> number: [type: :integer], ...> name: [type: :string, required: true] ...> ] ...> ...> # with invalid option ...> CliOptions.parse(["--name", "foo", "--invalid"], schema) {:error, "invalid option \"invalid\""} iex> # with missing required option ...> CliOptions.parse([], schema) {:error, "option :name is required"} iex> # with invalid type ...> CliOptions.parse(["--name", "foo", "--number", "asd"], schema) {:error, ":number expected an integer argument, got: asd"} iex> # with option re-definition ...> CliOptions.parse(["--name", "foo", "--name", "bar"], schema) {:error, "option :name has already been set with foo"}
Summary
Functions
Returns documentation for the given schema.
Parses argv
according to the provided schema
.
Similar as parse/2
but raises an CliOptions.ParseError
exception if invalid
CLI arguments are given.
Types
Functions
@spec docs(schema :: keyword() | CliOptions.Schema.t(), opts :: keyword()) :: String.t()
Returns documentation for the given schema.
You can use this to inject documentation in your docstrings. For example, say you have your schema in a mix task:
@options_schema [...]
With this, you can use docs/2
to inject documentation:
## Supported Options
#{CliOptions.docs(@options_schema)}"
Options
:sort
- if set totrue
the options will be sorted alphabetically.:sections
- a keyword list with options sections. If set the options docs will be added under the defined section, or at the root section if no:section
is defined in your schema.Notice that if
:sort
is set the options will be sorted within the sections. The sections order is not sorted and it follows the provided order.An entry for each section is expected in the
:sections
option with the following format:[ section_name: [ header: "Section Header", doc: "Optional extra docs for this docs section" ] ]
where:
:header
- The header that will be used for the section. Required.:doc
- Optional detailed section docs to be added before the actual options docs.
@spec parse(argv :: argv(), schema :: keyword() | CliOptions.Schema.t()) :: {:ok, parsed_options()} | {:error, String.t()}
Parses argv
according to the provided schema
.
schema
can be either a CliOptions.Schema
struct, or a keyword list. In
the latter case it will be first initialized to a CliOptions.Schema
.
If the provided CLI arguments are valid, then an {:ok, parsed}
tuple is
returned, where parsed
is a three element tuple of the form {opts, args, extra}
,
where:
opts
- the extracted command line optionsargs
- a list of the remaining arguments inargv
as stringsextra
- a list of unparsed arguments, if applicable.
If validation fails, an {:error, message}
tuple will be returned.
Creating the schema at compile time
To avoid the extra cost of initializing the schema, it is possible to create the schema once, and then use that valid schema directly. This is done by using the
CliOptions.Schema.new!/1
function first, and then passing the returned schema toparse/2
.Usually you will define the schema as a module attribute and then use it in your mix task.
defmodule Mix.Tasks.MyTask do use Mix.Task schema = [ user_name: [type: :string], verbose: [type: :boolean] ] @schema CliOptions.Schema.new!(schema) @impl Mix.Task def run(argv) do {opts, args, extra} = CliOptions.parse!(args, @schema) # your task code here end end
Options names and aliases
By default CliOptions
will use the hyphen-ized version of the option name
as the long name of the option.
iex> CliOptions.parse(["--user-name", "John"], user_name: [type: :string])
{:ok, {[user_name: "John"], [], []}}
Additionally you are allowed to set a short version (one-letter string) for the option.
iex> CliOptions.parse(["-U", "John"], user_name: [type: :string, short: "U"])
{:ok, {[user_name: "John"], [], []}}
You are also allowed to specifically set a long name for the option. In this case the auto-generated long name from the option name will not valid.
iex> CliOptions.parse(["-U", "John"], user_name: [type: :string, long: "user"])
{:error, "invalid option \"U\""}
iex> CliOptions.parse(
...> ["--user", "John"],
...> user_name: [type: :string, long: "user"]
...> )
{:ok, {[user_name: "John"], [], []}}
Additionally you can provide an arbitrary number of long and short aliases.
iex> schema = [
...> user_name: [
...> type: :string,
...> short: "u",
...> aliases: ["user", "user_name"],
...> short_aliases: ["U"]
...> ]
...> ]
...>
...> # all following argv are equivalent
...> inputs = [
...> ["--user-name", "John"],
...> ["--user", "John"],
...> ["--user_name", "John"],
...> ["-u", "John"],
...> ["-U", "John"]
...> ]
...>
...> for argv <- inputs, {opts, [], []} = CliOptions.parse!(argv, schema) do
...> opts == [user_name: "John"]
...> end
...> |> Enum.all?()
true
Strict parsing by default
Notice that
CliOptions
will return an error if a not expected option is encountered. Only options defined in the provided schema are allowed. If you need to support arbitrary options you will have to add them after the return separator and handle them in your application code.iex> schema = [ ...> file: [ ...> type: :string, ...> required: true ...> ], ...> number: [ ...> type: :integer, ...> short: "n" ...> ] ...> ] ...> ...> # parses valid arguments ...> CliOptions.parse(["--file", "foo.ex", "-n", "2"], schema) {:ok, {[file: "foo.ex", number: 2], [], []}} iex> # error if invalid argument is encountered ...> CliOptions.parse(["--file", "foo.ex", "-b", "2"], schema) {:error, "invalid option \"b\""} iex> # you can add extra arguments after the return separator ...> CliOptions.parse(["--file", "foo.ex", "--", "-b", "2"], schema) {:ok, {[file: "foo.ex"], [], ["-b", "2"]}}
Option types
By default all options are assumed to be strings. If you set a different :type
then
the option will be casted to that type. The supported types are:
:string
- the default, parses the argument as a string:integer
- parses the argument as an integer:float
- parses the argument as a float:atom
- converts the arguments to atoms:boolean
- parses the argument as a flag, e.g. no option is expected.:counter
- treats the argument as a flag that increases an associated counter
iex> schema = [
...> user: [
...> type: :string
...> ],
...> age: [
...> type: :integer
...> ],
...> height: [
...> type: :float
...> ]
...> ]
...>
...> {:ok, options} =
...> CliOptions.parse(
...> ["--user", "John", "--age", "34", "--height", "1.75"],
...> schema
...> )
...>
...> options
{[user: "John", age: 34, height: 1.75], [], []}
Counters
In some command line applications you may want to count how many times an option is given. In
this case no option argument is expected. Instead every time the option is encountered a counter
is incremented. You can define such an option by setting the type to :counter
.
iex> schema = [
...> verbosity: [
...> type: :counter,
...> short: "v"
...> ]
...> ]
...>
...> # counts the number -v flag is given
...> CliOptions.parse(["-v", "-v", "-v"], schema)
{:ok, {[verbosity: 3], [], []}}
iex> # if not set it is set to zero
...> CliOptions.parse([], schema)
{:ok, {[verbosity: 0], [], []}}
Default values and required options
Options can have default values. If no command line argument is provided then the parsed options will return the default value instead. For example:
iex> schema = [
...> verbose: [
...> type: :boolean,
...> default: true
...> ],
...> retries: [
...> type: :integer,
...> default: 1
...> ]
...> ]
...>
...> CliOptions.parse([], schema)
{:ok, {[verbose: true, retries: 1], [], []}}
Booleans and counters
Notice that for options of type
:boolean
or:counter
a default value is always implicitly set.iex> schema = [verbose: [type: :boolean], level: [type: :counter]] ...> ...> CliOptions.parse([], schema) {:ok, {[verbose: false, level: 0], [], []}}
Additionally you can mark an option as required. In this case an error will be returned if the option is not present in the command line arguments.
iex> schema = [file: [type: :string, required: true]]
...>
...> CliOptions.parse([], schema)
{:error, "option :file is required"}
iex> CliOptions.parse(["--file", "foo.ex"], schema)
{:ok, {[file: "foo.ex"], [], []}}
Options with multiple arguments
If you want to pass a cli option multiple times you can set the :multiple
option set to
true
. In this case every time the option is encountered the value will be appended to
the previously encountered values.
iex> schema = [
...> file: [
...> type: :string,
...> multiple: true,
...> short: "f"
...> ],
...> number: [
...> type: :integer,
...> multiple: true,
...> short: "n"
...> ]
...> ]
...>
...> # all file values are appended to the file option
...> CliOptions.parse(["-f", "foo.ex", "--file", "bar.ex", "-n", "1", "-n", "2"], schema)
{:ok, {[file: ["foo.ex", "bar.ex"], number: [1, 2]], [], []}}
iex> # notice that if an argument is passed once, the value will still be a list
...> CliOptions.parse(["-f", "foo.ex"], schema)
{:ok, {[file: ["foo.ex"]], [], []}}
iex> # all passed items are validated based on the expected type
...> CliOptions.parse(["-n", "2", "-n", "xyz"], schema)
{:error, ":number expected an integer argument, got: xyz"}
iex> # if multiple is not set then an error is returned if an option is passed twice
...> CliOptions.parse(["--file", "foo.ex", "--file", "xyz.ex"], file: [type: :string])
{:error, "option :file has already been set with foo.ex"}
Environment variable aliases
You can optionally define an environment variable alias for an option through the
:env
schema option. If set the environment variable will be used only if the
argument is not present in the args
.
iex> schema = [
...> mode: [
...> type: :string,
...> env: "CLI_OPTIONS_MODE"
...> ]
...> ]
...>
...> # assume the environment variable is set
...> System.put_env("CLI_OPTIONS_MODE", "parallel")
...>
...> # if the argument is provided by the user the environment variable is ignored
...> CliOptions.parse(["--mode", "sequential"], schema)
{:ok, {[mode: "sequential"], [], []}}
iex> # the environment variable will be used if not set
...> CliOptions.parse([], schema)
{:ok, {[mode: "parallel"], [], []}}
iex> System.delete_env("CLI_OPTIONS_MODE")
:ok
Boolean flags and environment variables
Notice that if the option is
:boolean
and an:env
alias is set, then the environment variable will be used only if it has a truthy value. A value is considered truthy if it is one of1
,true
(the match is case insensitive). In any other case the environment variable is ignored.iex> schema = [ ...> enable: [type: :boolean, env: "CLI_OPTIONS_ENABLE"] ...> ] ...> ...> System.put_env("CLI_OPTIONS_ENABLE", "1") ...> CliOptions.parse([], schema) {:ok, {[enable: true], [], []}} iex> System.put_env("CLI_OPTIONS_ENABLE", "TrUE") ...> CliOptions.parse([], schema) {:ok, {[enable: true], [], []}} iex> System.put_env("CLI_OPTIONS_ENABLE", "other") ...> CliOptions.parse([], schema) {:ok, {[enable: false], [], []}} iex> System.delete_env("CLI_OPTIONS_ENABLE") :ok
Return separator
The separator --
implies options should no longer be processed. Every argument
after the return separator will be added in the extra
field of the response.
iex> CliOptions.parse!(["--", "lib", "-n"], [])
{[], [], ["lib", "-n"]}
Notice that if the remaining arguments contain another return separator this will included in the extra:
iex> CliOptions.parse!(["--", "lib", "-n", "--", "bar"], [])
{[], [], ["lib", "-n", "--", "bar"]}
@spec parse!(argv :: argv(), schema :: keyword() | CliOptions.Schema.t()) :: parsed_options()
Similar as parse/2
but raises an CliOptions.ParseError
exception if invalid
CLI arguments are given.
If there are no errors an {opts, args, extra}
tuple is returned.
Examples
iex> CliOptions.parse!(["--file", "foo.ex"], [file: [type: :string]])
{[file: "foo.ex"], [], []}
iex> CliOptions.parse!([], [file: [type: :string, required: true]])
** (CliOptions.ParseError) option :file is required