Rust Clap recipes
Pavlo Myroniuk June 15, 2023 #clap #recipes #rustI bet you know but I still wanna recall it. Clap
is a full-featured, fast Command Line Argument Parser for Rust. docs.rs. crates.io.
Goals
Goals:
- Show non-standard examples of the cli arguments configuration.
- Tell you about alternate ways to parse cli args with the
clap
.
Non-goals:
- Write the "ultimate" guide to the
clap
library. - Create an introduction (or for newbies) article about the clap.
The current article is basically recipes. It means, here we have described concrete problems, ways how to solve them, and examples of the execution.
Recipes
Collecting multiple values
Imagine the following situation, you are writing some server. It can be the server side of some protocol, or proxy server, or something like that. At some point, you decided to add encryption. Now you need to add the user the ability to pass allowed encryption algorithms (ciphers) for server sessions.
Let's try to do it. The first idea that comes to mind is just to use a vector of strings:
/// Server config structure
Note 1: personally, I prefer the derive approach to configure clap instead of a builder approach.
Note 2: for all examples in this article, I use the same main
function:
/// Just prints the server configuration.
/// `Config` is a previously defined structure (like in the example above).
It'll work, here is an example:
# Config { enc_algs: ["aes256", "des3", "thebesttvarynka"] }
Good but here we have a big problem: values are not validated and the user can specify unsupported or just wrong algorithms. It'll be cool if we delegate the algorithm validation to the clap
.
First of all, we need to implement the Cipher
enum. Most likely, you'll have such an enum already implemented in the project, but here we need to write it:
// The next traits implementations are omitted
// and left for the reader as a homework 😝
//
// impl AsRef<str> for Cipher { ... }
// impl Display for Cipher { ... }
// impl TryFrom<&str> for Cipher { ... }
Pay attention that we also added the clap::ValueEnum
derive. It'll generate the ValueEnum
trait implementation and clap
will be able to parse the raw string into concrete enum value. Now time to test it:
# Config { enc_algs: [Aes256, Des3] }
# error: invalid value 'thebesttvarynka' for '--enc-algs <ENCRYPTION ALGORITHMS>'
# [possible values: aes128, aes256, des3]
#
# For more information, try '--help'.
Very cool 🔥. Clap validates the input for us and even tells the user possible values if smth goes wrong. You can read all code of the example above here.
In general, we can finish at this point this recipe, but I used to specify multiple values using a comma-separated string. The perfect arg looks for me like this:
# or even
"This move will cost us" some additional code writing 🥲. We need to create a wrapper over the Vec<Cipher>
and implement parsing for it:
;
// Implementation of this trait is left for the reader.
// impl fmt::Display for EncAlgorithms { ... }
And only after this, we can use it in our configuration:
/// Server config structure
Why we should create the wrapper? Why is custom parser +
Vec<Cipher>
not enough?
Yes, you can write just Vec<Cipher>
with custom value_parser
and it'll compile. But it'll fail in runtime with the following error:
thread 'main' panicked at 'Mismatch between definition and access of `enc_algs`. Could not downcast to cool_server::cipher::Cipher, need to downcast to alloc::vec::Vec<cool_server::cipher::Cipher>
', src/main.rs:37:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Understandable.
Let's try to run the implementation with a wrapper:
# Config { enc_algs: EncAlgorithms([Aes256, Des3]) }
# error: invalid value 'aes256:des3:thebesttvarynka' for '--enc-algs <ENCRYPTION ALGORITHMS LIST>': error: invalid value 'Invalid algorithm name: thebesttvarynka' for 'enc-algs'
#
# For more information, try '--help'.
# Server config structure
#
# Usage: cool-server [OPTIONS]
#
# Options:
# --enc-algs <ENCRYPTION ALGORITHMS LIST>
# Allowed encryption algorithms list (separated by ':') [default: aes256:aes128]
# -h, --help
# Print help
The full src code of the example above.
Okay, but are you sure that we can't do better? I am still thinking about a better solution.
IDK 😕. Let's explore the docs more thoroughly... Oh, it turns out it's called the value_delimiter:
/// Server config structure
Good job. It's way better now.
Yep. With this approach, we remove all custom parsing and formatting stuff. Moreover, the help message and error reporting became better:
# Server config structure
#
# Usage: cool-server [OPTIONS]
#
# Options:
# --enc-algs <CIPHERS>... Allowed encryption algorithms list (separated by ':' or space) [default: aes256] [possible values: aes128, aes256, des3]
# -h, --help Print help
# Config { enc_algs: [Aes256, Des3] }
# error: invalid value 'des4' for '--enc-algs <CIPHERS>...'
# [possible values: aes128, aes256, des3]
#
# tip: a similar value exists: 'des3'
#
# For more information, try '--help'.
The full src code of the example above.
Commands and arg groups
Before we start this recipe, I wanna recall one thing: the difference between commands and args. First of all, commands don't have any dashed or slashed in the name. They are just words. Commands specify what to do, whereas args specify how to do it. Example:
# What to do? `login`.
# How to do it? By taking a file with creds named `creds.json`.
In this recipe, we'll work with commands and args. You'll see a more complex example of the clap
configuration.
Interesting. What do we need to configure? 😄
We all know the Imgur site (if not, then just visit). It has an API. Let's imagine that we decided to write the CLI tool that helps us to work with the Imgur site using its API. So, now we need to design the tool interface. We do not plan to cover the whole API. Just downloading and uploading. A quick draft configuration:
/// Img tool config structure
We immediately have a few interesting moments: the Subcommand trait derive and the PathBuff
type in the api_key
field. We are not forced to use only simple types for args like String
s, numbers, etc. If you have a concrete type that describes your value (PathBuff
for file path, url::Url
for urls, or even custom ones), then use this type in the configuration. It'll handle more errors during parsing and make further work easier.
Now we add params for downloading command:
To download the image we need only two things: the source image link and the destination file path. This is how it works:
# Usage: img-tool --api-key <API_KEY> download --link <LINK> --dest-file <DEST_FILE>
#
# Options:
# --link <LINK> Source image link
# --dest-file <DEST_FILE> Path for the image
# -h, --help Print help
# Config { command: Download { link: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("imgur.com")), port: None, path: "/gallery/vNOUshX", query: None, fragment: None }, dest_file: "ferris.png" }, api_key: "key.json" }
# error: invalid value ':imgur.com/gallery/vNOUshX' for '--link <LINK>': relative URL without a base
#
# For more information, try '--help'.
Cool and pretty simple. But the upload is a little bit more complex. We wanna have two options where take the photo to upload: file image on the device or any public URL on the Internet. Here is the configuration for the upload command:
/// Possible types of the file source
Okay, we have two file sources: file or link. And we use ArgGroup to specify that the user must specify only one of them: either file or link. And here is the demo:
# Possible types of the file source
#
# Usage: img-tool --api-key <API_KEY> upload --folder <FOLDER> <--file <FILE>|--link <LINK>>
#
# Options:
# --file <FILE> Path to the image on the device
# --link <LINK> Url to the image on the Internet
# --folder <FOLDER> Folder for the image on the site
# -h, --help Print help
# Config { command: Upload { file_source: FileSource { file: Some("crab_ferris.png"), link: None }, folder: "ferris" }, api_key: "key.json" }
# error: the argument '--file <FILE>' cannot be used with '--link <LINK>'
#
# Usage: img-tool --api-key <API_KEY> upload --folder <FOLDER> <--file <FILE>|--link <LINK>>
#
# For more information, try '--help'.
# Config { command: Upload { file_source: FileSource { file: None, link: Some(Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("i.imgflip.com")), port: None, path: "/7gq1em.jpg", query: None, fragment: None }) }, folder: "ferris" }, api_key: "key.json" }
Another interesting thing: we can see the separated --file
and --link
args in the help message by the <>
triangle brackets. It shows the user that only one of the is needed. Cool, right? 😎
The full src code of the example above.
Parsing args into a custom structure
This recipe will be smaller than the previous ones and similar to the second one. But I just want to show that we can do such tricks.
We are going to improve prev example by adding a more flexible way to pass the API key. Assume that for authentication we need two tokens: app id and app secret. And we want to operate them as one structure. Usually, in such cases, developers take them as two separate strings and then create one structure based on those strings. But we a smarter and know how to use commands:
/// Img tool config structure
Let's test it and see the trick:
# Img tool config structure
#
# Usage: img-tool --api-app-id <API_APP_ID> --api-app-secret <API_APP_SECRET> <COMMAND>
#
# Commands:
# upload Upload image to the Imgur
# download
# help Print this message or the help of the given subcommand(s)
#
# Options:
# --api-app-id <API_APP_ID> app id
# --api-app-secret <API_APP_SECRET> app secret
# -h, --help Print help
We have two separate args api-app-is
and api-app-secret
, but they will be parsed and placed in the one ApiKey
structure. It's very convenient and we can continue to work with the auth data as one structure without any additional actions.
The full src code of the example above.
Unsolvable problem
This section is not an actual recipe. I'll tell you about a problem that doesn't have a perfect solution so far (or I just cannot find it).
Let's take the previous recipe and make the task more difficult. Assume that the user should specify the app id and secret OR API key file. In other words, the user should provide the one --api-key-file
arg or --api-app-id
and --api-app-secret
args. It means one OR two arguments.
I found a few similar questions on the Internet:
But both of them didn't have any useful answers. So far, the following code is the best how we can solve this problem:
/// Possible types of the api key source
// The `Config` structure remains unchanged since the last recipe
The main idea is to create an ArgGroup
and require additional args in the ApiKeyData
structure if one of the needed args is not specified. This approach has a lot of big inconveniences:
- Fields in the
ApiKeyData
structure are optional. Yes, during parsing they will be validated and 100% have values, but for further work, we are forced to unwrap them and create another structure. - The help message for the user is not fully informative. It shows the among those three args only one is required. That is a lie because we need a key file OR app id + secret.
Enough talking. Let's see it in action:
# Img tool config structure
#
# Usage: img-tool <--api-key-file <API_KEY_FILE>|--api-app-id <API_APP_ID>|--api-app-secret <API_APP_SECRET>> <COMMAND>
#
# Commands:
# upload Upload image to the Imgur
# download
# help Print this message or the help of the given subcommand(s)
#
# Options:
# --api-key-file <API_KEY_FILE> Path to the json file with API key
# --api-app-id <API_APP_ID> app id
# --api-app-secret <API_APP_SECRET> app secret
# -h, --help Print help
# Config { command: Download { link: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("imgur.com")), port: None, path: "/gallery/vNOUshX", query: None, fragment: None }, dest_file: "ferris.png" }, api_key: ApiKey { api_key_file: Some("key.json"), api_key_data: ApiKeyData { api_app_id: None, api_app_secret: None } } }
# Config { command: Download { link: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("imgur.com")), port: None, path: "/gallery/vNOUshX", query: None, fragment: None }, dest_file: "ferris.png" }, api_key: ApiKey { api_key_file: None, api_key_data: ApiKeyData { api_app_id: Some("tbt"), api_app_secret: Some("secret") } } }
# error: the following required arguments were not provided:
# --api-app-secret <API_APP_SECRET>
#
# Usage: img-tool <--api-key-file <API_KEY_FILE>|--api-app-id <API_APP_ID>|--api-app-secret <API_APP_SECRET>> <COMMAND>
#
# For more information, try '--help'.
The full src code of the example above. Conclusion: this problem is solvable but in a very inconvenient way.
References & final note
The end. The official reference has all you need.
As you can see, the clap
gives us a lot of opportunities to configure args parsing and validation. Sometimes it's really worth it to read the docs or references. Advice for the future: try to move as much work as you can to the clap
. It's a very powerful tool, so you shouldn't be bothered by manual parsing or validation of the args.