1use crate::{
2 attrs::PostDecor,
3 help::Help,
4 utils::{parse_arg, parse_opt_arg},
5};
6use quote::{quote, ToTokens};
7use syn::{
8 parse::{Parse, ParseStream},
9 parse_quote, token, Error, Expr, Ident, LitChar, LitStr, Result,
10};
11
12#[derive(Debug, Default)]
17pub(crate) struct CommandCfg {
18 pub(crate) name: Option<LitStr>,
19 pub(crate) long: Vec<LitStr>,
20 pub(crate) short: Vec<LitChar>,
21 pub(crate) help: Option<Help>,
22}
23
24#[derive(Debug, Default)]
25pub(crate) struct OptionsCfg {
26 pub(crate) cargo_helper: Option<LitStr>,
27 pub(crate) descr: Option<Help>,
28 pub(crate) footer: Option<Help>,
29 pub(crate) header: Option<Help>,
30 pub(crate) usage: Option<Box<Expr>>,
31 pub(crate) version: Option<Box<Expr>>,
32 pub(crate) max_width: Option<Box<Expr>>,
33 pub(crate) fallback_usage: bool,
34}
35
36#[derive(Debug, Default)]
37pub(crate) struct ParserCfg {
38 pub(crate) group_help: Option<Help>,
39}
40
41#[derive(Debug)]
42pub(crate) enum Mode {
43 Command {
44 command: CommandCfg,
45 options: OptionsCfg,
46 },
47 Options {
48 options: OptionsCfg,
49 },
50 Parser {
51 parser: ParserCfg,
52 },
53}
54
55#[derive(Debug)]
56pub(crate) enum HelpMsg {
57 Lit(String),
58 Custom(Box<Expr>),
59}
60
61impl From<String> for HelpMsg {
62 fn from(value: String) -> Self {
63 Self::Lit(value)
64 }
65}
66
67impl From<Box<Expr>> for HelpMsg {
68 fn from(value: Box<Expr>) -> Self {
69 Self::Custom(value)
70 }
71}
72
73impl ToTokens for HelpMsg {
74 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
75 match self {
76 HelpMsg::Lit(l) => l.to_tokens(tokens),
77 HelpMsg::Custom(l) => l.to_tokens(tokens),
78 }
79 }
80}
81
82#[derive(Debug)]
83pub(crate) struct TopInfo {
84 pub(crate) private: bool,
86 pub(crate) custom_name: Option<Ident>,
88 pub(crate) boxed: bool,
90 pub(crate) ignore_rustdoc: bool,
92
93 pub(crate) adjacent: bool,
94 pub(crate) mode: Mode,
95 pub(crate) attrs: Vec<PostDecor>,
96
97 pub(crate) bpaf_path: Option<syn::Path>,
99}
100
101impl Default for TopInfo {
102 fn default() -> Self {
103 Self {
104 private: false,
105 custom_name: None,
106 boxed: false,
107 adjacent: false,
108 mode: Mode::Parser {
109 parser: Default::default(),
110 },
111 attrs: Vec::new(),
112 ignore_rustdoc: false,
113 bpaf_path: None,
114 }
115 }
116}
117
118const TOP_NEED_OPTIONS: &str =
119 "You need to add `options` annotation at the beginning to use this one";
120
121const TOP_NEED_COMMAND: &str =
122 "You need to add `command` annotation at the beginning to use this one";
123
124const TOP_NEED_PARSER: &str = "This annotation can't be used with either `options` or `command`";
125
126fn with_options(
127 kw: &Ident,
128 cfg: Option<&mut OptionsCfg>,
129 f: impl FnOnce(&mut OptionsCfg),
130) -> Result<()> {
131 match cfg {
132 Some(cfg) => {
133 f(cfg);
134 Ok(())
135 }
136 None => Err(Error::new_spanned(kw, TOP_NEED_OPTIONS)),
137 }
138}
139
140fn with_command(
141 kw: &Ident,
142 cfg: Option<&mut CommandCfg>,
143 f: impl FnOnce(&mut CommandCfg),
144) -> Result<()> {
145 match cfg {
146 Some(cfg) => {
147 f(cfg);
148 Ok(())
149 }
150 None => Err(Error::new_spanned(kw, TOP_NEED_COMMAND)),
151 }
152}
153
154fn with_parser(
155 kw: &Ident,
156 cfg: Option<&mut ParserCfg>,
157 f: impl FnOnce(&mut ParserCfg),
158) -> Result<()> {
159 match cfg {
160 Some(cfg) => {
161 f(cfg);
162 Ok(())
163 }
164 None => Err(Error::new_spanned(kw, TOP_NEED_PARSER)),
165 }
166}
167
168impl Parse for TopInfo {
169 fn parse(input: ParseStream) -> Result<Self> {
170 let mut private = false;
171 let mut custom_name = None;
172 let mut boxed = false;
173 let mut ignore_rustdoc = false;
174 let mut command = None;
175 let mut options = None;
176 let mut parser = Some(ParserCfg::default());
177 let mut adjacent = false;
178 let mut attrs = Vec::new();
179 let mut first = true;
180 let mut bpaf_path = None;
181 loop {
182 let kw = input.parse::<Ident>()?;
183
184 if first && kw == "options" {
185 let mut cfg = OptionsCfg::default();
186 if let Some(helper) = parse_opt_arg(input)? {
187 cfg.cargo_helper = Some(helper);
188 }
189 options = Some(cfg);
190 parser = None;
191 } else if first && kw == "command" {
192 let mut cfg = CommandCfg::default();
193 if let Some(name) = parse_opt_arg(input)? {
194 cfg.name = Some(name);
195 }
196 options = Some(OptionsCfg::default());
197 command = Some(cfg);
198 parser = None;
199 } else if kw == "private" {
200 private = true;
201 } else if kw == "generate" {
202 custom_name = parse_arg(input)?;
203 } else if kw == "options" {
204 return Err(Error::new_spanned(
205 kw,
206 "This annotation must be first and used only once: try `#[bpaf(options, ...`",
207 ));
208 } else if kw == "command" {
209 return Err(Error::new_spanned(
210 kw,
211 "This annotation must be first: try `#[bpaf(command, ...`",
212 ));
213 } else if kw == "version" {
214 let version = parse_opt_arg(input)?
215 .unwrap_or_else(|| parse_quote!(env!("CARGO_PKG_VERSION")));
216 with_options(&kw, options.as_mut(), |cfg| cfg.version = Some(version))?;
217 } else if kw == "boxed" {
218 boxed = true;
219 } else if kw == "adjacent" {
220 adjacent = true;
221 } else if kw == "fallback_to_usage" {
222 if let Some(opts) = options.as_mut() {
223 opts.fallback_usage = true;
224 } else {
225 return Err(Error::new_spanned(
226 kw,
227 "This annotation only makes sense in combination with `options` or `command`",
228 ));
229 }
230 } else if kw == "short" {
231 let short = parse_arg(input)?;
232 with_command(&kw, command.as_mut(), |cfg| cfg.short.push(short))?;
233 } else if kw == "long" {
234 let long = parse_arg(input)?;
235 with_command(&kw, command.as_mut(), |cfg| cfg.long.push(long))?;
236 } else if kw == "header" {
237 let header = parse_arg(input)?;
238 with_options(&kw, options.as_mut(), |cfg| cfg.header = Some(header))?;
239 } else if kw == "footer" {
240 let footer = parse_arg(input)?;
241 with_options(&kw, options.as_mut(), |opt| opt.footer = Some(footer))?;
242 } else if kw == "usage" {
243 let usage = parse_arg(input)?;
244 with_options(&kw, options.as_mut(), |opt| opt.usage = Some(usage))?;
245 } else if kw == "group_help" {
246 let group_help = parse_arg(input)?;
247 with_parser(&kw, parser.as_mut(), |opt| {
248 opt.group_help = Some(group_help)
249 })?;
250 } else if kw == "ignore_rustdoc" {
251 ignore_rustdoc = true;
252 } else if kw == "descr" {
253 let descr = parse_arg(input)?;
254 with_options(&kw, options.as_mut(), |opt| opt.descr = Some(descr))?;
255 } else if kw == "help" {
256 let help = parse_arg(input)?;
257 with_command(&kw, command.as_mut(), |cfg| cfg.help = Some(help))?;
258 } else if kw == "path" {
259 bpaf_path.replace(parse_arg::<syn::Path>(input)?);
260 } else if kw == "max_width" {
261 let max_width = parse_arg(input)?;
262 with_options(&kw, options.as_mut(), |opt| opt.max_width = Some(max_width))?;
263 } else if let Some(pd) = PostDecor::parse(input, &kw)? {
264 attrs.push(pd);
265 } else {
266 return Err(Error::new_spanned(
267 kw,
268 "Unexpected attribute for top level annotation",
269 ));
270 }
271
272 if input.is_empty() {
273 break;
274 }
275 input.parse::<token::Comma>()?;
276 if input.is_empty() {
277 break;
278 }
279 first = false;
280 }
281
282 let mode = match (options, command) {
283 (Some(options), Some(command)) => Mode::Command { command, options },
284 (Some(options), None) => Mode::Options { options },
285 _ => Mode::Parser {
286 parser: parser.unwrap_or_default(),
287 },
288 };
289
290 Ok(TopInfo {
291 ignore_rustdoc,
292 private,
293 custom_name,
294 boxed,
295 adjacent,
296 mode,
297 attrs,
298 bpaf_path,
299 })
300 }
301}
302
303#[derive(Debug, Default)]
304pub(crate) struct Ed {
305 pub(crate) skip: bool,
306 pub(crate) attrs: Vec<EAttr>,
307}
308
309pub(crate) enum VariantMode {
310 Command,
311 Parser,
312}
313
314impl Parse for Ed {
315 fn parse(input: ParseStream) -> Result<Self> {
316 let mut attrs = Vec::new();
317 let mut skip = false;
318
319 let mode = {
320 let first = input.fork().parse::<Ident>()?;
321 if first == "command" {
322 VariantMode::Command
323 } else {
324 VariantMode::Parser
325 }
326 };
327
328 loop {
329 let kw = input.parse::<Ident>()?;
330
331 if kw == "command" {
332 attrs.push(if let Some(name) = parse_opt_arg(input)? {
333 EAttr::NamedCommand(name)
334 } else {
335 EAttr::UnnamedCommand
336 });
337 } else if kw == "short" {
338 if matches!(mode, VariantMode::Command) {
339 attrs.push(EAttr::CommandShort(parse_arg(input)?));
340 } else {
341 attrs.push(EAttr::UnitShort(parse_opt_arg(input)?));
342 }
343 } else if kw == "hide" {
344 attrs.push(EAttr::Hide);
345 } else if kw == "long" {
346 if matches!(mode, VariantMode::Command) {
347 attrs.push(EAttr::CommandLong(parse_arg(input)?));
348 } else {
349 attrs.push(EAttr::UnitLong(parse_opt_arg(input)?));
350 }
351 } else if kw == "fallback_to_usage" {
352 if matches!(mode, VariantMode::Command) {
353 attrs.push(EAttr::FallbackUsage);
354 } else {
355 return Err(Error::new_spanned(
356 kw,
357 "In this context this attribute requires \"command\" annotation",
358 ));
359 }
360 } else if kw == "skip" {
361 skip = true;
362 } else if kw == "adjacent" {
363 attrs.push(EAttr::Adjacent);
364 } else if kw == "usage" {
365 attrs.push(EAttr::Usage(parse_arg(input)?));
366 } else if kw == "header" {
367 attrs.push(EAttr::Header(parse_arg(input)?));
368 } else if kw == "footer" {
369 attrs.push(EAttr::Footer(parse_arg(input)?));
370 } else if kw == "env" {
371 attrs.push(EAttr::Env(parse_arg(input)?));
372 } else {
373 return Err(Error::new_spanned(
374 kw,
375 "Unexpected attribute for enum variant annotation",
376 ));
377 }
378
379 if input.is_empty() {
380 break;
381 }
382 input.parse::<token::Comma>()?;
383 if input.is_empty() {
384 break;
385 }
386 }
387
388 Ok(Ed { skip, attrs })
389 }
390}
391
392#[derive(Debug, Clone)]
393pub(crate) enum EAttr {
394 NamedCommand(LitStr),
395 UnnamedCommand,
396
397 FallbackUsage,
398 CommandShort(LitChar),
399 CommandLong(LitStr),
400 Adjacent,
401 Hide,
402 UnitShort(Option<LitChar>),
403 UnitLong(Option<LitStr>),
404 Descr(Help),
405 Header(Help),
406 Footer(Help),
407 Usage(Box<Expr>),
408 Env(Box<Expr>),
409 ToOptions,
410}
411
412impl ToTokens for EAttr {
413 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
414 match self {
415 Self::ToOptions => quote!(to_options()),
416 Self::NamedCommand(n) => quote!(command(#n)),
417 Self::CommandShort(n) => quote!(short(#n)),
418 Self::CommandLong(n) => quote!(long(#n)),
419 Self::Adjacent => quote!(adjacent()),
420 Self::Descr(d) => quote!(descr(#d)),
421 Self::Header(d) => quote!(header(#d)),
422 Self::Footer(d) => quote!(footer(#d)),
423 Self::Usage(u) => quote!(usage(#u)),
424 Self::Env(e) => quote!(env(#e)),
425 Self::Hide => quote!(hide()),
426 Self::FallbackUsage => quote!(fallback_to_usage()),
427
428 Self::UnnamedCommand | Self::UnitShort(_) | Self::UnitLong(_) => unreachable!(),
429 }
430 .to_tokens(tokens);
431 }
432}