From c752b5f50477e96a41cc6baf08ccd9f7e941f73e Mon Sep 17 00:00:00 2001 From: Benjamin Brittain Date: Thu, 23 Jan 2020 15:38:38 -0500 Subject: [PATCH] Migrate code into into stand-alone repo Adds Cargo.toml files --- .gitignore | 3 + docs/contributing.md => CONTRIBUTING.md | 0 Cargo.lock | 76 ++ Cargo.toml | 12 + README.md | 4 +- argh_derive/Cargo.toml | 18 + argh_derive/src/errors.rs | 161 ++++ argh_derive/src/help.rs | 219 +++++ argh_derive/src/lib.rs | 748 +++++++++++++++++ argh_derive/src/parse_attrs.rs | 552 +++++++++++++ argh_shared/Cargo.toml | 8 + argh_shared/src/lib.rs | 75 ++ docs/code-of-conduct.md => code-of-conduct.md | 0 src/lib.rs | 526 ++++++++++++ tests/lib.rs | 782 ++++++++++++++++++ 15 files changed, 3183 insertions(+), 1 deletion(-) create mode 100644 .gitignore rename docs/contributing.md => CONTRIBUTING.md (100%) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 argh_derive/Cargo.toml create mode 100644 argh_derive/src/errors.rs create mode 100644 argh_derive/src/help.rs create mode 100644 argh_derive/src/lib.rs create mode 100644 argh_derive/src/parse_attrs.rs create mode 100644 argh_shared/Cargo.toml create mode 100644 argh_shared/src/lib.rs rename docs/code-of-conduct.md => code-of-conduct.md (100%) create mode 100644 src/lib.rs create mode 100644 tests/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2f4b3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +*.swp diff --git a/docs/contributing.md b/CONTRIBUTING.md similarity index 100% rename from docs/contributing.md rename to CONTRIBUTING.md diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..010c5b7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "argh" +version = "0.1.0" +dependencies = [ + "argh_derive 0.1.0", + "argh_shared 0.1.0", +] + +[[package]] +name = "argh_derive" +version = "0.1.0" +dependencies = [ + "argh_shared 0.1.0", + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "argh_shared" +version = "0.1.0" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +"checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" +"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e326b8e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "argh" +version = "0.1.0" +authors = ["Taylor Cramer ", "Benjamin Brittain"] +edition = "2018" +license = "BSD License 2.0" +description = "Derive-based argument parser optimized for code size" +repository = "https://github.com/google/argh" + +[dependencies] +argh_shared = {path = "./argh_shared"} +argh_derive = {path = "./argh_derive"} diff --git a/README.md b/README.md index 68613dc..82d49f9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # Argh -**Argh is an opinionated Derive-based argument parsing optimized for code size** +**Argh is an opinionated Derive-based argument parser optimized for code size** + +NOTE: This is not an officially supported Google product. diff --git a/argh_derive/Cargo.toml b/argh_derive/Cargo.toml new file mode 100644 index 0000000..aa7207f --- /dev/null +++ b/argh_derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "argh_derive" +version = "0.1.0" +authors = ["Taylor Cramer ", "Benjamin Brittain"] +edition = "2018" +license = "BSD License 2.0" +description = "Derive-based argument parsing optimized for code size" +repository = "https://github.com/google/argh" + +[lib] +proc-macro = true + +[dependencies] +heck = "0.3.1" +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" +argh_shared = {path = "../argh_shared" } diff --git a/argh_derive/src/errors.rs b/argh_derive/src/errors.rs new file mode 100644 index 0000000..a5b69e6 --- /dev/null +++ b/argh_derive/src/errors.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use { + proc_macro2::{Span, TokenStream}, + quote::ToTokens, + std::cell::RefCell, +}; + +/// A type for collecting procedural macro errors. +#[derive(Default)] +pub struct Errors { + errors: RefCell>, +} + +/// Produce functions to expect particular variants of `syn::Lit` +macro_rules! expect_lit_fn { + ($(($fn_name:ident, $syn_type:ident, $variant:ident, $lit_name:literal),)*) => { + $( + pub fn $fn_name<'a>(&self, lit: &'a syn::Lit) -> Option<&'a syn::$syn_type> { + if let syn::Lit::$variant(inner) = lit { + Some(inner) + } else { + self.unexpected_lit($lit_name, lit); + None + } + } + )* + } +} + +/// Produce functions to expect particular variants of `syn::Meta` +macro_rules! expect_meta_fn { + ($(($fn_name:ident, $syn_type:ident, $variant:ident, $meta_name:literal),)*) => { + $( + pub fn $fn_name<'a>(&self, meta: &'a syn::Meta) -> Option<&'a syn::$syn_type> { + if let syn::Meta::$variant(inner) = meta { + Some(inner) + } else { + self.unexpected_meta($meta_name, meta); + None + } + } + )* + } +} + +impl Errors { + /// Issue an error like: + /// + /// Duplicate foo attribute + /// First foo attribute here + pub fn duplicate_attrs( + &self, + attr_kind: &str, + first: &impl syn::spanned::Spanned, + second: &impl syn::spanned::Spanned, + ) { + self.duplicate_attrs_inner(attr_kind, first.span(), second.span()) + } + + fn duplicate_attrs_inner(&self, attr_kind: &str, first: Span, second: Span) { + self.err_span(second, &["Duplicate ", attr_kind, " attribute"].concat()); + self.err_span(first, &["First ", attr_kind, " attribute here"].concat()); + } + + /// Error on literals, expecting attribute syntax. + pub fn expect_nested_meta<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Meta> { + match nm { + syn::NestedMeta::Lit(l) => { + self.err(l, "Unexpected literal"); + None + } + syn::NestedMeta::Meta(m) => Some(m), + } + } + + /// Error on attribute syntax, expecting literals + pub fn expect_nested_lit<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Lit> { + match nm { + syn::NestedMeta::Meta(m) => { + self.err(m, "Expected literal"); + None + } + syn::NestedMeta::Lit(l) => Some(l), + } + } + + expect_lit_fn![ + (expect_lit_str, LitStr, Str, "string"), + (expect_lit_char, LitChar, Char, "character"), + (expect_lit_int, LitInt, Int, "integer"), + ]; + + expect_meta_fn![ + (expect_meta_word, Path, Path, "path"), + (expect_meta_list, MetaList, List, "list"), + (expect_meta_name_value, MetaNameValue, NameValue, "name-value pair"), + ]; + + fn unexpected_lit(&self, expected: &str, found: &syn::Lit) { + fn lit_kind(lit: &syn::Lit) -> &'static str { + use syn::Lit::{Bool, Byte, ByteStr, Char, Float, Int, Str, Verbatim}; + match lit { + Str(_) => "string", + ByteStr(_) => "bytestring", + Byte(_) => "byte", + Char(_) => "character", + Int(_) => "integer", + Float(_) => "float", + Bool(_) => "boolean", + Verbatim(_) => "unknown (possibly extra-large integer)", + } + } + + self.err( + found, + &["Expected ", expected, " literal, found ", lit_kind(found), " literal"].concat(), + ) + } + + fn unexpected_meta(&self, expected: &str, found: &syn::Meta) { + fn meta_kind(meta: &syn::Meta) -> &'static str { + use syn::Meta::{List, NameValue, Path}; + match meta { + Path(_) => "path", + List(_) => "list", + NameValue(_) => "name-value pair", + } + } + + self.err( + found, + &["Expected ", expected, " attribute, found ", meta_kind(found), " attribute"].concat(), + ) + } + + /// Issue an error relating to a particular `Spanned` structure. + pub fn err(&self, spanned: &impl syn::spanned::Spanned, msg: &str) { + self.err_span(spanned.span(), msg); + } + + /// Issue an error relating to a particular `Span`. + pub fn err_span(&self, span: Span, msg: &str) { + self.push(syn::Error::new(span, 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); + } +} + +impl ToTokens for Errors { + /// Convert the errors into tokens that, when emit, will cause + /// the user of the macro to receive compiler errors. + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(self.errors.borrow().iter().map(|e| e.to_compile_error())); + } +} diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs new file mode 100644 index 0000000..80ce961 --- /dev/null +++ b/argh_derive/src/help.rs @@ -0,0 +1,219 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use { + crate::{ + errors::Errors, + parse_attrs::{Description, FieldKind, TypeAttrs}, + Optionality, StructField, + }, + argh_shared::INDENT, + proc_macro2::{Span, TokenStream}, + quote::quote, +}; + +const SECTION_SEPARATOR: &str = "\n\n"; + +/// Returns a `TokenStream` generating a `String` help message. +/// +/// Note: `fields` entries with `is_subcommand.is_some()` will be ignored +/// in favor of the `subcommand` argument. +pub(crate) fn help( + errors: &Errors, + cmd_name_str_array_ident: syn::Ident, + ty_attrs: &TypeAttrs, + fields: &[StructField<'_>], + subcommand: Option<&StructField<'_>>, +) -> TokenStream { + let mut format_lit = "Usage: {command_name}".to_string(); + + let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + for arg in positional { + format_lit.push(' '); + positional_usage(&mut format_lit, arg); + } + + let options = fields.iter().filter(|f| f.long_name.is_some()); + for option in options.clone() { + format_lit.push(' '); + option_usage(&mut format_lit, option); + } + + if let Some(subcommand) = subcommand { + format_lit.push(' '); + if !subcommand.optionality.is_required() { + format_lit.push('['); + } + format_lit.push_str(""); + if !subcommand.optionality.is_required() { + format_lit.push(']'); + } + format_lit.push_str(" []"); + } + + format_lit.push_str(SECTION_SEPARATOR); + + let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type"); + format_lit.push_str(&description); + + format_lit.push_str(SECTION_SEPARATOR); + format_lit.push_str("Options:"); + for option in options { + option_description(errors, &mut format_lit, option); + } + // Also include "help" + option_description_format(&mut format_lit, None, "--help", "display usage information"); + + let subcommand_calculation; + let subcommand_format_arg; + if let Some(subcommand) = subcommand { + format_lit.push_str(SECTION_SEPARATOR); + format_lit.push_str("Commands:{subcommands}"); + let subcommand_ty = subcommand.ty_without_wrapper; + subcommand_format_arg = quote! { subcommands = subcommands }; + subcommand_calculation = quote! { + let subcommands = argh::print_subcommands( + <#subcommand_ty as argh::SubCommands>::COMMANDS + ); + }; + } else { + subcommand_calculation = TokenStream::new(); + subcommand_format_arg = TokenStream::new() + } + + lits_section(&mut format_lit, "Examples:", &ty_attrs.examples); + + lits_section(&mut format_lit, "Notes:", &ty_attrs.notes); + + if ty_attrs.error_codes.len() != 0 { + 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())); + } + } + + format_lit.push_str("\n"); + + quote! { { + #subcommand_calculation + format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg) + } } +} + +/// 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 { + out.push_str(SECTION_SEPARATOR); + out.push_str(heading); + for lit in lits { + let value = lit.value(); + for line in value.split('\n') { + out.push('\n'); + out.push_str(INDENT); + out.push_str(line); + } + } + } +} + +/// Add positional arguments like `[...]` to a help format string. +fn positional_usage(out: &mut String, field: &StructField<'_>) { + if !field.optionality.is_required() { + out.push('['); + } + out.push('<'); + out.push_str(&field.name.to_string()); + if field.optionality == Optionality::Repeating { + out.push_str("..."); + } + out.push('>'); + if !field.optionality.is_required() { + out.push(']'); + } +} + +/// Add options like `[-f ]` to a help format string. +/// This function must only be called on options (things with `long_name.is_some()`) +fn option_usage(out: &mut String, field: &StructField<'_>) { + // bookend with `[` and `]` if optional + if !field.optionality.is_required() { + out.push('['); + } + + let long_name = field.long_name.as_ref().expect("missing long name for option"); + if let Some(short) = field.attrs.short.as_ref() { + out.push('-'); + out.push(short.value()); + } else { + out.push_str(long_name); + } + + match field.kind { + FieldKind::SubCommand | FieldKind::Positional => unreachable!(), // don't have long_name + FieldKind::Switch => {} + FieldKind::Option => { + out.push_str(" <"); + out.push_str(long_name.trim_start_matches("--")); + out.push('>'); + } + } + + if !field.optionality.is_required() { + out.push(']'); + } +} + +// TODO(cramertj) make it so this is only called at least once per object so +// as to avoid creating multiple errors. +pub fn require_description( + errors: &Errors, + err_span: Span, + desc: &Option, + kind: &str, // the thing being described ("type" or "field"), +) -> String { + desc.as_ref().map(|d| d.content.value().trim().to_owned()).unwrap_or_else(|| { + errors.err_span( + err_span, + &format!( + "#[derive(FromArgs)] {} with no description. +Add a doc comment or an `#[argh(description = \"...\")]` attribute.", + kind + ), + ); + "".to_string() + }) +} + +/// Describes an option like this: +/// -f, --force force, ignore minor errors. This description +/// is so long that it wraps to the next line. +fn option_description(errors: &Errors, out: &mut String, field: &StructField<'_>) { + let short = field.attrs.short.as_ref().map(|s| s.value()); + let long_with_leading_dashes = field.long_name.as_ref().expect("missing long name for option"); + let description = + require_description(errors, field.name.span(), &field.attrs.description, "field"); + + option_description_format(out, short, long_with_leading_dashes, &description) +} + +fn option_description_format( + out: &mut String, + short: Option, + long_with_leading_dashes: &str, + description: &str, +) { + let mut name = String::new(); + if let Some(short) = short { + name.push('-'); + name.push(short); + name.push_str(", "); + } + name.push_str(long_with_leading_dashes); + + let info = argh_shared::CommandInfo { name: &*name, description }; + argh_shared::write_description(out, &info); +} diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs new file mode 100644 index 0000000..8d15871 --- /dev/null +++ b/argh_derive/src/lib.rs @@ -0,0 +1,748 @@ +#![recursion_limit = "256"] +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + + +/// Implementation of the `FromArgs` and `argh(...)` derive attributes. +/// +/// For more thorough documentation, see the `argh` crate itself. +extern crate proc_macro; + +use { + crate::{ + errors::Errors, + parse_attrs::{FieldAttrs, FieldKind, TypeAttrs}, + }, + proc_macro2::{Span, TokenStream}, + quote::{quote, quote_spanned, ToTokens}, + std::str::FromStr, + syn::spanned::Spanned, +}; + +mod errors; +mod help; +mod parse_attrs; + +/// Entrypoint for `#[derive(FromArgs)]`. +#[proc_macro_derive(FromArgs, attributes(argh))] +pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse_macro_input!(input as syn::DeriveInput); + let gen = impl_from_args(&ast); + gen.into() +} + +/// Transform the input into a token stream containing any generated implementations, +/// 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::Union(_) => { + errors.err(input, "`#[derive(FromArgs)]` cannot be applied to unions"); + TokenStream::new() + } + }; + errors.to_tokens(&mut output_tokens); + output_tokens +} + +/// The kind of optionality a parameter has. +enum Optionality { + None, + Defaulted(TokenStream), + Optional, + Repeating, +} + +impl PartialEq 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, + } + } +} + +impl Optionality { + /// Whether or not this is `Optionality::None` + fn is_required(&self) -> bool { + if let Optionality::None = self { + true + } else { + false + } + } +} + +/// A field of a `#![derive(FromArgs)]` struct with attributes and some other +/// notable metadata appended. +struct StructField<'a> { + /// The original parsed field + field: &'a syn::Field, + /// The parsed attributes of the field + attrs: FieldAttrs, + /// The field name. This is contained optionally inside `field`, + /// but is duplicated non-optionally here to indicate that all field that + /// have reached this point must have a field name, and it no longer + /// needs to be unwrapped. + name: &'a syn::Ident, + /// Similar to `name` above, this is contained optionally inside `FieldAttrs`, + /// but here is fully present to indicate that we only have to consider fields + /// with a valid `kind` at this point. + kind: FieldKind, + // If `field.ty` is `Vec` or `Option`, this is `T`, otherwise it's `&field.ty`. + // This is used to enable consistent parsing code between optional and non-optional + // keyed and subcommand fields. + ty_without_wrapper: &'a syn::Type, + // Whether the field represents an optional value, such as an `Option` subcommand field + // or an `Option` or `Vec` keyed argument, or if it has a `default`. + optionality: Optionality, + // The `--`-prefixed name of the option, if one exists. + long_name: Option, +} + +impl<'a> StructField<'a> { + /// Attempts to parse a field of a `#[derive(FromArgs)]` struct, pulling out the + /// fields required for code generation. + fn new(errors: &Errors, field: &'a syn::Field, attrs: FieldAttrs) -> Option { + let name = field.ident.as_ref().expect("missing ident for named field"); + + // Ensure that one "kind" is present (switch, option, subcommand, positional) + let kind = if let Some(field_type) = &attrs.field_type { + field_type.kind + } else { + errors.err( + field, + concat!( + "Missing `argh` field kind attribute.\n", + "Expected one of: `switch`, `option`, `subcommand`, `positional`", + ), + ); + return None; + }; + + // Parse out whether a field is optional (`Option` or `Vec`). + let optionality; + let ty_without_wrapper; + match kind { + FieldKind::Switch => { + if !ty_expect_switch(errors, &field.ty) { + return None; + } + optionality = Optionality::Optional; + ty_without_wrapper = &field.ty; + } + FieldKind::Option | FieldKind::Positional => { + if let Some(default) = &attrs.default { + let tokens = match TokenStream::from_str(&default.value()) { + Ok(tokens) => tokens, + Err(_) => { + errors.err(&default, "Invalid tokens: unable to lex `default` value"); + return None; + } + }; + // Set the span of the generated tokens to the string literal + let tokens: TokenStream = tokens + .into_iter() + .map(|mut tree| { + tree.set_span(default.span().clone()); + tree + }) + .collect(); + optionality = Optionality::Defaulted(tokens); + ty_without_wrapper = &field.ty; + } else { + let mut inner = None; + optionality = if let Some(x) = ty_inner(&["Option"], &field.ty) { + inner = Some(x); + Optionality::Optional + } else if let Some(x) = ty_inner(&["Vec"], &field.ty) { + inner = Some(x); + Optionality::Repeating + } else { + Optionality::None + }; + ty_without_wrapper = inner.unwrap_or(&field.ty); + } + } + FieldKind::SubCommand => { + let inner = ty_inner(&["Option"], &field.ty); + optionality = + if inner.is_some() { Optionality::Optional } else { Optionality::None }; + ty_without_wrapper = inner.unwrap_or(&field.ty); + } + } + + // Determine the "long" name of options and switches. + // Defaults to the kebab-case'd field name if `#[argh(long = "...")]` is omitted. + let long_name = match kind { + FieldKind::Switch | FieldKind::Option => { + let long_name = attrs + .long + .as_ref() + .map(syn::LitStr::value) + .unwrap_or_else(|| heck::KebabCase::to_kebab_case(&*name.to_string())); + if long_name == "help" { + errors.err(field, "Custom `--help` flags are not supported."); + } + let long_name = format!("--{}", long_name); + Some(long_name) + } + FieldKind::SubCommand | FieldKind::Positional => None, + }; + + Some(StructField { field, attrs, kind, optionality, ty_without_wrapper, name, long_name }) + } +} + +/// Implements `FromArgs` and `TopLevelCommand` or `SubCommand` for a `#[derive(FromArgs)]` struct. +fn impl_from_args_struct( + errors: &Errors, + name: &syn::Ident, + type_attrs: &TypeAttrs, + ds: &syn::DataStruct, +) -> TokenStream { + let fields = match &ds.fields { + syn::Fields::Named(fields) => fields, + syn::Fields::Unnamed(_) => { + errors.err( + &ds.struct_token, + "`#![derive(FromArgs)]` is not currently supported on tuple structs", + ); + return TokenStream::new(); + } + syn::Fields::Unit => { + errors.err(&ds.struct_token, "#![derive(FromArgs)]` cannot be applied to unit structs"); + return TokenStream::new(); + } + }; + + let fields: Vec<_> = fields + .named + .iter() + .filter_map(|field| { + let attrs = FieldAttrs::parse(errors, field); + StructField::new(errors, field, attrs) + }) + .collect(); + + ensure_only_last_positional_is_optional(errors, &fields); + let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs); + let init_fields = declare_local_storage_for_fields(&fields); + let unwrap_fields = unwrap_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); + let positional_field_names = positional_fields.iter().map(|field| field.name.to_string()); + let last_positional_is_repeating = positional_fields + .last() + .map(|field| field.optionality == Optionality::Repeating) + .unwrap_or(false); + + let flag_output_table = fields.iter().filter_map(|field| { + let field_name = &field.field.ident; + match field.kind { + FieldKind::Option => Some(quote! { argh::CmdOption::Value(&mut #field_name) }), + FieldKind::Switch => Some(quote! { argh::CmdOption::Flag(&mut #field_name) }), + FieldKind::SubCommand | FieldKind::Positional => None, + } + }); + + 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() { + errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); + } + + let impl_span = Span::call_site(); + + let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span.clone()); + + let append_missing_requirements = + append_missing_requirements(&missing_requirements_ident, &fields); + + let check_subcommands = if let Some(subcommand) = subcommand { + let name = subcommand.name; + let ty = subcommand.ty_without_wrapper; + quote_spanned! { impl_span => + for __subcommand in <#ty as argh::SubCommands>::COMMANDS { + if __subcommand.name == __next_arg { + let mut __command = __cmd_name.to_owned(); + __command.push(__subcommand.name); + let __prepended_help; + let __remaining_args = if __help { + __prepended_help = argh::prepend_help(__remaining_args); + &__prepended_help + } else { + __remaining_args + }; + #name = Some(<#ty as argh::FromArgs>::from_args(&__command, __remaining_args)?); + // Unset `help`, since we handled it in the subcommand + __help = false; + break 'parse_args; + } + } + } + } else { + TokenStream::new() + }; + + // 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.clone()); + let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + + let trait_impl = quote_spanned! { impl_span => + impl argh::FromArgs for #name { + fn from_args(__cmd_name: &[&str], __args: &[&str]) + -> std::result::Result + { + #( #init_fields )* + let __flag_output_table = &mut [ + #( #flag_output_table, )* + ]; + + let __positional_output_table = &mut [ + #( ( + &mut #positional_field_idents as &mut argh::ParseValueSlot, + #positional_field_names + ), )* + ]; + + let mut __help = false; + let mut __remaining_args = __args; + let mut __positional_index = 0; + 'parse_args: while let Some(&__next_arg) = __remaining_args.get(0) { + __remaining_args = &__remaining_args[1..]; + if __next_arg == "--help" || __next_arg == "help" { + __help = true; + continue; + } + + if __next_arg.starts_with("-") { + if __help { + return Err( + "Trailing arguments are not allowed after `help`." + .to_string() + .into() + ); + } + + argh::parse_option( + __next_arg, + &mut __remaining_args, + __flag_output_table, + &[ #( #flag_str_to_output_table_map ,)* ], + )?; + continue; + } + + #check_subcommands + + if __positional_index < __positional_output_table.len() { + argh::parse_positional( + __next_arg, + &mut __positional_output_table[__positional_index], + )?; + + // Don't increment position if we're at the last arg + // *and* the last arg is repeating. + let __skip_increment = + #last_positional_is_repeating && + __positional_index == __positional_output_table.len() - 1; + + if !__skip_increment { + __positional_index += 1; + } + } else { + return std::result::Result::Err(argh::EarlyExit { + output: argh::unrecognized_arg(__next_arg), + status: std::result::Result::Err(()), + }); + } + } + + if __help { + return std::result::Result::Err(argh::EarlyExit { + output: #help, + status: std::result::Result::Ok(()), + }); + } + + let mut #missing_requirements_ident = argh::MissingRequirements::default(); + #( + #append_missing_requirements + )* + #missing_requirements_ident.err_on_any()?; + + Ok(Self { + #( #unwrap_fields, )* + }) + } + } + + #top_or_sub_cmd_impl + }; + + trait_impl.into() +} + +/// Ensures that only the last positional arg is non-required. +fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructField<'_>]) { + let mut first_non_required_span = None; + for field in fields { + if field.kind == FieldKind::Positional { + if let Some(first) = first_non_required_span { + errors.err_span( + first, + "Only the last positional argument may be `Option`, `Vec`, or defaulted.", + ); + errors.err(&field.field, "Later positional argument declared here."); + return; + } + if !field.optionality.is_required() { + first_non_required_span = Some(field.field.span()); + } + } + } +} + +/// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate. +fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs) -> TokenStream { + let description = + help::require_description(errors, name.span(), &type_attrs.description, "type"); + if type_attrs.is_subcommand.is_none() { + // Not a subcommand + quote! { + impl argh::TopLevelCommand for #name {} + } + } else { + let empty_str = syn::LitStr::new("", Span::call_site()); + let subcommand_name = type_attrs.name.as_ref().unwrap_or_else(|| { + errors.err(name, "`#[argh(name = \"...\")]` attribute is required for subcommands"); + &empty_str + }); + quote! { + impl argh::SubCommand for #name { + const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo { + name: #subcommand_name, + description: #description, + }; + } + } + } +} + +/// Declare a local slots to store each field in during parsing. +/// +/// Most fields are stored in `Option` locals. +/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a +/// function that knows how to decode the appropriate value. +fn declare_local_storage_for_fields<'a>( + fields: &'a [StructField<'a>], +) -> impl Iterator + 'a { + fields.iter().map(|field| { + let field_name = &field.field.ident; + let field_type = &field.ty_without_wrapper; + + // Wrap field types in `Option` if they aren't already `Option` or `Vec`-wrapped. + let field_slot_type = match field.optionality { + Optionality::Optional | Optionality::Repeating => (&field.field.ty).into_token_stream(), + Optionality::None | Optionality::Defaulted(_) => { + quote! { std::option::Option<#field_type> } + } + }; + + match field.kind { + FieldKind::Option | FieldKind::Positional => { + let from_str_fn = match &field.attrs.from_str_fn { + Some(from_str_fn) => from_str_fn.into_token_stream(), + None => { + quote! { + <#field_type as argh::FromArgValue>::from_arg_value + } + } + }; + + quote! { + let mut #field_name: argh::ParseValueSlotTy<#field_slot_type, #field_type> + = argh::ParseValueSlotTy { + slot: std::default::Default::default(), + parse_func: #from_str_fn, + }; + } + } + FieldKind::SubCommand => { + quote! { let mut #field_name: #field_slot_type = None; } + } + FieldKind::Switch => { + quote! { let mut #field_name: #field_slot_type = argh::Flag::default(); } + } + } + }) +} + +/// Unwrap non-optional fields and take options out of their tuple slots. +fn unwrap_fields<'a>(fields: &'a [StructField<'a>]) -> impl Iterator + 'a { + fields.iter().map(|field| { + 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::Optional | Optionality::Repeating => { + quote! { #field_name: #field_name.slot } + } + Optionality::Defaulted(tokens) => { + quote! { + #field_name: #field_name.slot.unwrap_or_else(|| #tokens) + } + } + }, + FieldKind::Switch => field_name.into_token_stream(), + FieldKind::SubCommand => match field.optionality { + Optionality::None => quote! { #field_name: #field_name.unwrap() }, + Optionality::Optional | Optionality::Repeating => field_name.into_token_stream(), + Optionality::Defaulted(_) => unreachable!(), + }, + } + }) +} + +/// Entries of tokens like `("--some-flag-key", 5)` that map from a flag key string +/// to an index in the output table. +fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec { + let mut flag_str_to_output_table_map = vec![]; + for (i, (field, long_name)) in fields + .iter() + .filter_map(|field| field.long_name.as_ref().map(|long_name| (field, long_name))) + .enumerate() + { + if let Some(short) = &field.attrs.short { + let short = format!("-{}", short.value()); + flag_str_to_output_table_map.push(quote! { (#short, #i) }); + } + + flag_str_to_output_table_map.push(quote! { (#long_name, #i) }); + } + flag_str_to_output_table_map +} + +/// For each non-optional field, add an entry to the `argh::MissingRequirements`. +fn append_missing_requirements<'a>( + // missing_requirements_ident + mri: &syn::Ident, + fields: &'a [StructField<'a>], +) -> impl Iterator + 'a { + let mri = mri.clone(); + fields.iter().filter(|f| f.optionality.is_required()).map(move |field| { + let field_name = field.name; + match field.kind { + FieldKind::Switch => unreachable!("switches are always optional"), + FieldKind::Positional => { + let name = field.name.to_string(); + quote! { + if #field_name.slot.is_none() { + #mri.missing_positional_arg(#name) + } + } + } + FieldKind::Option => { + let name = field.long_name.as_ref().expect("options always have a long name"); + quote! { + if #field_name.slot.is_none() { + #mri.missing_option(#name) + } + } + } + FieldKind::SubCommand => { + let ty = field.ty_without_wrapper; + quote! { + if #field_name.is_none() { + #mri.missing_subcommands( + <#ty as argh::SubCommands>::COMMANDS, + ) + } + } + } + } + }) +} + +/// Require that a type can be a `switch`. +/// Throws an error for all types except booleans and integers +fn ty_expect_switch(errors: &Errors, ty: &syn::Type) -> bool { + fn ty_can_be_switch(ty: &syn::Type) -> bool { + if let syn::Type::Path(path) = ty { + if path.qself.is_some() { + return false; + } + if path.path.segments.len() != 1 { + return false; + } + let ident = &path.path.segments[0].ident; + ["bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"] + .iter() + .any(|path| ident == path) + } else { + false + } + } + + let res = ty_can_be_switch(ty); + if !res { + errors.err(ty, "switches must be of type `bool` or integer type"); + } + res +} + +/// Returns `Some(T)` if a type is `wrapper_name` for any `wrapper_name` in `wrapper_names`. +fn ty_inner<'a>(wrapper_names: &[&str], ty: &'a syn::Type) -> Option<&'a syn::Type> { + if let syn::Type::Path(path) = ty { + if path.qself.is_some() { + return None; + } + // Since we only check the last path segment, it isn't necessarily the case that + // we're referring to `std::vec::Vec` or `std::option::Option`, but there isn't + // a fool proof way to check these since name resolution happens after macro expansion, + // so this is likely "good enough" (so long as people don't have their own types called + // `Option` or `Vec` that take one generic parameter they're looking to parse). + let last_segment = path.path.segments.last()?; + if !wrapper_names.iter().any(|name| last_segment.ident == *name) { + return None; + } + if let syn::PathArguments::AngleBracketed(gen_args) = &last_segment.arguments { + let generic_arg = gen_args.args.first()?; + if let syn::GenericArgument::Type(ty) = &generic_arg { + return Some(ty); + } + } + } + None +} + +/// Implements `FromArgs` and `SubCommands` for a `#![derive(FromArgs)]` enum. +fn impl_from_args_enum( + errors: &Errors, + name: &syn::Ident, + type_attrs: &TypeAttrs, + de: &syn::DataEnum, +) -> TokenStream { + parse_attrs::check_enum_type_attrs(errors, type_attrs, &de.enum_token.span); + + // An enum variant like `()` + struct SubCommandVariant<'a> { + name: &'a syn::Ident, + ty: &'a syn::Type, + } + + let variants: Vec> = 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 }) + }) + .collect(); + + let name_repeating = std::iter::repeat(name.clone()); + let variant_ty_1 = variants.iter().map(|x| x.ty); + let variant_ty_2 = variant_ty_1.clone(); + let variant_ty_3 = variant_ty_1.clone(); + let variant_names = variants.iter().map(|x| x.name); + + quote! { + impl argh::FromArgs for #name { + fn from_args(command_name: &[&str], args: &[&str]) + -> std::result::Result + { + let subcommand_name = *command_name.last().expect("no subcommand name"); + #( + if subcommand_name == <#variant_ty_1 as argh::SubCommand>::COMMAND.name { + return Ok(#name_repeating::#variant_names( + <#variant_ty_2 as argh::FromArgs>::from_args(command_name, args)? + )); + } + )* + unreachable!("no subcommand matched") + } + } + + impl argh::SubCommands for #name { + const COMMANDS: &'static [&'static argh::CommandInfo] = &[#( + <#variant_ty_3 as argh::SubCommand>::COMMAND, + )*]; + } + } +} + +/// Returns `Some(Bar)` if the field is a single-field unnamed variant like `Foo(Bar)`. +/// Otherwise, generates an error. +fn enum_only_single_field_unnamed_variants<'a>( + errors: &Errors, + variant_fields: &'a syn::Fields, +) -> Option<&'a syn::Type> { + macro_rules! with_enum_suggestion { + ($help_text:literal) => { + concat!( + $help_text, + "\nInstead, use a variant with a single unnamed field for each subcommand:\n", + " enum MyCommandEnum {\n", + " SubCommandOne(SubCommandOne),\n", + " SubCommandTwo(SubCommandTwo),\n", + " }", + ) + }; + } + + match variant_fields { + syn::Fields::Named(fields) => { + errors.err( + fields, + with_enum_suggestion!( + "`#![derive(FromArgs)]` `enum`s do not support variants with named fields." + ), + ); + None + } + syn::Fields::Unit => { + errors.err( + variant_fields, + with_enum_suggestion!( + "`#![derive(FromArgs)]` does not support `enum`s with no variants." + ), + ); + None + } + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() != 1 { + errors.err( + fields, + with_enum_suggestion!( + "`#![derive(FromArgs)]` `enum` variants must only contain one field." + ), + ); + None + } else { + // `unwrap` is okay because of the length check above. + let first_field = fields.unnamed.first().unwrap(); + Some(&first_field.ty) + } + } + } +} diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs new file mode 100644 index 0000000..df69ffd --- /dev/null +++ b/argh_derive/src/parse_attrs.rs @@ -0,0 +1,552 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use { + crate::errors::Errors, + proc_macro2::Span, + std::collections::hash_map::{Entry, HashMap}, +}; + +/// Attributes applied to a field of a `#![derive(FromArgs)]` struct. +#[derive(Default)] +pub struct FieldAttrs { + pub default: Option, + pub description: Option, + pub from_str_fn: Option, + pub field_type: Option, + pub long: Option, + pub short: Option, +} + +/// The purpose of a particular field on a `#![derive(FromArgs)]` struct. +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum FieldKind { + /// Switches are booleans that are set to "true" by passing the flag. + Switch, + /// Options are `--key value`. They may be optional (using `Option`), + /// or repeating (using `Vec`), or required (neither `Option` nor `Vec`) + Option, + /// Subcommand fields (of which there can be at most one) refer to enums + /// containing one of several potential subcommands. They may be optional + /// (using `Option`) or required (no `Option`). + SubCommand, + /// Positional arguments are parsed literally if the input + /// does not begin with `-` or `--` and is not a subcommand. + /// They are parsed in declaration order, and only the last positional + /// argument in a type may be an `Option`, `Vec`, or have a default value. + Positional, +} + +/// The type of a field on a `#![derive(FromArgs)]` struct. +/// +/// This is a simple wrapper around `FieldKind` which includes the `syn::Ident` +/// of the attribute containing the field kind. +pub struct FieldType { + pub kind: FieldKind, + pub ident: syn::Ident, +} + +/// A description of a `#![derive(FromArgs)]` struct. +/// +/// Defaults to the docstring if one is present, or `#[argh(description = "...")]` +/// if one is provided. +pub struct Description { + /// Whether the description was an explicit annotation or whether it was a doc string. + pub explicit: bool, + pub content: syn::LitStr, +} + +impl FieldAttrs { + pub fn parse(errors: &Errors, field: &syn::Field) -> Self { + let mut this = Self::default(); + + for attr in &field.attrs { + if is_doc_attr(attr) { + parse_attr_doc(errors, attr, &mut this.description); + continue; + } + + 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("default") { + 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) { + 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) { + this.parse_attr_from_str_fn(errors, m); + } + } else if name.is_ident("long") { + 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) { + this.parse_attr_short(errors, m); + } + } else if name.is_ident("subcommand") { + parse_attr_field_type( + errors, + meta, + FieldKind::SubCommand, + &mut this.field_type, + ); + } else if name.is_ident("switch") { + parse_attr_field_type(errors, meta, FieldKind::Switch, &mut this.field_type); + } else if name.is_ident("positional") { + parse_attr_field_type( + errors, + meta, + FieldKind::Positional, + &mut this.field_type, + ); + } else { + errors.err( + &meta, + concat!( + "Invalid field-level `argh` attribute\n", + "Expected one of: `default`, `description`, `from_str_fn`, `long`, ", + "`option`, `short`, `subcommand`, `switch`", + ), + ); + } + } + } + + if let (Some(default), Some(field_type)) = (&this.default, &this.field_type) { + match field_type.kind { + FieldKind::Option | FieldKind::Positional => {} + FieldKind::SubCommand | FieldKind::Switch => errors.err( + default, + "`default` may only be specified on `#[argh(option)]` \ + or `#[argh(subcommand)]` fields", + ), + } + } + + if let Some(d) = &this.description { + check_option_description(errors, d.content.value().trim(), d.content.span()); + } + + this + } + + fn parse_attr_from_str_fn(&mut self, errors: &Errors, m: &syn::MetaList) { + parse_attr_fn_name(errors, m, "from_str_fn", &mut self.from_str_fn) + } + + fn parse_attr_default(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + parse_attr_single_string(errors, m, "default", &mut self.default); + } + + fn parse_attr_long(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + parse_attr_single_string(errors, m, "long", &mut self.long); + let long = self.long.as_ref().unwrap(); + let value = long.value(); + if !value.is_ascii() { + errors.err(long, "Long names must be ASCII"); + } + if !value.chars().all(|c| c.is_lowercase()) { + errors.err(long, "Long names must be lowercase"); + } + } + + fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + if let Some(first) = &self.short { + errors.duplicate_attrs("short", first, m); + } else if let Some(lit_char) = errors.expect_lit_char(&m.lit) { + self.short = Some(lit_char.clone()); + if !lit_char.value().is_ascii() { + errors.err(lit_char, "Short names must be ASCII"); + } + } + } +} + +fn parse_attr_fn_name( + errors: &Errors, + m: &syn::MetaList, + attr_name: &str, + slot: &mut Option, +) { + if let Some(first) = slot { + errors.duplicate_attrs(attr_name, first, m); + } + + if m.nested.len() != 1 { + errors.err(&m.nested, "Expected a single argument containing the function name"); + return; + } + + // `unwrap` will not fail because of the call above + let nested = m.nested.first().unwrap(); + if let Some(path) = errors.expect_nested_meta(nested).and_then(|m| errors.expect_meta_word(m)) { + *slot = path.get_ident().cloned(); + } +} + +fn parse_attr_field_type( + errors: &Errors, + meta: &syn::Meta, + kind: FieldKind, + slot: &mut Option, +) { + 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() }); + } + } + } +} + +// Whether the attribute is one like `#[ ...]` +fn is_matching_attr(name: &str, attr: &syn::Attribute) -> bool { + attr.path.segments.len() == 1 && attr.path.segments[0].ident == name +} + +/// Checks for `#[doc ...]`, which is generated by doc comments. +fn is_doc_attr(attr: &syn::Attribute) -> bool { + is_matching_attr("doc", attr) +} + +/// Checks for `#[argh ...]` +fn is_argh_attr(attr: &syn::Attribute) -> bool { + is_matching_attr("argh", attr) +} + +fn attr_to_meta_subtype( + errors: &Errors, + attr: &syn::Attribute, + f: impl FnOnce(&syn::Meta) -> Option<&R>, +) -> Option { + match attr.parse_meta() { + Ok(meta) => f(&meta).cloned(), + Err(e) => { + errors.push(e); + None + } + } +} + +fn attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option { + attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_list(m)) +} + +fn attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option { + attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_name_value(m)) +} + +/// Filters out non-`#[argh(...)]` attributes and converts to `syn::MetaList`. +fn argh_attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option { + if !is_argh_attr(attr) { + return None; + } + attr_to_meta_list(errors, attr) +} + +/// Represents a `#[derive(FromArgs)]` type's top-level attributes. +#[derive(Default)] +pub struct TypeAttrs { + pub is_subcommand: Option, + pub name: Option, + pub description: Option, + pub examples: Vec, + pub notes: Vec, + pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, +} + +impl TypeAttrs { + /// Parse top-level `#[argh(...)]` attributes + pub fn parse(errors: &Errors, derive_input: &syn::DeriveInput) -> Self { + let mut this = TypeAttrs::default(); + + for attr in &derive_input.attrs { + if is_doc_attr(attr) { + parse_attr_doc(errors, attr, &mut this.description); + continue; + } + + 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("description") { + 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) { + this.parse_attr_error_code(errors, m); + } + } else if name.is_ident("example") { + 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) { + this.parse_attr_name(errors, m); + } + } else if name.is_ident("note") { + 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()) + { + this.parse_attr_subcommand(errors, ident); + } + } else { + errors.err( + &meta, + concat!( + "Invalid type-level `argh` attribute\n", + "Expected one of: `description`, `error_code`, `example`, `name`, ", + "`note`, `subcommand`", + ), + ); + } + } + } + + this.check_error_codes(errors); + this + } + + /// Checks that error codes are within range for `i32` and that they are + /// never duplicated. + fn check_error_codes(&self, errors: &Errors) { + // map from error code to index + let mut map: HashMap = HashMap::new(); + for (index, (lit_int, _lit_str)) in self.error_codes.iter().enumerate() { + let value = match lit_int.base10_parse::() { + Ok(v) => v, + Err(e) => { + errors.push(e); + continue; + } + }; + if value > (std::i32::MAX as u64) { + errors.err(lit_int, "Error code out of range for `i32`"); + } + match map.entry(value) { + Entry::Occupied(previous) => { + let previous_index = *previous.get(); + let (previous_lit_int, _previous_lit_str) = &self.error_codes[previous_index]; + errors.err(lit_int, &format!("Duplicate error code {}", value)); + errors.err( + previous_lit_int, + &format!("Error code {} previously defined here", value), + ); + } + Entry::Vacant(slot) => { + slot.insert(index); + } + } + } + } + + fn parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList) { + if ml.nested.len() != 2 { + errors.err(&ml, "Expected two arguments, an error number and a string description"); + return; + } + + let err_code = &ml.nested[0]; + let err_msg = &ml.nested[1]; + + let err_code = errors.expect_nested_lit(err_code).and_then(|l| errors.expect_lit_int(l)); + let err_msg = errors.expect_nested_lit(err_msg).and_then(|l| errors.expect_lit_str(l)); + + if let (Some(err_code), Some(err_msg)) = (err_code, err_msg) { + self.error_codes.push((err_code.clone(), err_msg.clone())); + } + } + + fn parse_attr_example(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + parse_attr_multi_string(errors, m, &mut self.examples) + } + + fn parse_attr_name(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + parse_attr_single_string(errors, m, "name", &mut self.name); + if let Some(name) = &self.name { + if name.value() == "help" { + errors.err(name, "Custom `help` commands are not supported."); + } + } + } + + fn parse_attr_note(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + parse_attr_multi_string(errors, m, &mut self.notes) + } + + fn parse_attr_subcommand(&mut self, errors: &Errors, ident: &syn::Ident) { + if let Some(first) = &self.is_subcommand { + errors.duplicate_attrs("subcommand", first, ident); + } else { + self.is_subcommand = Some(ident.clone()); + } + } +} + +fn check_option_description(errors: &Errors, desc: &str, span: Span) { + let chars = &mut desc.trim().chars(); + match (chars.next(), chars.next()) { + (Some(x), _) if x.is_lowercase() => {} + // If both the first and second letter are not lowercase, + // this is likely an initialism which should be allowed. + (Some(x), Some(y)) if !x.is_lowercase() && !y.is_lowercase() => {} + _ => { + errors.err_span(span, "Descriptions must begin with a lowercase letter"); + } + } +} + +fn parse_attr_single_string( + errors: &Errors, + m: &syn::MetaNameValue, + name: &str, + slot: &mut Option, +) { + if let Some(first) = slot { + errors.duplicate_attrs(name, first, m); + } else if let Some(lit_str) = errors.expect_lit_str(&m.lit) { + *slot = Some(lit_str.clone()); + } +} + +fn parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut Vec) { + if let Some(lit_str) = errors.expect_lit_str(&m.lit) { + list.push(lit_str.clone()); + } +} + +fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option) { + let nv = if let Some(nv) = attr_to_meta_name_value(errors, attr) { + nv + } else { + return; + }; + + // Don't replace an existing description. + if slot.as_ref().map(|d| d.explicit).unwrap_or(false) { + return; + } + + if let Some(lit_str) = errors.expect_lit_str(&nv.lit) { + let lit_str = if let Some(previous) = slot { + let previous = &previous.content; + let previous_span = previous.span(); + syn::LitStr::new(&(previous.value() + &*lit_str.value()), previous_span) + } else { + lit_str.clone() + }; + *slot = Some(Description { explicit: false, content: lit_str }); + } +} + +fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option) { + let lit_str = if let Some(lit_str) = errors.expect_lit_str(&m.lit) { lit_str } else { return }; + + // Don't allow multiple explicit (non doc-comment) descriptions + if let Some(description) = slot { + if description.explicit { + errors.duplicate_attrs("description", &description.content, lit_str); + } + } + + *slot = Some(Description { explicit: true, content: lit_str.clone() }); +} + +/// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]` +/// attribute and that it does not have any other type-level `#[argh(...)]` attributes. +pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) { + let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes } = type_attrs; + + // Ensure that `#[argh(subcommand)]` is present. + if is_subcommand.is_none() { + errors.err_span( + type_span.clone(), + concat!( + "`#![derive(FromArgs)]` on `enum`s can only be used to enumerate subcommands.\n", + "Consider adding `#[argh(subcommand)]` to the `enum` declaration.", + ), + ); + } + + // Error on all other type-level attributes. + if let Some(name) = name { + err_unused_enum_attr(errors, name); + } + if let Some(description) = description { + if description.explicit { + err_unused_enum_attr(errors, &description.content); + } + } + if let Some(example) = examples.first() { + err_unused_enum_attr(errors, example); + } + if let Some(note) = notes.first() { + err_unused_enum_attr(errors, note); + } + if let Some(err_code) = error_codes.first() { + err_unused_enum_attr(errors, &err_code.0); + } +} + +/// 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, + concat!( + "Unused `argh` attribute on `#![derive(FromArgs)]` enum. ", + "Such `enum`s can only be used to dispatch to subcommands, ", + "and should only contain the #[argh(subcommand)] attribute.", + ), + ); +} diff --git a/argh_shared/Cargo.toml b/argh_shared/Cargo.toml new file mode 100644 index 0000000..a9939ec --- /dev/null +++ b/argh_shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "argh_shared" +version = "0.1.0" +authors = ["Taylor Cramer ", "Benjamin Brittain"] +edition = "2018" +license = "BSD License 2.0" +description = "Derive-based argument parsing optimized for code size" +repository = "https://github.com/google/argh" diff --git a/argh_shared/src/lib.rs b/argh_shared/src/lib.rs new file mode 100644 index 0000000..1c1edf9 --- /dev/null +++ b/argh_shared/src/lib.rs @@ -0,0 +1,75 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//! Shared functionality between argh_derive and the argh runtime. +//! +//! This library is intended only for internal use by these two crates. + +/// Information about a particular command used for output. +pub struct CommandInfo<'a> { + /// The name of the command. + pub name: &'a str, + /// A short description of the command's functionality. + pub description: &'a str, +} + +pub const INDENT: &str = " "; +const DESCRIPTION_INDENT: usize = 20; +const WRAP_WIDTH: usize = 80; + +/// Write command names and descriptions to an output string. +pub fn write_description(out: &mut String, cmd: &CommandInfo<'_>) { + let mut current_line = INDENT.to_string(); + current_line.push_str(cmd.name); + + if !indent_description(&mut current_line) { + // Start the description on a new line if the flag names already + // add up to more than DESCRIPTION_INDENT. + new_line(&mut current_line, out); + } + + let mut words = cmd.description.split(' ').peekable(); + while let Some(first_word) = words.next() { + indent_description(&mut current_line); + current_line.push_str(first_word); + + 'inner: while let Some(&word) = words.peek() { + if (char_len(¤t_line) + char_len(word) + 1) > WRAP_WIDTH { + new_line(&mut current_line, out); + break 'inner; + } else { + // advance the iterator + let _ = words.next(); + current_line.push(' '); + current_line.push_str(word); + } + } + } + new_line(&mut current_line, out); +} + +// Indent the current line in to DESCRIPTION_INDENT chars. +// Returns a boolean indicating whether or not spacing was added. +fn indent_description(line: &mut String) -> bool { + let cur_len = char_len(line); + if cur_len < DESCRIPTION_INDENT { + let num_spaces = DESCRIPTION_INDENT - cur_len; + line.extend(std::iter::repeat(' ').take(num_spaces)); + true + } else { + false + } +} + +fn char_len(s: &str) -> usize { + s.chars().count() +} + +// Append a newline and the current line to the output, +// clearing the current line. +fn new_line(current_line: &mut String, out: &mut String) { + out.push('\n'); + out.push_str(current_line); + current_line.truncate(0); +} diff --git a/docs/code-of-conduct.md b/code-of-conduct.md similarity index 100% rename from docs/code-of-conduct.md rename to code-of-conduct.md diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..95021c5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,526 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//! Derive-based argument parsing optimized for code size and conformance +//! to the Fuchsia commandline tools specification +//! +//! The public API of this library consists primarily of the `FromArgs` +//! derive and the `from_env` function, which can be used to produce +//! a top-level `FromArgs` type from the current program's commandline +//! arguments. +//! +//! ## Basic Example +//! +//! ```rust,no_run +//! use argh::FromArgs; +//! +//! #[derive(FromArgs)] +//! /// Reach new heights. +//! struct GoUp { +//! /// whether or not to jump +//! #[argh(switch, short = 'j')] +//! jump: bool, +//! +//! /// how high to go +//! #[argh(option)] +//! height: usize, +//! +//! /// an optional nickname for the pilot +//! #[argh(option)] +//! pilot_nickname: Option, +//! } +//! +//! fn main() { +//! let up: GoUp = argh::from_env(); +//! } +//! ``` +//! +//! `./some_bin --help` will then output the following: +//! +//! ```bash +//! Usage: cmdname [-j] --height [--pilot-nickname ] +//! +//! Reach new heights. +//! +//! Options: +//! -j, --jump whether or not to jump +//! --height how high to go +//! --pilot-nickname an optional nickname for the pilot +//! --help display usage information +//! ``` +//! +//! The resulting program can then be used in any of these ways: +//! - `./some_bin --height 5` +//! - `./some_bin -j --height 5` +//! - `./some_bin --jump --height 5 --pilot-nickname Wes` +//! +//! Switches, like `jump`, are optional and will be set to true if provided. +//! +//! Options, like `height` and `pilot_nickname`, can be either required, +//! optional, or repeating, depending on whether they are contained in an +//! `Option` or a `Vec`. Default values can be provided using the +//! `#[argh(default = "")]` attribute. +//! +//! Custom option types can be deserialized so long as they implement the +//! `FromArgValue` trait (automatically implemented for all `FromStr` types). +//! If more customized parsing is required, you can supply a custom +//! `fn(&str) -> Result` using the `from_str_fn` attribute: +//! +//! ``` +//! # use argh::FromArgs; +//! +//! #[derive(FromArgs)] +//! /// Goofy thing. +//! struct FiveStruct { +//! /// always five +//! #[argh(option, from_str_fn(always_five))] +//! five: usize, +//! } +//! +//! fn always_five(_value: &str) -> Result { +//! Ok(5) +//! } +//! ``` +//! +//! Positional arguments can be declared using `#[argh(positional)]`. +//! These arguments will be parsed in order of their declaration in +//! the structure: +//! +//! ```rust +//! use argh::FromArgs; +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// A command with positional arguments. +//! struct WithPositional { +//! #[argh(positional)] +//! first: String, +//! } +//! ``` +//! +//! The last positional argument may include a default, or be wrapped in +//! `Option` or `Vec` to indicate an optional or repeating positional arugment. +//! +//! Subcommands are also supported. To use a subcommand, declare a separate +//! `FromArgs` type for each subcommand as well as an enum that cases +//! over each command: +//! +//! ```rust +//! # use argh::FromArgs; +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Top-level command. +//! struct TopLevel { +//! #[argh(subcommand)] +//! nested: MySubCommandEnum, +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! #[argh(subcommand)] +//! enum MySubCommandEnum { +//! One(SubCommandOne), +//! Two(SubCommandTwo), +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// First subcommand. +//! #[argh(subcommand, name = "one")] +//! struct SubCommandOne { +//! #[argh(option)] +//! /// how many x +//! x: usize, +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Second subcommand. +//! #[argh(subcommand, name = "two")] +//! struct SubCommandTwo { +//! #[argh(switch)] +//! /// whether to fooey +//! fooey: bool, +//! } +//! ``` + +#![deny(missing_docs)] + +use std::str::FromStr; + +pub use argh_derive::FromArgs; + +/// Information about a particular command used for output. +pub type CommandInfo = argh_shared::CommandInfo<'static>; + +/// Types which can be constructed from a set of commandline arguments. +pub trait FromArgs: Sized { + /// Construct the type from an input set of arguments. + /// + /// The first argument `command_name` is the identifier for the current + /// command, treating each segment as space-separated. This is to be + /// used in the output of `--help`, `--version`, and similar flags. + fn from_args(command_name: &[&str], args: &[&str]) -> Result; +} + +/// A top-level `FromArgs` implementation that is not a subcommand. +pub trait TopLevelCommand: FromArgs {} + +/// A `FromArgs` implementation that can parse into one or more subcommands. +pub trait SubCommands: FromArgs { + /// Info for the commands. + const COMMANDS: &'static [&'static CommandInfo]; +} + +/// A `FromArgs` implementation that represents a single subcommand. +pub trait SubCommand: FromArgs { + /// Information about the subcommand. + const COMMAND: &'static CommandInfo; +} + +impl SubCommands for T { + const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND]; +} + +/// Information to display to the user about why a `FromArgs` construction exited early. +/// +/// This can occur due to either failed parsing or a flag like `--help`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EarlyExit { + /// The output to display to the user of the commandline tool. + pub output: String, + /// Status of argument parsing. + /// + /// `Ok` if the command was parsed successfully and the early exit is due + /// to a flag like `--help` causing early exit with output. + /// + /// `Err` if the arguments were not successfully parsed. + // TODO replace with std::process::ExitCode when stable. + pub status: Result<(), ()>, +} + +impl From for EarlyExit { + fn from(err_msg: String) -> Self { + Self { output: err_msg, status: Err(()) } + } +} + +/// Create a `FromArgs` type from the current process's `env::args`. +/// +/// This function will exit early from the current process if argument parsing +/// was unsuccessful or if information like `--help` was requested. +pub fn from_env() -> T { + let strings: Vec = std::env::args().collect(); + let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + T::from_args(&[strs[0]], &strs[1..]).unwrap_or_else(|early_exit| { + println!("{}", early_exit.output); + std::process::exit(match early_exit.status { + Ok(()) => 0, + Err(()) => 1, + }) + }) +} + +/// Create a `FromArgs` type from the current process's `env::args`. +/// +/// This special cases usages where argh is being used in an environment where cargo is +/// driving the build. We skip the second env variable. +/// +/// This function will exit early from the current process if argument parsing +/// was unsuccessful or if information like `--help` was requested. +pub fn cargo_from_env() -> T { + let strings: Vec = std::env::args().collect(); + let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + T::from_args(&[strs[1]], &strs[2..]).unwrap_or_else(|early_exit| { + println!("{}", early_exit.output); + std::process::exit(match early_exit.status { + Ok(()) => 0, + Err(()) => 1, + }) + }) +} + +/// Types which can be constructed from a single commandline value. +/// +/// Any field type declared in a struct that derives `FromArgs` must implement +/// this trait. A blanket implementation exists for types implementing +/// `FromStr`. Custom types can implement this trait +/// directly. +pub trait FromArgValue: Sized { + /// Construct the type from a commandline value, returning an error string + /// on failure. + fn from_arg_value(value: &str) -> Result; +} + +impl FromArgValue for T +where + T: FromStr, + T::Err: std::fmt::Display, +{ + fn from_arg_value(value: &str) -> Result { + T::from_str(value).map_err(|x| x.to_string()) + } +} + +// The following items are all used by the generated code, and should not be considered part +// of this library's public API surface. + +// A trait for for slots that reserve space for a value and know how to parse that value +// from a command-line `&str` argument. +// +// This trait is only implemented for the type `ParseValueSlotTy`. This indirection is +// necessary to allow abstracting over `ParseValueSlotTy` instances with different +// generic parameters. +#[doc(hidden)] +pub trait ParseValueSlot { + fn fill_slot(&mut self, value: &str) -> Result<(), String>; +} + +// The concrete type implementing the `ParseValueSlot` trait. +// +// `T` is the type to be parsed from a single string. +// `Slot` is the type of the container that can hold a value or values of type `T`. +#[doc(hidden)] +pub struct ParseValueSlotTy { + // The slot for a parsed value. + pub slot: Slot, + // The function to parse the value from a string + pub parse_func: fn(&str) -> Result, +} + +// `ParseValueSlotTy, T>` is used as the slot for all non-repeating +// arguments, both optional and required. +impl ParseValueSlot for ParseValueSlotTy, T> { + fn fill_slot(&mut self, value: &str) -> Result<(), String> { + if self.slot.is_some() { + return Err("duplicate values provided".to_string()); + } + self.slot = Some((self.parse_func)(value)?); + Ok(()) + } +} + +// `ParseValueSlotTy, T>` is used as the slot for repeating arguments. +impl ParseValueSlot for ParseValueSlotTy, T> { + fn fill_slot(&mut self, value: &str) -> Result<(), String> { + self.slot.push((self.parse_func)(value)?); + Ok(()) + } +} + +/// A type which can be the receiver of a `Flag`. +pub trait Flag { + /// Creates a default instance of the flag value; + fn default() -> Self where Self: Sized; + /// Sets the flag. This function is called when the flag is provided. + fn set_flag(&mut self); +} + +impl Flag for bool { + fn default() -> Self { + false + } + fn set_flag(&mut self) { + *self = true; + } +} + +macro_rules! impl_flag_for_integers { + ($($ty:ty,)*) => { + $( + impl Flag for $ty { + fn default() -> Self { + 0 + } + fn set_flag(&mut self) { + *self = self.saturating_add(1); + } + } + )* + } +} + +impl_flag_for_integers![ + u8, u16, u32, u64, u128, + i8, i16, i32, i64, i128, +]; + +// `--` or `-` options, including a mutable reference to their value. +#[doc(hidden)] +pub enum CmdOption<'a> { + // A flag which is set to `true` when provided. + Flag(&'a mut dyn Flag), + // A value which is parsed from the string following the `--` argument, + // e.g. `--foo bar`. + Value(&'a mut dyn ParseValueSlot), +} + +#[doc(hidden)] +pub fn unrecognized_argument(x: &str) -> String { + ["Unrecognized argument: ", x, "\n"].concat() +} + +// A sentinel value that indicates that there is no +// output table mapping for the given flag. +// This is used for arguments like `--verbose` and `--quiet` +// that must be silently accepted if the `argh` user hasn't +// specified their behavior explicitly. +#[doc(hidden)] +pub const OUTPUT_TABLE_NONE: usize = std::usize::MAX; + +/// Parse a commandline option. +/// +/// `arg`: the current option argument being parsed (e.g. `--foo`). +/// `remaining_args`: the remaining command line arguments. This slice +/// will be advanced forwards if the option takes a value argument. +/// `output_table`: the storage for output data. +/// `arg_to_input`: a mapping from option string literals to the entry +/// in the output table. This may contain multiple entries mapping to +/// the same location in the table if both a short and long version +/// of the option exist (`-z` and `--zoo`). +#[doc(hidden)] +pub fn parse_option( + arg: &str, + remaining_args: &mut &[&str], + output_table: &mut [CmdOption<'_>], + arg_to_output: &[(&str, usize)], +) -> Result<(), String> { + let pos = arg_to_output + .iter() + .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None }) + .ok_or_else(|| unrecognized_argument(arg))?; + + if pos == OUTPUT_TABLE_NONE { + return Ok(()); + } + + match &mut output_table[pos] { + CmdOption::Flag(b) => b.set_flag(), + CmdOption::Value(pvs) => { + let value = remaining_args.get(0).ok_or_else(|| { + ["No value provided for option '", arg, "'.\n"].concat() + })?; + *remaining_args = &remaining_args[1..]; + pvs.fill_slot(value).map_err(|s| { + ["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"].concat() + })?; + } + } + + Ok(()) +} + +/// Parse a positional argument. +/// +/// arg: the argument supplied by the user +/// positional: a tuple containing slot to parse into and the name of the argument +#[doc(hidden)] +pub fn parse_positional( + arg: &str, + positional: &mut (&mut dyn ParseValueSlot, &'static str), +) -> Result<(), String> { + let (slot, name) = positional; + slot.fill_slot(arg).map_err(|s| { + ["Error parsing positional argument '", name, "' with value '", arg, ": ", &s].concat() + }) +} + +// Prepend `help` to a list of arguments. +// This is used to pass the `help` argument on to subcommands. +#[doc(hidden)] +pub fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> { + [&["help"], args].concat() +} + +#[doc(hidden)] +pub fn print_subcommands(commands: &[&CommandInfo]) -> String { + let mut out = String::new(); + for cmd in commands { + argh_shared::write_description(&mut out, cmd); + } + out +} + +#[doc(hidden)] +pub fn expected_subcommand(commands: &[&str]) -> String { + ["Expected one of the following subcommands: ", &commands.join(", "), "\n"].concat() +} + +#[doc(hidden)] +pub fn unrecognized_arg(arg: &str) -> String { + ["Unrecognized argument: ", arg, "\n"].concat() +} + +// An error string builder to report missing required options and subcommands. +#[doc(hidden)] +#[derive(Default)] +pub struct MissingRequirements { + options: Vec<&'static str>, + subcommands: Option<&'static [&'static CommandInfo]>, + positional_args: Vec<&'static str>, +} + +const NEWLINE_INDENT: &str = "\n "; + +impl MissingRequirements { + // Add a missing required option. + #[doc(hidden)] + pub fn missing_option(&mut self, name: &'static str) { + self.options.push(name) + } + + // Add a missing required subcommand. + #[doc(hidden)] + pub fn missing_subcommands(&mut self, commands: &'static [&'static CommandInfo]) { + self.subcommands = Some(commands); + } + + // Add a missing positional argument. + #[doc(hidden)] + pub fn missing_positional_arg(&mut self, name: &'static str) { + self.positional_args.push(name) + } + + // If any missing options or subcommands were provided, returns an error string + // describing the missing args. + #[doc(hidden)] + pub fn err_on_any(&self) -> Result<(), String> { + if self.options.is_empty() + && self.subcommands.is_none() + && self.positional_args.is_empty() + { + return Ok(()); + } + + let mut output = String::new(); + + if !self.positional_args.is_empty() { + output.push_str("Required positional arguments not provided:"); + for arg in &self.positional_args { + output.push_str(NEWLINE_INDENT); + output.push_str(arg); + } + } + + if !self.options.is_empty() { + output.push_str("Required options not provided:"); + for option in &self.options { + output.push_str(NEWLINE_INDENT); + output.push_str(option); + } + } + + if let Some(missing_subcommands) = self.subcommands { + if !self.options.is_empty() { + output.push_str("\n"); + } + output.push_str("One of the following subcommands must be present:"); + output.push_str(NEWLINE_INDENT); + output.push_str("help"); + for subcommand in missing_subcommands { + output.push_str(NEWLINE_INDENT); + output.push_str(subcommand.name); + } + } + + output.push('\n'); + + Err(output) + } +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..7cfa7df --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,782 @@ +#![cfg(test)] +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + + +use {argh::FromArgs, std::fmt::Debug}; + +#[test] +fn basic_example() { + #[derive(FromArgs, PartialEq, Debug)] + /// Reach new heights. + struct GoUp { + /// whether or not to jump + #[argh(switch, short = 'j')] + jump: bool, + + /// how high to go + #[argh(option)] + height: usize, + + /// an optional nickname for the pilot + #[argh(option)] + pilot_nickname: Option, + } + + let up = GoUp::from_args(&["cmdname"], &["--height", "5"]).expect("failed go_up"); + assert_eq!(up, GoUp { jump: false, height: 5, pilot_nickname: None }); +} + +#[test] +fn custom_from_str_example() { + #[derive(FromArgs)] + /// Goofy thing. + struct FiveStruct { + /// always five + #[argh(option, from_str_fn(always_five))] + five: usize, + } + + fn always_five(_value: &str) -> Result { + Ok(5) + } + + let f = FiveStruct::from_args(&["cmdname"], &["--five", "woot"]).expect("failed to five"); + assert_eq!(f.five, 5); +} + +#[test] +fn subcommand_example() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + let one = TopLevel::from_args(&["cmdname"], &["one", "--x", "2"]).expect("sc 1"); + assert_eq!(one, TopLevel { nested: MySubCommandEnum::One(SubCommandOne { x: 2 }) },); + + let two = TopLevel::from_args(&["cmdname"], &["two", "--fooey"]).expect("sc 2"); + assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); +} + +#[test] +fn multiline_doc_comment_description() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + + assert_help_string::( + r###"Usage: test_arg_0 [--s] + +Short description + +Options: + --s a switch with a description that is spread across a number + of lines of comments. + --help display usage information +"###, + ); +} + +#[test] +fn explicit_long_value_for_option() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option, long = "foo")] + /// bar bar + x: u8, + } + + let cmd = Cmd::from_args(&["cmdname"], &["--foo", "5"]).unwrap(); + assert_eq!(cmd.x, 5); +} + +/// Test that descriptions can start with an initialism despite +/// usually being required to start with a lowercase letter. +#[derive(FromArgs)] +#[allow(unused)] +struct DescriptionStartsWithInitialism { + /// URL fooey + #[argh(option)] + x: u8, +} + +#[test] +fn default_number() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(option, default = "5")] + /// fooey + x: u8, + } + + let cmd = Cmd::from_args(&["cmdname"], &[]).unwrap(); + assert_eq!(cmd.x, 5); +} + +#[test] +fn default_function() { + const MSG: &str = "hey I just met you"; + fn call_me_maybe() -> String { + MSG.to_string() + } + + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(option, default = "call_me_maybe()")] + /// fooey + msg: String, + } + + let cmd = Cmd::from_args(&["cmdname"], &[]).unwrap(); + assert_eq!(cmd.msg, MSG); +} + +#[test] +fn missing_option_value() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// fooey + msg: String, + } + + let e = Cmd::from_args(&["cmdname"], &["--msg"]) + .expect_err("Parsing missing option value should fail"); + assert_eq!(e.output, "No value provided for option \'--msg\'.\n"); + assert!(e.status.is_err()); +} + +fn assert_help_string(help_str: &str) { + match T::from_args(&["test_arg_0"], &["--help"]) { + Ok(_) => panic!("help was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help returned an error"); + } + } +} + +fn assert_output(args: &[&str], expected: T) { + let t = T::from_args(&["cmd"], args).expect("failed to parse"); + assert_eq!(t, expected); +} + +fn assert_error(args: &[&str], err_msg: &str) { + let e = T::from_args(&["cmd"], args).expect_err("unexpectedly succeeded parsing"); + assert_eq!(err_msg, e.output); + e.status.expect_err("error had a positive status"); +} + +mod positional { + use super::*; + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastRepeating { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: Vec, + } + + #[test] + fn repeating() { + assert_output(&["5"], LastRepeating { a: 5, b: vec![] }); + assert_output(&["5", "foo"], LastRepeating { a: 5, b: vec!["foo".into()] }); + assert_output( + &["5", "foo", "bar"], + LastRepeating { a: 5, b: vec!["foo".into(), "bar".into()] }, + ); + assert_help_string::( + r###"Usage: test_arg_0 [] + +Woot + +Options: + --help display usage information +"###, + ); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastOptional { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: Option, + } + + #[test] + fn optional() { + assert_output(&["5"], LastOptional { a: 5, b: None }); + assert_output(&["5", "6"], LastOptional { a: 5, b: Some("6".into()) }); + assert_error::(&["5", "6", "7"], "Unrecognized argument: 7\n"); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastDefaulted { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional, default = "5")] + /// fooey + b: u32, + } + + #[test] + fn defaulted() { + assert_output(&["5"], LastDefaulted { a: 5, b: 5 }); + assert_output(&["5", "6"], LastDefaulted { a: 5, b: 6 }); + assert_error::(&["5", "6", "7"], "Unrecognized argument: 7\n"); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastRequired { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: u32, + } + + #[test] + fn required() { + assert_output(&["5", "6"], LastRequired { a: 5, b: 6 }); + assert_error::( + &[], + r###"Required positional arguments not provided: + a + b +"###, + ); + assert_error::( + &["5"], + r###"Required positional arguments not provided: + b +"###, + ); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct WithSubcommand { + #[argh(positional)] + /// fooey + a: String, + #[argh(subcommand)] + /// fooey + b: Subcommand, + #[argh(positional)] + /// fooey + c: Vec, + } + + #[derive(FromArgs, Debug, PartialEq)] + #[argh(subcommand, name = "a")] + /// Subcommand of positional::WithSubcommand. + struct Subcommand { + #[argh(positional)] + /// fooey + a: String, + #[argh(positional)] + /// fooey + b: Vec, + } + + #[test] + fn mixed_with_subcommand() { + assert_output( + &["first", "a", "a"], + WithSubcommand { + a: "first".into(), + b: Subcommand { a: "a".into(), b: vec![] }, + c: vec![], + }, + ); + + assert_error::( + &["a", "a", "a"], + r###"Required positional arguments not provided: + a +"###, + ); + + assert_output( + &["1", "2", "3", "a", "b", "c"], + WithSubcommand { + a: "1".into(), + b: Subcommand { a: "b".into(), b: vec!["c".into()] }, + c: vec!["2".into(), "3".into()], + }, + ); + } +} + +/// Tests derived from +/// https://fuchsia.dev/fuchsia-src/development/api/cli and +/// https://fuchsia.dev/fuchsia-src/development/api/cli_help +mod fuchsia_commandline_tools_rubric { + use super::*; + + /// Tests for the three required command line argument types: + /// - exact text + /// - arguments + /// - options (i.e. switches and keys) + #[test] + fn three_command_line_argument_types() { + // TODO(cramertj) add support for exact text and positional arguments + } + + /// A piece of exact text may be required or optional + #[test] + fn exact_text_required_and_optional() { + // TODO(cramertj) add support for exact text + } + + /// Arguments are like function parameters or slots for data. + /// The order often matters. + #[test] + fn arguments_ordered() { + // TODO(cramertj) add support for ordered positional arguments + } + + /// If a single argument is repeated, order may not matter, e.g. `...` + #[test] + fn arguments_unordered() { + // TODO(cramertj) add support for repeated positional arguments + } + + // Short argument names must use one dash and a single letter. + // TODO(cramertj): this should be a compile-fail test + + // Short argument names are optional, but all choices are required to have a `--` option. + // TODO(cramertj): this should be a compile-fail test + + // Numeric options, such as `-1` and `-2`, are not allowed. + // TODO(cramertj): this should be a compile-fail test + + #[derive(FromArgs)] + /// One switch. + struct OneSwitch { + #[argh(switch, short = 's')] + /// just a switch + switchy: bool, + } + + /// The presence of a switch means the feature it represents is "on", + /// while its absence means that it is "off". + #[test] + fn switch_on_when_present() { + let on = OneSwitch::from_args(&["cmdname"], &["-s"]).expect("parsing on"); + assert!(on.switchy); + + let off = OneSwitch::from_args(&["cmdname"], &[]).expect("parsing off"); + assert!(!off.switchy); + } + + #[derive(FromArgs, Debug)] + /// Two Switches + struct TwoSwitches { + #[argh(switch, short = 'a')] + /// a + a: bool, + #[argh(switch, short = 'b')] + /// b + b: bool, + } + + /// Running switches together is not allowed + #[test] + fn switches_cannot_run_together() { + TwoSwitches::from_args(&["cmdname"], &["-a", "-b"]) + .expect("parsing separate should succeed"); + TwoSwitches::from_args(&["cmdname"], &["-ab"]).expect_err("parsing together should fail"); + } + + #[derive(FromArgs, Debug)] + /// One keyed option + struct OneOption { + #[argh(option)] + /// some description + foo: String, + } + + /// Do not use an equals punctuation or similar to separate the key and value. + #[test] + fn keyed_no_equals() { + OneOption::from_args(&["cmdname"], &["--foo", "bar"]) + .expect("Parsing option value as separate arg should succeed"); + + let e = OneOption::from_args(&["cmdname"], &["--foo=bar"]) + .expect_err("Parsing option value using `=` should fail"); + assert_eq!(e.output, "Unrecognized argument: --foo=bar\n"); + assert!(e.status.is_err()); + } + + // Two dashes on their own indicates the end of options. + // Subsequent values are given to the tool as-is. + // + // It's unclear exactly what "are given to the tool as-is" in means in this + // context, so we provide a few options for handling `--`, with it being + // an error by default. + // + // TODO(cramertj) implement some behavior for `--` + + /// Double-dash is treated as an error by default. + #[test] + fn double_dash_default_error() {} + + /// Double-dash can be ignored for later manual parsing. + #[test] + fn double_dash_ignore() {} + + /// Double-dash should be treated as the end of flags and optional arguments, + /// and the remainder of the values should be treated purely as positional arguments, + /// even when their syntax matches that of options. e.g. `foo -- -e` should be parsed + /// as passing a single positional argument with the value `-e`. + #[test] + fn double_dash_positional() {} + + /// Double-dash can be parsed into an optional field using a provided + /// `fn(&[&str]) -> Result`. + #[test] + fn double_dash_custom() {} + + /// Repeating switches may be used to apply more emphasis. + /// A common example is increasing verbosity by passing more `-v` switches. + #[test] + fn switches_repeating() { + #[derive(FromArgs, Debug)] + /// A type for testing repeating `-v` + struct CountVerbose { + #[argh(switch, short = 'v')] + /// increase the verbosity of the command. + verbose: i128, + } + + let cv = CountVerbose::from_args(&["cmdname"], &["-v", "-v", "-v"]) + .expect("Parsing verbose flags should succeed"); + assert_eq!(cv.verbose, 3); + } + + // When a tool has many subcommands, it should also have a help subcommand + // that displays help about the subcommands, e.g. `fx help build`. + // + // Elsewhere in the docs, it says the syntax `--help` is required, so we + // interpret that to mean: + // + // - `help` should always be accepted as a "keyword" in place of the first + // positional argument for both the main command and subcommands. + // + // - If followed by the name of a subcommand it should forward to the + // `--help` of said subcommand, otherwise it will fall back to the + // help of the righmost command / subcommand. + // + // - `--help` will always consider itself the only meaningful argument to + // the rightmost command / subcommand, and any following arguments will + // be treated as an error. + + #[derive(FromArgs, Debug)] + /// A type for testing `--help`/`help` + struct HelpTopLevel { + #[argh(subcommand)] + sub: HelpFirstSub, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "first")] + /// First subcommmand for testing `help`. + struct HelpFirstSub { + #[argh(subcommand)] + sub: HelpSecondSub, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "second")] + /// Second subcommand for testing `help`. + struct HelpSecondSub {} + + fn expect_help(args: &[&str], expected_help_string: &str) { + let e = HelpTopLevel::from_args(&["cmdname"], args).expect_err("should exit early"); + assert_eq!(expected_help_string, e.output); + e.status.expect("help returned an error"); + } + + const MAIN_HELP_STRING: &str = r###"Usage: cmdname [] + +A type for testing `--help`/`help` + +Options: + --help display usage information + +Commands: + first First subcommmand for testing `help`. +"###; + + const FIRST_HELP_STRING: &str = r###"Usage: cmdname first [] + +First subcommmand for testing `help`. + +Options: + --help display usage information + +Commands: + second Second subcommand for testing `help`. +"###; + + const SECOND_HELP_STRING: &str = r###"Usage: cmdname first second + +Second subcommand for testing `help`. + +Options: + --help display usage information +"###; + + #[test] + fn help_keyword_main() { + expect_help(&["help"], MAIN_HELP_STRING) + } + + #[test] + fn help_keyword_with_following_subcommand() { + expect_help(&["help", "first"], FIRST_HELP_STRING); + } + + #[test] + fn help_keyword_between_subcommands() { + expect_help(&["first", "help", "second"], SECOND_HELP_STRING); + } + + #[test] + fn help_keyword_with_two_trailing_subcommands() { + expect_help(&["help", "first", "second"], SECOND_HELP_STRING); + } + + #[test] + fn help_flag_main() { + expect_help(&["--help"], MAIN_HELP_STRING); + } + + #[test] + fn help_flag_subcommand() { + expect_help(&["first", "--help"], FIRST_HELP_STRING); + } + + #[test] + fn help_flag_trailing_arguments_are_an_error() { + let e = OneOption::from_args(&["cmdname"], &["--help", "--foo", "bar"]) + .expect_err("should exit early"); + assert_eq!("Trailing arguments are not allowed after `help`.", e.output); + e.status.expect_err("should be an error"); + } + + // Commandline tools are expected to support common switches: + // --help + // --quiet + // --verbose + // --version + + // help_is_supported (see above help_* tests) + + #[test] + fn quiet_is_supported() { + // TODO support quiet + } + + #[test] + fn verbose_is_supported() { + // TODO support verbose + } + + #[test] + fn version_is_supported() { + // TODO support version + } + + #[test] + fn quiet_is_not_supported_in_subcommands() { + // TODO support quiet + } + + #[test] + fn verbose_is_not_supported_in_subcommands() { + // TODO support verbose + } + + #[test] + fn version_is_not_supported_in_subcommands() { + // TODO support version + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh( + description = "Destroy the contents of .", + example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", + note = "Use `{command_name} help ` for details on [] for a subcommand.", + error_code(2, "The blade is too dull."), + error_code(3, "Out of fuel.") + )] + struct HelpExample { + /// force, ignore minor errors. This description is so long that it wraps to the next line. + #[argh(switch, short = 'f')] + force: bool, + + /// documentation + #[argh(switch)] + really_really_really_long_name_for_pat: bool, + + /// write repeatedly + #[argh(option, short = 's')] + scribble: String, + + /// say more. Defaults to $BLAST_VERBOSE. + #[argh(switch, short = 'v')] + verbose: bool, + + #[argh(subcommand)] + command: HelpExampleSubCommands, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum HelpExampleSubCommands { + BlowUp(BlowUp), + Grind(GrindCommand), + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "blow-up")] + /// explosively separate + struct BlowUp { + /// blow up bombs safely + #[argh(switch)] + safely: bool, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")] + struct GrindCommand { + /// wear a visor while grinding + #[argh(switch)] + safely: bool, + } + + #[test] + fn example_parses_correctly() { + let help_example = HelpExample::from_args( + &["<<>>"], + &["-f", "--scribble", "fooey", "blow-up", "--safely"], + ) + .unwrap(); + + assert_eq!( + help_example, + HelpExample { + force: true, + scribble: "fooey".to_string(), + really_really_really_long_name_for_pat: false, + verbose: false, + command: HelpExampleSubCommands::BlowUp(BlowUp { safely: true }), + }, + ); + } + + #[test] + fn example_errors_on_missing_required_option_and_missing_required_subcommand() { + let exit = HelpExample::from_args(&["<<>>"], &[]).unwrap_err(); + exit.status.unwrap_err(); + assert_eq!( + exit.output, + concat!( + "Required options not provided:\n", + " --scribble\n", + "One of the following subcommands must be present:\n", + " help\n", + " blow-up\n", + " grind\n", + ), + ); + } + + #[test] + fn help_example() { + assert_help_string::( + r###"Usage: test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] [] + +Destroy the contents of . + +Options: + -f, --force force, ignore minor errors. This description is so long that + it wraps to the next line. + --really-really-really-long-name-for-pat + documentation + -s, --scribble write repeatedly + -v, --verbose say more. Defaults to $BLAST_VERBOSE. + --help display usage information + +Commands: + blow-up explosively separate + grind make smaller by many small cuts + +Examples: + Scribble 'abc' and then run |grind|. + $ test_arg_0 -s 'abc' grind old.txt taxes.cp + +Notes: + Use `test_arg_0 help ` for details on [] for a subcommand. + +Error codes: + 2 The blade is too dull. + 3 Out of fuel. +"###, + ); + } +}