sequoia_man/
man.rs

1use std::path::{Path, PathBuf};
2
3use roff::{bold, italic, roman, Inline, Roff};
4
5/// The "manual" the manual page is meant for. The full Unix
6/// documentation is (or was) divided into separate manuals, some of
7/// which don't consist of manual pages.
8const MANUAL: &str = "User Commands";
9
10/// The "source" of the manual: who produced the manual.
11const SOURCE: &str = "Sequoia PGP";
12
13/// Emits a warning.
14macro_rules! warn {
15    {$($exp: expr),*} => {
16        println!("cargo:warning={}",
17                 format_args!($($exp),*));
18    };
19}
20
21/// Emits a warning and exits with an error.
22macro_rules! fail {
23    {$($exp: expr),*} => {
24        warn!($($exp),*);
25        std::process::exit(1);
26    };
27}
28
29/// Build a ManualPage or several.
30//
31/// The main command is sq itself. It can have multiple levels of
32/// subcommands, and we treat the leaves of the subcommand tree
33/// specially: every leaf gets a manual page, and every intermediate
34/// node gets an overview page. For example, "sq encrypt" is a leaf,
35/// as is "sq key generate", but "sq key" is not.
36pub struct Builder {
37    title: String,
38    section: String,
39    date: Option<String>,
40    source: Option<String>,
41    manual: Option<String>,
42    version: Option<String>,
43    see_also: Vec<String>,
44    maincmd: Command,
45    // The set of top-level, global options.  This is derived from
46    // maincmd.
47    top_level_global_args: Vec<CommandOption>,
48}
49
50impl Builder {
51    /// Return a builder to generate manual page.
52    ///
53    /// `cmd` is a `clap::Command` that has been built to represent
54    /// the sq command line interface. The manual pages are generated
55    /// automatically from that information.
56    ///
57    /// `version` is the bare version, which is usually obtained from
58    /// `env!("CARGO_PKG_VERSION")`.
59    ///
60    /// If `extra_version` is `Some`, then the version is created
61    /// `version (extra_version)`.
62    ///
63    /// For sequoia packages, this is usually:
64    /// `format!("sequoia-openpgp {}", sequoia_openpgp::VERSION)`.
65    pub fn new(cmd: &mut clap::Command,
66               version: &str, extra_version: Option<&str>)
67        -> Self
68    {
69        cmd.build();
70
71        let version = if let Some(extra_version) = extra_version {
72            format!("{} ({})", version, extra_version)
73        } else {
74            version.to_string()
75        };
76
77        let maincmd = Command::from_command(&[], cmd);
78        let options = maincmd.get_options();
79
80        // Also consider --help a global argument.  This argument is
81        // added by clap, so applications can't mark it as such.
82        let top_level_global_args = options.into_iter()
83            .filter(|a| a.global || a.long.as_deref() == Some("--help"))
84            .collect::<Vec<_>>();
85
86        Self {
87            title: cmd.get_name().into(),
88            section: "1".into(),
89            date: Some(version.clone()),
90            source: Some(SOURCE.to_string()),
91            manual: Some(MANUAL.to_string()),
92            version: Some(version),
93            see_also: Vec::new(),
94            maincmd,
95            top_level_global_args,
96        }
97    }
98
99    /// Returns whether the specified argument is a top-level global
100    /// argument.
101    fn is_top_level_global_arg(&self, arg: &CommandOption) -> bool {
102        self.top_level_global_args
103            .binary_search_by_key(&arg.sort_key(),
104                                  |a: &CommandOption| a.sort_key())
105            .is_ok()
106    }
107
108    /// Set the date for the manual page. This is typically typeset in
109    /// the center of the footer of the page.
110    pub fn date(&mut self, date: &str) -> &mut Self{
111        self.date = Some(date.into());
112        self
113    }
114
115    /// Set the source of the manual page. This is typically typeset on
116    /// left of the footer of the page.
117    pub fn source(&mut self, source: &str) -> &mut Self {
118        self.source = Some(source.into());
119        self
120    }
121
122    /// Set the manual this page belongs to. This is typically typeset
123    /// on the center of the header of the page.
124    pub fn manual(&mut self, manual: &str) -> &mut Self {
125        self.manual = Some(manual.into());
126        self
127    }
128
129    /// Return a one-line summary of the command. This goes in the NAME
130    /// section of the manual page.
131    fn summary(about: &str) -> String {
132        let line = if let Some(line) = about.lines().next() {
133            line
134        } else {
135            ""
136        };
137        line.to_string()
138    }
139
140    /// Text that is added to the see also section.
141    ///
142    /// For example:
143    ///
144    /// ```text
145    /// For the full documentation see <https://book.sequoia-pgp.org>.
146    /// ```
147    pub fn see_also<S>(&mut self, see_also: &[S]) -> &mut Self
148        where S: AsRef<str>
149    {
150        self.see_also = see_also.into_iter()
151            .map(|s| {
152                s.as_ref().to_string()
153            })
154            .collect();
155        self
156    }
157
158    /// Returns the name of the binary, e.g., `sq`, or `sq-git`.
159    pub fn binary(&self) -> String {
160        self.maincmd.name()
161    }
162
163    /// Build all manual pages for sq and one for each leaf subcommand.
164    pub fn build(&self) -> Vec<ManualPage> {
165        let mut pages = vec![];
166        self.maincmd.build(self, &mut pages);
167        pages
168    }
169
170    /// Set the title of the page.
171    fn th(&self, man: &mut ManualPage) {
172        let empty = String::new();
173        man.th(
174            &self.title.to_uppercase(),
175            &self.section.to_uppercase(),
176            self.date.as_ref().unwrap_or(&empty),
177            self.source.as_ref().unwrap_or(&empty),
178            self.manual.as_ref().unwrap_or(&empty),
179        )
180    }
181}
182
183/// The command for which we generate a manual page.
184//
185/// We collect all the information about a command here so that it's
186/// handy when we generate various parts of a manual page that includes
187/// this command.
188#[derive(Debug, PartialEq, Eq)]
189struct Command {
190    command_words: Vec<String>,
191    before_help: Option<String>,
192    after_help: Option<String>,
193    about: Option<String>,
194    long_about: Option<String>,
195    options: Vec<CommandOption>,
196    args: Vec<String>,
197    examples: Vec<String>,
198    exit_status: Vec<String>,
199    subcommands: Vec<Command>,
200}
201
202impl Command {
203    /// Create a new `Command`. The command words are the part of
204    /// the command line that invokes this command. For sq itself,
205    /// they're `["sq"]`, but for a subcommand they might be `["sq",
206    /// "key", "generate"]` for example.
207    fn new(command_words: Vec<String>) -> Self {
208        assert!(! command_words.is_empty());
209        Self {
210            command_words,
211            before_help: None,
212            after_help: None,
213            about: None,
214            long_about: None,
215            options: vec![],
216            args: vec![],
217            examples: vec![],
218            exit_status: vec![],
219            subcommands: vec![],
220        }
221    }
222
223    /// Returns whether the command is the top-level command.
224    fn top_level(&self) -> bool {
225        self.command_words.len() == 1
226    }
227
228    /// Returns whether the command is a leaf command.
229    ///
230    /// A leaf command is not the top-level command and has no subcommands.
231    fn leaf(&self) -> bool {
232        ! self.top_level() && self.subcommands.is_empty()
233    }
234
235    /// Returns whether this it command is a singleton, i.e., it is
236    /// the top-level command, and it has no subcommands.
237    fn singleton(&self) -> bool {
238        self.top_level() && self.subcommands.is_empty()
239    }
240
241    /// Return the name of the command, with command words separated by
242    /// spaces. This is suitable for, say, the NAME section.
243    fn name(&self) -> String {
244        self.command_words.join(" ")
245    }
246
247    /// Return name of the subcommand, without the main command name.
248    fn subcommand_name(&self) -> String {
249        let mut words = self.command_words.clone();
250        words.remove(0);
251        words.join(" ")
252    }
253
254    /// Return the name of the manual page for this command. This is
255    /// the command words separated by dashes. Thus "sq key generate"
256    /// would return "sq-key-generate". Manual page names mustn't
257    /// contain spaces, thus the dash.
258    fn manpage_name(&self) -> String {
259        self.command_words.join("-")
260    }
261
262    /// Return the description of the command. This is collected from
263    /// the various about and help texts given to `clap`.
264    fn description(&self) -> String {
265        let mut desc = String::new();
266        if let Some(text) = &self.before_help {
267            desc.push_str(text);
268            desc.push('\n');
269        }
270
271        if let Some(text) = &self.long_about {
272            desc.push_str(text);
273            desc.push('\n');
274        } else if let Some(text) = &self.about {
275            desc.push_str(text);
276            desc.push('\n');
277        }
278
279        if let Some(text) = &self.after_help {
280            desc.push_str(text);
281            desc.push('\n');
282        }
283        desc
284    }
285
286    /// Add the `before_help` help text for this command.
287    fn before_help(&mut self, help: &str) {
288        self.before_help = Some(self.extract_all(help));
289    }
290
291    /// Add the `after_help` help text for this command.
292    fn after_help(&mut self, help: &str) {
293        self.after_help = Some(self.extract_all(help));
294    }
295
296    /// Add the `about` help text for this command.
297    fn about(&mut self, help: &str) {
298        self.about = Some(self.extract_all(help));
299    }
300
301    /// Add the `long_about` help text for this command.
302    fn long_about(&mut self, help: &str) {
303        self.long_about = Some(self.extract_all(help));
304    }
305
306    /// Add an option to this command.
307    fn option(&mut self, opt: CommandOption) {
308        self.options.push(opt);
309    }
310
311    /// Add a positional argument to this command.
312    fn arg(&mut self, arg: &str) {
313        self.args.push(arg.into());
314    }
315
316    /// Extracts all additional sections.
317    fn extract_all(&mut self, text: &str) -> String {
318        let text = self.extract_example(text);
319        let text = self.extract_exit_status(&text);
320        text
321    }
322
323    /// Extract examples from help text: anything that follows a line
324    /// consisting of "Examples:".
325    ///
326    /// This is a convention specific to sq, not something that comes
327    /// from `clap`.
328    fn extract_example(&mut self, text: &str) -> String {
329        Self::extract("Examples:\n", &mut self.examples, text)
330    }
331
332    /// Extract exit status from help text: anything that follows a line
333    /// consisting of "Exit status:".
334    ///
335    /// This is a convention specific to sq, not something that comes
336    /// from `clap`.
337    fn extract_exit_status(&mut self, text: &str) -> String {
338        Self::extract("Exit status:\n", &mut self.exit_status, text)
339    }
340
341    /// Extracts sections, like examples, putting them into `into`.
342    fn extract(marker: &str, into: &mut Vec<String>, text: &str) -> String {
343        if let Some(pos) = text.find(marker) {
344            let (text, ex) = text.split_at(pos);
345            if let Some(ex) = ex.strip_prefix(marker) {
346                into.push(ex.into());
347            } else {
348                into.push(ex.into());
349            }
350            text.into()
351        } else {
352            text.into()
353        }
354    }
355
356    /// Does this command have any options?
357    fn has_options(&self) -> bool {
358        !self.options.is_empty()
359    }
360
361    /// Get the list of options for this command.
362    fn get_options(&self) -> Vec<CommandOption> {
363        let mut opts = self.options.clone();
364        opts.sort_by_cached_key(|opt| opt.sort_key());
365        opts
366    }
367
368    /// Does this command have examples?
369    fn has_examples(&self) -> bool {
370        !self.examples.is_empty()
371    }
372
373    /// Create a new `Command` from a `clap::Command` structure.
374    fn from_command(parent: &[String], cmd: &clap::Command) -> Self {
375        let mut words: Vec<String> = parent.into();
376        words.push(cmd.get_name().to_string());
377        let mut new = Self::new(words);
378        if let Some(text) = cmd.get_before_help() {
379            new.before_help(&text.to_string());
380        }
381        if let Some(text) = cmd.get_after_help() {
382            new.after_help(&text.to_string());
383        }
384        if let Some(text) = cmd.get_about() {
385            new.about(&text.to_string());
386        }
387        if let Some(text) = cmd.get_long_about() {
388            new.long_about(&text.to_string());
389        }
390        for arg in cmd.get_arguments()
391            .filter(|a| ! a.is_hide_set())
392        {
393            new.option(CommandOption::from_arg(arg));
394        }
395        for arg in cmd.get_positionals()
396            .filter(|a| ! a.is_hide_set())
397        {
398            if let Some(names) = arg.get_value_names() {
399                for name in names {
400                    new.arg(name);
401                }
402            }
403        }
404        let mut parent = parent.to_vec();
405        parent.push(cmd.get_name().into());
406        new.subcommands = cmd.get_subcommands()
407            .filter(|cmd| ! cmd.is_hide_set())
408            .filter(|cmd| cmd.get_name() != "help")
409            .map(|cmd| Command::from_command(&parent, cmd))
410            .collect();
411        new
412    }
413
414    /// Builds manpages recursively.
415    fn build(&self, builder: &Builder, acc: &mut Vec<ManualPage>) {
416        acc.push(self.build_command(builder));
417        for cmd in &self.subcommands {
418            cmd.build(builder, acc);
419        }
420    }
421
422    /// Build a manual page for an intermediate (i.e. non-leaf) command.
423    fn build_command(&self, builder: &Builder) -> ManualPage {
424        let filename = format!("{}.{}", self.manpage_name(), builder.section);
425        let mut man = ManualPage::new(PathBuf::from(filename));
426        builder.th(&mut man);
427
428        let about = &self.about.clone().unwrap();
429        let summary = Builder::summary(about);
430        man.name_section(&self.manpage_name(), &summary);
431
432        man.section("SYNOPSIS");
433        let bin_name = builder.binary();
434        if self.singleton() || self.leaf() {
435            // Show the command.
436            man.subcommand_synopsis(
437                &bin_name,
438                &self.subcommand_name(),
439                self.has_options(),
440                &self.args,
441                true,
442            );
443        } else {
444            // Show the subcommands.
445            for sub in &self.subcommands {
446                man.subcommand_synopsis(
447                    &bin_name,
448                    &sub.subcommand_name(),
449                    sub.has_options(),
450                    &sub.args,
451                    sub.leaf(),
452                );
453            }
454        }
455
456        man.section("DESCRIPTION");
457        man.text_with_period(&self.description());
458
459        // Show the options for top-level and leaf commands.
460        if self.top_level() || self.leaf()  {
461            let mut showed_options_header = false;
462
463            // Show command-specific arguments.
464            let mut self_opts = self.get_options();
465            if ! self.singleton() {
466                self_opts = self_opts.into_iter()
467                    .filter(|o| ! builder.is_top_level_global_arg(o))
468                    .collect::<Vec<_>>();
469            }
470
471            if ! self_opts.is_empty() {
472                if ! showed_options_header {
473                    showed_options_header = true;
474                    man.section("OPTIONS");
475                }
476
477                if ! self.top_level() {
478                    // This is not the top-level command.
479                    man.subsection("Subcommand options");
480                }
481
482                for opt in self_opts.iter() {
483                    man.option(opt);
484                }
485            }
486
487            // Show the global arguments at the top-level.  Otherwise,
488            // reference the top-level command.
489            let global_options = &builder.top_level_global_args;
490            if ! self.singleton() && ! global_options.is_empty() {
491                #[allow(unused_assignments)]
492                if ! showed_options_header {
493                    showed_options_header = true;
494                    man.section("OPTIONS");
495                }
496
497                man.subsection("Global options");
498                if self.top_level() {
499                    // At the top-level, we print out the top-level
500                    // global options.
501                    for opt in global_options.iter() {
502                        man.option(opt);
503                    }
504                } else {
505                    // Non-top-level commands just reference the top
506                    // level's documentation.
507                    man.roff.text(vec![
508                        roman("See "),
509                        bold(bin_name), roman("("), roman("1"), roman(")"),
510                        roman(" for a description of the global options."),
511                    ]);
512                }
513            }
514        }
515
516        if ! self.subcommands.is_empty() {
517            man.section("SUBCOMMANDS");
518            for sub in &self.subcommands {
519                let desc = sub.description();
520                if !desc.is_empty() {
521                    man.subsection(&sub.name());
522                    man.text_with_period(&desc);
523                }
524            }
525        }
526
527        man.exit_status_section(self);
528
529        // Maybe add environment section.
530        if self == &builder.maincmd {
531            let opts = self.get_options();
532            let mut envs: Vec<_> =
533                opts.iter().filter(|o| o.env.is_some()).collect();
534            envs.sort_by_key(|o| o.env.as_ref());
535
536            if ! envs.is_empty() {
537                man.section("ENVIRONMENT");
538                for opt in envs {
539                    man.env_option(opt);
540                }
541            }
542        }
543
544        if self.singleton() || self.leaf() {
545            man.examples_section(&[self]);
546        } else {
547            man.examples_section(
548                &std::iter::once(self)
549                    .chain(self.subcommands.iter())
550                    .collect::<Vec<_>>());
551        }
552
553        let mut see_also_shown = false;
554        let names: Vec<String> = (1..self.command_words.len())
555            .map(|n| self.command_words[0..n].join("-"))
556            .chain(self.subcommands.iter().map(|sub| sub.manpage_name()))
557            .collect();
558        if ! names.is_empty() {
559            if ! see_also_shown {
560                man.section("SEE ALSO");
561                see_also_shown = true;
562            }
563
564            man.man_page_refs(&names, &builder.section);
565            man.paragraph();
566        }
567        if ! builder.see_also.is_empty() {
568            #[allow(unused_assignments)]
569            if ! see_also_shown {
570                man.section("SEE ALSO");
571                see_also_shown = true;
572            }
573
574            for (i, see_also) in builder.see_also.iter().enumerate() {
575                if i > 0 {
576                    man.paragraph();
577                }
578                man.text(see_also);
579            }
580        }
581
582        man.version_section(&builder.version);
583
584        man
585    }
586}
587
588/// Represent a command line option for manual page generation.
589//
590/// This doesn't capture all the things that `clap` allows, but is
591/// sufficient for what sq actually uses.
592#[derive(Clone, Debug, PartialEq, Eq)]
593struct CommandOption {
594    /// Index for positional arguments.
595    index: Option<usize>,
596    short: Option<String>,
597    long: Option<String>,
598    /// Whether the global flag is set.
599    global: bool,
600    env: Option<String>,
601    value_names: Values,
602    possible_values: Vec<String>,
603    default_values: Vec<String>,
604    help: Option<String>,
605}
606
607/// Formal arguments for a command option.
608#[derive(Clone, Debug, PartialEq, Eq)]
609enum Values {
610    /// This option does not take any arguments.
611    None,
612
613    /// This option takes the given arguments.
614    Some(Vec<String>),
615
616    /// This option optionally takes the given arguments.
617    ///
618    /// XXX: Currently, this is all or nothing, i.e. either all
619    /// arguments are mandatory, or all are optional.  Let's see
620    /// whether we need something more complicated.
621    Optional(Vec<String>),
622}
623
624impl CommandOption {
625    /// Return a key for sorting a list of options.
626    ///
627    /// Manual pages list options in various places, and it enables
628    /// quicker lookup by readers if they lists are sorted
629    /// alphabetically.  By convention, such lists are sorted by short
630    /// option first, if one exists.  And, positional arguments are
631    /// sorted to the bottom, and in the order they appear on the
632    /// command line.
633    fn sort_key(&self) -> (usize, String) {
634        let mut key = String::new();
635        if let Some(name) = &self.short {
636            key.push_str(name.strip_prefix('-').unwrap());
637            key.push(',');
638        }
639        if let Some(name) = &self.long {
640            key.push_str(name.strip_prefix("--").unwrap());
641        }
642        (self.index.unwrap_or(0), key)
643    }
644}
645
646impl CommandOption {
647    /// Create a `CommandOption` from a `clap::Arg`.
648    fn from_arg(arg: &clap::Arg) -> Self {
649        let num_args = arg.get_num_args().unwrap_or_default();
650        let value_names = if num_args.takes_values() {
651            let names = arg.get_value_names()
652                .unwrap_or(&[])
653                .into_iter()
654                .map(|s| s.to_string())
655                .collect::<Vec<String>>();
656
657            if num_args.min_values() == 0 {
658                Values::Optional(names)
659            } else {
660                Values::Some(names)
661            }
662        } else {
663            // Flag.
664            Values::None
665        };
666
667        let long = arg.get_long().map(|o| format!("--{}", o));
668        let global = arg.is_global_set();
669        if global {
670            assert!(long.is_some(),
671                    "Global options must have a long name.");
672        }
673
674        Self {
675            index: arg.get_index(),
676            short: arg.get_short().map(|o| format!("-{}", o)),
677            long,
678            global,
679            value_names,
680            env: arg.get_env().and_then(|e| e.to_str().map(Into::into)),
681            possible_values: arg.get_possible_values().iter()
682                .map(|o| o.get_name().into()).collect(),
683            default_values: if num_args.takes_values() {
684                    // At least one argument.
685                    arg.get_default_values().iter()
686                        .map(|s| s.to_string_lossy().to_string())
687                        .collect()
688                } else {
689                    // No argument, i.e. a flag.  We don't want to
690                    // output `[default: false]` in that case.
691                    vec![]
692                },
693            help: arg.get_long_help().or(arg.get_help())
694                .map(|s| s.to_string()),
695        }
696    }
697}
698
699/// Troff code for a manual page.
700///
701/// The code is in [`troff`](https://en.wikipedia.org/wiki/Troff)
702/// format, as is usual for Unix manual page documentation. It's using
703/// the `man` macro package for `troff`.
704pub struct ManualPage {
705    filename: PathBuf,
706    roff: Roff,
707
708    // Are we in code mode (emitted .nf)?
709    in_code: bool,
710}
711
712impl ManualPage {
713    fn new(filename: PathBuf) -> Self {
714        Self {
715            filename,
716            roff: Roff::new(),
717            in_code: false,
718        }
719    }
720
721    /// Set the title of the manual page. The "TH" macro takes five
722    /// arguments: name of the command; section of the manual; the date
723    /// of latest update; the source of manual; and the name of the manual.
724    fn th(&mut self, name: &str, section: &str, date: &str, source: &str, manual: &str) {
725        self.roff
726            .control("TH", [name, section, date, source, manual]);
727    }
728
729    /// Typeset the NAME section: the title, and a line with the name
730    /// of the command, followed by a dash, and a one-line. The dash
731    /// should be escaped with backslash, but the `roff` crate does
732    /// that for us.
733    fn name_section(&mut self, name: &str, summary: &str) {
734        self.section("NAME");
735        self.roff.text([roman(&format!("{} - {}", name, summary))]);
736    }
737
738    /// Typeset code blocks, joining adjacent blocks.
739    fn code(&mut self, code: bool) {
740        match (self.in_code, code) {
741            (false, false) => (),
742            (false, true) => {
743                self.roff.control("nf", []);
744                self.in_code = true;
745            },
746            (true, false) => {
747                self.roff.control("fi", []);
748                self.in_code = false;
749            },
750            (true, true) => (),
751        }
752    }
753
754    /// Typeset the synopsis of a command. This is going to be part of
755    /// the SYNOPSIS section. There are conventions for how it should
756    /// be typeset. For sq, we simplify them by summarizing options
757    /// into a placeholder, and only listing command words and
758    /// positional arguments.
759    fn subcommand_synopsis(
760        &mut self,
761        bin: &str,
762        sub: &str,
763        sub_options: bool,
764        args: &[String],
765        is_leaf: bool,
766    ) {
767        let mut line = vec![
768            bold(if sub.is_empty() {
769                bin.to_string()
770            } else {
771                format!("{} {}", bin, sub)
772            }),
773            roman(" ["), italic("OPTIONS"), roman("] "),
774        ];
775
776        for (i, arg) in args.iter().enumerate() {
777            if i > 0 || ! sub_options {
778                line.push(roman(" "));
779            }
780            line.push(italic(arg));
781        }
782
783        if args.is_empty() {
784            line.push(roman(" "));
785        }
786
787        if ! is_leaf {
788            line.push(italic("SUBCOMMAND"));
789        }
790
791        self.roff.control("br", []);
792        self.roff.text(line);
793    }
794
795    /// Typeset an option, for the OPTIONS section. This is typeset
796    /// using "tagged paragraphs", where the first line lists the
797    /// aliases of the option, and any values it may take, and the rest
798    /// is indented paragraphs of text explaining what the option does.
799    fn option(&mut self, opt: &CommandOption) {
800        let mut line = vec![];
801
802        if let Some(short) = &opt.short {
803            line.push(bold(short));
804        }
805        if let Some(long) = &opt.long {
806            if opt.short.is_some() {
807                line.push(roman(", "));
808            }
809            line.push(bold(long));
810        }
811
812        match &opt.value_names {
813            Values::None => (),
814            Values::Some(values) | Values::Optional(values) => {
815                if matches!(opt.value_names, Values::Optional(_)) {
816                    line.push(roman("["));
817                }
818
819                if (opt.short.is_some() || opt.long.is_some())
820                    && values.len() == 1
821                {
822                    line.push(roman("="));
823                    line.push(italic(&values[0]));
824                } else {
825                    for value in values {
826                        line.push(roman(" "));
827                        line.push(italic(value));
828                    }
829                }
830
831                if matches!(opt.value_names, Values::Optional(_)) {
832                    line.push(roman("]"));
833                }
834            },
835        }
836
837        self.tagged_paragraph(line, &opt.help);
838
839        if ! opt.default_values.is_empty() {
840            self.indented_paragraph();
841            let mut line = vec![];
842            line.push(roman("[default: "));
843            for (i, v) in opt.default_values.iter().enumerate() {
844                if i > 0 {
845                    line.push(roman(", "));
846                }
847                line.push(bold(v));
848            }
849            line.push(roman("]"));
850            self.roff.text(line);
851        }
852
853        if ! opt.possible_values.is_empty() {
854            self.indented_paragraph();
855            let mut line = vec![];
856            line.push(roman("[possible values: "));
857            for (i, v) in opt.possible_values.iter().enumerate() {
858                if i > 0 {
859                    line.push(roman(", "));
860                }
861                line.push(bold(v));
862            }
863            line.push(roman("]"));
864            self.roff.text(line);
865        }
866    }
867
868    /// Typeset an option, for the ENVIRONMENT section.
869    ///
870    /// This is typeset using "tagged paragraphs", where the first
871    /// line lists the aliases of the option, and any values it may
872    /// take, and the rest is indented paragraphs of text explaining
873    /// what the option does.
874    fn env_option(&mut self, opt: &CommandOption) {
875        let mut line = vec![
876            bold(opt.env.as_ref().expect("must be an env")),
877        ];
878
879        match &opt.value_names {
880            Values::None => (),
881            Values::Some(values) | Values::Optional(values) => {
882                assert_eq!(values.len(), 1);
883                line.push(roman("="));
884                line.push(italic(&values[0]));
885            },
886        }
887
888        self.tagged_paragraph(line, &opt.help);
889
890        if ! opt.default_values.is_empty() {
891            self.indented_paragraph();
892            let mut line = vec![];
893            line.push(roman("[default: "));
894            for (i, v) in opt.default_values.iter().enumerate() {
895                if i > 0 {
896                    line.push(roman(", "));
897                }
898                line.push(bold(v));
899            }
900            line.push(roman("]"));
901            self.roff.text(line);
902        }
903
904        if ! opt.possible_values.is_empty() {
905            self.indented_paragraph();
906            let mut line = vec![];
907            line.push(roman("[possible values: "));
908            for (i, v) in opt.possible_values.iter().enumerate() {
909                if i > 0 {
910                    line.push(roman(", "));
911                }
912                line.push(bold(v));
913            }
914            line.push(roman("]"));
915            self.roff.text(line);
916        }
917    }
918
919    /// Typeset an EXIT STATUS section, if available.
920    fn exit_status_section(&mut self, c: &Command) {
921        if c.exit_status.is_empty() {
922            return;
923        }
924
925        self.section("EXIT STATUS");
926        for chunk in &c.exit_status {
927            self.text(&chunk);
928        }
929    }
930
931    /// Typeset an EXAMPLES section, if a command has examples.
932    fn examples_section(&mut self, subs: &[&Command]) {
933        if !subs.iter().any(|c| c.has_examples()) {
934            return;
935        }
936
937        self.section("EXAMPLES");
938        let mut need_para = false;
939        let need_subsections = subs.len() > 1;
940        for cmd in subs.iter() {
941            if need_para {
942                self.paragraph();
943                need_para = false;
944            }
945
946            if !cmd.examples.is_empty() {
947                if need_subsections {
948                    self.subsection(&cmd.name());
949                    need_para = false;
950                }
951
952                for ex in cmd.examples.iter() {
953                    // Was the last line a continuation?
954                    let mut continuation = false;
955
956                    // Was the last line part of the description?
957                    let mut description = false;
958
959                    for line in ex.lines() {
960                        if ! continuation
961                            && ! (description && line.starts_with("#"))
962                        {
963                            self.paragraph();
964                        }
965
966                        const TARGET_LINE_LENGTH: usize = 78;
967                        const RS_INDENTATION: usize = 7;
968                        const EXAMPLE_COMMAND_MAX_WIDTH: usize =
969                            TARGET_LINE_LENGTH - 2 * RS_INDENTATION;
970                        const EXAMPLE_CONTINUATION_MAX_WIDTH: usize =
971                            TARGET_LINE_LENGTH - 3 * RS_INDENTATION;
972
973                        if let Some(line) = line.strip_prefix("# ") {
974                            self.code(false);
975                            self.roff.text([roman(line)]);
976                        } else if let Some(line) = line.strip_prefix("$ ") {
977                            let line = line.trim();
978                            if line.len() > EXAMPLE_COMMAND_MAX_WIDTH {
979                                warn!("Command in example exceeds {} chars:",
980                                      EXAMPLE_COMMAND_MAX_WIDTH);
981                                fail!("{} ({} chars)", line, line.len());
982                            }
983                            self.code(true);
984                            self.roff.control("RS", []);
985                            self.roff.text([roman(line)]);
986                            self.roff.control("RE", []);
987                        } else if continuation {
988                            let line = line.trim();
989                            if line.len() > EXAMPLE_CONTINUATION_MAX_WIDTH {
990                                warn!("Continuation in example exceeds {} chars:",
991                                      EXAMPLE_CONTINUATION_MAX_WIDTH);
992                                fail!("{} ({} chars)", line, line.len());
993                            }
994                            self.code(true);
995                            self.roff.control("RS", []);
996                            self.roff.control("RS", []);
997                            self.roff.text([roman(line)]);
998                            self.roff.control("RE", []);
999                            self.roff.control("RE", []);
1000                        } else {
1001                            self.code(false);
1002                            self.roff.text([roman(line)]);
1003                        }
1004
1005                        // Update continuation for the next loop iteration.
1006                        continuation = line.ends_with("\\");
1007
1008                        // Update description for the next loop iteration.
1009                        description = line.starts_with("#");
1010
1011                        // We emitted at least one example, make sure
1012                        // to add a new paragraph before going to the
1013                        // next command.
1014                        need_para = true;
1015                    }
1016                }
1017
1018                self.code(false);
1019            }
1020        }
1021    }
1022
1023    /// Typeset the VERSION section, if the main command has a version
1024    /// set.
1025    fn version_section(&mut self, version: &Option<String>) {
1026        if let Some(v) = version {
1027            self.section("VERSION");
1028            self.roff.text([roman(v)]);
1029        }
1030    }
1031
1032    /// Start a new section with the SH troff command.
1033    fn section(&mut self, heading: &str) {
1034        self.roff.control("SH", [heading]);
1035    }
1036
1037    /// Start a new subsection with the SS troff command.
1038    fn subsection(&mut self, heading: &str) {
1039        self.roff.control("SS", [heading]);
1040    }
1041
1042    /// Start a new paragraph with the PP troff command.
1043    fn paragraph(&mut self) {
1044        self.roff.control("PP", []);
1045    }
1046
1047    /// Start a new indented paragraph with the IP troff command.
1048    fn indented_paragraph(&mut self) {
1049        self.roff.control("IP", []);
1050    }
1051
1052    /// Start a tagged paragraph with th TP troff command.
1053    ///
1054    /// This command takes the line after the command and typesets it,
1055    /// and the line after that starts an indented paragraph.
1056    ///
1057    /// Additionally, if `text` is given, it is rendered after the
1058    /// `line`, indenting it.
1059    fn tagged_paragraph(&mut self, line: Vec<Inline>, text: &Option<String>) {
1060        self.roff.control("TP", []);
1061        self.roff.text(line);
1062
1063        if let Some(text) = text {
1064            let mut paras = text.split("\n\n");
1065            if let Some(first) = paras.next() {
1066                self.roff.text([roman(first)]);
1067            }
1068            for para in paras {
1069                self.roff.control("IP", []);
1070                self.roff.text([roman(para)]);
1071            }
1072        }
1073    }
1074
1075    /// Typeset a list of references to manual pages, suitable for the
1076    /// SEE ALSO section. Manual page references are, by convention,
1077    /// typeset with the name of manual page in bold, and the section
1078    /// of the page in normal ("roman") font, enclosed in parentheses.
1079    /// The references are separated by commas, in normal font.
1080    fn man_page_refs(&mut self, names: &[String], section: &str) {
1081        let mut line = vec![];
1082        for name in names.iter() {
1083            if !line.is_empty() {
1084                line.push(roman(", "));
1085            }
1086            line.push(bold(name));
1087            line.push(roman("("));
1088            line.push(roman(section));
1089            line.push(roman(")"));
1090        }
1091        line.push(roman("."));
1092
1093        self.roff.control("nh", []);
1094        self.roff.text(line);
1095        self.roff.control("hy", []);
1096    }
1097
1098    /// Typeset normal text consisting of paragraphs. Paragraphs are
1099    /// separated by an empty line. All but the first paragraph are
1100    /// preceded by the troff paragraph command. The first one is not,
1101    /// to avoid unwanted empty lines in the output.
1102    fn text(&mut self, text: &str) {
1103        let mut paras = text.split("\n\n");
1104        if let Some(first) = paras.next() {
1105            self.roff.text([roman(first)]);
1106        }
1107        for para in paras {
1108            self.paragraph();
1109            self.roff.text([roman(para)]);
1110        }
1111    }
1112
1113    /// Like [`ManualPage::text`], but add a period, if missing, to the end of
1114    /// the first paragraph. In `clap` about texts, the first line
1115    /// conventionally doesn't end in a period, but in manual pages,
1116    /// when that text is used in a DESCRIPTION section, it should have
1117    /// a period.
1118    fn text_with_period(&mut self, text: &str) {
1119        let mut paras = text.split("\n\n");
1120        if let Some(first) = paras.next() {
1121            let first = if let Some(prefix) = first.strip_suffix(".\n") {
1122                format!("{}.", prefix)
1123            } else if let Some(prefix) = first.strip_suffix('\n') {
1124                format!("{}.", prefix)
1125            } else if first.ends_with('.') {
1126                first.to_string()
1127            } else {
1128                format!("{}.", first)
1129            };
1130            self.roff.text([roman(first)]);
1131        }
1132        for para in paras {
1133            self.paragraph();
1134            self.roff.text([roman(para)]);
1135        }
1136    }
1137
1138    /// What should the filename be, on disk, for this manual page?
1139    pub fn filename(&self) -> &Path {
1140        &self.filename
1141    }
1142
1143    /// Return the `troff` source code for the manual page.
1144    pub fn troff_source(&self) -> String {
1145        self.roff.to_roff()
1146    }
1147}