Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce #[structopt(rename_all_env)] #302

Merged
merged 2 commits into from
Dec 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 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)]
TeXitoi marked this conversation as resolved.
Show resolved Hide resolved
be_agressive: String,
}

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