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