feat: Usage parser macro

This is intended to replace the runtime usage parser and is not meant to
be a complete API in of itself, like `clap_app!`.  What is in scope is
everything that visually makes sense as in a usage string (see
[docopt](http://docopt.org/) for inspiration).  General setting of
attributes is out of scope.

This deviates from both `clap_app` and the runtime usage parser
- `clap_app` supported multiple values but has a bug because we made
  `Arg::value_name` non-appending, so we aren't supporting this yet
- We do not yet support optional flags that take a value
- In both, `...` is multiple occurrences and values while its only
  multiple occurrences for us
- We explicitly support optional values for flags
- Unlike `clap_app`, our name is optional
- Unlike runtime usage parser, our name syntax is simpler
- Unlike runtime usage parser, our name syntax does not allow modifiers

Its more limited than I would like.  Hopefully some people better with
macros can expand the feature set and turn more runtime errors into
compile-time errors.

This is to prepare for deprecating the runtime usage parser (#8).
This commit is contained in:
Ed Page 2021-11-19 10:19:18 -06:00
parent 136c8e229d
commit 263fe30568
2 changed files with 476 additions and 0 deletions

View file

@ -175,6 +175,300 @@ macro_rules! app_from_crate {
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! arg_impl {
( @string $val:ident ) => {
stringify!($val)
};
( @string $val:literal ) => {{
let ident_or_string_literal: &str = $val;
ident_or_string_literal
}};
( @string $val:tt ) => {
::std::compile_error!("Only identifiers or string literals supported");
};
( @string ) => {
None
};
( @char $val:ident ) => {{
let ident_or_char_literal = stringify!($val);
debug_assert_eq!(
ident_or_char_literal.len(),
1,
"Single-letter identifier expected, got {}",
ident_or_char_literal
);
ident_or_char_literal.chars().next().unwrap()
}};
( @char $val:literal ) => {{
let ident_or_char_literal: char = $val;
ident_or_char_literal
}};
( @char ) => {{
None
}};
(
@arg
($arg:expr)
--$long:ident
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert_eq!($arg.get_value_names(), None, "Flags should precede values");
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Flags should precede `...`");
let mut arg = $arg;
let long = $crate::arg_impl! { @string $long };
if arg.get_name().is_empty() {
arg = arg.name(long);
}
arg.long(long)
})
$($tail)*
}
};
(
@arg
($arg:expr)
--$long:literal
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert_eq!($arg.get_value_names(), None, "Flags should precede values");
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Flags should precede `...`");
let mut arg = $arg;
let long = $crate::arg_impl! { @string $long };
if arg.get_name().is_empty() {
arg = arg.name(long);
}
arg.long(long)
})
$($tail)*
}
};
(
@arg
($arg:expr)
-$short:ident
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert_eq!($arg.get_long(), None, "Short flags should precede long flags");
debug_assert_eq!($arg.get_value_names(), None, "Flags should precede values");
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Flags should precede `...`");
$arg.short($crate::arg_impl! { @char $short })
})
$($tail)*
}
};
(
@arg
($arg:expr)
-$short:literal
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert_eq!($arg.get_long(), None, "Short flags should precede long flags");
debug_assert_eq!($arg.get_value_names(), None, "Flags should precede values");
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Flags should precede `...`");
$arg.short($crate::arg_impl! { @char $short })
})
$($tail)*
}
};
(
@arg
($arg:expr)
<$value_name:ident>
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Values should precede `...`");
debug_assert_eq!($arg.get_value_names(), None, "Multiple values not yet supported");
let mut arg = $arg;
arg = arg.required(true);
arg = arg.takes_value(true);
let value_name = $crate::arg_impl! { @string $value_name };
if arg.get_name().is_empty() {
arg = arg.name(value_name);
}
arg.value_name(value_name)
})
$($tail)*
}
};
(
@arg
($arg:expr)
[$value_name:ident]
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
debug_assert!(!$arg.is_set($crate::ArgSettings::MultipleOccurrences), "Values should precede `...`");
debug_assert_eq!($arg.get_value_names(), None, "Multiple values not yet supported");
let mut arg = $arg;
if arg.get_long().is_none() && arg.get_short().is_none() {
arg = arg.required(false);
} else {
arg = arg.min_values(0).max_values(1);
}
arg = arg.takes_value(true);
let value_name = $crate::arg_impl! { @string $value_name };
if arg.get_name().is_empty() {
arg = arg.name(value_name);
}
arg.value_name(value_name)
})
$($tail)*
}
};
(
@arg
($arg:expr)
...
$($tail:tt)*
) => {
$crate::arg_impl! {
@arg
({
$arg.multiple_occurrences(true)
})
$($tail)*
}
};
(
@arg
($arg:expr)
$help:literal
) => {
$arg.help($help)
};
(
@arg
($arg:expr)
) => {
$arg
};
}
/// Create an [`Arg`] from a usage string.
///
/// Allows creation of basic settings for the [`Arg`].
///
/// **NOTE**: Not all settings may be set using the usage string method. Some properties are
/// only available via the builder pattern.
///
/// # Syntax
///
/// Usage strings typically following the form:
///
/// ```notrust
/// [explicit name] [short] [long] [value names] [...] [help string]
/// ```
///
/// ### Explicit Name
///
/// The name may be either a bare-word or a string, followed by a `:`, like `name:` or
/// `"name":`.
///
/// *Note:* This is an optional field, if it's omitted the argument will use one of the additional
/// fields as the name using the following priority order:
///
/// 1. Explicit Name
/// 2. Long
/// 3. Value Name
///
/// See [`Arg::name`][clap::Arg::name].
///
/// ### Short
///
/// A short flag is a `-` followed by either a bare-character or quoted character, like `-f` or
/// `-'f'`.
///
/// See [`Arg::short`][clap::Arg::sort].
///
/// ### Long
///
/// A long flag is a `--` followed by either a bare-word or a string, like `--foo` or
/// `--"foo"`.
///
/// See [`Arg::long`][clap::Arg::long].
///
/// ### Values (Value Notation)
///
/// This is set by placing bare-word between:
/// - `[]` like `[FOO]`
/// - Positional argument: optional
/// - Named argument: optional value
/// - `<>` like `<FOO>`: required
///
/// See [`Arg::value_name`][clap::Arg::value_name].
///
/// ### `...`
///
/// `...` (three consecutive dots/periods) specifies that this argument may occur multiple
/// times (not to be confused with multiple values per occurrence).
///
/// See [`Arg::multiple_occurrences`][clap::Arg::multiple_occurrences].
///
/// ### Help String
///
/// The help string is denoted between a pair of single quotes `''` and may contain any
/// characters.
///
/// # Examples
///
/// ```rust
/// # use clap::{App, Arg, arg};
/// App::new("prog")
/// .args(&[
/// arg!(--config <FILE> "a required file for the configuration and no short"),
/// arg!(-d --debug ... "turns on debugging information and allows multiples"),
/// arg!([input] "an optional input file to use")
/// ])
/// # ;
/// ```
/// [`Arg`]: ./struct.Arg.html
#[macro_export]
macro_rules! arg {
( $name:ident: $($tail:tt)+ ) => {
$crate::arg_impl! {
@arg ($crate::Arg::new($crate::arg_impl! { @string $name })) $($tail)+
}
};
( $($tail:tt)+ ) => {{
let arg = $crate::arg_impl! {
@arg ($crate::Arg::default()) $($tail)+
};
debug_assert!(!arg.get_name().is_empty(), "Without a value or long flag, the `name:` prefix is required");
arg
}};
}
/// Deprecated, see `clap::Parser` for a declarative API (Issue clap-rs/clap#2835)
#[deprecated(
since = "3.0.0",

