Resolve A Panic When Clap’s get_one() Function Downcast f64

Updated:

Rust has a crate called Clap that helps you define arguments more cleanly, create help documents, and parse them. At the center of argument parsing is the get_one() function, which helps convert input strings to boolean, usize, etc. However, one panic that I cannot understand occurs in my code. This article describes the process of finding and solving the cause of the error.

Minimal code for problem

Let’s write the following code. The following code sets the name of this program to “test” and receives one argument called “mass” with a short flag ‘m’. Then, it parses the input argument into an f64 type.

use clap::builder::Command;
use clap::{Arg, ArgMatches};

fn main(){
  let matches = Command::new("test")
                .arg(Arg::new("mass")
                    .short('m')
                    .takes_value(true))
                .get_matches()
  let mass: f64 = *matches.get_one::<f64>("mass").unwrap();
}

This code compiles without any problems. However, the problem occurs at runtime, and if you input the argument as follows, a panic occurs.

./minimal -m 1.0

The error message is “Mismatch between definition and access of mass. Could not downcast to f64, need to downcast to alloc::string::String”

Backtrace panic

Let’s check where the above panic is coming from. First, the above message is the result of the clap::parser::MatchesError::unwrap() function. Since the try_get_one() function does not return an Ok value, it panics with an error message.

The structure of the try_get_one() function is as follows.

impl ArgMatches {
    pub fn try_get_one<T: Any + Clone + Send + Sync + 'static>(
        &self,
        id: &str,
    ) -> Result<Option<&T>, MatchesError> {
        let id = Id::from(id);
        let arg = self.try_get_arg_t::<T>(&id)?;
        let value = match arg.and_then(|a| a.first()) {
            Some(value) => value,
            None => {
                return Ok(None);
            }
        };
        Ok(value
            .downcast_ref::<T>()
            .map(Some)
            .expect(INTERNAL_ERROR_MSG)) // enforced by `try_get_arg_t`
    }
}

The only place where the error is passed is try_get_arg_t().

impl ArgMatches {
    #[inline]
    fn try_get_arg(&self, arg: &Id) -> Result<Option<&MatchedArg>, MatchesError> {
        self.verify_arg(arg)?;
        Ok(self.args.get(arg))
    }

    #[inline]
    fn try_get_arg_t<T: Any + Send + Sync + 'static>(
        &self,
        arg: &Id,
    ) -> Result<Option<&MatchedArg>, MatchesError> {
        let arg = match self.try_get_arg(arg)? {
            Some(arg) => arg,
            None => {
                return Ok(None);
            }
        };
        self.verify_arg_t::<T>(arg)?;
        Ok(Some(arg))
    }
}

The only place where the error is passed is verify_arg() and verify_arg_t() functions.

impl ArgMatches {
    #[inline]
    fn verify_arg(&self, _arg: &Id) -> Result<(), MatchesError> {
        #[cfg(debug_assertions)]
        {
            if self.disable_asserts || *_arg == Id::empty_hash() || self.valid_args.contains(_arg) {
            } else if self.valid_subcommands.contains(_arg) {
                debug!(
                    "Subcommand `{:?}` used where an argument or group name was expected.",
                    _arg
                );
                return Err(MatchesError::UnknownArgument {});
            } else {
                debug!(
                    "`{:?}` is not an id of an argument or a group.\n\
                     Make sure you're using the name of the argument itself \
                     and not the name of short or long flags.",
                    _arg
                );
                return Err(MatchesError::UnknownArgument {});
            }
        }
        Ok(())
    }

    fn verify_arg_t<T: Any + Send + Sync + 'static>(
        &self,
        arg: &MatchedArg,
    ) -> Result<(), MatchesError> {
        let expected = AnyValueId::of::<T>();
        let actual = arg.infer_type_id(expected);
        if expected == actual {
            Ok(())
        } else {
            Err(MatchesError::Downcast { actual, expected })
        }
    }
}

The DownCast error occurs in the verify_arg_t() function, and specifically in the MatchedArgs::infer_type_id function.

impl MatchedArg {
    pub(crate) fn infer_type_id(&self, expected: AnyValueId) -> AnyValueId {
        self.type_id()
            .or_else(|| {
                self.vals_flatten()
                    .map(|v| v.type_id())
                    .find(|actual| *actual != expected)
            })
            .unwrap_or(expected)
    }
}

This function returns the type of the argument, or if there are multiple arguments, it returns the type that is different from the expected type. In other words, floating point related arguments are not recognized as f64 but as String. To check the structure of the argument, look at the self.args.get(arg) part inside try_get_arg(). The args field is a IndexMap<Id, MatchedArg> structure, and the definition of MatchedArg is given as follows.

#[derive(Debug, Clone)]
pub(crate) struct MatchedArg {
    occurs: u64,
    source: Option<ValueSource>,
    indices: Vec<usize>,
    type_id: Option<AnyValueId>,
    vals: Vec<Vec<AnyValue>>,
    raw_vals: Vec<Vec<OsString>>,
    ignore_case: bool,
}

impl MatchedArg {
    pub(crate) fn new_arg(arg: &crate::Arg) -> Self {
        let ignore_case = arg.is_ignore_case_set();
        Self {
            occurs: 0,
            source: None,
            indices: Vec::new(),
            type_id: Some(arg.get_value_parser().type_id()),
            vals: Vec::new(),
            raw_vals: Vec::new(),
            ignore_case,
        }
    }
}

type_id is determined by the get_value_parser() function. The get_value_parser() function returns the ValueParser struct assigned to the argument.

ValueParser in Clap

Then, is the problem that the parser for parsing f64 is not automatically assigned but needs to be specified beforehand?

fn main(){
  let matches = Command::new("test")
                .arg(Arg::new("mass")
                    .short('m')
                    .takes_value(true))
                    .value_parser(clap::value_parser!(f64))
                .get_matches()
}

If you modify the code as above, the same problem will no longer occur. Then, what is the parser for f64? As a result, I got the conclusion that it is _AnonymousValueParser(ValueParser::other(f64)). In other words, in the default structure of clap, no parser is provided for floating point types, and only when an f64 parser is specified explicitly, it can be parsed without any problems.