summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLars Wirzenius <liw@sequoia-pgp.org>2022-08-31 09:54:48 +0300
committerLars Wirzenius <liw@sequoia-pgp.org>2022-09-20 09:53:54 +0300
commit339af5f4fbec4620bbcdefbd14713a5570533cac (patch)
tree5b2ff02d3e277da37aa98e8f334443a36d05f8a4
parent68f956426fdef71fe7db0d686edef5b1a2b214be (diff)
sq: write out manual pages for sq
Generate an all-in-one manual page sq.1, and a separate manual page for each leaf level subcommand: sq-armor.1, seq-key-generate.1, etc. We've previously had a grouping of subcommands per top level subcommand: all sub-subcommands of "sq key" would go into sq-key.1. However, I found that to be hard to follow, as a reader. I prefer either all in one, or just the one subcommand in a page. Use custom code to generate the manual page, in sq/src/man.rs, because I wasn't happy with clap_mangen output, and wanted something more idiomatic. The custom code is a little specific for sq, and may or may not be possible to use for other programs. To trigger manual page generation, run sq with SQ_MAN set to the name of a directory where the manual pages should be written, when running sq. This was an easier way to do this than a new, hidden subcommand ("sq generate-man" or something like that). Add the roff crate as a dependency. It's used to generate troff source code for manual pages. Generating correct troff is tricky enough that there's no point in doing it manually. Move the "SEE ALSO" section in the "after_help" text for "sq verify" into the "before_help" so that it doesn't end up as plain text in the manual page. This was an easier change than making the "sq help text markup" parser in sq/src/man.rs understand the SEE ALSO heading. Sponsored-by: pep.foundation
-rw-r--r--Cargo.lock23
-rw-r--r--sq/Cargo.toml1
-rw-r--r--sq/README.md13
-rw-r--r--sq/sq-usage.md8
-rw-r--r--sq/src/man.rs777
-rw-r--r--sq/src/sq.rs13
-rw-r--r--sq/src/sq_cli/verify.rs8
7 files changed, 825 insertions, 18 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ca9bbfdb..a297228e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -433,9 +433,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.2.15"
+version = "3.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44bbe24bbd31a185bc2c4f7c2abe80bea13a20d57ee4e55be70ac512bdc76417"
+checksum = "68d43934757334b5c0519ff882e1ab9647ac0258b47c24c4f490d78e42697fd5"
dependencies = [
"atty",
"bitflags",
@@ -455,14 +455,14 @@ version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da92e6facd8d73c22745a5d3cbb59bdf8e46e3235c923e516527d8e81eec14a4"
dependencies = [
- "clap 3.2.15",
+ "clap 3.2.19",
]
[[package]]
name = "clap_derive"
-version = "3.2.15"
+version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck",
"proc-macro-error",
@@ -2556,7 +2556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f0c08002dd427499194cef0e292cfd515281777d5b9cc4c638028d2d3aebda4"
dependencies = [
"anyhow",
- "clap 3.2.15",
+ "clap 3.2.19",
"serde",
"serde_yaml",
"textwrap 0.15.0",
@@ -2564,6 +2564,12 @@ dependencies = [
]
[[package]]
+name = "roff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
+
+[[package]]
name = "rpassword"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2816,11 +2822,12 @@ dependencies = [
"buffered-reader",
"cfg-if",
"chrono",
- "clap 3.2.15",
+ "clap 3.2.19",
"clap_complete",
"fehler",
"itertools 0.10.3",
"predicates",
+ "roff",
"rpassword",
"sequoia-autocrypt",
"sequoia-net",
@@ -3087,7 +3094,7 @@ checksum = "0b14be17e3a06a320b4d9851ef7f512bcb19e9c931cd2b2493b61b4a7e6aa6d5"
dependencies = [
"anyhow",
"base64",
- "clap 3.2.15",
+ "clap 3.2.19",
"env_logger 0.9.0",
"file_diff",
"git-testament",
diff --git a/sq/Cargo.toml b/sq/Cargo.toml
index bfbd0a55..ab94e858 100644
--- a/sq/Cargo.toml
+++ b/sq/Cargo.toml
@@ -40,6 +40,7 @@ tokio = { version = "1.13.1" }
rpassword = "5.0"
serde_json = "1.0.80"
serde = { version = "1.0.137", features = ["derive"] }
+roff = "0.2.1"
[build-dependencies]
anyhow = "1.0.18"
diff --git a/sq/README.md b/sq/README.md
index ba52ca58..cda28891 100644
--- a/sq/README.md
+++ b/sq/README.md
@@ -16,6 +16,19 @@ $ sq help
These are collected as the [sq help][] page, for your convenience.
+## Generate manual pages
+
+To generate manual pages, run:
+
+~~~sh
+SQ_MAN=xyzzy cargo run
+~~~
+
+This will generate manual pages in the `xyzzy` directory. The
+directory will be created if it doesn't exist (but not any missing
+parent directories). There will be one page for all of `sq`, and one
+for each subcommand that doesn't have subcommands of its own.
+
[Sequoia-PGP]: https://sequoia-pgp.org/
[sq user guide]: https://sequoia-pgp.gitlab.io/sq-user-guide/
[sq help]: https://docs.sequoia-pgp.org/sq/index.html
diff --git a/sq/sq-usage.md b/sq/sq-usage.md
index 00d2a3ca..67b8612e 100644
--- a/sq/sq-usage.md
+++ b/sq/sq-usage.md
@@ -329,6 +329,9 @@ if it is larger, then the output will be truncated.
The converse operation is "sq sign".
+If you are looking for a standalone program to verify detached
+signatures, consider using sequoia-sqv.
+
USAGE:
sq verify [OPTIONS] [FILE]
@@ -362,11 +365,6 @@ $ sq verify --signer-cert juliet.pgp signed-message.pgp
# Verify a detached message
$ sq verify --signer-cert juliet.pgp --detached message.sig message.txt
-
-SEE ALSO:
-
-If you are looking for a standalone program to verify detached
-signatures, consider using sequoia-sqv.
```
## Subcommand sq key
diff --git a/sq/src/man.rs b/sq/src/man.rs
new file mode 100644
index 00000000..4c084ec9
--- /dev/null
+++ b/sq/src/man.rs
@@ -0,0 +1,777 @@
+//! Generate Unix manual pages for sq from its `clap::Command` value.
+//!
+//! A Unix manual page is a document marked up with the
+//! [troff](https://en.wikipedia.org/wiki/Troff) language. The troff
+//! markup is the source code for the page, and is formatted and
+//! displayed using the "man" command.
+//!
+//! Troff is a child of the 1970s and is one of the earlier markup
+//! languages. It has little resemblance to markup languages born in
+//! the 21st century, such as Markdown. However, it's not actually
+//! difficult, merely old, and sometimes weird. Some of the design of
+//! the troff language was dictated by the constraints of 1970s
+//! hardware, programming languages, and fashions in programming. Let
+//! not those scare you.
+//!
+//! The troff language supports "macros", a way to define new commands
+//! based on built-in commands. There are a number of popular macro
+//! packages for various purposes. One of the most popular ones for
+//! manual pages is called "man", and this module generates manual
+//! pages for that package. It's supported by the "man" command on all
+//! Unix systems.
+//!
+//! Note that this module doesn't aim to be a generic manual page
+//! generator. The scope is specifically the Sequoia sq command.
+
+use clap::{Arg, Command};
+use roff::{bold, italic, roman, Inline, Roff};
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+
+/// The "manual" the manual page is meant for. The full Unix
+/// documentation is (or was) divided into separate manuals, some of
+/// which don't consist of manual pages.
+const MANUAL: &str = "User Commands";
+
+/// The "source" of the manual: who produced the manual.
+const SOURCE: &str = "Sequoia-PGP";
+
+/// Text to add to the end of the "SEE ALSO" section of sq manual page.
+const SEE_ALSO: &str = "For the full documentation see <https://docs.sequoia-pgp.org/sq/>.";
+
+/// Generate manual page.
+///
+/// `cmd` is a `clap::Command` that has been built to represent the sq
+/// command line interface. The manual pages are generated
+/// automatically from that information.
+///
+/// This will produce a manual page for the whole sq, and one per
+/// subcommand. Each manual page knows what its filename should be.
+pub fn manpages(cmd: &Command) -> Vec<ManualPage> {
+ let mut builder = Builder::new(cmd, "1");
+ builder.date(env!("CARGO_PKG_VERSION"));
+ builder.source(SOURCE);
+ builder.manual(MANUAL);
+ builder.build()
+}
+
+/// Build a ManualPage or several.
+//
+/// The main command is sq itself. It can have multiple levels of
+/// subcommands, and we treat the leaves of the subcommand tree
+/// specially: the main command and the leaves get manual pages of
+/// their own. For example, "sq encrypt" is a leaf, as is "sq key
+/// generate", but "sq key" is not.
+struct Builder {
+ title: String,
+ section: String,
+ date: Option<String>,
+ source: Option<String>,
+ manual: Option<String>,
+ version: Option<String>,
+ maincmd: LeafCommand,
+ subcommands: HashMap<String, Vec<LeafCommand>>,
+}
+
+impl Builder {
+ fn new(cmd: &Command, section: &str) -> Self {
+ let mut subcommands: HashMap<String, Vec<LeafCommand>> = HashMap::new();
+ for sub in cmd.get_subcommands() {
+ let mut leaves = vec![];
+ let mut top = vec![cmd.get_name().into()];
+ Self::leaves(&mut leaves, &top, sub);
+ top.push(sub.get_name().into());
+ subcommands.insert(top.join(" "), leaves);
+ }
+
+ Self {
+ title: cmd.get_name().into(),
+ section: section.into(),
+ maincmd: LeafCommand::from_command(&[], cmd),
+ date: None,
+ source: None,
+ manual: None,
+ version: cmd.get_version().map(|v| v.to_string()),
+ subcommands,
+ }
+ }
+
+ // Set the date for the manual page. This is typically typeset in
+ // the center of the footer of the page.
+ fn date(&mut self, date: &str) {
+ self.date = Some(date.into());
+ }
+
+ // Set the source of the manual page. This is typically typeset on
+ // left of the footer of the page.
+ fn source(&mut self, source: &str) {
+ self.source = Some(source.into());
+ }
+
+ // Set the manual this page belongs to. This is typically typeset
+ // on the center of the header of the page.
+ fn manual(&mut self, manual: &str) {
+ self.manual = Some(manual.into());
+ }
+
+ // Return a one-line summary of the command. This goes in the NAME
+ // section of the manual page.
+ fn summary(about: &str) -> String {
+ let line = if let Some(line) = about.lines().next() {
+ line
+ } else {
+ ""
+ };
+ line.to_string()
+ }
+
+ // Collect into `cmds` all the subcommands that don't have subcommands.
+ fn leaves(cmds: &mut Vec<LeafCommand>, parent: &[String], cmd: &Command) {
+ if cmd.get_subcommands().count() == 0 {
+ cmds.push(LeafCommand::from_command(parent, cmd));
+ } else {
+ let mut parent = parent.to_vec();
+ parent.push(cmd.get_name().into());
+ for sub in cmd.get_subcommands() {
+ Self::leaves(cmds, &parent, sub);
+ }
+ }
+ }
+
+ // Build all manual pages for sq and one for each leaf subcommand.
+ fn build(&self) -> Vec<ManualPage> {
+ let mut pages = vec![self.build_all_in_one()];
+
+ for sub in self.all_subs() {
+ pages.push(self.build_one_subcommand(sub));
+ }
+
+ pages
+ }
+
+ // Build one manual page for sq and all its subcommands.
+ fn build_all_in_one(&self) -> ManualPage {
+ let filename = format!("{}.{}", self.title, self.section);
+ let mut man = ManualPage::new(PathBuf::from(filename));
+ self.th(&mut man);
+
+ let about = &self.maincmd.about.clone().unwrap();
+ let summary = Self::summary(about);
+ man.name_section(&self.maincmd.name(), &summary);
+
+ man.section("SYNOPSIS");
+ let bin_name = self.maincmd.name();
+ let mut topnames: Vec<&String> = self.subcommands.keys().collect();
+ topnames.sort();
+ for topname in topnames {
+ let subs = self.subcommands.get(topname).unwrap();
+ for sub in subs.iter() {
+ man.subcommand_synopsis(
+ &bin_name,
+ self.maincmd.has_options(),
+ &sub.subcommand_name(),
+ sub.has_options(),
+ &sub.args,
+ );
+ }
+ }
+
+ man.section("DESCRIPTION");
+ man.text_with_period(&self.maincmd.description());
+
+ if self.maincmd.has_options() {
+ man.section("OPTIONS");
+ for opt in self.maincmd.get_options().iter() {
+ man.option(opt);
+ }
+ }
+
+ if !self.subcommands.is_empty() {
+ man.section("SUBCOMMANDS");
+
+ for sub in self.all_subs().iter() {
+ let desc = sub.description();
+ if !desc.is_empty() {
+ man.subsection(&sub.name());
+ man.text_with_period(&desc);
+ }
+ }
+ }
+
+ man.examples_section(&self.all_subs());
+
+ man.section("SEE ALSO");
+ let names: Vec<String> = self
+ .all_subs()
+ .iter()
+ .map(|sub| sub.manpage_name())
+ .collect();
+ man.man_page_refs(&names, &self.section);
+ man.paragraph();
+ man.text(SEE_ALSO);
+
+ man.version_section(&self.version);
+
+ man
+ }
+
+ // Set the title of the page.
+ fn th(&self, man: &mut ManualPage) {
+ let empty = String::new();
+ man.th(
+ &self.title.to_uppercase(),
+ &self.section.to_uppercase(),
+ self.date.as_ref().unwrap_or(&empty),
+ self.source.as_ref().unwrap_or(&empty),
+ self.manual.as_ref().unwrap_or(&empty),
+ )
+ }
+
+ // Return a vector of all leaf subcommands.
+ fn all_subs(&self) -> Vec<&LeafCommand> {
+ let mut subs = vec![];
+ for (_, leaves) in self.subcommands.iter() {
+ for leaf in leaves.iter() {
+ subs.push((leaf.name(), leaf));
+ }
+ }
+ subs.sort_by_cached_key(|(name, _)| name.to_string());
+ subs.iter().map(|(_, leaf)| *leaf).collect()
+ }
+
+ // Build a manual page for one leaf subcommand.
+ fn build_one_subcommand(&self, leaf: &LeafCommand) -> ManualPage {
+ let filename = format!("{}.{}", leaf.manpage_name(), self.section);
+ let mut man = ManualPage::new(PathBuf::from(filename));
+ self.th(&mut man);
+
+ let about = &leaf.about.clone().unwrap();
+ let summary = Self::summary(about);
+ man.name_section(&leaf.name(), &summary);
+
+ man.section("SYNOPSIS");
+ let bin_name = self.maincmd.name();
+ let has_global_options = self.maincmd.has_options();
+ man.subcommand_synopsis(
+ &bin_name,
+ has_global_options,
+ &leaf.subcommand_name(),
+ leaf.has_options(),
+ &leaf.args,
+ );
+
+ man.section("DESCRIPTION");
+ man.text_with_period(&leaf.description());
+
+ let main_opts = self.maincmd.has_options();
+ let leaf_opts = leaf.has_options();
+ if main_opts || leaf_opts {
+ man.section("OPTIONS");
+ }
+ if main_opts {
+ if leaf_opts {
+ man.subsection("Global options");
+ }
+ for opt in self.maincmd.get_options().iter() {
+ man.option(opt);
+ }
+ }
+ if leaf.has_options() {
+ if main_opts {
+ man.subsection("Subcommand options");
+ }
+ for opt in leaf.get_options().iter() {
+ man.option(opt);
+ }
+ }
+
+ man.examples_section(&[leaf]);
+
+ man.section("SEE ALSO");
+ man.man_page_refs(&[self.maincmd.manpage_name()], &self.section);
+ man.paragraph();
+ man.text(SEE_ALSO);
+
+ man.version_section(&self.version);
+
+ man
+ }
+}
+
+/// The command for which we generate a manual page.
+//
+/// We collect all the information about a command here so that it's
+/// handy when we generate various parts of a manual page that includes
+/// this command.
+//
+/// Despite the name, this can be the main command, or one of the leaf
+/// subcommands.
+#[derive(Debug)]
+struct LeafCommand {
+ command_words: Vec<String>,
+ before_help: Option<String>,
+ after_help: Option<String>,
+ about: Option<String>,
+ long_about: Option<String>,
+ options: Vec<CommandOption>,
+ args: Vec<String>,
+ examples: Vec<String>,
+}
+
+impl LeafCommand {
+ // Create a new `LeafCommand`. The command words are the part of
+ // the command line that invokes this command. For sq itself,
+ // they're `["sq"]`, but for a subcommand they might be `["sq",
+ // "key", "generate"]` for example.
+ fn new(command_words: Vec<String>) -> Self {
+ Self {
+ command_words,
+ before_help: None,
+ after_help: None,
+ about: None,
+ long_about: None,
+ options: vec![],
+ args: vec![],
+ examples: vec![],
+ }
+ }
+
+ // Return the name of the command, with command words separated by
+ // spaces. This is suitable for, say, the NAME section.
+ fn name(&self) -> String {
+ self.command_words.join(" ")
+ }
+
+ // Return name of the subcommand, without the main command name.
+ fn subcommand_name(&self) -> String {
+ let mut words = self.command_words.clone();
+ words.remove(0);
+ words.join(" ")
+ }
+
+ // Return the name of the manual page for this command. This is
+ // the command words separated by dashes. Thus "sq key generate"
+ // would return "sq-key-generate". Manual page names mustn't
+ // contain spaces, thus the dash.
+ fn manpage_name(&self) -> String {
+ self.command_words.join("-")
+ }
+
+ // Return the description of the command. This is collected from
+ // the various about and help texts given to `clap`.
+ fn description(&self) -> String {
+ let mut desc = String::new();
+ if let Some(text) = &self.before_help {
+ desc.push_str(text);
+ desc.push('\n');
+ }
+
+ if let Some(text) = &self.long_about {
+ desc.push_str(text);
+ desc.push('\n');
+ } else if let Some(text) = &self.about {
+ desc.push_str(text);
+ desc.push('\n');
+ }
+
+ if let Some(text) = &self.after_help {
+ desc.push_str(text);
+ desc.push('\n');
+ }
+ desc
+ }
+
+ // Add the `before_help` help text for this command.
+ fn before_help(&mut self, help: &str) {
+ self.before_help = Some(self.extract_example(help));
+ }
+
+ // Add the `after_help` help text for this command.
+ fn after_help(&mut self, help: &str) {
+ self.after_help = Some(self.extract_example(help));
+ }
+
+ // Add the `about` help text for this command.
+ fn about(&mut self, help: &str) {
+ self.about = Some(self.extract_example(help));
+ }
+
+ // Add the `long_about` help text for this command.
+ fn long_about(&mut self, help: &str) {
+ self.long_about = Some(self.extract_example(help));
+ }
+
+ // Add an option to this command.
+ fn option(&mut self, opt: CommandOption) {
+ self.options.push(opt);
+ }
+
+ // Add a positional argument to this command.
+ fn arg(&mut self, arg: &str) {
+ self.args.push(arg.into());
+ }
+
+ // Extract examples from help text: anything that follows a line
+ // consisting of "EXAMPLES:". This is a convention specific to sq,
+ // not something that comes from `clap`.
+ fn extract_example(&mut self, text: &str) -> String {
+ const H: &str = "EXAMPLES:\n";
+ if let Some(pos) = text.find(H) {
+ let (text, ex) = text.split_at(pos);
+ if let Some(ex) = ex.strip_prefix(H) {
+ self.examples.push(ex.into());
+ } else {
+ self.examples.push(ex.into());
+ }
+ text.into()
+ } else {
+ text.into()
+ }
+ }
+
+ // Does this command have any options?
+ fn has_options(&self) -> bool {
+ !self.options.is_empty()
+ }
+
+ // Get the list of options for this command.
+ fn get_options(&self) -> Vec<CommandOption> {
+ let mut opts = self.options.clone();
+ opts.sort_by_cached_key(|opt| opt.sort_key());
+ opts
+ }
+
+ // Does this command have examples?
+ fn has_examples(&self) -> bool {
+ !self.examples.is_empty()
+ }
+
+ // Create a new `LeafComand` from a `clap::Command` structure.
+ fn from_command(parent: &[String], cmd: &Command) -> Self {
+ let mut words: Vec<String> = parent.into();
+ words.push(cmd.get_name().to_string());
+ let mut leaf = Self::new(words);
+ if let Some(text) = cmd.get_before_help() {
+ leaf.before_help(text);
+ }
+ if let Some(text) = cmd.get_after_help() {
+ leaf.after_help(text);
+ }
+ if let Some(text) = cmd.get_about() {
+ leaf.about(text);
+ }
+ if let Some(text) = cmd.get_long_about() {
+ leaf.long_about(text);
+ }
+ for arg in cmd.get_arguments() {
+ if !arg.is_positional() {
+ leaf.option(CommandOption::from_arg(arg));
+ }
+ }
+ for arg in cmd.get_positionals() {
+ if let Some(names) = arg.get_value_names() {
+ for name in names {
+ leaf.arg(name);
+ }
+ }
+ }
+ leaf
+ }
+}
+
+/// Represent a command line option for manual page generation.
+//
+/// This doesn't capture all the things that `clap` allows, but is
+/// sufficient for what sq actually uses.
+#[derive(Clone, Debug)]
+struct CommandOption {
+ short: Option<String>,
+ long: Option<String>,
+ value_name: Option<String>,
+ help: Option<String>,
+}
+
+impl CommandOption {
+ // Return a key for sorting a list of options. Manual pages list
+ // options in various places, and it enables quicker lookup by
+ // readers if they lists are sorted alphabetically. By convention,
+ // such lists are sorted by short option first, if one exists.
+ fn sort_key(&self) -> String {
+ let mut key = String::new();
+ if let Some(name) = &self.short {
+ key.push_str(name.strip_prefix('-').unwrap());
+ key.push(',');
+ }
+ if let Some(name) = &self.long {
+ key.push_str(name.strip_prefix("--").unwrap());
+ }
+ key
+ }
+}
+
+impl CommandOption {
+ // Create a `CommandOption` from a `clap::Arg`.
+ fn from_arg(arg: &Arg) -> Self {
+ let value_name = if let Some(names) = arg.get_value_names() {
+ names.first().map(|name| name.to_string())
+ } else {
+ None
+ };
+
+ Self {
+ short: arg.get_short().map(|o| format!("-{}", o)),
+ long: arg.get_long().map(|o| format!("--{}", o)),
+ value_name,
+ help: arg.get_help().map(|s| s.into()),
+ }
+ }
+}
+
+/// Troff code for a manual page.
+///
+/// The code is in [`troff`](https://en.wikipedia.org/wiki/Troff)
+/// format, as is usual for Unix manual page documentation. It's using
+/// the `man` macro package for `troff`.
+pub struct ManualPage {
+ filename: PathBuf,
+ roff: Roff,
+}
+
+impl ManualPage {
+ fn new(filename: PathBuf) -> Self {
+ Self {
+ filename,
+ roff: Roff::new(),
+ }
+ }
+
+ // Set the title of the manual page. The "TH" macro takes five
+ // arguments: name of the command; section of the manual; the date
+ // of latest update; the source of manual; and the name of the manual.
+ fn th(&mut self, name: &str, section: &str, date: &str, source: &str, manual: &str) {
+ self.roff
+ .control("TH", [name, section, date, source, manual]);
+ }
+
+ // Typeset the NAME section: the title, and a line with the name
+ // of the command, followed by a dash, and a one-line. The dash
+ // should be escaped with backslash, but the `roff` crate does
+ // that for us.
+ fn name_section(&mut self, name: &str, summary: &str) {
+ self.section("NAME");
+ self.roff.text([roman(&format!("{} - {}", name, summary))]);
+ }
+
+ // Typeset the synopsis of a command. This is going to be part of
+ // the SYNOPSIS section. There are conventions for how it should
+ // be typeset. For sq, we simplify them by summarizing options
+ // into a placeholder, and only listing command words and
+ // positional arguments.
+ fn subcommand_synopsis(
+ &mut self,
+ bin: &str,
+ global_options: bool,
+ sub: &str,
+ sub_options: bool,
+ args: &[String],
+ ) {
+ let options = vec![roman(" ["), italic("GLOBAL OPTIONS"), roman("] ")];
+ let local_options = vec![roman(" ["), italic("OPTIONS"), roman("] ")];
+ self.roff.control("br", []);
+ let mut line = vec![bold(bin)];
+ if global_options {
+ line.extend_from_slice(&options);
+ }
+ line.push(bold(sub));
+ if sub_options {
+ line.extend_from_slice(&local_options);
+ }
+ for (i, arg) in args.iter().enumerate() {
+ if i > 0 {
+ line.push(roman(" "));
+ }
+ line.push(italic(arg));
+ }
+
+ if args.is_empty() {
+ line.push(roman(" "));
+ }
+
+ self.roff.text(line);
+ }
+
+ // Typeset an option, for the OPTIONS section. This is typeset
+ // using "tagged paragraphs", where the first line lists the
+ // aliases of the option, and any values it may take, and the rest
+ // is indented paragraphs of text explaining what the option does.
+ fn option(&mut self, opt: &CommandOption) {
+ let mut line = vec![];
+
+ if let Some(short) = &opt.short {
+ line.push(bold(short));
+ }
+ if let Some(long) = &opt.long {
+ if opt.short.is_some() {
+ line.push(roman(", "));
+ }
+ line.push(bold(long));
+ }
+
+ if let Some(value) = &opt.value_name {
+ line.push(roman("="));
+ line.push(italic(value));
+ }
+
+ self.tagged_paragraph(line);
+ if let Some(help) = &opt.help {
+ self.text(help);
+ }
+ }
+
+ // Typeset an EXAMPLES section, if a command has examples.
+ fn examples_section(&mut self, leaves: &[&LeafCommand]) {
+ if !leaves.iter().any(|leaf| leaf.has_examples()) {
+ return;
+ }
+
+ self.section("EXAMPLES");
+ let mut need_para = false;
+ let need_subsect