blob: 5bf02b19d5f4021eb2325e3c460481b83e69fb76 [file] [log] [blame]
Andrew Walbrandc8aa4b2022-01-13 12:20:07 +00001// 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
5use {
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
16const 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.
22pub(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.
118fn 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.
134fn 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()`)
152fn 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.
190pub 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.
201Add 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
211fn 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
221fn 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.
229fn 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
238fn 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}