Search code examples
rustclosureslifetimeclap

argument validation in clap v4


I am using crate clap v4。When I try to write something validating arguments against regex, I had some problem with lifetimes.

My code as following:

pub fn validator_regex(r: &'static str) -> ValueParser {
    ValueParser::from(move |s: &str| -> std::result::Result<&str, Error> {
        let reg = regex::Regex::new(r).unwrap();
        match reg.is_match(s) {
            true => Ok(s),
            false => Err(Error::from(format!("not matches {}", r))),
        }
    })
}

pub fn validator_identifier() -> ValueParser {
    validator_regex("^[-_0-9a-zA-Z]+$")
}

And compilation error:

error: lifetime may not live long enough
   --> main\./src\cmd\cmd_util.rs:440:21
    |
437 |     ValueParser::from(move |s: &str| -> std::result::Result<&str, Error> {
    |                                -                            - let's call the lifetime of this reference `'2`
    |                                |
    |                                let's call the lifetime of this reference `'1`
...
440 |             true => Ok(s),
    |                     ^^^^^ returning this value requires that `'1` must outlive `'2`

Could any help me on these two questions:

  • how to validate regex in clap v4
  • why there' s lifetime may not live long enough error in this code, since the returned &str lives as long as the closure argument, and this can be compiled normally
pub fn validator_regex(r: &'static str) -> impl Fn(&str) -> Result<&str, String> {
    move |s: &str| -> Result<&str, String> {
        let reg = regex::Regex::new(r).unwrap();
        match reg.is_match(s) {
            true => Ok(s),
            false => Err(format!("not match {}", r)),
        }
    }
}

Solution

  • Let's take a look at the From impl you're invoking:

    impl<P> From<P> for ValueParser
    where
        P: TypedValueParser + Send + Sync + 'static,
    

    Ok, take a look at TypedValueParser:

    impl<F, T, E> TypedValueParser for F
    where
        F: Fn(&str) -> Result<T, E> + Clone + Send + Sync + 'static,
        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
        T: Send + Sync + Clone,
    

    So, the signature is not Fn(&str) -> Result<&str, E>, it is <T> Fn(&str) -> Result<T, E>. Is it different? Can't we just put &str in the place of T?

    It is very different. Let's annotate the lifetimes:

    // Fn(&str) -> Result<&str, E>
    for<'a> Fn(&'a str) -> Result<&'a str, E>
    
    // <T> Fn(&str) -> Result<T, E>
    <&'b str> for<'a> Fn(&'a str) -> Result<&'b str, E>
    

    Where does 'b come from? It can't be HRTB, as it is declared in the impl header (T) and not the trait bound (Fn). Therefore, it must be a fixed lifetime. See the mismatch? The lifetimes I called 'a and 'b are what the compiler calls '1 and '2, respectively. If 'b is fixed, it cannot be derived from the dynamic HRTB 'a.

    The fix is simple: just don't return &str, return String instead:

    pub fn validator_regex(r: &'static str) -> ValueParser {
        ValueParser::from(move |s: &str| -> std::result::Result<String, Error> {
            let reg = regex::Regex::new(r).unwrap();
            match reg.is_match(s) {
                true => Ok(s.to_owned()),
                false => Err(Error::from(format!("not matches {}", r))),
            }
        })
    }