Search code examples
rusterror-handlingcommand-line-interfacecommand-line-argumentsclap

How to verify that the command line arguments can be found in a given array?


I'm writing a CLI program using clap. Usage will be:

$ my_tool command word1 word2 word3

I have a hard-coded array that contains a set of accepted words (1):

pub const WORDS: [&str; 2048] = ["abandon", "ability", "able", "about", "above", ...]

I want to check that all the words (word1, word2, word3) provided by the user can be found in the array because I'll be looking for the indexes of the words in a function.

What I'm doing is:

Collecting the words in a vector:

use clap::{Parser, Subcommand};

// --snip--

#[derive(Parser)]
#[clap(author, version, about)]
pub struct Cli {
    /// Operation to perform
    #[clap(subcommand)]
    pub command: Subcommands,
}

#[derive(Subcommand)]
pub enum Subcommands {
    /// Splits seedphrase
    Split {
        /// Seedprhase to split
        #[clap(value_parser, required(true))]
        seedphrase: Vec<String>,
    },
    // --snip--

And propagating an error from the function that looks for the indexes (using anyhow) (2):

pub fn words_to_indexes(words: Vec<&str>) -> Result<Vec<i32>, anyhow::Error> {
    let mut indexes: Vec<i32> = Vec::new();
    for word in words {
        indexes.push(
            WORDS
                .iter()
                .position(|x| x == &word)
                .context(format!("Word '{}' is not on the BIP39 wordlist", word))?
                as i32,
        );
    }
    Ok(indexes)
}

This error is propagated all the way up to the main function so anyhow takes care of printing it with context.

This solution works, but I realized that as it's only caused by bad arguments from the user, it would make more sense to do this check right at the beginning of the program. That is, indicating clap to verify that the words provided are in the wordlist. Is there a way to do this kind of check with clap? And if there is, How?

Another solution I thought of is to simply manually verify arguments at the beginning of the program. That is, in the main function right after calling Cli::parse() or in the run function that calls all the other necessary logic functions in the program (3). However, I realized that if this can be done "natively" with clap it should be simpler and have already defined "standard" prints.

(1) It is the BIP39 wordlist.

(2) Another function converts the Vec<String> to a Vec<&str>, don't worry about that.

(3) The organization of the project is based on chapter 12 of the book.


Solution

  • You can use clap's PossibleValuesParser for this:

    use clap::{builder::PossibleValuesParser, Parser, Subcommand};
    
    const WORDS: [&str; 3] = ["crypto", "is", "garbage"];
    
    #[derive(Parser)]
    #[clap(author, version, about)]
    pub struct Cli {
        /// Operation to perform
        #[clap(subcommand)]
        pub command: Command,
    }
    
    #[derive(Subcommand)]
    pub enum Command {
        /// Splits seedphrase
        Split {
            /// Seedprhase to split
            #[clap(value_parser = PossibleValuesParser::new(WORDS), required(true))]
            //                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // tell clap to only allow the words in WORDS to appear in seedphrase.
            seedphrase: Vec<String>,
        },
    }