diff --git a/examples/basic.rs b/examples/basic.rs index 8427c08e..4197da52 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -11,23 +11,36 @@ extern crate structopt_derive; use structopt::StructOpt; +/// A basic example #[derive(StructOpt, Debug)] -#[structopt(name = "basic", about = "A basic example")] +#[structopt(name = "basic")] struct Opt { - #[structopt(short = "d", long = "debug", help = "Activate debug mode")] + /// Activate debug mode + #[structopt(short = "d", long = "debug")] debug: bool, - #[structopt(short = "v", long = "verbose", help = "Verbose mode")] + + /// Verbose mode + #[structopt(short = "v", long = "verbose")] verbose: u64, - #[structopt(short = "s", long = "speed", help = "Set speed", default_value = "42")] + + /// Set speed + #[structopt(short = "s", long = "speed", default_value = "42")] speed: f64, - #[structopt(short = "o", long = "output", help = "Output file")] + + /// Output file + #[structopt(short = "o", long = "output")] output: String, - #[structopt(short = "c", long = "car", help = "Number of car")] + + /// Number of car + #[structopt(short = "c", long = "car")] car: Option, + + /// admin_level to consider #[structopt(short = "l", long = "level")] - #[structopt(help = "admin_level to consider")] level: Vec, - #[structopt(name = "FILE", help = "Files to process")] + + /// Files to process + #[structopt(name = "FILE")] files: Vec, } diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index b93bafc1..62a127b6 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -129,31 +129,64 @@ fn sub_type(t: &syn::Ty) -> Option<&syn::Ty> { } } -fn extract_attrs<'a>(attrs: &'a [Attribute]) -> Box + 'a> { - let iter = attrs.iter() +#[derive(Debug, Clone, Copy)] +enum AttrSource { Struct, Field, } + +fn extract_attrs<'a>(attrs: &'a [Attribute], attr_source: AttrSource) -> Box + 'a> { + let settings_attrs = attrs.iter() .filter_map(|attr| match attr.value { MetaItem::List(ref i, ref v) if i.as_ref() == "structopt" => Some(v), _ => None, }).flat_map(|v| v.iter().filter_map(|mi| match *mi { - NestedMetaItem::MetaItem(MetaItem::NameValue(ref i, ref l)) => Some((i, l)), + NestedMetaItem::MetaItem(MetaItem::NameValue(ref i, ref l)) => + Some((i.clone(), l.clone())), _ => None, })); - Box::new(iter) + + let doc_comments = attrs.iter() + .filter_map(move |attr| { + if let Attribute { + value: MetaItem::NameValue(ref name, Lit::Str(ref value, StrStyle::Cooked)), + is_sugared_doc: true, + .. + } = *attr { + if name != "doc" { return None; } + let text = value.trim_left_matches("//!") + .trim_left_matches("///") + .trim_left_matches("/*!") + .trim_left_matches("/**") + .trim(); + + // Clap's `App` has an `about` method to set a description, + // it's `Field`s have a `help` method instead. + if let AttrSource::Struct = attr_source { + Some(("about".into(), text.into())) + } else { + Some(("help".into(), text.into())) + } + } else { + None + } + }); + + Box::new(doc_comments.chain(settings_attrs)) } -fn from_attr_or_env(attrs: &[(&Ident, &Lit)], key: &str, env: &str) -> Lit { +fn from_attr_or_env(attrs: &[(Ident, Lit)], key: &str, env: &str) -> Lit { let default = std::env::var(env).unwrap_or("".into()); attrs.iter() - .find(|&&(i, _)| i.as_ref() == key) - .map(|&(_, l)| l.clone()) + .filter(|&&(ref i, _)| i.as_ref() == key) + .last() + .map(|&(_, ref l)| l.clone()) .unwrap_or_else(|| Lit::Str(default, StrStyle::Cooked)) } fn gen_name(field: &Field) -> Ident { - extract_attrs(&field.attrs) - .find(|&(i, _)| i.as_ref() == "name") - .and_then(|(_, l)| match *l { - Lit::Str(ref s, _) => Some(Ident::new(s.clone())), + extract_attrs(&field.attrs, AttrSource::Field) + .filter(|&(ref i, _)| i.as_ref() == "name") + .last() + .and_then(|(_, ref l)| match l { + &Lit::Str(ref s, _) => Some(Ident::new(s.clone())), _ => None, }) .unwrap_or(field.ident.as_ref().unwrap().clone()) @@ -196,7 +229,7 @@ fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { } fn gen_clap(ast: &DeriveInput, s: &[Field]) -> quote::Tokens { - let struct_attrs: Vec<_> = extract_attrs(&ast.attrs).collect(); + let struct_attrs: Vec<_> = extract_attrs(&ast.attrs, AttrSource::Struct).collect(); let name = from_attr_or_env(&struct_attrs, "name", "CARGO_PKG_NAME"); let version = from_attr_or_env(&struct_attrs, "version", "CARGO_PKG_VERSION"); let author = from_attr_or_env(&struct_attrs, "author", "CARGO_PKG_AUTHORS"); @@ -220,14 +253,14 @@ fn gen_clap(ast: &DeriveInput, s: &[Field]) -> quote::Tokens { Ty::Option => quote!( .takes_value(true).multiple(false).#validator ), Ty::Vec => quote!( .takes_value(true).multiple(true).#validator ), Ty::Other => { - let required = extract_attrs(&field.attrs) - .find(|&(i, _)| i.as_ref() == "default_value") + let required = extract_attrs(&field.attrs, AttrSource::Field) + .find(|&(ref i, _)| i.as_ref() == "default_value") .is_none(); quote!( .takes_value(true).multiple(false).required(#required).#validator ) }, }; - let from_attr = extract_attrs(&field.attrs) - .filter(|&(i, _)| i.as_ref() != "name") + let from_attr = extract_attrs(&field.attrs, AttrSource::Field) + .filter(|&(ref i, _)| i.as_ref() != "name") .map(|(i, l)| quote!(.#i(#l))); quote!( .arg(_structopt::clap::Arg::with_name(stringify!(#name)) #modifier #(#from_attr)*) ) }); diff --git a/tests/doc-comments-help.rs b/tests/doc-comments-help.rs new file mode 100644 index 00000000..4d7da7c8 --- /dev/null +++ b/tests/doc-comments-help.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2017 structopt Developers +// +// This work is free. You can redistribute it and/or modify it under +// the terms of the Do What The Fuck You Want To Public License, +// Version 2, as published by Sam Hocevar. See the COPYING file for +// more details. + +extern crate structopt; +#[macro_use] +extern crate structopt_derive; + +use structopt::StructOpt; + +#[test] +fn commets_intead_of_actual_help() { + /// Lorem ipsum + #[derive(StructOpt, PartialEq, Debug)] + struct LoremIpsum { + /// Fooify a bar + #[structopt(short = "f", long = "foo")] + foo: bool, + } + + let mut output = Vec::new(); + LoremIpsum::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("Lorem ipsum")); + assert!(output.contains("Fooify a bar")); +} + +#[test] +fn help_is_better_than_comments() { + /// Lorem ipsum + #[derive(StructOpt, PartialEq, Debug)] + #[structopt(name = "lorem-ipsum", about = "Dolor sit amet")] + struct LoremIpsum { + /// Fooify a bar + #[structopt(short = "f", long = "foo", help = "DO NOT PASS A BAR UNDER ANY CIRCUMSTANCES")] + foo: bool, + } + + let mut output = Vec::new(); + LoremIpsum::clap().write_long_help(&mut output).unwrap(); + let output = String::from_utf8(output).unwrap(); + + println!("{}", output); + assert!(output.contains("Dolor sit amet")); + assert!(!output.contains("Lorem ipsum")); + assert!(output.contains("DO NOT PASS A BAR")); +}