use std::path::{Path, PathBuf};
use roff::{bold, italic, roman, Inline, Roff};
const MANUAL: &str = "User Commands";
const SOURCE: &str = "Sequoia PGP";
macro_rules! warn {
{$($exp: expr),*} => {
println!("cargo:warning={}",
format_args!($($exp),*));
};
}
macro_rules! fail {
{$($exp: expr),*} => {
warn!($($exp),*);
std::process::exit(1);
};
}
pub struct Builder {
title: String,
section: String,
date: Option<String>,
source: Option<String>,
manual: Option<String>,
version: Option<String>,
see_also: Vec<String>,
maincmd: Command,
top_level_global_args: Vec<CommandOption>,
}
impl Builder {
pub fn new(cmd: &mut clap::Command,
version: &str, extra_version: Option<&str>)
-> Self
{
cmd.build();
let version = if let Some(extra_version) = extra_version {
format!("{} ({})", version, extra_version)
} else {
version.to_string()
};
let maincmd = Command::from_command(&[], cmd);
let options = maincmd.get_options();
let top_level_global_args = options.into_iter()
.filter(|a| a.global || a.long.as_deref() == Some("--help"))
.collect::<Vec<_>>();
Self {
title: cmd.get_name().into(),
section: "1".into(),
date: Some(version.clone()),
source: Some(SOURCE.to_string()),
manual: Some(MANUAL.to_string()),
version: Some(version),
see_also: Vec::new(),
maincmd,
top_level_global_args,
}
}
fn is_top_level_global_arg(&self, arg: &CommandOption) -> bool {
self.top_level_global_args
.binary_search_by_key(&arg.sort_key(),
|a: &CommandOption| a.sort_key())
.is_ok()
}
pub fn date(&mut self, date: &str) -> &mut Self{
self.date = Some(date.into());
self
}
pub fn source(&mut self, source: &str) -> &mut Self {
self.source = Some(source.into());
self
}
pub fn manual(&mut self, manual: &str) -> &mut Self {
self.manual = Some(manual.into());
self
}
fn summary(about: &str) -> String {
let line = if let Some(line) = about.lines().next() {
line
} else {
""
};
line.to_string()
}
pub fn see_also<S>(&mut self, see_also: &[S]) -> &mut Self
where S: AsRef<str>
{
self.see_also = see_also.into_iter()
.map(|s| {
s.as_ref().to_string()
})
.collect();
self
}
pub fn binary(&self) -> String {
self.maincmd.name()
}
pub fn build(&self) -> Vec<ManualPage> {
let mut pages = vec![];
self.maincmd.build(self, &mut pages);
pages
}
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),
)
}
}
#[derive(Debug, PartialEq, Eq)]
struct Command {
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>,
exit_status: Vec<String>,
subcommands: Vec<Command>,
}
impl Command {
fn new(command_words: Vec<String>) -> Self {
assert!(! command_words.is_empty());
Self {
command_words,
before_help: None,
after_help: None,
about: None,
long_about: None,
options: vec![],
args: vec![],
examples: vec![],
exit_status: vec![],
subcommands: vec![],
}
}
fn top_level(&self) -> bool {
self.command_words.len() == 1
}
fn leaf(&self) -> bool {
! self.top_level() && self.subcommands.is_empty()
}
fn singleton(&self) -> bool {
self.top_level() && self.subcommands.is_empty()
}
fn name(&self) -> String {
self.command_words.join(" ")
}
fn subcommand_name(&self) -> String {
let mut words = self.command_words.clone();
words.remove(0);
words.join(" ")
}
fn manpage_name(&self) -> String {
self.command_words.join("-")
}
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
}
fn before_help(&mut self, help: &str) {
self.before_help = Some(self.extract_all(help));
}
fn after_help(&mut self, help: &str) {
self.after_help = Some(self.extract_all(help));
}
fn about(&mut self, help: &str) {
self.about = Some(self.extract_all(help));
}
fn long_about(&mut self, help: &str) {
self.long_about = Some(self.extract_all(help));
}
fn option(&mut self, opt: CommandOption) {
self.options.push(opt);
}
fn arg(&mut self, arg: &str) {
self.args.push(arg.into());
}
fn extract_all(&mut self, text: &str) -> String {
let text = self.extract_example(text);
let text = self.extract_exit_status(&text);
text
}
fn extract_example(&mut self, text: &str) -> String {
Self::extract("Examples:\n", &mut self.examples, text)
}
fn extract_exit_status(&mut self, text: &str) -> String {
Self::extract("Exit status:\n", &mut self.exit_status, text)
}
fn extract(marker: &str, into: &mut Vec<String>, text: &str) -> String {
if let Some(pos) = text.find(marker) {
let (text, ex) = text.split_at(pos);
if let Some(ex) = ex.strip_prefix(marker) {
into.push(ex.into());
} else {
into.push(ex.into());
}
text.into()
} else {
text.into()
}
}
fn has_options(&self) -> bool {
!self.options.is_empty()
}
fn get_options(&self) -> Vec<CommandOption> {
let mut opts = self.options.clone();
opts.sort_by_cached_key(|opt| opt.sort_key());
opts
}
fn has_examples(&self) -> bool {
!self.examples.is_empty()
}
fn from_command(parent: &[String], cmd: &clap::Command) -> Self {
let mut words: Vec<String> = parent.into();
words.push(cmd.get_name().to_string());
let mut new = Self::new(words);
if let Some(text) = cmd.get_before_help() {
new.before_help(&text.to_string());
}
if let Some(text) = cmd.get_after_help() {
new.after_help(&text.to_string());
}
if let Some(text) = cmd.get_about() {
new.about(&text.to_string());
}
if let Some(text) = cmd.get_long_about() {
new.long_about(&text.to_string());
}
for arg in cmd.get_arguments()
.filter(|a| ! a.is_hide_set())
{
new.option(CommandOption::from_arg(arg));
}
for arg in cmd.get_positionals()
.filter(|a| ! a.is_hide_set())
{
if let Some(names) = arg.get_value_names() {
for name in names {
new.arg(name);
}
}
}
let mut parent = parent.to_vec();
parent.push(cmd.get_name().into());
new.subcommands = cmd.get_subcommands()
.filter(|cmd| ! cmd.is_hide_set())
.filter(|cmd| cmd.get_name() != "help")
.map(|cmd| Command::from_command(&parent, cmd))
.collect();
new
}
fn build(&self, builder: &Builder, acc: &mut Vec<ManualPage>) {
acc.push(self.build_command(builder));
for cmd in &self.subcommands {
cmd.build(builder, acc);
}
}
fn build_command(&self, builder: &Builder) -> ManualPage {
let filename = format!("{}.{}", self.manpage_name(), builder.section);
let mut man = ManualPage::new(PathBuf::from(filename));
builder.th(&mut man);
let about = &self.about.clone().unwrap();
let summary = Builder::summary(about);
man.name_section(&self.manpage_name(), &summary);
man.section("SYNOPSIS");
let bin_name = builder.binary();
if self.singleton() || self.leaf() {
man.subcommand_synopsis(
&bin_name,
&self.subcommand_name(),
self.has_options(),
&self.args,
true,
);
} else {
for sub in &self.subcommands {
man.subcommand_synopsis(
&bin_name,
&sub.subcommand_name(),
sub.has_options(),
&sub.args,
sub.leaf(),
);
}
}
man.section("DESCRIPTION");
man.text_with_period(&self.description());
if self.top_level() || self.leaf() {
let mut showed_options_header = false;
let mut self_opts = self.get_options();
if ! self.singleton() {
self_opts = self_opts.into_iter()
.filter(|o| ! builder.is_top_level_global_arg(o))
.collect::<Vec<_>>();
}
if ! self_opts.is_empty() {
if ! showed_options_header {
showed_options_header = true;
man.section("OPTIONS");
}
if ! self.top_level() {
man.subsection("Subcommand options");
}
for opt in self_opts.iter() {
man.option(opt);
}
}
let global_options = &builder.top_level_global_args;
if ! self.singleton() && ! global_options.is_empty() {
#[allow(unused_assignments)]
if ! showed_options_header {
showed_options_header = true;
man.section("OPTIONS");
}
man.subsection("Global options");
if self.top_level() {
for opt in global_options.iter() {
man.option(opt);
}
} else {
man.roff.text(vec![
roman("See "),
bold(bin_name), roman("("), roman("1"), roman(")"),
roman(" for a description of the global options."),
]);
}
}
}
if ! self.subcommands.is_empty() {
man.section("SUBCOMMANDS");
for sub in &self.subcommands {
let desc = sub.description();
if !desc.is_empty() {
man.subsection(&sub.name());
man.text_with_period(&desc);
}
}
}
man.exit_status_section(self);
if self == &builder.maincmd {
let opts = self.get_options();
let mut envs: Vec<_> =
opts.iter().filter(|o| o.env.is_some()).collect();
envs.sort_by_key(|o| o.env.as_ref());
if ! envs.is_empty() {
man.section("ENVIRONMENT");
for opt in envs {
man.env_option(opt);
}
}
}
if self.singleton() || self.leaf() {
man.examples_section(&[self]);
} else {
man.examples_section(
&std::iter::once(self)
.chain(self.subcommands.iter())
.collect::<Vec<_>>());
}
let mut see_also_shown = false;
let names: Vec<String> = (1..self.command_words.len())
.map(|n| self.command_words[0..n].join("-"))
.chain(self.subcommands.iter().map(|sub| sub.manpage_name()))
.collect();
if ! names.is_empty() {
if ! see_also_shown {
man.section("SEE ALSO");
see_also_shown = true;
}
man.man_page_refs(&names, &builder.section);
man.paragraph();
}
if ! builder.see_also.is_empty() {
#[allow(unused_assignments)]
if ! see_also_shown {
man.section("SEE ALSO");
see_also_shown = true;
}
for (i, see_also) in builder.see_also.iter().enumerate() {
if i > 0 {
man.paragraph();
}
man.text(see_also);
}
}
man.version_section(&builder.version);
man
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct CommandOption {
index: Option<usize>,
short: Option<String>,
long: Option<String>,
global: bool,
env: Option<String>,
value_names: Values,
possible_values: Vec<String>,
default_values: Vec<String>,
help: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Values {
None,
Some(Vec<String>),
Optional(Vec<String>),
}
impl CommandOption {
fn sort_key(&self) -> (usize, 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());
}
(self.index.unwrap_or(0), key)
}
}
impl CommandOption {
fn from_arg(arg: &clap::Arg) -> Self {
let num_args = arg.get_num_args().unwrap_or_default();
let value_names = if num_args.takes_values() {
let names = arg.get_value_names()
.unwrap_or(&[])
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>();
if num_args.min_values() == 0 {
Values::Optional(names)
} else {
Values::Some(names)
}
} else {
Values::None
};
let long = arg.get_long().map(|o| format!("--{}", o));
let global = arg.is_global_set();
if global {
assert!(long.is_some(),
"Global options must have a long name.");
}
Self {
index: arg.get_index(),
short: arg.get_short().map(|o| format!("-{}", o)),
long,
global,
value_names,
env: arg.get_env().and_then(|e| e.to_str().map(Into::into)),
possible_values: arg.get_possible_values().iter()
.map(|o| o.get_name().into()).collect(),
default_values: if num_args.takes_values() {
arg.get_default_values().iter()
.map(|s| s.to_string_lossy().to_string())
.collect()
} else {
vec![]
},
help: arg.get_long_help().or(arg.get_help())
.map(|s| s.to_string()),
}
}
}
pub struct ManualPage {
filename: PathBuf,
roff: Roff,
in_code: bool,
}
impl ManualPage {
fn new(filename: PathBuf) -> Self {
Self {
filename,
roff: Roff::new(),
in_code: false,
}
}
fn th(&mut self, name: &str, section: &str, date: &str, source: &str, manual: &str) {
self.roff
.control("TH", [name, section, date, source, manual]);
}
fn name_section(&mut self, name: &str, summary: &str) {
self.section("NAME");
self.roff.text([roman(&format!("{} - {}", name, summary))]);
}
fn code(&mut self, code: bool) {
match (self.in_code, code) {
(false, false) => (),
(false, true) => {
self.roff.control("nf", []);
self.in_code = true;
},
(true, false) => {
self.roff.control("fi", []);
self.in_code = false;
},
(true, true) => (),
}
}
fn subcommand_synopsis(
&mut self,
bin: &str,
sub: &str,
sub_options: bool,
args: &[String],
is_leaf: bool,
) {
let mut line = vec![
bold(if sub.is_empty() {
bin.to_string()
} else {
format!("{} {}", bin, sub)
}),
roman(" ["), italic("OPTIONS"), roman("] "),
];
for (i, arg) in args.iter().enumerate() {
if i > 0 || ! sub_options {
line.push(roman(" "));
}
line.push(italic(arg));
}
if args.is_empty() {
line.push(roman(" "));
}
if ! is_leaf {
line.push(italic("SUBCOMMAND"));
}
self.roff.control("br", []);
self.roff.text(line);
}
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));
}
match &opt.value_names {
Values::None => (),
Values::Some(values) | Values::Optional(values) => {
if matches!(opt.value_names, Values::Optional(_)) {
line.push(roman("["));
}
if (opt.short.is_some() || opt.long.is_some())
&& values.len() == 1
{
line.push(roman("="));
line.push(italic(&values[0]));
} else {
for value in values {
line.push(roman(" "));
line.push(italic(value));
}
}
if matches!(opt.value_names, Values::Optional(_)) {
line.push(roman("]"));
}
},
}
self.tagged_paragraph(line, &opt.help);
if ! opt.default_values.is_empty() {
self.indented_paragraph();
let mut line = vec![];
line.push(roman("[default: "));
for (i, v) in opt.default_values.iter().enumerate() {
if i > 0 {
line.push(roman(", "));
}
line.push(bold(v));
}
line.push(roman("]"));
self.roff.text(line);
}
if ! opt.possible_values.is_empty() {
self.indented_paragraph();
let mut line = vec![];
line.push(roman("[possible values: "));
for (i, v) in opt.possible_values.iter().enumerate() {
if i > 0 {
line.push(roman(", "));
}
line.push(bold(v));
}
line.push(roman("]"));
self.roff.text(line);
}
}
fn env_option(&mut self, opt: &CommandOption) {
let mut line = vec![
bold(opt.env.as_ref().expect("must be an env")),
];
match &opt.value_names {
Values::None => (),
Values::Some(values) | Values::Optional(values) => {
assert_eq!(values.len(), 1);
line.push(roman("="));
line.push(italic(&values[0]));
},
}
self.tagged_paragraph(line, &opt.help);
if ! opt.default_values.is_empty() {
self.indented_paragraph();
let mut line = vec![];
line.push(roman("[default: "));
for (i, v) in opt.default_values.iter().enumerate() {
if i > 0 {
line.push(roman(", "));
}
line.push(bold(v));
}
line.push(roman("]"));
self.roff.text(line);
}
if ! opt.possible_values.is_empty() {
self.indented_paragraph();
let mut line = vec![];
line.push(roman("[possible values: "));
for (i, v) in opt.possible_values.iter().enumerate() {
if i > 0 {
line.push(roman(", "));
}
line.push(bold(v));
}
line.push(roman("]"));
self.roff.text(line);
}
}
fn exit_status_section(&mut self, c: &Command) {
if c.exit_status.is_empty() {
return;
}
self.section("EXIT STATUS");
for chunk in &c.exit_status {
self.text(&chunk);
}
}
fn examples_section(&mut self, subs: &[&Command]) {
if !subs.iter().any(|c| c.has_examples()) {
return;
}
self.section("EXAMPLES");
let mut need_para = false;
let need_subsections = subs.len() > 1;
for cmd in subs.iter() {
if need_para {
self.paragraph();
need_para = false;
}
if !cmd.examples.is_empty() {
if need_subsections {
self.subsection(&cmd.name());
need_para = false;
}
for ex in cmd.examples.iter() {
let mut continuation = false;
let mut description = false;
for line in ex.lines() {
if ! continuation
&& ! (description && line.starts_with("#"))
{
self.paragraph();
}
const TARGET_LINE_LENGTH: usize = 78;
const RS_INDENTATION: usize = 7;
const EXAMPLE_COMMAND_MAX_WIDTH: usize =
TARGET_LINE_LENGTH - 2 * RS_INDENTATION;
const EXAMPLE_CONTINUATION_MAX_WIDTH: usize =
TARGET_LINE_LENGTH - 3 * RS_INDENTATION;
if let Some(line) = line.strip_prefix("# ") {
self.code(false);
self.roff.text([roman(line)]);
} else if let Some(line) = line.strip_prefix("$ ") {
let line = line.trim();
if line.len() > EXAMPLE_COMMAND_MAX_WIDTH {
warn!("Command in example exceeds {} chars:",
EXAMPLE_COMMAND_MAX_WIDTH);
fail!("{} ({} chars)", line, line.len());
}
self.code(true);
self.roff.control("RS", []);
self.roff.text([roman(line)]);
self.roff.control("RE", []);
} else if continuation {
let line = line.trim();
if line.len() > EXAMPLE_CONTINUATION_MAX_WIDTH {
warn!("Continuation in example exceeds {} chars:",
EXAMPLE_CONTINUATION_MAX_WIDTH);
fail!("{} ({} chars)", line, line.len());
}
self.code(true);
self.roff.control("RS", []);
self.roff.control("RS", []);
self.roff.text([roman(line)]);
self.roff.control("RE", []);
self.roff.control("RE", []);
} else {
self.code(false);
self.roff.text([roman(line)]);
}
continuation = line.ends_with("\\");
description = line.starts_with("#");
need_para = true;
}
}
self.code(false);
}
}
}
fn version_section(&mut self, version: &Option<String>) {
if let Some(v) = version {
self.section("VERSION");
self.roff.text([roman(v)]);
}
}
fn section(&mut self, heading: &str) {
self.roff.control("SH", [heading]);
}
fn subsection(&mut self, heading: &str) {
self.roff.control("SS", [heading]);
}
fn paragraph(&mut self) {
self.roff.control("PP", []);
}
fn indented_paragraph(&mut self) {
self.roff.control("IP", []);
}
fn tagged_paragraph(&mut self, line: Vec<Inline>, text: &Option<String>) {
self.roff.control("TP", []);
self.roff.text(line);
if let Some(text) = text {
let mut paras = text.split("\n\n");
if let Some(first) = paras.next() {
self.roff.text([roman(first)]);
}
for para in paras {
self.roff.control("IP", []);
self.roff.text([roman(para)]);
}
}
}
fn man_page_refs(&mut self, names: &[String], section: &str) {
let mut line = vec![];
for name in names.iter() {
if !line.is_empty() {
line.push(roman(", "));
}
line.push(bold(name));
line.push(roman("("));
line.push(roman(section));
line.push(roman(")"));
}
line.push(roman("."));
self.roff.control("nh", []);
self.roff.text(line);
self.roff.control("hy", []);
}
fn text(&mut self, text: &str) {
let mut paras = text.split("\n\n");
if let Some(first) = paras.next() {
self.roff.text([roman(first)]);
}
for para in paras {
self.paragraph();
self.roff.text([roman(para)]);
}
}
fn text_with_period(&mut self, text: &str) {
let mut paras = text.split("\n\n");
if let Some(first) = paras.next() {
let first = if let Some(prefix) = first.strip_suffix(".\n") {
format!("{}.", prefix)
} else if let Some(prefix) = first.strip_suffix('\n') {
format!("{}.", prefix)
} else if first.ends_with('.') {
first.to_string()
} else {
format!("{}.", first)
};
self.roff.text([roman(first)]);
}
for para in paras {
self.paragraph();
self.roff.text([roman(para)]);
}
}
pub fn filename(&self) -> &Path {
&self.filename
}
pub fn troff_source(&self) -> String {
self.roff.to_roff()
}
}