Skip to content

Commit

Permalink
Introduce #[structopt(rename_all_env)]
Browse files Browse the repository at this point in the history
  • Loading branch information
CreepySkeleton committed Dec 6, 2019
1 parent 405f51c commit 1ca22d7
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 11 deletions.
33 changes: 29 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@
//! #[derive(StructOpt)]
//! struct Foo {
//! #[structopt(short, long, env = "PARAMETER_VALUE")]
//! param: String
//! parameter_value: String
//! }
//! # fn main() {}
//! ```
Expand All @@ -487,7 +487,7 @@
//! $ cargo run -- --help
//! ...
//! OPTIONS:
//! -p, --param <param> [env: PARAMETER_VALUE=env_value]
//! -p, --parameter_value <parameter_value> [env: PARAMETER_VALUE=env_value]
//! ```
//!
//! In some cases this may be undesirable, for example when being used for passing
Expand All @@ -499,11 +499,36 @@
//! #[derive(StructOpt)]
//! struct Foo {
//! #[structopt(long = "secret", env = "SECRET_VALUE", hide_env_values = true)]
//! param: String
//! secret_value: String
//! }
//! ```
//!
//! ### Auto-deriving environment variables
//!
//! Environment variables tend to be called after the corresponding `struct`'s field,
//! as in example above. The field is `secret_value` and the env var is "SECRET_VALUE";
//! the name is the same, except casing is different.
//!
//! It's pretty tedious and error-prone to type the same name twice,
//! so you can ask `structopt` to do that for you.
//!
//! ```
//! # use structopt::StructOpt;
//!
//! #[derive(StructOpt)]
//! struct Foo {
//! #[structopt(long = "secret", env)]
//! secret_value: String
//! }
//! # fn main() {}
//! ```
//!
//! It works just like `#[structopt(short/long)]`: if `env` is not set to some concrete
//! value the value will be derived from the field's name. This is controlled by
//! `#[structopt(rename_all_env)]`.
//!
//! `rename_all_env` works almost exactly as `rename_all` (including overriding)
//! except default casing is `SCREAMING_SNAKE_CASE` instead of `kebab-case`.
//!
//! ## Skipping fields
//!
//! Sometimes you may want to add a field to your `Opt` struct that is not
Expand Down
40 changes: 36 additions & 4 deletions structopt-derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub enum Name {
pub struct Attrs {
name: Name,
casing: Sp<CasingStyle>,
env_casing: Sp<CasingStyle>,
methods: Vec<Method>,
parser: Sp<Parser>,
author: Option<Method>,
Expand Down Expand Up @@ -208,10 +209,16 @@ impl Name {
}

impl Attrs {
fn new(default_span: Span, name: Name, casing: Sp<CasingStyle>) -> Self {
fn new(
default_span: Span,
name: Name,
casing: Sp<CasingStyle>,
env_casing: Sp<CasingStyle>,
) -> Self {
Self {
name,
casing,
env_casing,
methods: vec![],
parser: Parser::default_spanned(default_span),
about: None,
Expand Down Expand Up @@ -248,6 +255,13 @@ impl Attrs {
);
}

Env(ident) => {
self.push_str_method(
ident.into(),
self.name.clone().translate(*self.env_casing).into(),
);
}

Subcommand(ident) => {
let ty = Sp::call_site(Ty::Other);
let kind = Sp::new(Kind::Subcommand(ty), ident.span());
Expand Down Expand Up @@ -290,6 +304,10 @@ impl Attrs {
self.casing = CasingStyle::from_lit(casing_lit);
}

RenameAllEnv(_, casing_lit) => {
self.env_casing = CasingStyle::from_lit(casing_lit);
}

Parse(ident, spec) => {
self.has_custom_parser = true;
self.parser = Parser::from_spec(ident, spec);
Expand Down Expand Up @@ -387,8 +405,9 @@ impl Attrs {
attrs: &[Attribute],
name: Name,
argument_casing: Sp<CasingStyle>,
env_casing: Sp<CasingStyle>,
) -> Self {
let mut res = Self::new(span, name, argument_casing);
let mut res = Self::new(span, name, argument_casing, env_casing);
res.push_attrs(attrs);
res.push_doc_comment(attrs, "about");

Expand All @@ -406,9 +425,18 @@ impl Attrs {
}
}

pub fn from_field(field: &syn::Field, struct_casing: Sp<CasingStyle>) -> Self {
pub fn from_field(
field: &syn::Field,
struct_casing: Sp<CasingStyle>,
env_casing: Sp<CasingStyle>,
) -> Self {
let name = field.ident.clone().unwrap();
let mut res = Self::new(field.span(), Name::Derived(name.clone()), struct_casing);
let mut res = Self::new(
field.span(),
Name::Derived(name.clone()),
struct_casing,
env_casing,
);
res.push_doc_comment(&field.attrs, "help");
res.push_attrs(&field.attrs);

Expand Down Expand Up @@ -594,6 +622,10 @@ impl Attrs {
self.casing.clone()
}

pub fn env_casing(&self) -> Sp<CasingStyle> {
self.env_casing.clone()
}

pub fn is_positional(&self) -> bool {
self.methods
.iter()
Expand Down
24 changes: 21 additions & 3 deletions structopt-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, *};
/// Default casing style for generated arguments.
const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab;

/// Default casing style for environment variables
const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake;

/// Output for the `gen_xxx()` methods were we need more than a simple stream of tokens.
///
/// The output of a generation method is not only the stream of new tokens but also the attribute
Expand Down Expand Up @@ -60,7 +63,11 @@ fn gen_augmentation(
parent_attribute: &Attrs,
) -> TokenStream {
let mut subcmds = fields.iter().filter_map(|field| {
let attrs = Attrs::from_field(field, parent_attribute.casing());
let attrs = Attrs::from_field(
field,
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let kind = attrs.kind();
if let Kind::Subcommand(ty) = &*kind {
let subcmd_type = match (**ty, sub_type(&field.ty)) {
Expand Down Expand Up @@ -97,7 +104,11 @@ fn gen_augmentation(
}

let args = fields.iter().filter_map(|field| {
let attrs = Attrs::from_field(field, parent_attribute.casing());
let attrs = Attrs::from_field(
field,
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let kind = attrs.kind();
match &*kind {
Kind::Subcommand(_) | Kind::Skip(_) => None,
Expand Down Expand Up @@ -219,7 +230,11 @@ fn gen_augmentation(

fn gen_constructor(fields: &Punctuated<Field, Comma>, parent_attribute: &Attrs) -> TokenStream {
let fields = fields.iter().map(|field| {
let attrs = Attrs::from_field(field, parent_attribute.casing());
let attrs = Attrs::from_field(
field,
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let field_name = field.ident.as_ref().unwrap();
let kind = attrs.kind();
match &*kind {
Expand Down Expand Up @@ -361,6 +376,7 @@ fn gen_clap(attrs: &[Attribute]) -> GenOutput {
attrs,
Name::Assigned(LitStr::new(&name, Span::call_site())),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let tokens = {
let name = attrs.cased_name();
Expand Down Expand Up @@ -429,6 +445,7 @@ fn gen_augment_clap_enum(
&variant.attrs,
Name::Derived(variant.ident.clone()),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let app_var = Ident::new("subcommand", Span::call_site());
let arg_block = match variant.fields {
Expand Down Expand Up @@ -497,6 +514,7 @@ fn gen_from_subcommand(
&variant.attrs,
Name::Derived(variant.ident.clone()),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let sub_name = attrs.cased_name();
let variant_name = &variant.ident;
Expand Down
4 changes: 4 additions & 0 deletions structopt-derive/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum StructOptAttr {
// single-identifier attributes
Short(Ident),
Long(Ident),
Env(Ident),
Flatten(Ident),
Subcommand(Ident),
NoVersion(Ident),
Expand All @@ -40,6 +41,7 @@ pub enum StructOptAttr {

// ident = "string literal"
Version(Ident, LitStr),
RenameAllEnv(Ident, LitStr),
RenameAll(Ident, LitStr),
NameLitStr(Ident, LitStr),

Expand Down Expand Up @@ -84,6 +86,7 @@ impl Parse for StructOptAttr {

match &*name_str.to_string() {
"rename_all" => Ok(RenameAll(name, lit)),
"rename_all_env" => Ok(RenameAllEnv(name, lit)),

"version" => {
check_empty_lit("version");
Expand Down Expand Up @@ -176,6 +179,7 @@ impl Parse for StructOptAttr {
match name_str.as_ref() {
"long" => Ok(Long(name)),
"short" => Ok(Short(name)),
"env" => Ok(Env(name)),
"flatten" => Ok(Flatten(name)),
"subcommand" => Ok(Subcommand(name)),
"no_version" => Ok(NoVersion(name)),
Expand Down
46 changes: 46 additions & 0 deletions tests/rename_all_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
mod utils;

use structopt::StructOpt;
use utils::*;

#[test]
fn it_works() {
#[derive(Debug, PartialEq, StructOpt)]
#[structopt(rename_all_env = "kebab")]
struct BehaviorModel {
#[structopt(env)]
be_nice: String,
}

let help = get_help::<BehaviorModel>();
assert!(help.contains("[env: be-nice=]"));
}

#[test]
fn default_is_screaming() {
#[derive(Debug, PartialEq, StructOpt)]
struct BehaviorModel {
#[structopt(env)]
be_nice: String,
}

let help = get_help::<BehaviorModel>();
assert!(help.contains("[env: BE_NICE=]"));
}

#[test]
fn overridable() {
#[derive(Debug, PartialEq, StructOpt)]
#[structopt(rename_all_env = "kebab")]
struct BehaviorModel {
#[structopt(env)]
be_nice: String,

#[structopt(rename_all_env = "pascal", env)]
be_agressive: String,
}

let help = get_help::<BehaviorModel>();
assert!(help.contains("[env: be-nice=]"));
assert!(help.contains("[env: BeAgressive=]"));
}

0 comments on commit 1ca22d7

Please sign in to comment.