Finlay Weber
Finlay Weber

Reputation: 4163

How to parse command line argument to non-unit enum with clap?

I have this enum:

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum TheAge {
    Adult,
    Age(u8)
}

And the cli struct

#[derive(Parser)]
#[command(author, version, about, long_about)]
pub struct Cli {
    #[arg(short, long, value_enum)]
    pub age: TheAge
}

This fails with the error:

error: `#[derive(ValueEnum)]` only supports unit variants. Non-unit variants must be skipped

When I remove the Age(u8) from the enum, this compiles.

Any tips on how to use an enum that is not unit variants?

Upvotes: 4

Views: 1733

Answers (3)

Bryan Larsen
Bryan Larsen

Reputation: 10006

@cafce25's great answer returns Adult for an invalid age. If you want an error instead, impl FromStr rather than From<&str>:

use clap::Parser;
use std::str::FromStr;
use anyhow::Context;

#[derive(Parser)]
#[command(author, version, about, long_about)]
pub struct Cli {
    #[arg(short, long)]
    pub age: TheAge
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum TheAge {
    Adult,
    Age(u8)
}

const LEGAL_ADULT_AGE: u8 = 18;
impl FromStr for TheAge {
    type Err = anyhow::Error;
    
    fn from_str(v: &str) -> Result<TheAge, Self::Err> {
        v.parse::<u8>().map(|a| {
            if a >= LEGAL_ADULT_AGE {
                TheAge::Adult
            } else {
                TheAge::Age(a)
            }
        }).context("invalid age")
    }
}

fn main() {
    dbg!(Cli::parse_from(["", "--age", "18"]).age); // and above -> Adult
    dbg!(Cli::parse_from(["", "--age", "17"]).age); // and below -> Age(17)
    dbg!(Cli::parse_from(["", "--age", "anything_else"]).age); // -> Error
}

Upvotes: 0

Ben Moss
Ben Moss

Reputation: 179

Using rust-analyzer in VSCode I'm able to expand the ValueEnum macro into the actual code it will generate if Age was just a regular unit variant:

impl clap::ValueEnum for TheAge {
    fn value_variants<'a>() -> &'a [Self] {
        &[Self::Adult, Self::Age]
    }
    fn to_possible_value<'a>(&self) -> ::std::option::Option<clap::builder::PossibleValue> {
        match self {
            Self::Adult => Some({ clap::builder::PossibleValue::new("adult") }),
            Self::Age => Some({ clap::builder::PossibleValue::new("age") }),
            _ => None,
        }
    }
}

So given this we can use this code to handle the fact that we want Age(u8):

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum TheAge {
    Adult,
    Age(u8),
}

impl clap::ValueEnum for TheAge {
    fn value_variants<'a>() -> &'a [Self] {
        &[TheAge::Adult, TheAge::Age(0)]
    }
    fn to_possible_value<'a>(&self) -> ::std::option::Option<clap::builder::PossibleValue> {
        match self {
            Self::Adult => Some(clap::builder::PossibleValue::new("adult")),
            Self::Age(_) => Some(clap::builder::PossibleValue::new("age")),
        }
    }
}

The only tricky bit was figuring out that using Self::Adult will give us the error

cannot return reference to temporary value
returns a reference to data owned by the current function

I'm not really sure why using TheAge:: rather than Self:: fixes this, but it seems to work.

Upvotes: 0

cafce25
cafce25

Reputation: 27539

#[derive(ValueEnum)] does not support non-unit variants, so you can't derive it.

And if you look at the required items it's sort of clear why:

impl ValueEnum for TheAge {
    fn value_variants() -> &'a [Self] { todo!() }
    fn to_possible_value(&self) -> Option<PossibleValue> { todo!() }
}

value_variants is supposed to return

All possible argument values, in display order.

that's not really feasible when "all possible values" includes every single u8 (even if that's only 257 values total, it still makes a messy UI). There is no way to generically generate all values of a type so you can't #[derive(ValueEnum)], it might make sense to implement it by hand in some cases though (for example when the values are enums with only a few variants).

Instead you can implement From<&str> for that struct and it will work:

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum TheAge {
    Adult,
    Age(u8)
}

const LEGAL_ADULT_AGE: u8 = 18;
impl From<&str> for TheAge {
    fn from(v: &str) -> TheAge {
        v.parse::<u8>().map_or(TheAge::Adult, |a| {
            if a >= LEGAL_ADULT_AGE {
                TheAge::Adult
            } else {
                TheAge::Age(a)
            }
        })
    }
}

fn main() {
    dbg!(Cli::parse_from(["", "--age", "18"]).age); // and above -> Adult
    dbg!(Cli::parse_from(["", "--age", "17"]).age); // and below -> Age(17)
    dbg!(Cli::parse_from(["", "--age", "anything_else"]).age); // -> Adult
}

Upvotes: 5

Related Questions