View Source CLIX.Parser (clix v0.1.0)

The command line arguments parser.

It designed to:

  • parse both sub-commands, positional arguments, and optional arguments
  • pepare read-to-use result

Quick start

iex> # 1. build a spec
iex> spec = CLIX.Spec.new({:hello, %{
iex>   args: [
iex>     msg: %{}
iex>   ],
iex>   opts: [
iex>     debug: %{short: "d", long: "debug", type: :boolean},
iex>     verbose: %{short: "v", long: "verbose", type: :boolean, action: :count},
iex>     to: %{short: "t", long: "to", type: :string, action: :append}
iex>   ]
iex> }})
iex>
iex> # 2. parse argv with spec
iex>
iex> # bad argv
iex> CLIX.Parser.parse(spec, [])
{[], %{}, %{debug: false, verbose: 0, to: []}, [
  {:missing_arg, %{message: nil, type: :string, value: nil, value_name: "MSG", nargs: nil}}
]}
iex>
iex> # good argv in strict order
iex> CLIX.Parser.parse(spec, ["--debug", "-vvvv", "-t", "John", "-t", "Dave", "aloha"])
{[], %{msg: "aloha"}, %{debug: true, to: ["John", "Dave"], verbose: 4}, []}
iex>
iex> # good argv in intermixed order
iex> CLIX.Parser.parse(spec, ["--debug", "-vvvv", "aloha", "-t", "John", "-t", "Dave", ])
{[], %{msg: "aloha"}, %{debug: true, to: ["John", "Dave"], verbose: 4}, []}

Read the doc of CLIX.Spec and CLIX.Parser for more information.

The parsing of sub-commands

A sub-command always takes precedence over arguments. It means that a subcommand should always placed after the root command.

An example cloning kamal, which demostrates nested sub-commands, global options, etc.

iex> # 1. build a spec
iex> spec = CLIX.Spec.new({:kamal, %{
iex>   cmds: [
iex>     setup: %{
iex>       help: "Setup all accessories, push the env, and deploy app to servers",
iex>       opts: [
iex>         skip_push: %{
iex>           short: "P",
iex>           long: "skip-push",
iex>           type: :boolean,
iex>           help: "Skip image build and push"
iex>         }
iex>       ]
iex>     },
iex>     app: %{
iex>       help: "Manage application",
iex>       cmds: %{
iex>         boot: %{
iex>           help: "Boot app on servers (or reboot app if already running)",
iex>         },
iex>         containers: %{
iex>           help: "Show app containers on servers"
iex>         }
iex>       }
iex>     }
iex>   ],
iex>   opts: [
iex>     verbose: %{
iex>       short: "v",
iex>       long: "verbose",
iex>       type: :boolean,
iex>       action: :count,
iex>       help: "Detailed logging"
iex>     },
iex>     quiet: %{
iex>       short: "q",
iex>       long: "quiet",
iex>       type: :boolean,
iex>       help: "Minimal logging"
iex>     }
iex>   ]
iex> }})
iex>
iex> # 2. parse argv with spec
iex>
iex> # root command
iex> CLIX.Parser.parse(spec, [])
{[], %{}, %{verbose: 0, quiet: false}, []}
iex>
iex> # sub-command - setup
iex> CLIX.Parser.parse(spec, ["setup", "-P"])
{[:setup], %{}, %{verbose: 0, quiet: false, skip_push: true}, []}
iex>
iex> # sub-command - app
iex> CLIX.Parser.parse(spec, ["app"])
{[:app], %{}, %{verbose: 0, quiet: false}, []}
iex>
iex> # nested sub-command - app boot
iex> CLIX.Parser.parse(spec, ["app", "boot"])
{[:app, :boot], %{}, %{verbose: 0, quiet: false}, []}
iex>
iex> # nested sub-command - app containers
iex> CLIX.Parser.parse(spec, ["app", "containers"])
{[:app, :containers], %{}, %{verbose: 0, quiet: false}, []}

The parsing of positional arguments

The key part here is how to allocate a limited number of arguments to as many different positional arguments as possible. With the design of CLIX.Spec.nargs/0, we can easily achieve it.

An example cloning cp (cp <SRC>... <DST>):

iex> spec = CLIX.Spec.new({:cp, %{args: [
iex>   src: %{nargs: :+},
iex>   dst: %{}
iex> ]}})
iex>
iex> CLIX.Parser.parse(spec, [])
{[], %{}, %{}, [
  {:missing_arg, %{message: nil, type: :string, value: nil, nargs: :+, value_name: "SRC"}},
  {:missing_arg, %{message: nil, type: :string, value: nil, nargs: nil, value_name: "DST"}}
]}
iex>
iex> CLIX.Parser.parse(spec, ["src1"])
{[], %{src: ["src1"]}, %{}, [
  {:missing_arg, %{message: nil, type: :string, value: nil, nargs: nil, value_name: "DST"}}
]}
iex>
iex> CLIX.Parser.parse(spec, ["src1", "dst"])
{[], %{src: ["src1"], dst: "dst"}, %{}, []}
iex>
iex> CLIX.Parser.parse(spec, ["src1", "src2", "dst"])
{[], %{src: ["src1", "src2"], dst: "dst"}, %{}, []}

