mirror of
https://github.com/topjohnwu/argh.git
synced 2024-11-23 11:49:45 +00:00
Migrate code into into stand-alone repo
Adds Cargo.toml files
This commit is contained in:
parent
d850e7bf68
commit
c752b5f504
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
*.swp
|
76
Cargo.lock
generated
Normal file
76
Cargo.lock
generated
Normal file
@ -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"
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "argh"
|
||||
version = "0.1.0"
|
||||
authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain<bwb@google.com>"]
|
||||
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"}
|
@ -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.
|
||||
|
18
argh_derive/Cargo.toml
Normal file
18
argh_derive/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "argh_derive"
|
||||
version = "0.1.0"
|
||||
authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain<bwb@google.com>"]
|
||||
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" }
|
161
argh_derive/src/errors.rs
Normal file
161
argh_derive/src/errors.rs
Normal file
@ -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<Vec<syn::Error>>,
|
||||
}
|
||||
|
||||
/// 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()));
|
||||
}
|
||||
}
|
219
argh_derive/src/help.rs
Normal file
219
argh_derive/src/help.rs
Normal file
@ -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("<command>");
|
||||
if !subcommand.optionality.is_required() {
|
||||
format_lit.push(']');
|
||||
}
|
||||
format_lit.push_str(" [<args>]");
|
||||
}
|
||||
|
||||
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 `[<foo>...]` 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 <foo>]` 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<Description>,
|
||||
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<char>,
|
||||
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);
|
||||
}
|
748
argh_derive/src/lib.rs
Normal file
748
argh_derive/src/lib.rs
Normal file
@ -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<Optionality> for Optionality {
|
||||
fn eq(&self, other: &Optionality) -> bool {
|
||||
use Optionality::*;
|
||||
match (self, other) {
|
||||
(None, None) | (Optional, Optional) | (Repeating, Repeating) => true,
|
||||
// NB: (Defaulted, Defaulted) can't contain the same token streams
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<T>` or `Option<T>`, 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<String>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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<Self, argh::EarlyExit>
|
||||
{
|
||||
#( #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<FieldType>` 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<Item = TokenStream> + '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<Item = TokenStream> + '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<TokenStream> {
|
||||
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<Item = TokenStream> + '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<T>` 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 `<name>(<ty>)`
|
||||
struct SubCommandVariant<'a> {
|
||||
name: &'a syn::Ident,
|
||||
ty: &'a syn::Type,
|
||||
}
|
||||
|
||||
let variants: Vec<SubCommandVariant<'_>> = de
|
||||
.variants
|
||||
.iter()
|
||||
.filter_map(|variant| {
|
||||
parse_attrs::check_enum_variant_attrs(errors, variant);
|
||||
let name = &variant.ident;
|
||||
let ty = enum_only_single_field_unnamed_variants(errors, &variant.fields)?;
|
||||
Some(SubCommandVariant { name, ty })
|
||||
})
|
||||
.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<Self, argh::EarlyExit>
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
552
argh_derive/src/parse_attrs.rs
Normal file
552
argh_derive/src/parse_attrs.rs
Normal file
@ -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<syn::LitStr>,
|
||||
pub description: Option<Description>,
|
||||
pub from_str_fn: Option<syn::Ident>,
|
||||
pub field_type: Option<FieldType>,
|
||||
pub long: Option<syn::LitStr>,
|
||||
pub short: Option<syn::LitChar>,
|
||||
}
|
||||
|
||||
/// 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<syn::Ident>,
|
||||
) {
|
||||
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<FieldType>,
|
||||
) {
|
||||
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 `#[<name> ...]`
|
||||
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<R: Clone>(
|
||||
errors: &Errors,
|
||||
attr: &syn::Attribute,
|
||||
f: impl FnOnce(&syn::Meta) -> Option<&R>,
|
||||
) -> Option<R> {
|
||||
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<syn::MetaList> {
|
||||
attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_list(m))
|
||||
}
|
||||
|
||||
fn attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaNameValue> {
|
||||
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<syn::MetaList> {
|
||||
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<syn::Ident>,
|
||||
pub name: Option<syn::LitStr>,
|
||||
pub description: Option<Description>,
|
||||
pub examples: Vec<syn::LitStr>,
|
||||
pub notes: Vec<syn::LitStr>,
|
||||
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<u64, usize> = HashMap::new();
|
||||
for (index, (lit_int, _lit_str)) in self.error_codes.iter().enumerate() {
|
||||
let value = match lit_int.base10_parse::<u64>() {
|
||||
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<syn::LitStr>,
|
||||
) {
|
||||
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<syn::LitStr>) {
|
||||
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<Description>) {
|
||||
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<Description>) {
|
||||
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.",
|
||||
),
|
||||
);
|
||||
}
|
8
argh_shared/Cargo.toml
Normal file
8
argh_shared/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "argh_shared"
|
||||
version = "0.1.0"
|
||||
authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain<bwb@google.com>"]
|
||||
edition = "2018"
|
||||
license = "BSD License 2.0"
|
||||
description = "Derive-based argument parsing optimized for code size"
|
||||
repository = "https://github.com/google/argh"
|
75
argh_shared/src/lib.rs
Normal file
75
argh_shared/src/lib.rs
Normal file
@ -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);
|
||||
}
|
526
src/lib.rs
Normal file
526
src/lib.rs
Normal file
@ -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<String>,
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let up: GoUp = argh::from_env();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! `./some_bin --help` will then output the following:
|
||||
//!
|
||||
//! ```bash
|
||||
//! Usage: cmdname [-j] --height <height> [--pilot-nickname <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 = "<your_code_here>")]` 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<T, String>` 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<usize, String> {
|
||||
//! 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<Self, EarlyExit>;
|
||||
}
|
||||
|
||||
/// 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<T: SubCommand> 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<String> 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: TopLevelCommand>() -> T {
|
||||
let strings: Vec<String> = 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: TopLevelCommand>() -> T {
|
||||
let strings: Vec<String> = 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<Error: Display>`. 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<Self, String>;
|
||||
}
|
||||
|
||||
impl<T> FromArgValue for T
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
fn from_arg_value(value: &str) -> Result<Self, String> {
|
||||
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<Slot, T> {
|
||||
// The slot for a parsed value.
|
||||
pub slot: Slot,
|
||||
// The function to parse the value from a string
|
||||
pub parse_func: fn(&str) -> Result<T, String>,
|
||||
}
|
||||
|
||||
// `ParseValueSlotTy<Option<T>, T>` is used as the slot for all non-repeating
|
||||
// arguments, both optional and required.
|
||||
impl<T> ParseValueSlot for ParseValueSlotTy<Option<T>, 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<Vec<T>, T>` is used as the slot for repeating arguments.
|
||||
impl<T> ParseValueSlot for ParseValueSlotTy<Vec<T>, 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)
|
||||
}
|
||||
}
|
782
tests/lib.rs
Normal file
782
tests/lib.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
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<usize, String> {
|
||||
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::<Cmd>(
|
||||
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<T: FromArgs>(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<T: FromArgs + Debug + PartialEq>(args: &[&str], expected: T) {
|
||||
let t = T::from_args(&["cmd"], args).expect("failed to parse");
|
||||
assert_eq!(t, expected);
|
||||
}
|
||||
|
||||
fn assert_error<T: FromArgs + Debug>(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<String>,
|
||||
}
|
||||
|
||||
#[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::<LastRepeating>(
|
||||
r###"Usage: test_arg_0 <a> [<b...>]
|
||||
|
||||
Woot
|
||||
|
||||
Options:
|
||||
--help display usage information
|
||||
"###,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(FromArgs, Debug, PartialEq)]
|
||||
/// Woot
|
||||
struct LastOptional {
|
||||
#[argh(positional)]
|
||||
/// fooey
|
||||
a: u32,
|
||||
#[argh(positional)]
|
||||
/// fooey
|
||||
b: Option<String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional() {
|
||||
assert_output(&["5"], LastOptional { a: 5, b: None });
|
||||
assert_output(&["5", "6"], LastOptional { a: 5, b: Some("6".into()) });
|
||||
assert_error::<LastOptional>(&["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::<LastDefaulted>(&["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::<LastRequired>(
|
||||
&[],
|
||||
r###"Required positional arguments not provided:
|
||||
a
|
||||
b
|
||||
"###,
|
||||
);
|
||||
assert_error::<LastRequired>(
|
||||
&["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<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, Debug, PartialEq)]
|
||||
#[argh(subcommand, name = "a")]
|
||||
/// Subcommand of positional::WithSubcommand.
|
||||
struct Subcommand {
|
||||
#[argh(positional)]
|
||||
/// fooey
|
||||
a: String,
|
||||
#[argh(positional)]
|
||||
/// fooey
|
||||
b: Vec<String>,
|
||||
}
|
||||
|
||||
#[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::<WithSubcommand>(
|
||||
&["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. `<files>...`
|
||||
#[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<T, EarlyExit>`.
|
||||
#[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 <command> [<args>]
|
||||
|
||||
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 <command> [<args>]
|
||||
|
||||
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 <file>.",
|
||||
example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp",
|
||||
note = "Use `{command_name} help <command>` for details on [<args>] 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 <scribble> 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(
|
||||
&["<<<arg0>>>"],
|
||||
&["-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(&["<<<arg0>>>"], &[]).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::<HelpExample>(
|
||||
r###"Usage: test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s <scribble> [-v] <command> [<args>]
|
||||
|
||||
Destroy the contents of <file>.
|
||||
|
||||
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 <scribble> 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 <command>` for details on [<args>] for a subcommand.
|
||||
|
||||
Error codes:
|
||||
2 The blade is too dull.
|
||||
3 Out of fuel.
|
||||
"###,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user