Mahesh Bansod
Mahesh Bansod

Reputation: 2033

How to have number or string as a CLI argument in clap

I want to have an argument that can be a number or a string. Not sure how to achieve this besides implementing FromStr myself. It seems like a simple use-case to handle, not sure what macros to use.
Here, I want the testapp_id to be a number or a string.
I'm aiming for the usage to be like this:
cargo run -- testapps status 3 or cargo run -- testapps status app4

#[derive(Parser, Debug)]
#[clap(author,version,about)]
enum Submodules {
    #[clap(subcommand)]
    Testapps(TestappsCommands)
}

#[derive(Subcommand, Debug)]
enum TestappsCommands {
    List,
    Status {
        #[clap(flatten)]
        testapp_id: NumberOrString
    },
}

#[derive(Debug)]
enum NumberOrString {
    Number(u32),
    Str(String),
}

I was thinking flatten is what I could use for this but it's still complaining about missing implementation of FromStr .
It works fine if I use only u32 or String type for testapp_id , why not both?

Upvotes: 1

Views: 1089

Answers (2)

Alex Vergara
Alex Vergara

Reputation: 2248

One common way in Rust of approach the problem it's with the use of generics. You can make your field's type to accept a generic for that field in particular.

#[derive(Subcommand, Debug)]
enum TestappsCommands<T> {
    List,
    Status {
        #[clap(flatten)]
        testapp_id: T
    },
}

but this it's not enought. Because you now have a type T instead of the NumberOrString, which doesn't solve your problem.

I don't know if you design the NumberOrString just for the purpose of making the testapp_id accept both numbers and strings, but, if you don't need it, you may desire to write some bounds.

Bounds are a way of constraint generics over a type T, where some restrictions are applied.

Instead of using your enum, you can write this:

use std::str::FromStr;

// ... your code...

#[derive(Subcommand, Debug)]
enum TestappsCommands<T: FromStr> {
    List,
    Status {
        #[clap(flatten)]
        testapp_id: T
    },
}

and now your field would only contain values of any type T that already implements FromStr.

Due to your requeriment of accept both numbers or strings, FromStr it's already implemented in Rust for all the signed and unsigned variants for a "number".

You can read it better in the docs: https://doc.rust-lang.org/std/str/trait.FromStr.html

Upvotes: 2

Jmb
Jmb

Reputation: 23404

Don't use flatten for this. Instead you need to implement FromStr for your enum, e.g.:

impl FromStr for NumberOrString {
    type Err = &'static str;    // The actual type doesn't matter since we never error, but it must implement `Display`
    fn from_str(s: &str) -> Result<Self, Self::Err>
    {
        Ok (s.parse::<u32>()
             .map (NumberOrString::Number)
             .unwrap_or_else (|_| NumberOrString::Str (s.to_string())))
    }
}

Playground

Upvotes: 2

Related Questions