argparse(3)

๐Ÿ“ฆ
version 26.2.5.21 ยท 2 versions
, in fedora 43, fedora 44

๐Ÿ’– argparse ๐Ÿ’– (Erlang Module Definition)

okay so bestie, welcome to argparse โ€” the command line arguments parser. this is an Erlang module, it lives in section 3 of the manual, and it is genuinely about to change your CLI game. no cap. ๐Ÿซถ


โœจ NAME โœจ

argparse โ€” Command line arguments parser. that's the whole vibe. ๐Ÿ’…


๐ŸŒธ DESCRIPTION ๐ŸŒธ

So argparse is the module that implements a command line parser, and here's the iconic part: the parser thinks of your whole CLI as a tree. Commands are the branches ๐ŸŒฟ, and arguments are the leaves ๐Ÿƒ. And the parser always starts at the root command, which is named after progname โ€” that's the name of the program that started Erlang. main character energy from the very first step, honestly.

A command specification can hold a handler definition for each command, plus any number of argument specifications. When the parser succeeds, argparse calls the matching handler and passes in the arguments it pulled out of the command line. Those arguments come in two flavors:

  • positional arguments โ€” they occupy a specific position in the command line
  • optional arguments โ€” they can live anywhere, but they have to be prefixed with a specified character

argparse also auto-generates your help and usage messages, bestie. and when users hand the program invalid arguments? it issues errors. it's giving firm but fair. โœจ


๐Ÿš€ QUICK START ๐Ÿš€

argparse is designed to work with escript, and the example below is a fully functioning Erlang program that takes two command line arguments and prints their product. slay.

#!/usr/bin/env escript

main(Args) ->
    argparse:run(Args, cli(), #{progname => mul}).

cli() ->
    #{
        arguments => [
            #{name => left, type => integer},
            #{name => right, type => integer}
        ],
        handler =>
            fun (#{left := Left, right := Right}) ->
                io:format("~b~n", [Left * Right])
            end
    }.

If you run this script with no arguments at all, you get an error, and it comes with the usage information attached. (rude of the user, honestly. it ate and left no crumbs of patience.)

So what's happening: the cli function defines a single command with an embedded handler that accepts a map. The keys of that map are the argument names defined by the argument field of the command โ€” left and right in this example. The values come straight from the command line and get converted into integers, because the type specification asked for exactly that. And both arguments in this example are required โ€” which is why they're defined as positional. ๐Ÿ’…


๐ŸŒณ COMMAND HIERARCHY ๐ŸŒณ

A command can contain nested commands, and that's how you get a whole hierarchy. And here's the cute part: arguments defined at an upper level command get automatically added to all nested commands. obsessed.

Here's the nested commands example (assuming progname is nested):

cli() ->
  #{
    %% top level argument applicable to all commands
    arguments => [#{name => top}],
      commands => #{
        "first" => #{
          %% argument applicable to "first" command and
          %%  all commands nested into "first"
          arguments => [#{name => mid}],
          commands => #{
            "second" => #{
              %% argument only applicable for "second" command
              arguments => [#{name => bottom}],
              handler => fun (A) -> io:format("~p~n", [A]) end
            }
          }
        }
      }
    }
  }.

So in that example above, we've defined a 3-level hierarchy. First up is the script itself (nested), and it accepts the one and only argument top. Now โ€” because it has no associated handler, run/3 will not accept user input that omits the nested command selection. periodt. So for this example, the user has to supply 5 arguments on the command line: two of them being command names, and the other 3 being required positional arguments:

#{top => "one",mid => "two",bottom => "three"}

Also important, bestie: commands have preference over positional argument values. In the example above, commands and positional arguments are interleaving, and argparse matches the command name first. it knows what it wants. ๐Ÿ’…


๐ŸŽ€ ARGUMENTS ๐ŸŽ€

argparse supports positional arguments and optional arguments. Optional arguments โ€” or options for short โ€” have to be prefixed with a special character (- is the default on all operating systems, iykyk). Both options and positional arguments have 1 or more associated values. See the argument specification below for the full tea on supported combinations. โ˜•

