View Source CliOptions (CliOptions v0.1.0)

An opinionated command line arguments parser.


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.


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"}



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.


@type argv() :: [String.t()]
@type extra() :: [String.t()]
@type options() :: keyword()
@type parsed_options() :: {options(), argv(), extra()}


Link to this function

docs(schema, opts \\ [])

View Source
@spec docs(schema :: keyword(), 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



  • :sort - if set to true the options will be sorted alphabetically.
@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 options
  • args - a list of the remaining arguments in argv as strings
  • extra - 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!/1 function first, and then passing the returned schema to parse/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]


  @impl Mix.Task
  def run(argv) do
    {opts, args, extra} = CliOptions.parse!(args, @schema)
    # your task code here

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?()

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], [], []}


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"}

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()) ::

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.


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