View file

@ -517,3 +517,185 @@ fn validator() {
assert_eq!(matches.value_of_t::<u16>("func1").ok(), Some(34));
assert_eq!(matches.value_of_t::<u16>("func2").ok(), Some(56));
}
mod arg {
#[test]
fn name_explicit() {
let arg = clap::arg!(foo: --bar <NUM>);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_long(), Some("bar"));
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(arg.is_set(clap::ArgSettings::Required));
}
#[test]
fn name_from_long() {
let arg = clap::arg!(--bar <NUM>);
assert_eq!(arg.get_name(), "bar");
assert_eq!(arg.get_long(), Some("bar"));
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(arg.is_set(clap::ArgSettings::Required));
}
#[test]
fn name_from_value() {
let arg = clap::arg!(<NUM>);
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_long(), None);
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(arg.is_set(clap::ArgSettings::Required));
}
#[test]
#[should_panic]
fn name_none_fails() {
clap::arg!("Help");
}
#[test]
#[should_panic]
fn short_only_fails() {
clap::arg!(-b);
}
#[test]
fn short() {
let arg = clap::arg!(foo: -b);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -'b');
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -b ...);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_short(), Some('b'));
assert!(arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -b "How to use it");
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), Some("How to use it"));
}
#[test]
fn short_and_long() {
let arg = clap::arg!(foo: -b --hello);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_long(), Some("hello"));
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -'b' --hello);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_long(), Some("hello"));
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -b --hello ...);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_long(), Some("hello"));
assert_eq!(arg.get_short(), Some('b'));
assert!(arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: -b --hello "How to use it");
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_long(), Some("hello"));
assert_eq!(arg.get_short(), Some('b'));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), Some("How to use it"));
}
#[test]
fn positional() {
let arg = clap::arg!(<NUM>);
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!([NUM]);
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(!arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(<NUM>);
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(foo: <NUM>);
assert_eq!(arg.get_name(), "foo");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(<NUM> ...);
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), None);
let arg = clap::arg!(<NUM> "How to use it");
assert_eq!(arg.get_name(), "NUM");
assert_eq!(arg.get_value_names(), Some(vec!["NUM"].as_slice()));
assert!(!arg.is_set(clap::ArgSettings::MultipleOccurrences));
assert!(arg.is_set(clap::ArgSettings::Required));
assert_eq!(arg.get_help(), Some("How to use it"));
}
}
mod arg_impl {
#[test]
fn string_ident() {
let expected = "one";
let actual = clap::arg_impl! { @string one };
assert_eq!(actual, expected);
}
#[test]
fn string_literal() {
let expected = "one";
let actual = clap::arg_impl! { @string "one" };
assert_eq!(actual, expected);
}
#[test]
fn char_ident() {
let expected = 'o';
let actual = clap::arg_impl! { @char o };
assert_eq!(actual, expected);
}
#[test]
fn char_literal() {
let expected = 'o';
let actual = clap::arg_impl! { @char 'o' };
assert_eq!(actual, expected);
}
}