On the user input side:

  • short options may be concatenated with their values
  • long options support values separated by =

Check out this definition:

cli() ->
  #{
    arguments => [
      #{name => long, long => "-long"},
      #{name => short, short => $s}
    ],
    handler => fun (Args) -> io:format("~p~n", [Args]) end
  }.

Running ./args --long=VALUE prints #{long => "VALUE"}, and running ./args -sVALUE prints #{short => "VALUE"}. iconic behavior, honestly. โœจ

argparse also supports boolean flags concatenation: it is possible to shorten -r -f -v down to -rfv. efficiency queen. ๐Ÿ’…

But โ€” and listen closely โ€” shortened option names are not supported. It is not possible to use --my-argum instead of --my-argument-name, even when such an option could be unambiguously found. so type the whole thing out, bestie. ๐Ÿ˜ญ


๐Ÿ”ฎ DATA TYPES ๐Ÿ”ฎ

arg_type()

arg_type() =
    boolean | float |
    {float, Choice :: [float()]} |
    {float, [{min, float()} | {max, float()}]} |
    integer |
    {integer, Choices :: [integer()]} |
    {integer, [{min, integer()} | {max, integer()}]} |
    string |
    {string, Choices :: [string()]} |
    {string, Re :: string()} |
    {string, Re :: string(), ReOptions :: [term()]} |
    binary |
    {binary, Choices :: [binary()]} |
    {binary, Re :: binary()} |
    {binary, Re :: binary(), ReOptions :: [term()]} |
    atom |
    {atom, Choices :: [atom()]} |
    {atom, unsafe} |
    {custom, fun((string()) -> term())}

This defines the type conversion applied to the string that got retrieved from user input. If the conversion goes through, the resulting value gets validated using the optional Choices, or the minimums and maximums โ€” and heads up, those min/max options are for integer and floating point values only. Strings and binary values can be validated using regular expressions. And you can even define a custom type conversion function that accepts a string and returns an Erlang term. Oh and โ€” if that function raises an error with a badarg reason, the argument is treated as invalid. so, be normal about it. ๐Ÿ™ˆ

argument_help()

argument_help() =
    {unicode:chardata(),
     [unicode:chardata() | type | default] |
     fun(() -> unicode:chardata())}

This is a user-defined help template to print in the command usage. The first element of the tuple must be a string, and it gets printed as part of the usage header. The second element of the tuple can be either a list containing strings plus the type and default atoms, or a user-defined function that must return a string. And psst โ€” a plain string should be wrapped as a list, like ["string is nested"]. ๐Ÿ’…

argument_name()

argument_name() = atom() | string() | binary()

The argument name is what's used to populate the argument map. that's it, that's the job. ๐Ÿ’–

argument()

argument() =
    #{name := argument_name(),
      short => char(),
      long => string(),
      required => boolean(),
      default => term(),
      type => arg_type(),
      action =>
          store |
          {store, term()} |
          append |
          {append, term()} |
          count | extend,
      nargs =>
          integer() >= 1 |
          maybe |
          {maybe, term()} |
          list | nonempty_list | all,
      help => hidden | unicode:chardata() | argument_help()}

This is the argument specification. It defines a single named argument that gets returned in the argument map. The only required field is name โ€” everything else has defaults. queen of low maintenance. ๐Ÿ‘‘

If either the short or the long field is specified, the argument is treated as optional. Optional arguments don't have a specific order and can appear anywhere in the command line. Positional arguments are ordered the same way as they appear in the arguments list of the command specification. ๐ŸŒˆ

By default, all positional arguments must be present in the command line, and the parser will return an error otherwise. Options, however, may be omitted โ€” and in that case the resulting argument map will either contain the default value, or not have the key at all. it's flexible like that. โœจ

name:

Sets the argument name in the parsed argument map. If help isn't defined, name is also used to generate the default usage message.

short:

Defines a short (single character) form of an optional argument.

