aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Vander Stoep <jeffv@google.com>2022-12-05 15:48:43 +0100
committerJeff Vander Stoep <jeffv@google.com>2022-12-05 15:54:36 +0100
commit695f3045d96c31f6dd705afb43be52f3b9ef9b23 (patch)
tree9c38bf5cadc25895c2512aebc854ed88f86e3953
parent373aa013134e5f330ea9e744d479f591270e71f0 (diff)
downloadargh_derive-695f3045d96c31f6dd705afb43be52f3b9ef9b23.tar.gz
Upgrade argh_derive to 0.1.9
Test: atest Change-Id: I3064e06a015d329e1f51b734ebf1b21697275a78
-rw-r--r--Android.bp4
-rw-r--r--Cargo.toml13
-rw-r--r--Cargo.toml.orig6
-rw-r--r--METADATA8
-rw-r--r--README.md13
-rw-r--r--cargo2android.json3
-rw-r--r--patches/0001-Use-libheck-0.3.3.patch28
-rw-r--r--src/errors.rs5
-rw-r--r--src/help.rs38
-rw-r--r--src/lib.rs225
-rw-r--r--src/parse_attrs.rs131
11 files changed, 324 insertions, 150 deletions
diff --git a/Android.bp b/Android.bp
index b8a4514..9648d44 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22,12 +22,12 @@ rust_proc_macro {
name: "libargh_derive",
crate_name: "argh_derive",
cargo_env_compat: true,
- cargo_pkg_version: "0.1.7",
+ cargo_pkg_version: "0.1.9",
srcs: ["src/lib.rs"],
edition: "2018",
rustlibs: [
"libargh_shared",
- "libheck_deprecated",
+ "libheck",
"libproc_macro2",
"libquote",
"libsyn",
diff --git a/Cargo.toml b/Cargo.toml
index 82f7d44..bf0ee93 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,8 +12,12 @@
[package]
edition = "2018"
name = "argh_derive"
-version = "0.1.7"
-authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"]
+version = "0.1.9"
+authors = [
+ "Taylor Cramer <cramertj@google.com>",
+ "Benjamin Brittain <bwb@google.com>",
+ "Erick Tryzelaar <etryzelaar@google.com>",
+]
description = "Derive-based argument parsing optimized for code size"
readme = "README.md"
license = "BSD-3-Clause"
@@ -21,11 +25,12 @@ repository = "https://github.com/google/argh"
[lib]
proc-macro = true
+
[dependencies.argh_shared]
-version = "0.1.7"
+version = "0.1.9"
[dependencies.heck]
-version = "0.3.1"
+version = "0.4"
[dependencies.proc-macro2]
version = "1.0"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index b74329d..f6614e9 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -1,6 +1,6 @@
[package]
name = "argh_derive"
-version = "0.1.7"
+version = "0.1.9"
authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"]
edition = "2018"
license = "BSD-3-Clause"
@@ -12,8 +12,8 @@ readme = "README.md"
proc-macro = true
[dependencies]
-heck = "0.3.1"
+heck = "0.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = "1.0"
-argh_shared = { version = "0.1.7", path = "../argh_shared" }
+argh_shared = { version = "0.1.9", path = "../argh_shared" }
diff --git a/METADATA b/METADATA
index fb0a64d..c0071c8 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://static.crates.io/crates/argh_derive/argh_derive-0.1.7.crate"
+ value: "https://static.crates.io/crates/argh_derive/argh_derive-0.1.9.crate"
}
- version: "0.1.7"
+ version: "0.1.9"
license_type: NOTICE
last_upgrade_date {
year: 2022
- month: 1
- day: 13
+ month: 12
+ day: 5
}
}
diff --git a/README.md b/README.md
index 4e949e4..7368162 100644
--- a/README.md
+++ b/README.md
@@ -175,3 +175,16 @@ struct SubCommandTwo {
```
NOTE: This is not an officially supported Google product.
+
+
+## How to debug the expanded derive macro for `argh`
+
+The `argh::FromArgs` derive macro can be debugged with the [cargo-expand](https://crates.io/crates/cargo-expand) crate.
+
+### Expand the derive macro in `examples/simple_example.rs`
+
+See [argh/examples/simple_example.rs](./argh/examples/simple_example.rs) for the example struct we wish to expand.
+
+First, install `cargo-expand` by running `cargo install cargo-expand`. Note this requires the nightly build of Rust.
+
+Once installed, run `cargo expand` with in the `argh` package and you can see the expanded code.
diff --git a/cargo2android.json b/cargo2android.json
index 4456e1f..fe43c3f 100644
--- a/cargo2android.json
+++ b/cargo2android.json
@@ -1,5 +1,4 @@
{
"run": true,
- "device": true,
- "patch": "patches/0001-Use-libheck-0.3.3.patch"
+ "device": true
}
diff --git a/patches/0001-Use-libheck-0.3.3.patch b/patches/0001-Use-libheck-0.3.3.patch
deleted file mode 100644
index e43b4c0..0000000
--- a/patches/0001-Use-libheck-0.3.3.patch
+++ /dev/null
@@ -1,28 +0,0 @@
-From 5dd441921a3868023533fa6818b425660232cdad Mon Sep 17 00:00:00 2001
-From: Matthew Maurer <mmaurer@google.com>
-Date: Wed, 7 Sep 2022 16:10:32 -0700
-Subject: [PATCH] Use libheck 0.3.3
-
-Upstream has not yet migrated to 0.4.
-
-Change-Id: I0b7ccbb4b33ae77c88da6697562d0b7e8582e728
----
- Android.bp | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/Android.bp b/Android.bp
-index 1d3fdfe..54e460b 100644
---- a/Android.bp
-+++ b/Android.bp
-@@ -29,7 +29,7 @@ rust_proc_macro {
- edition: "2018",
- rustlibs: [
- "libargh_shared",
-- "libheck",
-+ "libheck_deprecated",
- "libproc_macro2",
- "libquote",
- "libsyn",
---
-2.37.2.789.g6183377224-goog
-
diff --git a/src/errors.rs b/src/errors.rs
index a5b69e6..cfad383 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -146,6 +146,11 @@ impl Errors {
self.push(syn::Error::new(span, msg));
}
+ /// Issue an error spanning over the given syntax tree node.
+ pub fn err_span_tokens<T: ToTokens>(&self, tokens: T, msg: &str) {
+ self.push(syn::Error::new_spanned(tokens, msg));
+ }
+
/// Push a `syn::Error` onto the list of errors to issue.
pub fn push(&self, err: syn::Error) {
self.errors.borrow_mut().push(err);
diff --git a/src/help.rs b/src/help.rs
index 5bf02b1..c9daad5 100644
--- a/src/help.rs
+++ b/src/help.rs
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+use std::fmt::Write;
use {
crate::{
errors::Errors,
@@ -26,9 +27,11 @@ pub(crate) fn help(
fields: &[StructField<'_>],
subcommand: Option<&StructField<'_>>,
) -> TokenStream {
+ #![allow(clippy::format_push_string)]
let mut format_lit = "Usage: {command_name}".to_string();
- let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional);
+ let positional =
+ fields.iter().filter(|f| f.kind == FieldKind::Positional && f.attrs.greedy.is_none());
let mut has_positional = false;
for arg in positional.clone() {
has_positional = true;
@@ -42,6 +45,13 @@ pub(crate) fn help(
option_usage(&mut format_lit, option);
}
+ let remain =
+ fields.iter().filter(|f| f.kind == FieldKind::Positional && f.attrs.greedy.is_some());
+ for arg in remain {
+ format_lit.push(' ');
+ positional_usage(&mut format_lit, arg);
+ }
+
if let Some(subcommand) = subcommand {
format_lit.push(' ');
if !subcommand.optionality.is_required() {
@@ -85,6 +95,12 @@ pub(crate) fn help(
subcommand_calculation = quote! {
let subcommands = argh::print_subcommands(
<#subcommand_ty as argh::SubCommands>::COMMANDS
+ .iter()
+ .copied()
+ .chain(
+ <#subcommand_ty as argh::SubCommands>::dynamic_commands()
+ .iter()
+ .copied())
);
};
} else {
@@ -96,17 +112,17 @@ pub(crate) fn help(
lits_section(&mut format_lit, "Notes:", &ty_attrs.notes);
- if ty_attrs.error_codes.len() != 0 {
+ if !ty_attrs.error_codes.is_empty() {
format_lit.push_str(SECTION_SEPARATOR);
format_lit.push_str("Error codes:");
for (code, text) in &ty_attrs.error_codes {
format_lit.push('\n');
format_lit.push_str(INDENT);
- format_lit.push_str(&format!("{} {}", code, text.value()));
+ write!(format_lit, "{} {}", code, text.value()).unwrap();
}
}
- format_lit.push_str("\n");
+ format_lit.push('\n');
quote! { {
#subcommand_calculation
@@ -116,7 +132,7 @@ pub(crate) fn help(
/// A section composed of exactly just the literals provided to the program.
fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) {
- if lits.len() != 0 {
+ if !lits.is_empty() {
out.push_str(SECTION_SEPARATOR);
out.push_str(heading);
for lit in lits {
@@ -135,13 +151,17 @@ fn positional_usage(out: &mut String, field: &StructField<'_>) {
if !field.optionality.is_required() {
out.push('[');
}
- out.push('<');
+ if field.attrs.greedy.is_none() {
+ out.push('<');
+ }
let name = field.arg_name();
out.push_str(&name);
if field.optionality == Optionality::Repeating {
out.push_str("...");
}
- out.push('>');
+ if field.attrs.greedy.is_none() {
+ out.push('>');
+ }
if !field.optionality.is_required() {
out.push(']');
}
@@ -219,7 +239,7 @@ fn positional_description(out: &mut String, field: &StructField<'_>) {
}
fn positional_description_format(out: &mut String, name: &str, description: &str) {
- let info = argh_shared::CommandInfo { name: &*name, description };
+ let info = argh_shared::CommandInfo { name, description };
argh_shared::write_description(out, &info);
}
@@ -249,6 +269,6 @@ fn option_description_format(
}
name.push_str(long_with_leading_dashes);
- let info = argh_shared::CommandInfo { name: &*name, description };
+ let info = argh_shared::CommandInfo { name: &name, description };
argh_shared::write_description(out, &info);
}
diff --git a/src/lib.rs b/src/lib.rs
index a123d4e..49a6c17 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,9 +13,10 @@ use {
errors::Errors,
parse_attrs::{FieldAttrs, FieldKind, TypeAttrs},
},
+ heck::ToKebabCase,
proc_macro2::{Span, TokenStream},
quote::{quote, quote_spanned, ToTokens},
- std::str::FromStr,
+ std::{collections::HashMap, str::FromStr},
syn::{spanned::Spanned, LitStr},
};
@@ -35,16 +36,14 @@ pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
/// as well as all errors that occurred.
fn impl_from_args(input: &syn::DeriveInput) -> TokenStream {
let errors = &Errors::default();
- if input.generics.params.len() != 0 {
- errors.err(
- &input.generics,
- "`#![derive(FromArgs)]` cannot be applied to types with generic parameters",
- );
- }
let type_attrs = &TypeAttrs::parse(errors, input);
let mut output_tokens = match &input.data {
- syn::Data::Struct(ds) => impl_from_args_struct(errors, &input.ident, type_attrs, ds),
- syn::Data::Enum(de) => impl_from_args_enum(errors, &input.ident, type_attrs, de),
+ syn::Data::Struct(ds) => {
+ impl_from_args_struct(errors, &input.ident, type_attrs, &input.generics, ds)
+ }
+ syn::Data::Enum(de) => {
+ impl_from_args_enum(errors, &input.ident, type_attrs, &input.generics, de)
+ }
syn::Data::Union(_) => {
errors.err(input, "`#[derive(FromArgs)]` cannot be applied to unions");
TokenStream::new()
@@ -65,22 +64,15 @@ enum Optionality {
impl PartialEq<Optionality> for Optionality {
fn eq(&self, other: &Optionality) -> bool {
use Optionality::*;
- match (self, other) {
- (None, None) | (Optional, Optional) | (Repeating, Repeating) => true,
- // NB: (Defaulted, Defaulted) can't contain the same token streams
- _ => false,
- }
+ // NB: (Defaulted, Defaulted) can't contain the same token streams
+ matches!((self, other), (Optional, Optional) | (Repeating, Repeating))
}
}
impl Optionality {
/// Whether or not this is `Optionality::None`
fn is_required(&self) -> bool {
- if let Optionality::None = self {
- true
- } else {
- false
- }
+ matches!(self, Optionality::None)
}
}
@@ -125,7 +117,7 @@ impl<'a> StructField<'a> {
field,
concat!(
"Missing `argh` field kind attribute.\n",
- "Expected one of: `switch`, `option`, `subcommand`, `positional`",
+ "Expected one of: `switch`, `option`, `remaining`, `subcommand`, `positional`",
),
);
return None;
@@ -191,7 +183,7 @@ impl<'a> StructField<'a> {
.long
.as_ref()
.map(syn::LitStr::value)
- .unwrap_or_else(|| heck::KebabCase::to_kebab_case(&*name.to_string()));
+ .unwrap_or_else(|| name.to_string().to_kebab_case());
if long_name == "help" {
errors.err(field, "Custom `--help` flags are not supported.");
}
@@ -214,6 +206,7 @@ fn impl_from_args_struct(
errors: &Errors,
name: &syn::Ident,
type_attrs: &TypeAttrs,
+ generic_args: &syn::Generics,
ds: &syn::DataStruct,
) -> TokenStream {
let fields = match &ds.fields {
@@ -240,6 +233,7 @@ fn impl_from_args_struct(
})
.collect();
+ ensure_unique_names(errors, &fields);
ensure_only_last_positional_is_optional(errors, &fields);
let impl_span = Span::call_site();
@@ -249,10 +243,12 @@ fn impl_from_args_struct(
let redact_arg_values_method =
impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields);
- let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs);
+ let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs, generic_args);
+ let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
let trait_impl = quote_spanned! { impl_span =>
- impl argh::FromArgs for #name {
+ #[automatically_derived]
+ impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause {
#from_args_method
#redact_arg_values_method
@@ -269,8 +265,8 @@ fn impl_from_args_struct_from_args<'a>(
type_attrs: &TypeAttrs,
fields: &'a [StructField<'a>],
) -> TokenStream {
- let init_fields = declare_local_storage_for_from_args_fields(&fields);
- let unwrap_fields = unwrap_from_args_fields(&fields);
+ let init_fields = declare_local_storage_for_from_args_fields(fields);
+ let unwrap_fields = unwrap_from_args_fields(fields);
let positional_fields: Vec<&StructField<'_>> =
fields.iter().filter(|field| field.kind == FieldKind::Positional).collect();
let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident);
@@ -279,6 +275,10 @@ fn impl_from_args_struct_from_args<'a>(
.last()
.map(|field| field.optionality == Optionality::Repeating)
.unwrap_or(false);
+ let last_positional_is_greedy = positional_fields
+ .last()
+ .map(|field| field.kind == FieldKind::Positional && field.attrs.greedy.is_some())
+ .unwrap_or(false);
let flag_output_table = fields.iter().filter_map(|field| {
let field_name = &field.field.ident;
@@ -289,13 +289,13 @@ fn impl_from_args_struct_from_args<'a>(
}
});
- let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields);
+ let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(fields);
let mut subcommands_iter =
fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse();
let subcommand: Option<&StructField<'_>> = subcommands_iter.next();
- while let Some(dup_subcommand) = subcommands_iter.next() {
+ for dup_subcommand in subcommands_iter {
errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field);
}
@@ -304,7 +304,7 @@ fn impl_from_args_struct_from_args<'a>(
let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span);
let append_missing_requirements =
- append_missing_requirements(&missing_requirements_ident, &fields);
+ append_missing_requirements(&missing_requirements_ident, fields);
let parse_subcommands = if let Some(subcommand) = subcommand {
let name = subcommand.name;
@@ -312,6 +312,7 @@ fn impl_from_args_struct_from_args<'a>(
quote_spanned! { impl_span =>
Some(argh::ParseStructSubCommand {
subcommands: <#ty as argh::SubCommands>::COMMANDS,
+ dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(),
parse_func: &mut |__command, __remaining_args| {
#name = Some(<#ty as argh::FromArgs>::from_args(__command, __remaining_args)?);
Ok(())
@@ -324,12 +325,14 @@ fn impl_from_args_struct_from_args<'a>(
// Identifier referring to a value containing the name of the current command as an `&[&str]`.
let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span);
- let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand);
+ let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand);
let method_impl = quote_spanned! { impl_span =>
fn from_args(__cmd_name: &[&str], __args: &[&str])
-> std::result::Result<Self, argh::EarlyExit>
{
+ #![allow(clippy::unwrap_in_result)]
+
#( #init_fields )*
argh::parse_struct_args(
@@ -349,6 +352,7 @@ fn impl_from_args_struct_from_args<'a>(
)*
],
last_is_repeating: #last_positional_is_repeating,
+ last_is_greedy: #last_positional_is_greedy,
},
#parse_subcommands,
&|| #help,
@@ -374,8 +378,8 @@ fn impl_from_args_struct_redact_arg_values<'a>(
type_attrs: &TypeAttrs,
fields: &'a [StructField<'a>],
) -> TokenStream {
- let init_fields = declare_local_storage_for_redacted_fields(&fields);
- let unwrap_fields = unwrap_redacted_fields(&fields);
+ let init_fields = declare_local_storage_for_redacted_fields(fields);
+ let unwrap_fields = unwrap_redacted_fields(fields);
let positional_fields: Vec<&StructField<'_>> =
fields.iter().filter(|field| field.kind == FieldKind::Positional).collect();
@@ -385,6 +389,10 @@ fn impl_from_args_struct_redact_arg_values<'a>(
.last()
.map(|field| field.optionality == Optionality::Repeating)
.unwrap_or(false);
+ let last_positional_is_greedy = positional_fields
+ .last()
+ .map(|field| field.kind == FieldKind::Positional && field.attrs.greedy.is_some())
+ .unwrap_or(false);
let flag_output_table = fields.iter().filter_map(|field| {
let field_name = &field.field.ident;
@@ -395,13 +403,13 @@ fn impl_from_args_struct_redact_arg_values<'a>(
}
});
- let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields);
+ let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(fields);
let mut subcommands_iter =
fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse();
let subcommand: Option<&StructField<'_>> = subcommands_iter.next();
- while let Some(dup_subcommand) = subcommands_iter.next() {
+ for dup_subcommand in subcommands_iter {
errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field);
}
@@ -410,7 +418,7 @@ fn impl_from_args_struct_redact_arg_values<'a>(
let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span);
let append_missing_requirements =
- append_missing_requirements(&missing_requirements_ident, &fields);
+ append_missing_requirements(&missing_requirements_ident, fields);
let redact_subcommands = if let Some(subcommand) = subcommand {
let name = subcommand.name;
@@ -418,6 +426,7 @@ fn impl_from_args_struct_redact_arg_values<'a>(
quote_spanned! { impl_span =>
Some(argh::ParseStructSubCommand {
subcommands: <#ty as argh::SubCommands>::COMMANDS,
+ dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(),
parse_func: &mut |__command, __remaining_args| {
#name = Some(<#ty as argh::FromArgs>::redact_arg_values(__command, __remaining_args)?);
Ok(())
@@ -428,15 +437,15 @@ fn impl_from_args_struct_redact_arg_values<'a>(
quote_spanned! { impl_span => None }
};
- let cmd_name = if type_attrs.is_subcommand.is_none() {
- quote! { __cmd_name.last().expect("no command name").to_string() }
+ let unwrap_cmd_name_err_string = if type_attrs.is_subcommand.is_none() {
+ quote! { "no command name" }
} else {
- quote! { __cmd_name.last().expect("no subcommand name").to_string() }
+ quote! { "no subcommand name" }
};
// Identifier referring to a value containing the name of the current command as an `&[&str]`.
let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span);
- let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand);
+ let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand);
let method_impl = quote_spanned! { impl_span =>
fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> {
@@ -459,6 +468,7 @@ fn impl_from_args_struct_redact_arg_values<'a>(
)*
],
last_is_repeating: #last_positional_is_repeating,
+ last_is_greedy: #last_positional_is_greedy,
},
#redact_subcommands,
&|| #help,
@@ -471,7 +481,11 @@ fn impl_from_args_struct_redact_arg_values<'a>(
#missing_requirements_ident.err_on_any()?;
let mut __redacted = vec![
- #cmd_name,
+ if let Some(cmd_name) = __cmd_name.last() {
+ (*cmd_name).to_owned()
+ } else {
+ return Err(argh::EarlyExit::from(#unwrap_cmd_name_err_string.to_owned()));
+ }
];
#( #unwrap_fields )*
@@ -503,14 +517,54 @@ fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructFiel
}
}
+/// Ensures that only one short or long name is used.
+fn ensure_unique_names(errors: &Errors, fields: &[StructField<'_>]) {
+ let mut seen_short_names = HashMap::new();
+ let mut seen_long_names = HashMap::new();
+
+ for field in fields {
+ if let Some(short_name) = &field.attrs.short {
+ let short_name = short_name.value();
+ if let Some(first_use_field) = seen_short_names.get(&short_name) {
+ errors.err_span_tokens(
+ first_use_field,
+ &format!("The short name of \"-{}\" was already used here.", short_name),
+ );
+ errors.err_span_tokens(&field.field, "Later usage here.");
+ }
+
+ seen_short_names.insert(short_name, &field.field);
+ }
+
+ if let Some(long_name) = &field.long_name {
+ if let Some(first_use_field) = seen_long_names.get(&long_name) {
+ errors.err_span_tokens(
+ *first_use_field,
+ &format!("The long name of \"{}\" was already used here.", long_name),
+ );
+ errors.err_span_tokens(&field.field, "Later usage here.");
+ }
+
+ seen_long_names.insert(long_name, field.field);
+ }
+ }
+}
+
/// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate.
-fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs) -> TokenStream {
+fn top_or_sub_cmd_impl(
+ errors: &Errors,
+ name: &syn::Ident,
+ type_attrs: &TypeAttrs,
+ generic_args: &syn::Generics,
+) -> TokenStream {
let description =
help::require_description(errors, name.span(), &type_attrs.description, "type");
+ let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
if type_attrs.is_subcommand.is_none() {
// Not a subcommand
quote! {
- impl argh::TopLevelCommand for #name {}
+ #[automatically_derived]
+ impl #impl_generics argh::TopLevelCommand for #name #ty_generics #where_clause {}
}
} else {
let empty_str = syn::LitStr::new("", Span::call_site());
@@ -519,7 +573,8 @@ fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttr
&empty_str
});
quote! {
- impl argh::SubCommand for #name {
+ #[automatically_derived]
+ impl #impl_generics argh::SubCommand for #name #ty_generics #where_clause {
const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo {
name: #subcommand_name,
description: #description,
@@ -586,7 +641,9 @@ fn unwrap_from_args_fields<'a>(
let field_name = field.name;
match field.kind {
FieldKind::Option | FieldKind::Positional => match &field.optionality {
- Optionality::None => quote! { #field_name: #field_name.slot.unwrap() },
+ Optionality::None => quote! {
+ #field_name: #field_name.slot.unwrap()
+ },
Optionality::Optional | Optionality::Repeating => {
quote! { #field_name: #field_name.slot }
}
@@ -639,7 +696,7 @@ fn declare_local_storage_for_redacted_fields<'a>(
let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> =
argh::ParseValueSlotTy {
slot: std::default::Default::default(),
- parse_func: |arg, _| { Ok(arg.to_string()) },
+ parse_func: |arg, _| { Ok(arg.to_owned()) },
};
}
}
@@ -658,7 +715,7 @@ fn declare_local_storage_for_redacted_fields<'a>(
let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> =
argh::ParseValueSlotTy {
slot: std::default::Default::default(),
- parse_func: |_, _| { Ok(#arg_name.to_string()) },
+ parse_func: |_, _| { Ok(#arg_name.to_owned()) },
};
}
}
@@ -765,7 +822,14 @@ fn append_missing_requirements<'a>(
quote! {
if #field_name.is_none() {
#mri.missing_subcommands(
- <#ty as argh::SubCommands>::COMMANDS,
+ <#ty as argh::SubCommands>::COMMANDS
+ .iter()
+ .cloned()
+ .chain(
+ <#ty as argh::SubCommands>::dynamic_commands()
+ .iter()
+ .copied()
+ ),
)
}
}
@@ -831,6 +895,7 @@ fn impl_from_args_enum(
errors: &Errors,
name: &syn::Ident,
type_attrs: &TypeAttrs,
+ generic_args: &syn::Generics,
de: &syn::DataEnum,
) -> TokenStream {
parse_attrs::check_enum_type_attrs(errors, type_attrs, &de.enum_token.span);
@@ -841,27 +906,66 @@ fn impl_from_args_enum(
ty: &'a syn::Type,
}
+ let mut dynamic_type_and_variant = None;
+
let variants: Vec<SubCommandVariant<'_>> = de
.variants
.iter()
.filter_map(|variant| {
- parse_attrs::check_enum_variant_attrs(errors, variant);
let name = &variant.ident;
let ty = enum_only_single_field_unnamed_variants(errors, &variant.fields)?;
- Some(SubCommandVariant { name, ty })
+ if parse_attrs::VariantAttrs::parse(errors, variant).is_dynamic.is_some() {
+ if dynamic_type_and_variant.is_some() {
+ errors.err(variant, "Only one variant can have the `dynamic` attribute");
+ }
+ dynamic_type_and_variant = Some((ty, name));
+ None
+ } else {
+ Some(SubCommandVariant { name, ty })
+ }
})
.collect();
let name_repeating = std::iter::repeat(name.clone());
let variant_ty = variants.iter().map(|x| x.ty).collect::<Vec<_>>();
let variant_names = variants.iter().map(|x| x.name).collect::<Vec<_>>();
+ let dynamic_from_args =
+ dynamic_type_and_variant.as_ref().map(|(dynamic_type, dynamic_variant)| {
+ quote! {
+ if let Some(result) = <#dynamic_type as argh::DynamicSubCommand>::try_from_args(
+ command_name, args) {
+ return result.map(#name::#dynamic_variant);
+ }
+ }
+ });
+ let dynamic_redact_arg_values = dynamic_type_and_variant.as_ref().map(|(dynamic_type, _)| {
+ quote! {
+ if let Some(result) = <#dynamic_type as argh::DynamicSubCommand>::try_redact_arg_values(
+ command_name, args) {
+ return result;
+ }
+ }
+ });
+ let dynamic_commands = dynamic_type_and_variant.as_ref().map(|(dynamic_type, _)| {
+ quote! {
+ fn dynamic_commands() -> &'static [&'static argh::CommandInfo] {
+ <#dynamic_type as argh::DynamicSubCommand>::commands()
+ }
+ }
+ });
+ let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
quote! {
- impl argh::FromArgs for #name {
+ impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause {
fn from_args(command_name: &[&str], args: &[&str])
-> std::result::Result<Self, argh::EarlyExit>
{
- let subcommand_name = *command_name.last().expect("no subcommand name");
+ let subcommand_name = if let Some(subcommand_name) = command_name.last() {
+ *subcommand_name
+ } else {
+ return Err(argh::EarlyExit::from("no subcommand name".to_owned()));
+ };
+
#(
if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name {
return Ok(#name_repeating::#variant_names(
@@ -869,24 +973,37 @@ fn impl_from_args_enum(
));
}
)*
- unreachable!("no subcommand matched")
+
+ #dynamic_from_args
+
+ Err(argh::EarlyExit::from("no subcommand matched".to_owned()))
}
fn redact_arg_values(command_name: &[&str], args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> {
- let subcommand_name = *command_name.last().expect("no subcommand name");
+ let subcommand_name = if let Some(subcommand_name) = command_name.last() {
+ *subcommand_name
+ } else {
+ return Err(argh::EarlyExit::from("no subcommand name".to_owned()));
+ };
+
#(
if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name {
return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args);
}
)*
- unreachable!("no subcommand matched")
+
+ #dynamic_redact_arg_values
+
+ Err(argh::EarlyExit::from("no subcommand matched".to_owned()))
}
}
- impl argh::SubCommands for #name {
+ impl #impl_generics argh::SubCommands for #name #ty_generics #where_clause {
const COMMANDS: &'static [&'static argh::CommandInfo] = &[#(
<#variant_ty as argh::SubCommand>::COMMAND,
)*];
+
+ #dynamic_commands
}
}
}
diff --git a/src/parse_attrs.rs b/src/parse_attrs.rs
index 83807ed..f5fa841 100644
--- a/src/parse_attrs.rs
+++ b/src/parse_attrs.rs
@@ -18,6 +18,7 @@ pub struct FieldAttrs {
pub long: Option<syn::LitStr>,
pub short: Option<syn::LitChar>,
pub arg_name: Option<syn::LitStr>,
+ pub greedy: Option<syn::Path>,
}
/// The purpose of a particular field on a `#![derive(FromArgs)]` struct.
@@ -79,29 +80,29 @@ impl FieldAttrs {
let name = meta.path();
if name.is_ident("arg_name") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_arg_name(errors, m);
}
} else if name.is_ident("default") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_default(errors, m);
}
} else if name.is_ident("description") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
parse_attr_description(errors, m, &mut this.description);
}
} else if name.is_ident("from_str_fn") {
- if let Some(m) = errors.expect_meta_list(&meta) {
+ if let Some(m) = errors.expect_meta_list(meta) {
this.parse_attr_from_str_fn(errors, m);
}
} else if name.is_ident("long") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_long(errors, m);
}
} else if name.is_ident("option") {
parse_attr_field_type(errors, meta, FieldKind::Option, &mut this.field_type);
} else if name.is_ident("short") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_short(errors, m);
}
} else if name.is_ident("subcommand") {
@@ -120,13 +121,15 @@ impl FieldAttrs {
FieldKind::Positional,
&mut this.field_type,
);
+ } else if name.is_ident("greedy") {
+ this.greedy = Some(name.clone());
} else {
errors.err(
&meta,
concat!(
"Invalid field-level `argh` attribute\n",
- "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `long`, ",
- "`option`, `short`, `subcommand`, `switch`",
+ "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `greedy`, ",
+ "`long`, `option`, `short`, `subcommand`, `switch`",
),
);
}
@@ -144,6 +147,16 @@ impl FieldAttrs {
}
}
+ match (&this.greedy, this.field_type.as_ref().map(|f| f.kind)) {
+ (Some(_), Some(FieldKind::Positional)) => {}
+ (Some(greedy), Some(_)) => errors.err(
+ &greedy,
+ "`greedy` may only be specified on `#[argh(positional)]` \
+ fields",
+ ),
+ _ => {}
+ }
+
if let Some(d) = &this.description {
check_option_description(errors, d.content.value().trim(), d.content.span());
}
@@ -170,7 +183,7 @@ impl FieldAttrs {
if !value.is_ascii() {
errors.err(long, "Long names must be ASCII");
}
- if !value.chars().all(|c| c.is_lowercase() || c == '-') {
+ if !value.chars().all(|c| c.is_lowercase() || c == '-' || c.is_ascii_digit()) {
errors.err(long, "Long names must be lowercase");
}
}
@@ -218,10 +231,8 @@ fn parse_attr_field_type(
if let Some(path) = errors.expect_meta_word(meta) {
if let Some(first) = slot {
errors.duplicate_attrs("field kind", &first.ident, path);
- } else {
- if let Some(word) = path.get_ident() {
- *slot = Some(FieldType { kind, ident: word.clone() });
- }
+ } else if let Some(word) = path.get_ident() {
+ *slot = Some(FieldType { kind, ident: word.clone() });
}
}
}
@@ -304,28 +315,27 @@ impl TypeAttrs {
let name = meta.path();
if name.is_ident("description") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
parse_attr_description(errors, m, &mut this.description);
}
} else if name.is_ident("error_code") {
- if let Some(m) = errors.expect_meta_list(&meta) {
+ if let Some(m) = errors.expect_meta_list(meta) {
this.parse_attr_error_code(errors, m);
}
} else if name.is_ident("example") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_example(errors, m);
}
} else if name.is_ident("name") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_name(errors, m);
}
} else if name.is_ident("note") {
- if let Some(m) = errors.expect_meta_name_value(&meta) {
+ if let Some(m) = errors.expect_meta_name_value(meta) {
this.parse_attr_note(errors, m);
}
} else if name.is_ident("subcommand") {
- if let Some(ident) = errors.expect_meta_word(&meta).and_then(|p| p.get_ident())
- {
+ if let Some(ident) = errors.expect_meta_word(meta).and_then(|p| p.get_ident()) {
this.parse_attr_subcommand(errors, ident);
}
} else {
@@ -421,6 +431,62 @@ impl TypeAttrs {
}
}
+/// Represents an enum variant's attributes.
+#[derive(Default)]
+pub struct VariantAttrs {
+ pub is_dynamic: Option<syn::Path>,
+}
+
+impl VariantAttrs {
+ /// Parse enum variant `#[argh(...)]` attributes
+ pub fn parse(errors: &Errors, variant: &syn::Variant) -> Self {
+ let mut this = VariantAttrs::default();
+
+ let fields = match &variant.fields {
+ syn::Fields::Named(fields) => Some(&fields.named),
+ syn::Fields::Unnamed(fields) => Some(&fields.unnamed),
+ syn::Fields::Unit => None,
+ };
+
+ for field in fields.into_iter().flatten() {
+ for attr in &field.attrs {
+ if is_argh_attr(attr) {
+ err_unused_enum_attr(errors, attr);
+ }
+ }
+ }
+
+ for attr in &variant.attrs {
+ let ml = if let Some(ml) = argh_attr_to_meta_list(errors, attr) {
+ ml
+ } else {
+ continue;
+ };
+
+ for meta in &ml.nested {
+ let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
+
+ let name = meta.path();
+ if name.is_ident("dynamic") {
+ if let Some(prev) = this.is_dynamic.as_ref() {
+ errors.duplicate_attrs("dynamic", prev, meta);
+ } else {
+ this.is_dynamic = errors.expect_meta_word(meta).cloned();
+ }
+ } else {
+ errors.err(
+ &meta,
+ "Invalid variant-level `argh` attribute\n\
+ Variants can only have the #[argh(dynamic)] attribute.",
+ );
+ }
+ }
+ }
+
+ this
+ }
+}
+
fn check_option_description(errors: &Errors, desc: &str, span: Span) {
let chars = &mut desc.trim().chars();
match (chars.next(), chars.next()) {
@@ -498,7 +564,7 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span:
// Ensure that `#[argh(subcommand)]` is present.
if is_subcommand.is_none() {
errors.err_span(
- type_span.clone(),
+ *type_span,
concat!(
"`#![derive(FromArgs)]` on `enum`s can only be used to enumerate subcommands.\n",
"Consider adding `#[argh(subcommand)]` to the `enum` declaration.",
@@ -526,29 +592,6 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span:
}
}
-/// Checks that an enum variant and its fields have no `#[argh(...)]` attributes.
-pub fn check_enum_variant_attrs(errors: &Errors, variant: &syn::Variant) {
- for attr in &variant.attrs {
- if is_argh_attr(attr) {
- err_unused_enum_attr(errors, attr);
- }
- }
-
- let fields = match &variant.fields {
- syn::Fields::Named(fields) => &fields.named,
- syn::Fields::Unnamed(fields) => &fields.unnamed,
- syn::Fields::Unit => return,
- };
-
- for field in fields {
- for attr in &field.attrs {
- if is_argh_attr(attr) {
- err_unused_enum_attr(errors, attr);
- }
- }
- }
-}
-
fn err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned) {
errors.err(
location,