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