Search code examples
rustdocopt

Is there a way to use docopt to pass a vector of u8 from the command line?


Is there a way to make the user prompt for the bytes inside brackets and separated by commas or something similar?

./main bytes [0, 1, 2, 3, 4, 5]

I managed to make it look like this:

./main bytes 0 1 2 3 4 5

This is my code:

extern crate docopt;
#[macro_use]
extern crate serde_derive;

use docopt::Docopt;

const USAGE: &'static str = "
    Puzzle Solver.

    Usage:
      puzzle_solver string <text>
      puzzle_solver bytes [<bin>...] 
      puzzle_solver (-h | --help)
      puzzle_solver --version

    Options:
      -h --help     Show this screen.
      --version     Show version.
    ";

#[derive(Debug, Deserialize)]
struct Args {
    cmd_string: bool,
    arg_text: Option<String>,
    cmd_bytes: bool,
    arg_bin: Option<Vec<u8>>,
}

fn main() {
    let args: Args = Docopt::new(USAGE)
        .and_then(|d| d.deserialize())
        .unwrap_or_else(|e| e.exit());

    println!("ARGS: {:?}", args);
}

Solution

  • It's possible, but you have to implement Deserialize by hand.

    Vec<u8> already implements Deserialize, and that implementation doesn't know about strings containing comma-delimited bracketed lists, nor does docopt::Deserializer, since the normal way to pass a list on the command line is element-by-element. So you have to make a new type that will deserialize from the format you want.

    Naturally, you can also implement Deref<Target = Vec<u8>> and DerefMut for Bytes, if you want to treat it as a Vec<u8>. Some people might consider this a slight misuse of Deref, but it's probably fine in a situation like this.

    extern crate docopt;
    extern crate serde;
    #[macro_use]
    extern crate serde_derive;
    
    use docopt::Docopt;
    use serde::de;
    use std::fmt;
    
    const USAGE: &'static str = "
        Puzzle Solver.
    
        Usage:
          puzzle_solver string <text>
          puzzle_solver bytes <bin>
          puzzle_solver (-h | --help)
          puzzle_solver --version
    
        Options:
          -h --help     Show this screen.
          --version     Show version.
        ";
    
    #[derive(Debug, Deserialize)]
    struct Args {
        cmd_string: bool,
        arg_text: Option<String>,
        cmd_bytes: bool,
        arg_bin: Option<Bytes>,
    }
    
    #[derive(Debug)]
    struct Bytes(Vec<u8>);
    
    impl<'de> de::Deserialize<'de> for Bytes {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: de::Deserializer<'de>,
        {
            struct BytesVisitor;
    
            impl<'de> de::Visitor<'de> for BytesVisitor {
                type Value = Bytes;
    
                fn expecting(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
                    write!(formatter, "a bracketed, comma-delimited string")
                }
    
                fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
                    let v = if v.starts_with('[') && v.ends_with(']') {
                        &v[1..v.len() - 1]
                    } else {
                        return Err(E::custom(format!("expected a bracketed list, got {:?}", v)));
                    };
                    let values: Result<Vec<u8>, _> = v.split(",").map(|s| s.trim().parse()).collect();
                    Ok(Bytes(values.map_err(E::custom)?))
                }
            }
    
            deserializer.deserialize_str(BytesVisitor)
        }
    }
    

    Here it is in the playground. These are the changes I made to get this to work:

    1. Replace [<bin>...] with <bin> so docopt will know to look for a single thing and not a sequence of... things. (If you don't do this, docopt actually just throws you an empty string.)
    2. Introduce the newtype Bytes wrapper around Vec<u8>.
    3. Implement serde::de::Deserialize for Bytes. This entails creating a struct that implements the serde::de::Visitor trait, putting the code that picks apart the string inside its visit_str method, and passing the visitor to deserialize_str, which tells the Deserializer to expect a string and pass it to the visitor's visit_str.

    I didn't realize it until almost done, but you could implement visit_seq instead, and make it parse bytes [1, 2, 3] (without quoting the list). But I wouldn't, because that defies command line convention; if you're using the shell to split the arguments anyway, you should go the whole way and just accept bytes 1 2 3.