20 January 2022

How to parse console arguments in your Rust application with Clap

In a previous article I explained how to use ArgParse module to read console arguments in  your Python applications. Rust has it's own crates to parse consoles arguments too. In this article I'm going to explain how to use clap crate for that.

One wonderful thing about Rust is that inside its hard rustacean shell it uses to have a pythonista heart. Using clap you'll find many of ArgParse features. Actually concept are quite similar: while you called verbs as subparsers in ArgParser, here in clap you're going to call them subcommands, and ArgParse arguments are simply named at clap like arg. So if you're used to ArgParser you'll likely feel at home using clap. I'm going to assume you read my ArgParse article to not to repeat myself explaining the same concepts. 

Like any other Rust crate you need to include clap in your Cargo.toml file:

After that you can use clap in your source code. To illustrate explanations, I'm going to use as an example the command parsing I use in my project cifra-rust

As you can see there, you can use clap directly in you main() function but I like to abstract it in a generic function that returns my own defined Configuration struct type. That way if I switch from clap to any other parser crate change will be smoother for my app (reduce coupling). So, as I do at python I define a parse_arguments() function that returns a Configuration type.

There you can see that root parser is defined at clap using App::new(). As other rust crates, clap makes heavy use of builder pattern to configure its parsing. That way you can configure command version, author or long description ("long_about"), between other options, as you can see from lines 277 to 280:

Clap behaviour can be customized with setting() call. A typical parameter for that call is AppSettings::ArgRequiredElseHelp, to show help if command is called with no arguments:

A subparser is created calling subcommand() and passing it a new App instance:

With call about() you can define a short description, about the command or arguments, that will appear when --help is called.

Usually parser (and subparsers) will need arguments. Arguments are defined calling arg() in the parser they belong to. Here you have an example:

In last example you can see that subparser create (line 285) has two arguments: dictionary_name (line 287) and initial_words_file (line 292). Note that every arg() call uses as a parameter and Arg instance. Argument configuration is done using builder pattern over Arg instances.

Argument dictionary_name is required because it is configured as required(true) at line 288. Be aware that although you can use a flag here you are discouraged to do so. By definition, all required arguments should be positional (i.e. require no flag), the only time a flag could be used in required arguments is when called operation is destructive and user is required to prove he knows what he is doing providing that extra flag. When a positional argument is used you may call index() to specify the position of this argument relative to other positional arguments. Actually, I've found out later that you can leave off index() and in that case index will be assigned in order of evaluation. What index() allows you is setting indexes out of order.

When you call takes_value(true) on an argument, the value provided by user is stored in a key called like the argument. For instance, the takes_value(true) at line 290 makes the provided value to be stored in a key called dictionary_name. If your are using an optional flag with no value (i.e. a boolean flag) you could call takes_value(false) or just omit it.

The call to value_name() is equivalent to metavar parameter at python's argparse. It lets you define the string you want to be used to represent this parameter when help is called with --help argument.

Oddly, arguments don't use about() to define their help strings but help() instead.

You can find an optional argument definition from line 292 to 298. There, long version of flag is defined with long() and short one with short() call. In this example, this argument could be called both "--initial_words_file <PATH_TO_FILE_WITH_WORDS>" and "-i <PATH_TO_FILE_WITH_WORDS>".

It can be useful call validator(), as it let you define a function to be called over provided argument to assert it is what you expect to receive. Provided function should receive an string parameter with provided argument and it should return a Result<(), String>. If you make your checks and find correct the argument the function should return an Ok(()), or an Err("Here you write your error message") instead.

Chaining methods on nested arguments and subcommands you can define an entire command tree. Once you finish you have to make one final call to get_matches_from() to the root parser. You pass a vector of strings to that method, with every individual command argument:

Usually you'll pass a vector of strings from args() which returns a vector with every command argument given by user when application was called from console.

Note that my main() function is almost empty. That is because that way I can call _main() (note the underscore) from my integration tests, entering my own argument vector, to simulate a user calling the application from console.

What get_matches_from() returns is a ArgMatches type. That type retuns every argument value if be search by its key name. From line 149 to 245 we implement a method to create a Configuration type using an ArgMatches contents.

Be aware that when you only have a parser you can get values directly using value_of() method. But if you have subcommands you have first to get the specific ArgMatches for that subcommand branch doing a call to subcommand_matches():

In last example you can see the usual workflow:
  • You go deep getting the ArgMatches of the branch you are interested in. (lines 160-161)
  • Once you have the ArgMatches you want, you use value_of() to get an specific argument value. (line 164)
  • For optionals parameters you can make a is_present() call to check if that one was provided or not. (line 165). 
Using those methods, you can retrieve every provided value and build up your configuration to run your application.

As you can see you can make really powerful command parser with clap getting every functionality you are used at ArgParse but in a rustacean environment.