0

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);
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Do you know this https://github.com/TeXitoi/structopt? That's a really cool crate for command line arguments if you can use procedural macros (currently in nightly compiler). – Boiethios Dec 15 '17 at 09:12
  • @boiethios Could you give me an example that fits my needs using this crate? – sadmachine Dec 15 '17 at 13:18
  • This crate will not solve your particular problem, because as `docopt` (and as every other similar crate), it relies on `FromStr` to parse the input. But it is far more easier to use than the others. See the example. – Boiethios Dec 15 '17 at 13:23
  • I will post an answer. The solution to your problem is to implement `FromStr` – Boiethios Dec 15 '17 at 13:24
  • So the solution would be: take a String and make it a vector of u8? – sadmachine Dec 15 '17 at 13:27
  • Yep. You must create your own type `struct MyVec(Vec);` and implement `FromStr` for it. – Boiethios Dec 15 '17 at 13:30
  • @Boiethios: FYI, structopt doesn't require the nightly compiler! [Procedural macros were stabilized in Rust 1.15](https://blog.rust-lang.org/2017/02/02/Rust-1.15.html) :) – Joe Clay Dec 15 '17 at 17:01
  • @JoeClay It seems that I missed some information :D – Boiethios Dec 15 '17 at 17:11
  • Do you understand that the command line is split into arguments by the shell, and not under docopt's control? Would `./main bytes "[0, 1, 2, 3, 4, 5]"` (with quotes) be acceptable? – trent Dec 16 '17 at 12:47
  • Yes, I understand. With quotes would be acceptable of course. – sadmachine Dec 16 '17 at 13:00

1 Answers1

2

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.

trent
  • 25,033
  • 7
  • 51
  • 90