bpaf/
meta_help.rs

1use std::collections::BTreeSet;
2
3use crate::{
4    buffer::{Block, Doc, Style, Token},
5    info::Info,
6    item::{Item, ShortLong},
7    Meta,
8};
9
10#[doc(hidden)]
11#[derive(Debug, Clone, Copy)]
12pub struct Metavar(pub(crate) &'static str);
13
14#[derive(Debug, Clone, Copy)]
15pub(crate) enum HelpItem<'a> {
16    DecorSuffix {
17        help: &'a Doc,
18        ty: HiTy,
19    },
20    GroupStart {
21        help: &'a Doc,
22        ty: HiTy,
23    },
24    GroupEnd {
25        ty: HiTy,
26    },
27    Any {
28        metavar: &'a Doc,
29        anywhere: bool,
30        help: Option<&'a Doc>,
31    },
32    Positional {
33        metavar: Metavar,
34        help: Option<&'a Doc>,
35    },
36    Command {
37        name: &'static str,
38        short: Option<char>,
39        help: Option<&'a Doc>,
40        meta: &'a Meta,
41        #[cfg(feature = "docgen")]
42        info: &'a Info,
43    },
44    Flag {
45        name: ShortLong,
46        env: Option<&'static str>,
47        help: Option<&'a Doc>,
48    },
49    Argument {
50        name: ShortLong,
51        metavar: Metavar,
52        env: Option<&'static str>,
53        help: Option<&'a Doc>,
54    },
55    AnywhereStart {
56        inner: &'a Meta,
57        ty: HiTy,
58    },
59    AnywhereStop {
60        ty: HiTy,
61    },
62}
63impl HelpItem<'_> {
64    fn has_help(&self) -> bool {
65        match self {
66            HelpItem::Positional { help, .. }
67            | HelpItem::Command { help, .. }
68            | HelpItem::Flag { help, .. }
69            | HelpItem::Any { help, .. }
70            | HelpItem::Argument { help, .. } => help.is_some(),
71            HelpItem::GroupStart { .. } | HelpItem::DecorSuffix { .. } => true,
72            HelpItem::GroupEnd { .. }
73            | HelpItem::AnywhereStart { .. }
74            | HelpItem::AnywhereStop { .. } => false,
75        }
76    }
77
78    fn ty(&self) -> HiTy {
79        match self {
80            HelpItem::GroupStart { ty, .. }
81            | HelpItem::DecorSuffix { ty, .. }
82            | HelpItem::GroupEnd { ty }
83            | HelpItem::AnywhereStart { ty, .. }
84            | HelpItem::AnywhereStop { ty } => *ty,
85            HelpItem::Any {
86                anywhere: false, ..
87            }
88            | HelpItem::Positional { .. } => HiTy::Positional,
89            HelpItem::Command { .. } => HiTy::Command,
90            HelpItem::Any { anywhere: true, .. }
91            | HelpItem::Flag { .. }
92            | HelpItem::Argument { .. } => HiTy::Flag,
93        }
94    }
95}
96
97#[derive(Default, Debug)]
98/// A collection of all the help items separated into flags, positionals and commands
99///
100/// Items are stored as references and can be trivially copied
101pub(crate) struct HelpItems<'a> {
102    pub(crate) items: Vec<HelpItem<'a>>,
103}
104
105#[derive(Copy, Clone, Eq, PartialEq, Debug)]
106pub(crate) enum HiTy {
107    Flag,
108    Command,
109    Positional,
110}
111
112enum ItemBlock {
113    No,
114    Decor(HiTy),
115    Anywhere(HiTy),
116}
117
118pub(crate) struct HelpItemsIter<'a, 'b> {
119    items: &'b [HelpItem<'a>],
120    target: HiTy,
121    cur: usize,
122    block: ItemBlock,
123}
124
125impl<'a, 'b> Iterator for HelpItemsIter<'a, 'b> {
126    type Item = &'b HelpItem<'a>;
127
128    fn next(&mut self) -> Option<Self::Item> {
129        loop {
130            let item = self.items.get(self.cur)?;
131            self.cur += 1;
132
133            let keep = match item {
134                HelpItem::AnywhereStart { ty, .. } => {
135                    self.block = ItemBlock::Anywhere(*ty);
136                    *ty == self.target
137                }
138                HelpItem::GroupStart { ty, .. } => {
139                    self.block = ItemBlock::Decor(*ty);
140                    *ty == self.target
141                }
142                HelpItem::GroupEnd { ty, .. } | HelpItem::AnywhereStop { ty, .. } => {
143                    self.block = ItemBlock::No;
144                    *ty == self.target
145                }
146                HelpItem::DecorSuffix { .. }
147                | HelpItem::Any { .. }
148                | HelpItem::Command { .. }
149                | HelpItem::Positional { .. }
150                | HelpItem::Flag { .. }
151                | HelpItem::Argument { .. } => {
152                    let ty = item.ty();
153                    match self.block {
154                        ItemBlock::No => ty == self.target,
155                        ItemBlock::Decor(t) => t == self.target,
156                        ItemBlock::Anywhere(t) => t == self.target && item.has_help(),
157                    }
158                }
159            };
160            if keep {
161                return Some(item);
162            }
163        }
164    }
165}
166
167impl<'a> HelpItems<'a> {
168    #[inline(never)]
169    fn items_of_ty(&self, target: HiTy) -> impl Iterator<Item = &HelpItem> {
170        HelpItemsIter {
171            items: &self.items,
172            target,
173            cur: 0,
174            block: ItemBlock::No,
175        }
176    }
177}
178
179impl Meta {
180    fn peek_front_ty(&self) -> Option<HiTy> {
181        match self {
182            Meta::And(xs) | Meta::Or(xs) => xs.iter().find_map(Meta::peek_front_ty),
183            Meta::Optional(x)
184            | Meta::Required(x)
185            | Meta::Adjacent(x)
186            | Meta::Many(x)
187            | Meta::Subsection(x, _)
188            | Meta::Suffix(x, _)
189            | Meta::Strict(x)
190            | Meta::CustomUsage(x, _) => x.peek_front_ty(),
191            Meta::Item(i) => Some(HiTy::from(i.as_ref())),
192            Meta::Skip => None,
193        }
194    }
195}
196
197impl<'a> HelpItems<'a> {
198    /// Recursively classify contents of the Meta
199    pub(crate) fn append_meta(&mut self, meta: &'a Meta) {
200        fn go<'a>(hi: &mut HelpItems<'a>, meta: &'a Meta, no_ss: bool) {
201            match meta {
202                Meta::And(xs) | Meta::Or(xs) => {
203                    for x in xs {
204                        go(hi, x, no_ss);
205                    }
206                }
207                Meta::Adjacent(m) => {
208                    if let Some(ty) = m.peek_front_ty() {
209                        hi.items.push(HelpItem::AnywhereStart {
210                            inner: m.as_ref(),
211                            ty,
212                        });
213                        go(hi, m, no_ss);
214                        hi.items.push(HelpItem::AnywhereStop { ty });
215                    }
216                }
217                Meta::CustomUsage(x, _)
218                | Meta::Required(x)
219                | Meta::Optional(x)
220                | Meta::Many(x)
221                | Meta::Strict(x) => go(hi, x, no_ss),
222                Meta::Item(item) => {
223                    if matches!(item.as_ref(), Item::Positional { help: None, .. }) {
224                        return;
225                    }
226                    hi.items.push(HelpItem::from(item.as_ref()));
227                }
228                Meta::Subsection(m, help) => {
229                    if let Some(ty) = m.peek_front_ty() {
230                        if no_ss {
231                            go(hi, m, true);
232                        } else {
233                            hi.items.push(HelpItem::GroupStart { help, ty });
234                            go(hi, m, true);
235                            hi.items.push(HelpItem::GroupEnd { ty });
236                        }
237                    }
238                }
239                Meta::Suffix(m, help) => {
240                    if let Some(ty) = m.peek_front_ty() {
241                        go(hi, m, no_ss);
242                        hi.items.push(HelpItem::DecorSuffix { help, ty });
243                    }
244                }
245                Meta::Skip => (),
246            }
247        }
248        go(self, meta, false);
249    }
250
251    fn find_group(&self) -> Option<std::ops::RangeInclusive<usize>> {
252        let start = self
253            .items
254            .iter()
255            .position(|i| matches!(i, HelpItem::GroupStart { .. }))?;
256        let end = self
257            .items
258            .iter()
259            .position(|i| matches!(i, HelpItem::GroupEnd { .. }))?;
260        Some(start..=end)
261    }
262}
263
264impl From<&Item> for HiTy {
265    fn from(value: &Item) -> Self {
266        match value {
267            Item::Positional { .. }
268            | Item::Any {
269                anywhere: false, ..
270            } => Self::Positional,
271            Item::Command { .. } => Self::Command,
272            Item::Any { anywhere: true, .. } | Item::Flag { .. } | Item::Argument { .. } => {
273                Self::Flag
274            }
275        }
276    }
277}
278
279impl<'a> From<&'a Item> for HelpItem<'a> {
280    // {{{
281    fn from(item: &'a Item) -> Self {
282        match item {
283            Item::Positional { metavar, help } => Self::Positional {
284                metavar: *metavar,
285                help: help.as_ref(),
286            },
287            Item::Command {
288                name,
289                short,
290                help,
291                meta,
292                #[cfg(feature = "docgen")]
293                info,
294                #[cfg(not(feature = "docgen"))]
295                    info: _,
296            } => Self::Command {
297                name,
298                short: *short,
299                help: help.as_ref(),
300                meta,
301                #[cfg(feature = "docgen")]
302                info,
303            },
304            Item::Flag {
305                name,
306                env,
307                help,
308                shorts: _,
309            } => Self::Flag {
310                name: *name,
311                env: *env,
312                help: help.as_ref(),
313            },
314            Item::Argument {
315                name,
316                metavar,
317                env,
318                help,
319                shorts: _,
320            } => Self::Argument {
321                name: *name,
322                metavar: *metavar,
323                env: *env,
324                help: help.as_ref(),
325            },
326            Item::Any {
327                metavar,
328                anywhere,
329                help,
330            } => Self::Any {
331                metavar,
332                anywhere: *anywhere,
333                help: help.as_ref(),
334            },
335        }
336    }
337} // }}}
338
339impl Doc {
340    #[inline(never)]
341    pub(crate) fn metavar(&mut self, metavar: Metavar) {
342        if metavar
343            .0
344            .chars()
345            .all(|c| c.is_uppercase() || c.is_ascii_digit() || c == '-' || c == '_')
346        {
347            self.write_str(metavar.0, Style::Metavar);
348        } else {
349            self.write_char('<', Style::Metavar);
350            self.write_str(metavar.0, Style::Metavar);
351            self.write_char('>', Style::Metavar);
352        }
353    }
354}
355
356#[allow(clippy::too_many_lines)] // lines are _very_ boring
357fn write_help_item(buf: &mut Doc, item: &HelpItem, include_env: bool) {
358    match item {
359        HelpItem::GroupStart { help, .. } => {
360            buf.token(Token::BlockStart(Block::Block));
361            buf.token(Token::BlockStart(Block::Section2));
362            buf.em_doc(help);
363            buf.token(Token::BlockEnd(Block::Section2));
364            buf.token(Token::BlockStart(Block::DefinitionList));
365        }
366        HelpItem::GroupEnd { .. } => {
367            buf.token(Token::BlockEnd(Block::DefinitionList));
368            buf.token(Token::BlockEnd(Block::Block));
369        }
370        HelpItem::DecorSuffix { help, .. } => {
371            buf.token(Token::BlockStart(Block::ItemTerm));
372            buf.token(Token::BlockEnd(Block::ItemTerm));
373            buf.token(Token::BlockStart(Block::ItemBody));
374            buf.doc(help);
375            buf.token(Token::BlockEnd(Block::ItemBody));
376        }
377        HelpItem::Any {
378            metavar,
379            help,
380            anywhere: _,
381        } => {
382            buf.token(Token::BlockStart(Block::ItemTerm));
383            buf.doc(metavar);
384            buf.token(Token::BlockEnd(Block::ItemTerm));
385            if let Some(help) = help {
386                buf.token(Token::BlockStart(Block::ItemBody));
387                buf.doc(help);
388                buf.token(Token::BlockEnd(Block::ItemBody));
389            }
390        }
391        HelpItem::Positional { metavar, help } => {
392            buf.token(Token::BlockStart(Block::ItemTerm));
393            buf.metavar(*metavar);
394            buf.token(Token::BlockEnd(Block::ItemTerm));
395            if let Some(help) = help {
396                buf.token(Token::BlockStart(Block::ItemBody));
397                buf.doc(help);
398                buf.token(Token::BlockEnd(Block::ItemBody));
399            }
400        }
401        HelpItem::Command {
402            name,
403            short,
404            help,
405            meta: _,
406            #[cfg(feature = "docgen")]
407                info: _,
408        } => {
409            buf.token(Token::BlockStart(Block::ItemTerm));
410            buf.write_str(name, Style::Literal);
411            if let Some(short) = short {
412                buf.write_str(", ", Style::Text);
413                buf.write_char(*short, Style::Literal);
414            }
415            buf.token(Token::BlockEnd(Block::ItemTerm));
416            if let Some(help) = help {
417                buf.token(Token::BlockStart(Block::ItemBody));
418                buf.doc(help);
419                buf.token(Token::BlockEnd(Block::ItemBody));
420            }
421        }
422        HelpItem::Flag { name, env, help } => {
423            buf.token(Token::BlockStart(Block::ItemTerm));
424            write_shortlong(buf, *name);
425            buf.token(Token::BlockEnd(Block::ItemTerm));
426            if let Some(help) = help {
427                buf.token(Token::BlockStart(Block::ItemBody));
428                buf.doc(help);
429                buf.token(Token::BlockEnd(Block::ItemBody));
430            }
431            if let Some(env) = env {
432                let val = if std::env::var_os(env).is_some() {
433                    ": set"
434                } else {
435                    ": not set"
436                };
437                if help.is_some() {
438                    buf.token(Token::BlockStart(Block::ItemTerm));
439                    buf.token(Token::BlockEnd(Block::ItemTerm));
440                }
441                buf.token(Token::BlockStart(Block::ItemBody));
442                if include_env {
443                    buf.write_str(&format!("[env:{}{}]", env, val), Style::Text);
444                } else {
445                    buf.text("Uses environment variable ");
446                    buf.literal(env);
447                }
448                buf.token(Token::BlockEnd(Block::ItemBody));
449            }
450        }
451        HelpItem::Argument {
452            name,
453            metavar,
454            env,
455            help,
456        } => {
457            buf.token(Token::BlockStart(Block::ItemTerm));
458            write_shortlong(buf, *name);
459            buf.write_str("=", Style::Text);
460            buf.metavar(*metavar);
461            buf.token(Token::BlockEnd(Block::ItemTerm));
462
463            if let Some(help) = help {
464                buf.token(Token::BlockStart(Block::ItemBody));
465                buf.doc(help);
466                buf.token(Token::BlockEnd(Block::ItemBody));
467            }
468
469            if let Some(env) = env {
470                let val = match std::env::var_os(env) {
471                    Some(s) => std::borrow::Cow::from(format!(" = {:?}", s.to_string_lossy())),
472                    None => std::borrow::Cow::Borrowed(": N/A"),
473                };
474
475                if help.is_some() {
476                    buf.token(Token::BlockStart(Block::ItemTerm));
477                    buf.token(Token::BlockEnd(Block::ItemTerm));
478                }
479                buf.token(Token::BlockStart(Block::ItemBody));
480
481                if include_env {
482                    buf.write_str(&format!("[env:{}{}]", env, val), Style::Text);
483                } else {
484                    buf.text("Uses environment variable ");
485                    buf.literal(env);
486                }
487
488                buf.token(Token::BlockEnd(Block::ItemBody));
489            }
490        }
491        HelpItem::AnywhereStart { inner, .. } => {
492            buf.token(Token::BlockStart(Block::Section3));
493            buf.write_meta(inner, true);
494            buf.token(Token::BlockEnd(Block::Section3));
495        }
496        HelpItem::AnywhereStop { .. } => {
497            buf.token(Token::BlockStart(Block::Block));
498            buf.token(Token::BlockEnd(Block::Block));
499        }
500    }
501}
502
503fn write_shortlong(buf: &mut Doc, name: ShortLong) {
504    match name {
505        ShortLong::Short(s) => {
506            buf.write_char('-', Style::Literal);
507            buf.write_char(s, Style::Literal);
508        }
509        ShortLong::Long(l) => {
510            buf.write_str("    --", Style::Literal);
511            buf.write_str(l, Style::Literal);
512        }
513        ShortLong::Both(s, l) => {
514            buf.write_char('-', Style::Literal);
515            buf.write_char(s, Style::Literal);
516            buf.write_str(", ", Style::Text);
517            buf.write_str("--", Style::Literal);
518            buf.write_str(l, Style::Literal);
519        }
520    }
521}
522
523#[inline(never)]
524pub(crate) fn render_help(
525    path: &[String],
526    info: &Info,
527    parser_meta: &Meta,
528    help_meta: &Meta,
529    include_env: bool,
530) -> Doc {
531    parser_meta.positional_invariant_check(false);
532    let mut buf = Doc::default();
533
534    if let Some(t) = &info.descr {
535        buf.token(Token::BlockStart(Block::Block));
536        buf.doc(t);
537        buf.token(Token::BlockEnd(Block::Block));
538    }
539
540    buf.token(Token::BlockStart(Block::Block));
541    if let Some(usage) = &info.usage {
542        buf.doc(usage);
543    } else {
544        buf.write_str("Usage", Style::Emphasis);
545        buf.write_str(": ", Style::Text);
546        buf.token(Token::BlockStart(Block::Mono));
547        buf.write_path(path);
548        buf.write_meta(parser_meta, true);
549        buf.token(Token::BlockEnd(Block::Mono));
550    }
551    buf.token(Token::BlockEnd(Block::Block));
552
553    if let Some(t) = &info.header {
554        buf.token(Token::BlockStart(Block::Block));
555        buf.doc(t);
556        buf.token(Token::BlockEnd(Block::Block));
557    }
558
559    let mut items = HelpItems::default();
560    items.append_meta(parser_meta);
561    items.append_meta(help_meta);
562
563    buf.write_help_item_groups(items, include_env);
564
565    if let Some(footer) = &info.footer {
566        buf.token(Token::BlockStart(Block::Block));
567        buf.doc(footer);
568        buf.token(Token::BlockEnd(Block::Block));
569    }
570    buf
571}
572
573#[derive(Default)]
574struct Dedup {
575    items: BTreeSet<String>,
576    keep: bool,
577}
578
579impl Dedup {
580    fn check(&mut self, item: &HelpItem) -> bool {
581        match item {
582            HelpItem::DecorSuffix { .. } => std::mem::take(&mut self.keep),
583            HelpItem::GroupStart { .. }
584            | HelpItem::GroupEnd { .. }
585            | HelpItem::AnywhereStart { .. }
586            | HelpItem::AnywhereStop { .. } => {
587                self.keep = true;
588                true
589            }
590            HelpItem::Any { metavar, help, .. } => {
591                self.keep = self.items.insert(format!("{:?} {:?}", metavar, help));
592                self.keep
593            }
594            HelpItem::Positional { metavar, help } => {
595                self.keep = self.items.insert(format!("{:?} {:?}", metavar.0, help));
596                self.keep
597            }
598            HelpItem::Command { name, help, .. } => {
599                self.keep = self.items.insert(format!("{:?} {:?}", name, help));
600                self.keep
601            }
602            HelpItem::Flag { name, help, .. } => {
603                self.keep = self.items.insert(format!("{:?} {:?}", name, help));
604                self.keep
605            }
606            HelpItem::Argument {
607                name,
608                metavar,
609                help,
610                ..
611            } => {
612                self.keep = self
613                    .items
614                    .insert(format!("{:?} {} {:?}", name, metavar.0, help));
615                self.keep
616            }
617        }
618    }
619}
620
621impl Doc {
622    #[inline(never)]
623    pub(crate) fn write_help_item_groups(&mut self, mut items: HelpItems, include_env: bool) {
624        while let Some(range) = items.find_group() {
625            let mut dd = Dedup::default();
626            for item in items.items.drain(range) {
627                if dd.check(&item) {
628                    write_help_item(self, &item, include_env);
629                }
630            }
631        }
632
633        for (ty, name) in [
634            (HiTy::Positional, "Available positional items:"),
635            (HiTy::Flag, "Available options:"),
636            (HiTy::Command, "Available commands:"),
637        ] {
638            self.write_help_items(&items, ty, name, include_env);
639        }
640    }
641
642    #[inline(never)]
643    fn write_help_items(&mut self, items: &HelpItems, ty: HiTy, name: &str, include_env: bool) {
644        let mut xs = items.items_of_ty(ty).peekable();
645        if xs.peek().is_some() {
646            self.token(Token::BlockStart(Block::Block));
647            self.token(Token::BlockStart(Block::Section2));
648            self.write_str(name, Style::Emphasis);
649            self.token(Token::BlockEnd(Block::Section2));
650            self.token(Token::BlockStart(Block::DefinitionList));
651            let mut dd = Dedup::default();
652            for item in xs {
653                if dd.check(item) {
654                    write_help_item(self, item, include_env);
655                }
656            }
657            self.token(Token::BlockEnd(Block::DefinitionList));
658            self.token(Token::BlockEnd(Block::Block));
659        }
660    }
661
662    pub(crate) fn write_path(&mut self, path: &[String]) {
663        for item in path {
664            self.write_str(item, Style::Literal);
665            self.write_char(' ', Style::Text);
666        }
667    }
668}