1 // Copyright (c) 2020 Google LLC All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 
5 use std::fmt::Write;
6 use {
7     crate::{
8         errors::Errors,
9         parse_attrs::{Description, FieldKind, TypeAttrs},
10         Optionality, StructField,
11     },
12     argh_shared::INDENT,
13     proc_macro2::{Span, TokenStream},
14     quote::quote,
15 };
16 
17 const SECTION_SEPARATOR: &str = "\n\n";
18 
19 /// Returns a `TokenStream` generating a `String` help message.
20 ///
21 /// Note: `fields` entries with `is_subcommand.is_some()` will be ignored
22 /// in favor of the `subcommand` argument.
help( errors: &Errors, cmd_name_str_array_ident: syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, ) -> TokenStream23 pub(crate) fn help(
24     errors: &Errors,
25     cmd_name_str_array_ident: syn::Ident,
26     ty_attrs: &TypeAttrs,
27     fields: &[StructField<'_>],
28     subcommand: Option<&StructField<'_>>,
29 ) -> TokenStream {
30     let mut format_lit = "Usage: {command_name}".to_string();
31 
32     let positional = fields.iter().filter(|f| {
33         f.kind == FieldKind::Positional && f.attrs.greedy.is_none() && !f.attrs.hidden_help
34     });
35     let mut has_positional = false;
36     for arg in positional.clone() {
37         has_positional = true;
38         format_lit.push(' ');
39         positional_usage(&mut format_lit, arg);
40     }
41 
42     let options = fields.iter().filter(|f| f.long_name.is_some() && !f.attrs.hidden_help);
43     for option in options.clone() {
44         format_lit.push(' ');
45         option_usage(&mut format_lit, option);
46     }
47 
48     let remain = fields.iter().filter(|f| {
49         f.kind == FieldKind::Positional && f.attrs.greedy.is_some() && !f.attrs.hidden_help
50     });
51     for arg in remain {
52         format_lit.push(' ');
53         positional_usage(&mut format_lit, arg);
54     }
55 
56     if let Some(subcommand) = subcommand {
57         format_lit.push(' ');
58         if !subcommand.optionality.is_required() {
59             format_lit.push('[');
60         }
61         format_lit.push_str("<command>");
62         if !subcommand.optionality.is_required() {
63             format_lit.push(']');
64         }
65         format_lit.push_str(" [<args>]");
66     }
67 
68     format_lit.push_str(SECTION_SEPARATOR);
69 
70     let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type");
71     format_lit.push_str(&description);
72 
73     if has_positional {
74         format_lit.push_str(SECTION_SEPARATOR);
75         format_lit.push_str("Positional Arguments:");
76         for arg in positional {
77             positional_description(&mut format_lit, arg);
78         }
79     }
80 
81     format_lit.push_str(SECTION_SEPARATOR);
82     format_lit.push_str("Options:");
83     for option in options {
84         option_description(errors, &mut format_lit, option);
85     }
86     // Also include "help"
87     option_description_format(&mut format_lit, None, "--help", "display usage information");
88 
89     let subcommand_calculation;
90     let subcommand_format_arg;
91     if let Some(subcommand) = subcommand {
92         format_lit.push_str(SECTION_SEPARATOR);
93         format_lit.push_str("Commands:{subcommands}");
94         let subcommand_ty = subcommand.ty_without_wrapper;
95         subcommand_format_arg = quote! { subcommands = subcommands };
96         subcommand_calculation = quote! {
97             let subcommands = argh::print_subcommands(
98                 <#subcommand_ty as argh::SubCommands>::COMMANDS
99                     .iter()
100                     .copied()
101                     .chain(
102                         <#subcommand_ty as argh::SubCommands>::dynamic_commands()
103                             .iter()
104                             .copied())
105             );
106         };
107     } else {
108         subcommand_calculation = TokenStream::new();
109         subcommand_format_arg = TokenStream::new()
110     }
111 
112     lits_section(&mut format_lit, "Examples:", &ty_attrs.examples);
113 
114     lits_section(&mut format_lit, "Notes:", &ty_attrs.notes);
115 
116     if !ty_attrs.error_codes.is_empty() {
117         format_lit.push_str(SECTION_SEPARATOR);
118         format_lit.push_str("Error codes:");
119         for (code, text) in &ty_attrs.error_codes {
120             format_lit.push('\n');
121             format_lit.push_str(INDENT);
122             write!(format_lit, "{} {}", code, text.value()).unwrap();
123         }
124     }
125 
126     format_lit.push('\n');
127 
128     quote! { {
129         #subcommand_calculation
130         format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg)
131     } }
132 }
133 
134 /// A section composed of exactly just the literals provided to the program.
lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr])135 fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) {
136     if !lits.is_empty() {
137         out.push_str(SECTION_SEPARATOR);
138         out.push_str(heading);
139         for lit in lits {
140             let value = lit.value();
141             for line in value.split('\n') {
142                 out.push('\n');
143                 out.push_str(INDENT);
144                 out.push_str(line);
145             }
146         }
147     }
148 }
149 
150 /// Add positional arguments like `[<foo>...]` to a help format string.
positional_usage(out: &mut String, field: &StructField<'_>)151 fn positional_usage(out: &mut String, field: &StructField<'_>) {
152     if !field.optionality.is_required() {
153         out.push('[');
154     }
155     if field.attrs.greedy.is_none() {
156         out.push('<');
157     }
158     let name = field.positional_arg_name();
159     out.push_str(&name);
160     if field.optionality == Optionality::Repeating {
161         out.push_str("...");
162     }
163     if field.attrs.greedy.is_none() {
164         out.push('>');
165     }
166     if !field.optionality.is_required() {
167         out.push(']');
168     }
169 }
170 
171 /// Add options like `[-f <foo>]` to a help format string.
172 /// This function must only be called on options (things with `long_name.is_some()`)
option_usage(out: &mut String, field: &StructField<'_>)173 fn option_usage(out: &mut String, field: &StructField<'_>) {
174     // bookend with `[` and `]` if optional
175     if !field.optionality.is_required() {
176         out.push('[');
177     }
178 
179     let long_name = field.long_name.as_ref().expect("missing long name for option");
180     if let Some(short) = field.attrs.short.as_ref() {
181         out.push('-');
182         out.push(short.value());
183     } else {
184         out.push_str(long_name);
185     }
186 
187     match field.kind {
188         FieldKind::SubCommand | FieldKind::Positional => unreachable!(), // don't have long_name
189         FieldKind::Switch => {}
190         FieldKind::Option => {
191             out.push_str(" <");
192             if let Some(arg_name) = &field.attrs.arg_name {
193                 out.push_str(&arg_name.value());
194             } else {
195                 out.push_str(long_name.trim_start_matches("--"));
196             }
197             if field.optionality == Optionality::Repeating {
198                 out.push_str("...");
199             }
200             out.push('>');
201         }
202     }
203 
204     if !field.optionality.is_required() {
205         out.push(']');
206     }
207 }
208 
209 // TODO(cramertj) make it so this is only called at least once per object so
210 // as to avoid creating multiple errors.
require_description( errors: &Errors, err_span: Span, desc: &Option<Description>, kind: &str, ) -> String211 pub fn require_description(
212     errors: &Errors,
213     err_span: Span,
214     desc: &Option<Description>,
215     kind: &str, // the thing being described ("type" or "field"),
216 ) -> String {
217     desc.as_ref().map(|d| d.content.value().trim().to_owned()).unwrap_or_else(|| {
218         errors.err_span(
219             err_span,
220             &format!(
221                 "#[derive(FromArgs)] {} with no description.
222 Add a doc comment or an `#[argh(description = \"...\")]` attribute.",
223                 kind
224             ),
225         );
226         "".to_string()
227     })
228 }
229 
230 /// Describes a positional argument like this:
231 ///  hello       positional argument description
positional_description(out: &mut String, field: &StructField<'_>)232 fn positional_description(out: &mut String, field: &StructField<'_>) {
233     let field_name = field.positional_arg_name();
234 
235     let mut description = String::from("");
236     if let Some(desc) = &field.attrs.description {
237         description = desc.content.value().trim().to_owned();
238     }
239     positional_description_format(out, &field_name, &description)
240 }
241 
positional_description_format(out: &mut String, name: &str, description: &str)242 fn positional_description_format(out: &mut String, name: &str, description: &str) {
243     let info = argh_shared::CommandInfo { name, description };
244     argh_shared::write_description(out, &info);
245 }
246 
247 /// Describes an option like this:
248 ///  -f, --force       force, ignore minor errors. This description
249 ///                    is so long that it wraps to the next line.
option_description(errors: &Errors, out: &mut String, field: &StructField<'_>)250 fn option_description(errors: &Errors, out: &mut String, field: &StructField<'_>) {
251     let short = field.attrs.short.as_ref().map(|s| s.value());
252     let long_with_leading_dashes = field.long_name.as_ref().expect("missing long name for option");
253     let description =
254         require_description(errors, field.name.span(), &field.attrs.description, "field");
255 
256     option_description_format(out, short, long_with_leading_dashes, &description)
257 }
258 
option_description_format( out: &mut String, short: Option<char>, long_with_leading_dashes: &str, description: &str, )259 fn option_description_format(
260     out: &mut String,
261     short: Option<char>,
262     long_with_leading_dashes: &str,
263     description: &str,
264 ) {
265     let mut name = String::new();
266     if let Some(short) = short {
267         name.push('-');
268         name.push(short);
269         name.push_str(", ");
270     }
271     name.push_str(long_with_leading_dashes);
272 
273     let info = argh_shared::CommandInfo { name: &name, description };
274     argh_shared::write_description(out, &info);
275 }
276