Or, an example cloning httpie (httpie [METHOD] <URL> [REQUEST_ITEM]):

iex> spec = CLIX.Spec.new({:httpie, %{args: [
iex>   method: %{nargs: :"?", default: "GET"},
iex>   url: %{},
iex>   request_items: %{nargs: :*}
iex> ]}})
iex>
iex> CLIX.Parser.parse(spec, ["https://example.com"])
{[], %{method: "GET", url: "https://example.com", request_items: []}, %{}, []}
iex>
iex> CLIX.Parser.parse(spec, ["POST", "https://example.com"])
{[], %{method: "POST", url: "https://example.com", request_items: []}, %{}, []}
iex>
iex> CLIX.Parser.parse(spec, ["POST", "https://example.com", "name=Joe", "email=Joe@example.com"])
{[], %{method: "POST", url: "https://example.com", request_items: ["name=Joe", "email=Joe@example.com"]}, %{}, []}

The algo is borrowed from Python's argparse.

The parsing of optional arguments

Supported syntax

The syntax of GNU's getopt (implicitly involves POSIX's getopt) is supported.

# short opts
-f
-o <value>
-o<value>     # equals to -o <value>
-abc          # equals to -a -b -c
-abco<value>  # equals to -a -b -c -o <value>

# long opts
--flag
--option <value>
--option=<value>

The parsing modes

:intermixed mode

This's the GNU's way of parsing optional arguments. The positional arguments and optional arguments can be intermixed.

For example:

program arg1 -f arg2 -o value arg3
# equals to 'program -f -o value arg1 arg2 arg3'

:strict mode

This's the POSIX's way of parsing optional arguments:

  • requires all optional arguments to appear before positional arguments.
  • any optional arguments after the first positional arguments are treated as positiontal arguments.

It equals to set POSIXLY_CORRECT env for GNU's getopt.

For example:

program -f -o value arg1 arg2 arg3

program -f arg1 -o value arg2 arg3
# equals to 'program -f -- arg1 -o value arg2 arg3'

About the internal

It would be helpful to give you (the possible contributor) some information about the internal.

Overview

When parsing command line arguments, it processes them in multiple stages.

  • stage 0 - parse sub-commands.
  • stage 1 - parse optional arguments and collecting positional arguments.
  • stage 2 - parse positional arguments.

Variable names

  • For argument specs, use pos_specs, opt_specs.
  • For raw arguments, use pos_argv, opt_argv.
  • For parsed arguments, use pos_args, opt_args.

Verify compatibility with GNU's getopt

I'm using jamesodhunt/test-getopt.

Summary

Types

The list of command line arguments to be parsed.

The errors of parsing.

The options of parsing.

A raw command line argument.

The result of parsing.

Functions

Parses argv with given spec.

Types

arg_error_detail()

@type arg_error_detail() :: %{
  value_name: CLIX.Spec.value_name(),
  type: CLIX.Spec.type(),
  nargs: CLIX.Spec.nargs(),
  value: raw_arg() | nil,
  message: String.t() | nil
}

argv()

@type argv() :: [raw_arg()]

The list of command line arguments to be parsed.

In general, it's obtained by calling System.argv/0.

error()

@type error() ::
  {:unknown_arg, raw_arg()}
  | {:missing_arg, arg_error_detail()}
  | {:invalid_arg, arg_error_detail()}
  | {:unknown_opt, raw_arg()}
  | {:missing_opt, opt_error_detail()}
  | {:invalid_opt, opt_error_detail()}

errors()

@type errors() :: [error()]

The errors of parsing.

opt()

@type opt() :: {:mode, :intermixed | :strict}

opt_error_detail()

@type opt_error_detail() :: %{
  prefixed_name: String.t(),
  value_name: CLIX.Spec.value_name(),
  type: CLIX.Spec.type(),
  action: CLIX.Spec.action(),
  value: raw_arg() | nil,
  message: String.t() | nil
}

opts()

@type opts() :: [opt()]

The options of parsing.

parsed_args()

@type parsed_args() :: %{required(atom()) => any()}

parsed_opts()

@type parsed_opts() :: %{required(atom()) => any()}

raw_arg()

@type raw_arg() :: String.t()

A raw command line argument.

result()

@type result() :: {subcmd_path(), parsed_args(), parsed_opts(), errors()}

The result of parsing.

subcmd_path()

@type subcmd_path() :: [CLIX.Spec.cmd_name()]

Functions

parse(spec, argv, opts \\ [])

@spec parse(CLIX.Spec.t(), argv(), opts()) :: result()

Parses argv with given spec.

Available opts:

  • :mode - :intermixed (default) / :strict