use std::path::PathBuf;
use winnow::combinator::alt;
use winnow::prelude::*;
use winnow::stream::BStr;
use winnow_args::combinator::{Named, args, long, short};
use winnow_args::{Args, Argv, Error, ErrorKind, words};
#[derive(Debug, PartialEq, Default)]
struct Cli {
verbose: bool,
quiet: bool,
path: Option<PathBuf>,
jobs: u32,
set: Option<String>,
accent: bool,
}
const VERBOSE: Named = short('v').long("verbose");
const QUIET: Named = short('q');
const PATH: Named = short('p').long("path");
const JOBS: Named = short('j').long("jobs");
const SET: Named = long("set");
const ACCENT: Named = short('é');
fn combinator(input: &mut Argv<'_>) -> Result<Cli, Error> {
let mut cli = Cli::default();
let mut jobs = None;
args(alt((
VERBOSE.switch().map(|()| cli.verbose = true),
QUIET.switch().map(|()| cli.quiet = true),
PATH.argument_as().map(|p| cli.path = Some(p)),
JOBS.argument_as().map(|j| jobs = Some(j)),
SET.argument_as().map(|s| cli.set = Some(s)),
ACCENT.switch().map(|()| cli.accent = true),
)))
.parse_next(input)?;
cli.jobs = jobs.ok_or_else(|| JOBS.missing(input))?;
Ok(cli)
}
#[derive(Args, Debug, PartialEq)]
struct Derived {
#[arg(short, long)]
verbose: bool,
#[arg(short)]
quiet: bool,
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short, long)]
jobs: u32,
set: Option<String>,
#[arg(short = 'é')]
accent: bool,
}
impl From<Derived> for Cli {
fn from(d: Derived) -> Self {
let Derived {
verbose,
quiet,
path,
jobs,
set,
accent,
} = d;
Cli {
verbose,
quiet,
path,
jobs,
set,
accent,
}
}
}
fn parse(line: &[&str]) -> Result<Cli, Error> {
let words = crate::words(line);
let a = combinator.parse_next(&mut Argv::new(&words));
let b = Derived::parse_words(&words).map(Cli::from);
assert_eq!(a, b, "combinator and derive disagree on {line:?}");
a
}
fn ok(line: &[&str]) -> Cli {
parse(line).unwrap_or_else(|e| panic!("{line:?}: {e}"))
}
fn err(line: &[&str]) -> Error {
match parse(line) {
Ok(cli) => panic!("{line:?} parsed as {cli:?}"),
Err(e) => e,
}
}
fn path(p: &str) -> Option<PathBuf> {
Some(PathBuf::from(p))
}
#[test]
fn long_forms() {
assert_eq!(ok(&["-j1", "--path=/a"]).path, path("/a"));
assert_eq!(ok(&["-j1", "--path", "/a"]).path, path("/a"));
assert_eq!(ok(&["-j1", "--path="]).path, path(""));
assert_eq!(ok(&["-j1", "--set=a=b"]).set.as_deref(), Some("a=b"));
assert!(ok(&["-j1", "--verbose"]).verbose);
}
#[test]
fn short_forms() {
assert_eq!(ok(&["-j1", "-p", "/a"]).path, path("/a"));
assert_eq!(ok(&["-j1", "-p/a"]).path, path("/a"));
assert_eq!(ok(&["-j1", "-p=/a"]).path, path("/a"));
assert_eq!(ok(&["-j1", "-p==/a"]).path, path("=/a"));
assert_eq!(ok(&["-j", "7"]).jobs, 7);
assert!(ok(&["-j1", "-é"]).accent);
}
#[test]
fn bundles() {
let cli = ok(&["-vqj3"]);
assert!(cli.verbose && cli.quiet);
assert_eq!(cli.jobs, 3);
let cli = ok(&["-vp", "/a", "-j2"]);
assert!(cli.verbose);
assert_eq!(cli.path, path("/a"));
let cli = ok(&["-j1", "-vpq"]);
assert!(!cli.quiet);
assert_eq!(cli.path, path("q"));
assert!(ok(&["-j1", "-vé"]).accent);
}
#[test]
fn order_and_repetition() {
assert_eq!(
ok(&["--path=/a", "-v", "-j1"]),
ok(&["-j1", "-v", "--path=/a"])
);
assert!(ok(&["-vv", "-j1", "--verbose"]).verbose);
assert_eq!(ok(&["-j1", "-p/a", "--path=/b"]).path, path("/b"));
assert_eq!(ok(&["-j1", "-j2"]).jobs, 2);
}
#[test]
fn values_that_look_like_flags() {
assert_eq!(ok(&["-j1", "--path", "-"]).path, path("-"));
assert_eq!(ok(&["-j1", "-p", "-"]).path, path("-"));
assert_eq!(ok(&["-j1", "--path=--verbose"]).path, path("--verbose"));
assert_eq!(ok(&["-j1", "-p-v"]).path, path("-v"));
}
#[test]
fn words_are_never_resplit() {
assert_eq!(ok(&["-j1", "--path", "/a b"]).path, path("/a b"));
assert_eq!(ok(&["-j1", "-p", " "]).path, path(" "));
assert_eq!(ok(&["-j1", "--set=a\0b"]).set.as_deref(), Some("a\0b"));
assert_eq!(ok(&["-j1", "--set", ""]).set.as_deref(), Some(""));
}
#[test]
fn separator() {
assert_eq!(ok(&["-j1", "--"]), ok(&["-j1"]));
let e = err(&["-j1", "--", "-v"]);
assert_eq!(e.kind(), ErrorKind::UnexpectedArg);
assert_eq!(e.token(), Some("-v"));
let e = err(&["-j1", "--", "--"]);
assert_eq!(e.kind(), ErrorKind::UnexpectedArg);
assert_eq!(e.token(), Some("--"));
}
#[test]
fn errors() {
let e = err(&["-j1", "--nope"]);
assert_eq!(
(e.kind(), e.token()),
(ErrorKind::UnknownFlag, Some("--nope"))
);
assert_eq!(err(&["-j1", "--verb"]).kind(), ErrorKind::UnknownFlag);
let e = err(&["-j1", "-vx"]);
assert_eq!((e.kind(), e.token()), (ErrorKind::UnknownFlag, Some("-x")));
assert_eq!(e.offset(), 6, "points at the letter inside the bundle");
let e = err(&["-j1", "stray"]);
assert_eq!(
(e.kind(), e.token()),
(ErrorKind::UnexpectedArg, Some("stray"))
);
let e = err(&["-j1", "--path"]);
assert_eq!(
(e.kind(), e.token()),
(ErrorKind::MissingValue, Some("--path"))
);
let e = err(&["-j1", "--path", "--verbose"]);
assert_eq!(e.kind(), ErrorKind::MissingValue);
let e = err(&["-j1", "--path", "--"]);
assert_eq!(e.kind(), ErrorKind::MissingValue);
let e = err(&["-j1", "-p"]);
assert_eq!((e.kind(), e.token()), (ErrorKind::MissingValue, Some("-p")));
let e = err(&["-j1", "--verbose=yes"]);
assert_eq!(
(e.kind(), e.value()),
(ErrorKind::UnexpectedValue, Some("yes"))
);
let e = err(&["-v"]);
assert_eq!(
(e.kind(), e.token()),
(ErrorKind::MissingRequired, Some("--jobs"))
);
let e = err(&["-jmany"]);
assert_eq!(
(e.kind(), e.value()),
(ErrorKind::InvalidValue, Some("many"))
);
assert_eq!(
e.to_string(),
"invalid value `many` for `-j`: invalid digit found in string"
);
}
#[cfg(unix)]
#[test]
fn non_utf8_values_survive() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt as _;
let raw = OsStr::from_bytes(b"/tmp/\xff");
let cli = Derived::parse_words(&words(&[OsStr::new("-j1"), OsStr::new("-p"), raw])).unwrap();
assert_eq!(cli.path.as_deref().map(|p| p.as_os_str()), Some(raw));
let e =
Derived::parse_words(&words(&[OsStr::new("-j1"), OsStr::new("--set"), raw])).unwrap_err();
assert_eq!(e.kind(), ErrorKind::InvalidValue);
}
#[test]
fn composes_as_a_winnow_parser() {
let words = [BStr::new("-j4")];
let jobs = Derived::parse_argv
.map(|d| d.jobs)
.parse_next(&mut Argv::new(&words))
.unwrap();
assert_eq!(jobs, 4);
}
mod from_str {
use std::net::Ipv4Addr;
use std::num::NonZeroU8;
use winnow_args::{Args, ErrorKind, with_env};
#[derive(Args, Debug, PartialEq)]
struct Cli {
#[arg(long, from_str)]
bind: Option<Ipv4Addr>,
#[arg(long, from_str, default = "3")]
jobs: NonZeroU8,
#[arg(long, from_str, env = "PEERS", delimiter = ',')]
peer: Vec<Ipv4Addr>,
#[arg(positional, from_str)]
host: Ipv4Addr,
}
#[test]
fn a_value_is_parsed_with_its_from_str() {
let cli = Cli::parse_from([
"--bind",
"10.0.0.1",
"--peer=10.0.0.2,10.0.0.3",
"127.0.0.1",
]);
assert_eq!(
cli.unwrap(),
Cli {
bind: Some(Ipv4Addr::new(10, 0, 0, 1)),
jobs: NonZeroU8::new(3).unwrap(),
peer: vec![Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(10, 0, 0, 3)],
host: Ipv4Addr::LOCALHOST,
}
);
let cli = with_env(&[("PEERS", "10.0.0.9")], || Cli::parse_from(["1.2.3.4"]));
assert_eq!(cli.unwrap().peer, [Ipv4Addr::new(10, 0, 0, 9)]);
}
#[test]
fn its_error_is_the_message() {
let error = Cli::parse_from(["--jobs", "0", "127.0.0.1"]).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidValue);
assert!(error.to_string().contains("--jobs"), "{error}");
let error = Cli::parse_from(["localhost"]).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidValue);
}
}