Skip to content

Instantly share code, notes, and snippets.

@3noch
Created February 14, 2022 09:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 3noch/0cd7019f33a1a5e8284b9f2cc17562f4 to your computer and use it in GitHub Desktop.
Save 3noch/0cd7019f33a1a5e8284b9f2cc17562f4 to your computer and use it in GitHub Desktop.
tiny_cli.rs
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Error {
PositionalArgExpected(String),
FlagExpectedValue(String),
ConversionFailed(String),
}
/// A trait for types that can represent a command-line argument or flag.
trait Arg {
type Parsed; // Type resulting from parsing the argument
fn help(&self) -> String; // Pattern description (e.g. "file" or "[--flag value]")
// A function parsing a list of raw CLI strings. A successful result is the
// parsed data and the remaining vector of raw CLI strings that need to be parsed.
fn parse<'a>(&self, args: &[&'a str]) -> Result<(Self::Parsed, Vec<&'a str>), Error>;
}
// If both elements of a tuple are Args, then the tuple is an Arg.
impl<T: Arg, U: Arg> Arg for (T, U) {
type Parsed = (T::Parsed, U::Parsed);
fn help(&self) -> String {
format!("{} {}", self.0.help(), self.1.help())
}
fn parse<'a>(&self, args: &[&'a str]) -> Result<(Self::Parsed, Vec<&'a str>), Error> {
let (a, rest1) = self.0.parse(args)?;
let (b, rest2) = self.1.parse(&rest1)?;
Ok(((a, b), rest2))
}
}
// A generic positional CLI argument.
struct Positional<T> {
name: String,
t: std::marker::PhantomData<T>
}
impl<T> Positional<T> {
fn new(name: &str) -> Self {
Self{name: name.to_string(), t: std::marker::PhantomData}
}
}
impl<T: FromStr> Arg for Positional<T>
where <T as std::str::FromStr>::Err: std::fmt::Display
{
type Parsed = T;
fn help(&self) -> String {
self.name.clone()
}
fn parse<'a>(&self, args: &[&'a str]) -> Result<(Self::Parsed, Vec<&'a str>), Error> {
match args {
[x, xs@..] => match T::from_str(x) {
Err(e) => Err(Error::ConversionFailed(e.to_string())),
Ok(thing) => Ok((thing, xs.to_vec())),
},
_ => Err(Error::PositionalArgExpected(self.name.clone())),
}
}
}
// A generic flag CLI argument that expects a value.
// NOTE: Currently this parser is dumb and essentially treats the flag as a
// positional *pair* of arguments.
struct Flag<T> {
flag: String,
t: std::marker::PhantomData<T>,
}
impl<T> Flag<T> {
fn new(flag: &str) -> Self {
Self{ flag: flag.to_string(), t: std::marker::PhantomData }
}
}
impl<T: FromStr> Arg for Flag<T>
where <T as std::str::FromStr>::Err: std::fmt::Display
{
type Parsed = Option<T>;
fn help(&self) -> String {
format!("[--{} value]", self.flag)
}
fn parse<'a>(&self, args: &[&'a str]) -> Result<(Self::Parsed, Vec<&'a str>), Error> {
let expected_flag = format!("--{}", self.flag);
match args {
[x] if x == &expected_flag => Err(Error::FlagExpectedValue(self.flag.clone())),
[x, v, xs@..] if x == &expected_flag => match T::from_str(v) {
Err(e) => Err(Error::ConversionFailed(e.to_string())),
Ok(thing) => Ok((Some(thing), xs.to_vec())),
}
_ => Ok((None, args.to_vec())),
}
}
}
fn main() -> Result<(), Error> {
let spec = (Positional::<String>::new("file"), (Positional::<u32>::new("size"), Flag::<u32>::new("thing")));
let (data, rest) = spec.parse(&vec!["a", "77", "--thing"])?;
println!("{}", spec.help());
println!("{:?}", data);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment