From 2d3e10f446024d8f6cbf76c650e29a617d210b41 Mon Sep 17 00:00:00 2001 From: CreepySkeleton Date: Sat, 21 Dec 2019 17:46:44 +0300 Subject: [PATCH 1/3] Default value for `default_value` (yeah, sounds awkward) Fix test --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/lib.rs | 59 +++++++++++++++++++++++++++++++---- structopt-derive/src/attrs.rs | 59 +++++++++++++++++++++++++++++++++-- structopt-derive/src/parse.rs | 3 ++ tests/default_value.rs | 19 +++++++++++ 6 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 tests/default_value.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8da55f..a4066553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * You don't have to apply `#[no_version]` to every `enum` variant anymore. Just annotate the `enum` and the setting will be propagated down ([#242](https://github.com/TeXitoi/structopt/issues/242)). +* [Auto-default](https://docs.rs/structopt/0.3/structopt/#default-values). # v0.3.7 (2019-12-28) diff --git a/Cargo.toml b/Cargo.toml index b43728bf..fad77c0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ travis-ci = { repository = "TeXitoi/structopt" } [dependencies] clap = { version = "2.33", default-features = false } structopt-derive = { path = "structopt-derive", version = "=0.4.0" } +lazy_static = "1.4.0" [dev-dependencies] trybuild = "1.0.5" diff --git a/src/lib.rs b/src/lib.rs index 70c0768c..e655290d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ //! - Arguments //! - [Type magic](#type-magic) //! - [Specifying argument types](#specifying-argument-types) +//! - [Default values](#default-values) //! - [Help messages](#help-messages) //! - [Environment variable fallback](#environment-variable-fallback) //! - [Skipping fields](#skipping-fields) @@ -259,7 +260,12 @@ //! //! Usable only on field-level. //! -//! - [`rename_all`](#specifying-argument-types): [`rename_all = "kebab"/"snake"/"screaming-snake"/"camel"/"pascal"/"verbatim"]` +//! - [`defautl_value`](#default-values): `default_value [= "default value"]` +//! +//! Usable only on field-level. +//! +//! - [`rename_all`](#specifying-argument-types): +//! [`rename_all = "kebab"/"snake"/"screaming-snake"/"camel"/"pascal"/"verbatim"]` //! //! Usable both on top level and field level. //! @@ -283,7 +289,8 @@ //! //! Usable only on field-level. //! -//! - [`rename_all_env`](##auto-deriving-environment-variables): [`rename_all_env = "kebab"/"snake"/"screaming-snake"/"camel"/"pascal"/"verbatim"]` +//! - [`rename_all_env`](##auto-deriving-environment-variables): +//! [`rename_all_env = "kebab"/"snake"/"screaming-snake"/"camel"/"pascal"/"verbatim"]` //! //! Usable both on top level and field level. //! @@ -315,7 +322,7 @@ //! If you would like to use a custom string parser other than `FromStr`, see //! the [same titled section](#custom-string-parsers) below. //! -//! **Note:** +//! **Important:** //! _________________ //! Pay attention that *only literal occurrence* of this types is special, for example //! `Option` is special while `::std::option::Option` is not. @@ -419,6 +426,47 @@ //! # } //! ``` //! +//! ## Default values +//! +//! In clap, default values for options can be specified via [`Arg::default_value`]. +//! +//! Of course, you can use as a raw method: +//! ``` +//! # use structopt::StructOpt; +//! #[derive(StructOpt)] +//! struct Opt { +//! #[structopt(default_value = "", long)] +//! prefix: String +//! } +//! ``` +//! +//! This is quite mundane and error-prone to type the `"..."` default by yourself, +//! especially when the Rust ecosystem uses the [`Default`] trait for that. +//! It would be wonderful to have `structopt` to take the `Default_default` and fill it +//! for you. And yes, `structopt` can do that. +//! +//! Unfortunately, `default_value` takes `&str` but `Default::default` +//! gives us some `Self` value. We need to map `Self` to `&str` somehow. +//! +//! `structopt` solves this problem via [`ToString`] trait. +//! +//! To be able to use auto-default the type must implement *both* `Default` and `ToString`: +//! +//! ``` +//! # use structopt::StructOpt; +//! #[derive(StructOpt)] +//! struct Opt { +//! // just leave the `= "..."` part and structopt will figure it for you +//! #[structopt(default_value, long)] +//! prefix: String // `String` implements both `Default` and `ToString` +//! } +//! ``` +//! +//! [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html +//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html +//! [`Arg::default_value`]: https://docs.rs/clap/2.33.0/clap/struct.Arg.html#method.default_value +//! +//! //! ## Help messages //! //! In clap, help messages for the whole binary can be specified @@ -583,8 +631,6 @@ //! /// //! /// Hello! //! ``` -//! -//! Summary //! ______________ //! //! [`App::about`]: https://docs.rs/clap/2/clap/struct.App.html#method.about @@ -921,8 +967,9 @@ pub use structopt_derive::*; use std::ffi::OsString; -/// Re-export of clap +/// Re-exports pub use clap; +pub use lazy_static; /// A struct that is converted from command line arguments. pub trait StructOpt { diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs index 1233b55d..1d10753c 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -14,8 +14,10 @@ use std::env; use heck::{CamelCase, KebabCase, MixedCase, ShoutySnakeCase, SnakeCase}; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; -use quote::{quote, quote_spanned, ToTokens}; -use syn::{self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Ident, LitStr, MetaNameValue}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::{ + self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Ident, LitStr, MetaNameValue, Type, +}; #[derive(Clone)] pub enum Kind { @@ -75,6 +77,7 @@ pub struct Attrs { name: Name, casing: Sp, env_casing: Sp, + ty: Option, doc_comment: Vec, methods: Vec, parser: Sp, @@ -216,6 +219,7 @@ impl Attrs { default_span: Span, name: Name, parent_attrs: Option<&Attrs>, + ty: Option, casing: Sp, env_casing: Sp, ) -> Self { @@ -226,6 +230,7 @@ impl Attrs { Self { name, + ty, casing, env_casing, doc_comment: vec![], @@ -291,6 +296,39 @@ impl Attrs { VerbatimDocComment(ident) => self.verbatim_doc_comment = Some(ident), + DefaultValue(ident, lit) => { + let val = if let Some(lit) = lit { + quote!(#lit) + } else { + let ty = if let Some(ty) = self.ty.as_ref() { + ty + } else { + abort!( + ident.span(), + "#[structopt(default_value)] (without an argument) can be used \ + only on field level"; + + note = "see \ + https://docs.rs/structopt/0.3.5/structopt/#magical-methods") + }; + + let static_name = format_ident!("__STRUCTOPT_DEFAULT_VALUE_{}", fresh_id()); + + quote_spanned!(ident.span()=> { + ::structopt::lazy_static::lazy_static! { + static ref #static_name: &'static str = { + let val = <#ty as ::std::default::Default>::default(); + let s = ::std::string::ToString::to_string(&val); + ::std::boxed::Box::leak(s.into_boxed_str()) + }; + } + *#static_name + }) + }; + + self.methods.push(Method::new(ident, val)); + } + About(ident, about) => { self.about = Method::from_lit_or_env(ident, about, "CARGO_PKG_DESCRIPTION"); } @@ -357,7 +395,7 @@ impl Attrs { argument_casing: Sp, env_casing: Sp, ) -> Self { - let mut res = Self::new(span, name, parent_attrs, argument_casing, env_casing); + let mut res = Self::new(span, name, parent_attrs, None, argument_casing, env_casing); res.push_attrs(attrs); res.push_doc_comment(attrs, "about"); @@ -386,6 +424,7 @@ impl Attrs { field.span(), Name::Derived(name.clone()), parent_attrs, + Some(field.ty.clone()), struct_casing, env_casing, ); @@ -603,6 +642,20 @@ impl Attrs { } } +fn fresh_id() -> usize { + use std::cell::Cell; + + thread_local! { + static NEXT_ID: Cell = Cell::new(0); + } + + NEXT_ID.with(|next_id| { + let id = next_id.get(); + next_id.set(id + 1); + id + }) +} + /// replace all `:` with `, ` when not inside the `<>` /// /// `"author1:author2:author3" => "author1, author2, author3"` diff --git a/structopt-derive/src/parse.rs b/structopt-derive/src/parse.rs index a7047429..0386153f 100644 --- a/structopt-derive/src/parse.rs +++ b/structopt-derive/src/parse.rs @@ -39,6 +39,7 @@ pub enum StructOptAttr { // ident [= "string literal"] About(Ident, Option), Author(Ident, Option), + DefaultValue(Ident, Option), // ident = "string literal" Version(Ident, LitStr), @@ -88,6 +89,7 @@ impl Parse for StructOptAttr { match &*name_str.to_string() { "rename_all" => Ok(RenameAll(name, lit)), "rename_all_env" => Ok(RenameAllEnv(name, lit)), + "default_value" => Ok(DefaultValue(name, Some(lit))), "version" => { check_empty_lit("version"); @@ -186,6 +188,7 @@ impl Parse for StructOptAttr { "no_version" => Ok(NoVersion(name)), "verbatim_doc_comment" => Ok(VerbatimDocComment(name)), + "default_value" => Ok(DefaultValue(name, None)), "about" => (Ok(About(name, None))), "author" => (Ok(Author(name, None))), diff --git a/tests/default_value.rs b/tests/default_value.rs new file mode 100644 index 00000000..383bd230 --- /dev/null +++ b/tests/default_value.rs @@ -0,0 +1,19 @@ +use structopt::StructOpt; + +mod utils; + +use utils::*; + +#[test] +fn auto_default_value() { + #[derive(StructOpt, PartialEq, Debug)] + struct Opt { + #[structopt(default_value)] + arg: i32, + } + assert_eq!(Opt { arg: 0 }, Opt::from_iter(&["test"])); + assert_eq!(Opt { arg: 1 }, Opt::from_iter(&["test", "1"])); + + let help = get_long_help::(); + assert!(help.contains("[default: 0]")); +} From d20337d64c1ba00e07d998584172bf69e0007378 Mon Sep 17 00:00:00 2001 From: CreepySkeleton Date: Sun, 29 Dec 2019 18:24:14 +0300 Subject: [PATCH 2/3] Ditch id generator --- structopt-derive/src/attrs.rs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs index 1d10753c..3cba66d5 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -14,7 +14,7 @@ use std::env; use heck::{CamelCase, KebabCase, MixedCase, ShoutySnakeCase, SnakeCase}; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; -use quote::{format_ident, quote, quote_spanned, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Ident, LitStr, MetaNameValue, Type, }; @@ -312,17 +312,15 @@ impl Attrs { https://docs.rs/structopt/0.3.5/structopt/#magical-methods") }; - let static_name = format_ident!("__STRUCTOPT_DEFAULT_VALUE_{}", fresh_id()); - quote_spanned!(ident.span()=> { ::structopt::lazy_static::lazy_static! { - static ref #static_name: &'static str = { + static ref DEFAULT_VALUE: &'static str = { let val = <#ty as ::std::default::Default>::default(); let s = ::std::string::ToString::to_string(&val); ::std::boxed::Box::leak(s.into_boxed_str()) }; } - *#static_name + *DEFAULT_VALUE }) }; @@ -642,20 +640,6 @@ impl Attrs { } } -fn fresh_id() -> usize { - use std::cell::Cell; - - thread_local! { - static NEXT_ID: Cell = Cell::new(0); - } - - NEXT_ID.with(|next_id| { - let id = next_id.get(); - next_id.set(id + 1); - id - }) -} - /// replace all `:` with `, ` when not inside the `<>` /// /// `"author1:author2:author3" => "author1, author2, author3"` From 8cb9ec795bde02f2269c6d3ebdac6f9fe03a786f Mon Sep 17 00:00:00 2001 From: CreepySkeleton Date: Sun, 29 Dec 2019 18:48:35 +0300 Subject: [PATCH 3/3] Hide reexport --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index e655290d..d762b128 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -969,6 +969,9 @@ use std::ffi::OsString; /// Re-exports pub use clap; + +/// **This is NOT PUBLIC API**. +#[doc(hidden)] pub use lazy_static; /// A struct that is converted from command line arguments.