From eaeb653889aac13629d64e03e6ab64f65a4a9d99 Mon Sep 17 00:00:00 2001 From: CreepySkeleton Date: Sun, 29 Dec 2019 18:53:12 +0300 Subject: [PATCH] Default value for `default_value` (yeah, sounds awkward) --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/lib.rs | 62 +++++++++++++++++++++++++++++++---- structopt-derive/src/attrs.rs | 41 +++++++++++++++++++++-- structopt-derive/src/parse.rs | 3 ++ tests/default_value.rs | 19 +++++++++++ 6 files changed, 119 insertions(+), 8 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..d762b128 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,9 +967,13 @@ pub use structopt_derive::*; use std::ffi::OsString; -/// Re-export of clap +/// 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. pub trait StructOpt { /// Returns the corresponding `clap::App`. diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs index 1233b55d..3cba66d5 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -15,7 +15,9 @@ 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 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,37 @@ 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") + }; + + quote_spanned!(ident.span()=> { + ::structopt::lazy_static::lazy_static! { + 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()) + }; + } + *DEFAULT_VALUE + }) + }; + + self.methods.push(Method::new(ident, val)); + } + About(ident, about) => { self.about = Method::from_lit_or_env(ident, about, "CARGO_PKG_DESCRIPTION"); } @@ -357,7 +393,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 +422,7 @@ impl Attrs { field.span(), Name::Derived(name.clone()), parent_attrs, + Some(field.ty.clone()), struct_casing, env_casing, ); 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]")); +}