%% Define a command accepting argument named myarg, with short form $a:
1> Cmd = #{arguments => [#{name => myarg, short => $a}]}.
%% Parse command line "-a str":
2> {ok, ArgMap, _, _} = argparse:parse(["-a", "str"], Cmd), ArgMap.

#{myarg => "str"}

%% Option value can be concatenated with the switch: "-astr"
3> {ok, ArgMap, _, _} = argparse:parse(["-astr"], Cmd), ArgMap.

#{myarg => "str"}

By default, all options expect a single value following the option switch. The only exception is an option of a boolean type. (booleans stay unbothered. ๐Ÿ’…)

long:

Defines a long form of an optional argument.

1> Cmd = #{arguments => [#{name => myarg, long => "name"}]}.
%% Parse command line "-name Erlang":
2> {ok, ArgMap, _, _} = argparse:parse(["-name", "Erlang"], Cmd), ArgMap.

#{myarg => "Erlang"}
%% Or use "=" to separate the switch and the value:
3> {ok, ArgMap, _, _} = argparse:parse(["-name=Erlang"], Cmd), ArgMap.

#{myarg => "Erlang"}

If neither short nor long is defined, the argument is treated as positional.

required:

Forces the parser to expect the argument to be present in the command line. By default, all positional arguments are required, and all options are not. no pressure on your flags, bestie.

default:

Specifies the default value to put in the parsed argument map if the value isn't supplied in the command line.

