Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

enums

rust

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?

like image 895
Finlay Weber Avatar asked Oct 16 '25 05:10

Finlay Weber


2 Answers

#[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
}
like image 128
cafce25 Avatar answered Oct 18 '25 23:10

cafce25


@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
}
like image 41
Bryan Larsen Avatar answered Oct 18 '25 23:10

Bryan Larsen