diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e22ce4a..cb4f1b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +* Support `Option>` type for fields by [@sphynx](https://github.com/sphynx) + ([#188](https://github.com/TeXitoi/structopt/issues/188)) + # v0.2.15 (2018-03-08) * Fix [#168](https://github.com/TeXitoi/structopt/issues/168) by [@TeXitoi](https://github.com/TeXitoi) diff --git a/examples/example.rs b/examples/example.rs index c188794f..e7d4f9b0 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -30,6 +30,17 @@ struct Opt { /// command line. #[structopt(help = "Output file, stdout if not present")] output: Option, + + /// An optional parameter with optional value, will be `None` if + /// not present on the command line, will be `Some(None)` if no + /// argument is provided (i.e. `--log`) and will be + /// `Some(Some(String))` if argument is provided (e.g. `--log + /// log.txt`). + #[structopt( + long = "log", + help = "Log file, stdout if no file, no logging if not present" + )] + log: Option>, } fn main() { diff --git a/src/lib.rs b/src/lib.rs index e37e6823..8a23a537 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,12 +104,13 @@ //! //! The type of the field gives the kind of argument: //! -//! Type | Effect | Added method call to `clap::Arg` -//! ---------------------|---------------------------------------------------|-------------------------------------- -//! `bool` | `true` if the flag is present | `.takes_value(false).multiple(false)` -//! `Option` | optional positional argument or option | `.takes_value(true).multiple(false)` -//! `Vec` | list of options or the other positional arguments | `.takes_value(true).multiple(true)` -//! `T: FromStr` | required option or positional argument | `.takes_value(true).multiple(false).required(!has_default)` +//! Type | Effect | Added method call to `clap::Arg` +//! -----------------------------|---------------------------------------------------|-------------------------------------- +//! `bool` | `true` if the flag is present | `.takes_value(false).multiple(false)` +//! `Option` | optional positional argument or option | `.takes_value(true).multiple(false)` +//! `Option>` | optional option with optional value | `.takes_value(true).multiple(false).min_values(0).max_values(1)` +//! `Vec` | list of options or the other positional arguments | `.takes_value(true).multiple(true)` +//! `T: FromStr` | required option or positional argument | `.takes_value(true).multiple(false).required(!has_default)` //! //! The `FromStr` trait is used to convert the argument to the given //! type, and the `Arg::validator` method is set to a method using diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs index a1c873bd..80625fbb 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -10,7 +10,10 @@ use heck::{CamelCase, KebabCase, MixedCase, ShoutySnakeCase, SnakeCase}; use proc_macro2::{Span, TokenStream}; use std::{env, mem}; use syn::Type::Path; -use syn::{self, Attribute, Ident, LitStr, MetaList, MetaNameValue, TypePath}; +use syn::{ + self, AngleBracketedGenericArguments, Attribute, GenericArgument, Ident, LitStr, MetaList, + MetaNameValue, PathArguments, PathSegment, TypePath, +}; #[derive(Copy, Clone, PartialEq, Debug)] pub enum Kind { @@ -23,6 +26,7 @@ pub enum Ty { Bool, Vec, Option, + OptionOption, Other, } #[derive(Debug)] @@ -400,7 +404,10 @@ impl Attrs { { match segments.iter().last().unwrap().ident.to_string().as_str() { "bool" => Ty::Bool, - "Option" => Ty::Option, + "Option" => match sub_type(ty).map(Attrs::ty_from_field) { + Some(Ty::Option) => Ty::OptionOption, + _ => Ty::Option, + }, "Vec" => Ty::Vec, _ => Ty::Other, } @@ -430,7 +437,11 @@ impl Attrs { if !res.methods.iter().all(|m| m.name == "help") { panic!("methods in attributes is not allowed for subcommand"); } - res.kind = Kind::Subcommand(Self::ty_from_field(&field.ty)); + let ty = Self::ty_from_field(&field.ty); + if ty == Ty::OptionOption { + panic!("Option> type is not allowed for subcommand") + } + res.kind = Kind::Subcommand(ty); } Kind::Arg(_) => { let mut ty = Self::ty_from_field(&field.ty); @@ -457,6 +468,12 @@ impl Attrs { panic!("required is meaningless for Option") } } + Ty::OptionOption => { + // If it's a positional argument. + if !(res.has_method("long") || res.has_method("short")) { + panic!("Option> type is meaningless for positional argument") + } + } _ => (), } res.kind = Kind::Arg(ty); @@ -495,3 +512,27 @@ impl Attrs { self.casing } } + +pub fn sub_type(t: &syn::Type) -> Option<&syn::Type> { + let segs = match *t { + syn::Type::Path(TypePath { + path: syn::Path { ref segments, .. }, + .. + }) => segments, + _ => return None, + }; + match *segs.iter().last().unwrap() { + PathSegment { + arguments: + PathArguments::AngleBracketed(AngleBracketedGenericArguments { ref args, .. }), + .. + } if args.len() == 1 => { + if let GenericArgument::Type(ref ty) = args[0] { + Some(ty) + } else { + None + } + } + _ => None, + } +} diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 79dc451c..69997cf2 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -19,7 +19,7 @@ extern crate proc_macro2; mod attrs; -use attrs::{Attrs, CasingStyle, Kind, Parser, Ty}; +use attrs::{sub_type, Attrs, CasingStyle, Kind, Parser, Ty}; use proc_macro2::{Span, TokenStream}; use syn::punctuated::Punctuated; use syn::token::Comma; @@ -46,30 +46,6 @@ pub fn structopt(input: proc_macro::TokenStream) -> proc_macro::TokenStream { gen.into() } -fn sub_type(t: &syn::Type) -> Option<&syn::Type> { - let segs = match *t { - syn::Type::Path(TypePath { - path: syn::Path { ref segments, .. }, - .. - }) => segments, - _ => return None, - }; - match *segs.iter().last().unwrap() { - PathSegment { - arguments: - PathArguments::AngleBracketed(AngleBracketedGenericArguments { ref args, .. }), - .. - } if args.len() == 1 => { - if let GenericArgument::Type(ref ty) = args[0] { - Some(ty) - } else { - None - } - } - _ => None, - } -} - /// Generate a block of code to add arguments/subcommands corresponding to /// the `fields` to an app. fn gen_augmentation( @@ -129,6 +105,7 @@ fn gen_augmentation( Kind::Arg(ty) => { let convert_type = match ty { Ty::Vec | Ty::Option => sub_type(&field.ty).unwrap_or(&field.ty), + Ty::OptionOption => sub_type(&field.ty).and_then(sub_type).unwrap_or(&field.ty), _ => &field.ty, }; @@ -158,6 +135,9 @@ fn gen_augmentation( let modifier = match ty { Ty::Bool => quote!( .takes_value(false).multiple(false) ), Ty::Option => quote!( .takes_value(true).multiple(false) #validator ), + Ty::OptionOption => { + quote! ( .takes_value(true).multiple(false).min_values(0).max_values(1) #validator ) + } Ty::Vec => quote!( .takes_value(true).multiple(true) #validator ), Ty::Other if occurrences => quote!( .takes_value(false).multiple(true) ), Ty::Other => { @@ -229,6 +209,13 @@ fn gen_constructor(fields: &Punctuated, parent_attribute: &Attrs) matches.#value_of(#name) .map(#parse) }, + Ty::OptionOption => quote! { + if matches.is_present(#name) { + Some(matches.#value_of(#name).map(#parse)) + } else { + None + } + }, Ty::Vec => quote! { matches.#values_of(#name) .map(|v| v.map(#parse).collect()) diff --git a/tests/options.rs b/tests/options.rs index 072595d5..cd65cec8 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -151,3 +151,83 @@ fn option_from_str() { assert_eq!(Opt { a: None }, Opt::from_iter(&["test"])); assert_eq!(Opt { a: Some(A) }, Opt::from_iter(&["test", "foo"])); } + +#[test] +fn optional_argument_for_optional_option() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a")] + arg: Option>, + } + assert_eq!( + Opt { + arg: Some(Some(42)) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a42"])) + ); + assert_eq!( + Opt { arg: Some(None) }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a"])) + ); + assert_eq!( + Opt { arg: None }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); + assert!(Opt::clap() + .get_matches_from_safe(&["test", "-a42", "-a24"]) + .is_err()); +} + +#[test] +fn two_option_options() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(short = "a")] + arg: Option>, + + #[structopt(long = "field")] + field: Option>, + } + assert_eq!( + Opt { + arg: Some(Some(42)), + field: Some(Some("f".into())) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a42", "--field", "f"])) + ); + assert_eq!( + Opt { + arg: Some(Some(42)), + field: Some(None) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a42", "--field"])) + ); + assert_eq!( + Opt { + arg: Some(None), + field: Some(None) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a", "--field"])) + ); + assert_eq!( + Opt { + arg: Some(None), + field: Some(Some("f".into())) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "-a", "--field", "f"])) + ); + assert_eq!( + Opt { + arg: None, + field: Some(None) + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test", "--field"])) + ); + assert_eq!( + Opt { + arg: None, + field: None + }, + Opt::from_clap(&Opt::clap().get_matches_from(&["test"])) + ); +}