1> argparse:parse([], #{arguments => [#{name => myarg, short => $m}]}).

{ok,#{}, ...
2> argparse:parse([], #{arguments => [#{name => myarg, short => $m, default => "def"}]}).

{ok,#{myarg => "def"}, ...

type:

Defines the type conversion and validation routine. The default is string, assuming no conversion. the chillest possible default, honestly. ๐Ÿฆ‹

nargs:

Defines the number of following arguments to consume from the command line. By default, the parser consumes the next argument and converts it into an Erlang term according to the specified type.

pos_integer():

Consume exactly this number of positional arguments, and fail if there are not enough. The value in the argument map contains a list of exactly this length. Example โ€” defining a positional argument expecting 3 integer values:

1> Cmd = #{arguments => [#{name => ints, type => integer, nargs => 3}]},
argparse:parse(["1", "2", "3"], Cmd).

{ok, #{ints => [1, 2, 3]}, ...

more parts incoming, bestie โ€” don't go anywhere ๐Ÿ’–

Another lil example, this one defining an option accepted as -env and expecting two string arguments:

1> Cmd = #{arguments => [#{name => env, long => "env", nargs => 2}]},
argparse:parse(["-env", "key", "value"], Cmd).

{ok, #{env => ["key", "value"]}, ...
  • list โ€” eat everything until the next option shows up</p> ๐Ÿฝ๏ธ It consumes all the following arguments until it hits the next option (one starting with an option prefix). Heads up bestie: this can totally result in an empty list getting dropped into the arguments map, no cap.
1> Cmd = #{arguments => [
  #{name => nodes, long => "nodes", nargs => list},
  #{name => verbose, short => $v, type => boolean}
]},
argparse:parse(["-nodes", "one", "two", "-v"], Cmd).

{ok, #{nodes => ["one", "two"], verbose => true}, ...
  • nonempty_list โ€” same vibe as list but it's high maintenance ๐Ÿ’… expects at least one argument. If the very next command line argument is an option switch (one starting with the prefix), it returns an error. Like, it needs something, bestie.
  • 'maybe' โ€” the "if you feel like it" energy ๐Ÿฆ‹ Consumes the next argument from the command line if it does not start with an option prefix. Otherwise, it just adds a default value to the arguments map.
1> Cmd = #{arguments => [
  #{name => level, short => $l, nargs => 'maybe', default => "error"},
  #{name => verbose, short => $v, type => boolean}
]},
argparse:parse(["-l", "info", "-v"], Cmd).

{ok,#{level => "info",verbose => true}, ...

%% When "info" is omitted, argument maps receives the default "error"
2> argparse:parse(["-l", "-v"], Cmd).

{ok,#{level => "error",verbose => true}, ...
  • {'maybe', term()} โ€” same as above but you get to pick the consolation prize ๐ŸŽ€ Consumes the next argument from the command line, if it does not start with an option prefix. Otherwise, it adds a specified Erlang term to the arguments map.
  • all โ€” the chaos gremlin option, and we respect it โœจ Folds all remaining command line arguments into a list, ignoring any option prefixes or switches. Genuinely useful for proxying arguments into another command line utility.
1> Cmd = #{arguments => [
    #{name => verbose, short => $v, type => boolean},
    #{name => raw, long => "-", nargs => all}
]},
argparse:parse(["-v", "--", "-kernel", "arg", "opt"], Cmd).

{ok,#{raw => ["-kernel","arg","opt"],verbose => true}, ...

the action field, but make it a moment ๐Ÿ’ซ

  • action โ€” Defines an action to take when the argument is found in the command line. The default action is store. The roster:
  • store โ€” the default, the classic, the moment โญ Store the value in the arguments map. Overwrites the value previously written. (Yes, it just Says No to your earlier input, iconic.)
1> Cmd = #{arguments => [#{name => str, short => $s}]},
argparse:parse(["-s", "one", "-s", "two"], Cmd).

{ok, #{str => "two"}, ...
  • {store, term()} โ€” no reading, just vibes ๐Ÿ’œ Stores the specified term instead of reading the value from the command line.
1> Cmd = #{arguments => [#{name => str, short => $s, action => {store, "two"}}]},
argparse:parse(["-s"], Cmd).

{ok, #{str => "two"}, ...
  • append โ€” collect them all, Pokรฉmon style ๐Ÿ”ฅ Appends the repeating occurrences of the argument instead of overwriting.
1> Cmd = #{arguments => [#{name => node, short => $n, action => append}]},
argparse:parse(["-n", "one", "-n", "two", "-n", "three"], Cmd).

{ok, #{node => ["one", "two", "three"]}, ...

%% Always produces a list - even if there is one occurrence
2> argparse:parse(["-n", "one"], Cmd).

{ok, #{node => ["one"]}, ...
  • {append, term()} โ€” same as append but it doesn't take from the command line ๐Ÿงš Instead of consuming the argument from the command line, it appends a provided term().
  • count โ€” counting is self-care actually ๐Ÿ‘‘ Puts a counter as a value in the arguments map. Perfect for implementing a verbosity option, iykyk:
1> Cmd = #{arguments => [#{name => verbose, short => $v, action => count}]},
argparse:parse(["-v"], Cmd).

{ok, #{verbose => 1}, ...

2> argparse:parse(["-vvvv"], Cmd).

{ok, #{verbose => 4}, ...
  • extend โ€” flattening the drama into one big list ๐ŸŒˆ Works as append, but flattens the resulting list. Valid only for nargs set to list, nonempty_list, all or pos_integer().
1> Cmd = #{arguments => [#{name => duet, short => $d, nargs => 2, action => extend}]},
argparse:parse(["-d", "a", "b", "-d", "c", "d"], Cmd).

{ok, #{duet => ["a", "b", "c", "d"]}, ...

%% 'append' would result in {ok, #{duet => [["a", "b"],["c", "d"]]},
  • help โ€” the lil glow-up for your usage text ๐Ÿ’– Specifies help/usage text for the argument. argparse provides automatic generation based on the argument name, type and default value, but for better usability it is recommended to have a proper description. Setting this field to hidden suppresses usage output for this argument. Ghost mode, activated. ๐Ÿ‘ป

๐Ÿ’– arg_map()

arg_map() = #{argument_name() => term()}

Arguments map is the map of argument names to the values extracted from the command line. It is passed to the matching command handler. If an argument is omitted, but has the default value is specified, it is added to the map. When no default value specified, and argument is not present in the command line, corresponding key is not present in the resulting map. So like, absence is real and you have to handle it bestie. ๐Ÿ’…

๐Ÿ’– handler()

handler() =
    optional |
    fun((arg_map()) -> term()) |
    {module(), Fn :: atom()} |
    {fun(() -> term()), term()} |
    {module(), atom(), term()}

Command handler specification. Called by run/3 upon successful parser return. The whole squad:

  • fun((arg_map()) -> term()) โ€” the straightforward one โœจ A function accepting argument map. See the basic example in the Quick Start section.
  • {Module :: module(), Function :: atom()} โ€” the "somebody else wrote this" one ๐Ÿฆ‹ Function named Function, exported from Module, accepting argument map.
  • {fun(() -> term()), Default :: term()} โ€” the plug-and-play one ๐ŸŽ€ Function accepting as many arguments as there are in the arguments list for this command. Arguments missing from the parsed map are replaced with the Default. Convenient way to expose existing functions, lowkey a hack but a cute one.
1> Cmd = #{arguments => [
      #{name => x, type => float},
      #{name => y, type => float, short => $p}],
  handler => {fun math:pow/2, 1}},
argparse:run(["2", "-p", "3"], Cmd, #{}).

8.0

%% default term 1 is passed to math:pow/2
2> argparse:run(["2"], Cmd, #{}).

2.0
  • {Module :: module(), Function :: atom(), Default :: term()} โ€” literally the same thing, different syntax ๐Ÿ’œ Function named Function, exported from Module, accepting as many arguments as defined for this command. Arguments missing from the parsed map are replaced with the Default. Effectively, just a different syntax to the same functionality as demonstrated in the code above.

๐Ÿ’– command_help()

command_help() =
    [unicode:chardata() | usage | commands | arguments | options]

User-defined help template. Use this option to mix custom and predefined usage text. Help template may contain unicode strings, and following atoms โ€” think of them as little help-card inserts you drop into your own text:

  • usage โ€” Formatted command line usage text โญ e.g. rm [-rf] <directory>.
  • commands โ€” Expanded list of sub-commands.
  • arguments โ€” Detailed description of positional arguments.
  • options โ€” Detailed description of optional arguments.

๐Ÿ’– command()

command() =
    #{commands => #{string() => command()},
      arguments => [argument()],
      help => hidden | unicode:chardata() | command_help(),
      handler => handler()}

Command specification. May contain nested commands, forming a hierarchy. Yes queen, commands within commands within commands, it's giving inception. ๐ŸŒธ

  • commands โ€” Maps of nested commands. Keys must be strings, matching command line input. Basic utilities do not need to specify any nested commands.
  • arguments โ€” List of arguments accepted by this command, and all nested commands in the hierarchy.
  • help โ€” Specifies help/usage text for this command. Pass hidden to remove this command from the usage output.
  • handler โ€” Specifies a callback function to call by run/3 when the parser is successful.

๐Ÿ’– cmd_path()

cmd_path() = [string()]

Path to the nested command. First element is always the progname, subsequent elements are nested command names. A whole little breadcrumb trail of where you are. ๐Ÿ“

๐Ÿ’– parser_error()

parser_error() =
    {Path :: cmd_path(),
     Expected :: argument() | undefined,
     Actual :: string() | undefined,
     Details :: unicode:chardata()}

Returned from parse/2,3 when the user input cannot be parsed according to the command specification. The "it's not me, it's your input" moment. ๐Ÿ˜ญ

First element is the path to the command that was considered when the parser detected an error. Second element, Expected, is the argument specification that caused an error. It could be undefined, meaning that Actual argument had no corresponding specification in the arguments list for the current command.

When Actual is set to undefined, it means that a required argument is missing from the command line. If both Expected and Actual have values, it means validation error. Yeah, two different flavours of "no". ๐Ÿ’€

Use format_error/1 to generate a human-readable error description, unless there is a need to provide localised error messages.

๐Ÿ’– parser_options()

parser_options() =
    #{prefixes => [char()],
      default => term(),
      progname => string() | atom(),
      command => cmd_path(),
      columns => integer() >= 1}

Options changing parser behaviour. The lil knobs and dials. ๐ŸŽ›๏ธ

  • prefixes โ€” Changes the option prefix (the default is -).
  • default โ€” Specifies the default value for all optional arguments. When this field is set, resulting argument map will contain all argument names. Useful for easy pattern matching on the argument map in the handler function.
  • progname โ€” the program (a.k.a. root command) name, bestie. It comes back as the first element of the command path, and it's what gets printed in help/usage text. Pls set this one, fr โ€” if you leave it empty the default gets figured out with init:get_argument(progname), and that is often set to erl instead of your actual script name. So your cute lil utility would be out here introducing itself as erl to everyone. Embarrassing. Don't let that be you. ๐Ÿ’…
  • command โ€” the path to the nested command for help/2. Highkey useful when you've got a big ol' complex utility with a hundred subcommands and you just want to limit the output to one of them. It's also used by the default error handling logic. ๐Ÿซถ
  • columns โ€” the help/usage text width in characters for help/2. Default value is 80. Classic, iconic, never misses. โœจ
parse_result() =
    {ok, arg_map(), Path :: cmd_path(), command()} |
    {error, parser_error()}

This is the lil guy that comes back from parse/2,3, okay? It's giving the whole receipts: the arguments that got extracted from the command line, the path to the nested command (if there even was one), and a (potentially nested) command specification that the parser had in mind when it finished successfully. And the expectation here is that the command contains a handler definition, which will be called with the argument map passed in. That's the whole point. The map goes in, the handler goes brrr. ๐ŸŒˆ


โœจ EXPORTS โœจ

Okay bestie, this is the main character section. Every function you can actually call. Let's go. ๐ŸŽ€

format_error(Reason :: parser_error()) -> unicode:chardata()

Generates human-readable text for a parser error. That's it, that's the function. It does not include help/usage information, and it does not provide localisation. So it's just the error, plain and simple, in English, no frills. ๐Ÿ’€

help(Command :: command()) -> string()

help(Command :: command(), Options :: parser_options()) -> unicode:chardata()

Generates help/usage information text for the command you supply โ€” or for any nested command when the command option is specified. โœจ Arguments are displayed in the same order as specified in Command, which, taste. Does not provide localisation. It expects progname to be set, otherwise it defaults to the return value of init:get_argument(progname). (Yes, same deal as before โ€” you've been warned, set your progname.) ๐Ÿ‘‘

parse(Args :: [string()], Command :: command()) -> parse_result()

parse(Args :: [string()], Command :: command(), Options :: parser_options()) -> parse_result()

Parses command line arguments according to the command specification. Heads up: it raises an exception if the command specification is not valid. Use erl_error:format_exception/3,4 to see a friendlier message than whatever raw chaos you'd otherwise get. ๐Ÿ™ˆ

Now the important bit: invalid command line input does not raise an exception. Instead, parse/2,3 returns the tuple {error, parser_error()}. So bad user input = a tuple, bad spec = a crash. Different energies entirely, iykyk.

Also, no cap: this function does not call the command handler. It just parses. If you want the handler to run, that's run/3's job, bestie. ๐Ÿ’–

run(Args :: [string()], Command :: command(), Options :: parser_options()) -> term()

This one does the most, literally. It parses command line arguments and calls the matching command handler. ๐ŸŽ‰

And if anything goes wrong โ€” in the command specification or in the user-provided command line input โ€” it prints a human-readable error, prints help/usage information for the discovered command, and halts the emulator with code 1. Full send. No survivors. ๐Ÿ’€

Warning: This function is designed to work as an entry point to a standalone escript. Therefore, it halts the emulator for any error detected. Do not use this function through remote procedure call, or it may result in an unexpected shutdown of a remote node. Like. Do not. I am begging. ๐Ÿ˜ญ

And that's the whole page, bestie!! You made it to the end, which is honestly iconic behavior. ๐ŸŒธ This lil masterpiece was written by Maxim Fedorov, it ships with stdlib 5.2.3.6, and you're reading the argparse(3) manual page. Go forth and parse responsibly. Periodt. ๐Ÿ’…โœจ๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from