use std::io::Write;
use clap::CommandFactory;
use clap_complete::aot::Shell;
use crate::cli::{Cli, CliError, Command};
const BOOK_URL: &str = env!("CARGO_PKG_HOMEPAGE");
pub fn write(command: &Command, out: &mut impl Write) -> Result<(), CliError> {
match command {
Command::Completions { shell } => write_completions(*shell, out),
Command::Man => write_man(out),
_ => unreachable!("both callers match the two generator commands first"),
}
}
pub fn write_completions(shell: Shell, out: &mut impl Write) -> Result<(), CliError> {
let mut command = Cli::command();
let name = command.get_name().to_string();
clap_complete::aot::generate(shell, &mut command, name, out);
Ok(())
}
pub fn write_man(out: &mut impl Write) -> Result<(), CliError> {
let man = clap_mangen::Man::new(Cli::command()).section("1");
let render = |out: &mut dyn Write| -> std::io::Result<()> {
man.render_title(out)?;
man.render_name_section(out)?;
man.render_synopsis_section(out)?;
man.render_description_section(out)?;
man.render_options_section(out)?;
man.render_subcommands_section(out)?;
render_environment_section(out)?;
render_files_section(out)?;
render_see_also_section(out)?;
man.render_version_section(out)
};
render(out).map_err(|error| CliError(format!("cannot write the man page: {error}")))
}
fn render_environment_section(out: &mut dyn Write) -> std::io::Result<()> {
writeln!(out, ".SH ENVIRONMENT")?;
writeln!(out, ".TP")?;
writeln!(out, "\\fBACME_PROXY_CONFIG\\fR")?;
writeln!(
out,
"Path to the configuration file, without its extension. \
Defaults to \\fBconfig\\fR in the working directory."
)?;
writeln!(out, ".TP")?;
writeln!(out, "\\fBACME_PROXY_*\\fR")?;
writeln!(
out,
"Per-key overrides of the configuration file, section and key separated \
by a double underscore: \\fBACME_PROXY_SERVER__BIND_ADDRESS\\fR \
sets \\fBserver.bind_address\\fR. List-valued keys are comma-separated."
)?;
writeln!(out, ".TP")?;
writeln!(out, "\\fBNO_COLOR\\fR")?;
writeln!(
out,
"Set and non-empty, suppresses colour in the human-readable output. \
\\fB\\-\\-color always\\fR outranks it; \\fB\\-\\-json\\fR output never \
carries colour at any setting."
)?;
writeln!(out, ".TP")?;
writeln!(out, "\\fBRUST_LOG\\fR")?;
writeln!(
out,
"Overrides the log filter from \\fB[logging]\\fR, in \
\\fBtracing-subscriber\\fR's \\fBEnvFilter\\fR syntax."
)
}
fn render_files_section(out: &mut dyn Write) -> std::io::Result<()> {
writeln!(out, ".SH FILES")?;
writeln!(out, ".TP")?;
writeln!(out, "\\fBconfig.toml\\fR")?;
writeln!(
out,
"The configuration, read from the working directory unless \
\\fBACME_PROXY_CONFIG\\fR says otherwise. There is no \
\\fB\\-\\-config\\fR flag: every subcommand reads the same one the \
server does, which is what makes the admin commands act on the same \
database."
)
}
fn render_see_also_section(out: &mut dyn Write) -> std::io::Result<()> {
writeln!(out, ".SH SEE ALSO")?;
writeln!(
out,
"The full documentation, including the per-flag reference for every \
subcommand above, the configuration reference and the operator \
guides:"
)?;
writeln!(out, ".UR {BOOK_URL}")?;
writeln!(out, ".UE")
}
#[cfg(test)]
mod tests {
use super::*;
use clap::ValueEnum;
fn completions(shell: Shell) -> String {
let mut out = Vec::new();
write_completions(shell, &mut out).expect("a completion script must render");
String::from_utf8(out).expect("clap generates UTF-8")
}
fn man() -> String {
let mut out = Vec::new();
write_man(&mut out).expect("the man page must render");
String::from_utf8(out).expect("roff is written as UTF-8 here")
}
#[test]
fn every_shell_generates_a_script_naming_the_binary() {
for shell in Shell::value_variants() {
let script = completions(*shell);
assert!(!script.is_empty(), "{shell} generated nothing");
assert!(
script.contains("acme-proxy"),
"{shell}'s script does not name the binary"
);
}
}
#[test]
fn the_scripts_reach_the_deepest_subcommand() {
for shell in [Shell::Bash, Shell::Zsh, Shell::Elvish, Shell::PowerShell] {
assert!(
completions(shell).contains("recovery-codes"),
"{shell}'s script stops short of the deepest subcommand"
);
}
}
#[test]
fn the_man_page_carries_a_title_and_the_subcommands() {
let page = man();
assert!(
page.contains(".TH acme-proxy 1"),
"no roff title line: {page:.120}"
);
assert!(
page.contains("acme\\-proxy"),
"the page does not name itself"
);
for subcommand in [
"serve", "account", "order", "audit", "nonce", "profile", "eab", "admin",
] {
assert!(
page.contains(subcommand),
"the SUBCOMMANDS section omits `{subcommand}`"
);
}
}
#[test]
fn the_man_page_carries_the_hand_written_sections() {
let page = man();
for section in [
".SH ENVIRONMENT",
"ACME_PROXY_CONFIG",
"NO_COLOR",
"RUST_LOG",
".SH FILES",
"config.toml",
".SH SEE ALSO",
BOOK_URL,
] {
assert!(page.contains(section), "the page omits `{section}`");
}
}
#[test]
fn the_hand_written_sections_precede_the_version() {
let page = man();
let environment = page
.find(".SH ENVIRONMENT")
.expect("ENVIRONMENT is rendered");
let see_also = page.find(".SH SEE ALSO").expect("SEE ALSO is rendered");
let version = page.find(".SH VERSION").expect("VERSION is rendered");
assert!(environment < see_also, "SEE ALSO comes before ENVIRONMENT");
assert!(see_also < version, "VERSION comes before SEE ALSO");
}
#[test]
fn write_routes_both_generator_commands() {
let mut script = Vec::new();
write(&Command::Completions { shell: Shell::Fish }, &mut script)
.expect("completions must render");
assert!(String::from_utf8_lossy(&script).contains("acme-proxy"));
let mut page = Vec::new();
write(&Command::Man, &mut page).expect("the man page must render");
assert!(String::from_utf8_lossy(&page).contains(".TH acme-proxy 1"));
}
#[test]
fn a_broken_writer_becomes_a_cli_error() {
struct Broken;
impl Write for Broken {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let error = write_man(&mut Broken).expect_err("a broken pipe must be reported");
assert!(
error.to_string().starts_with("cannot write the man page: "),
"{error}"
);
}
#[test]
fn a_pipe_closing_partway_is_reported_from_every_section() {
struct FailsAfter(usize);
impl Write for FailsAfter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.0 == 0 {
return Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe));
}
self.0 -= 1;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut counting = FailsAfter(usize::MAX);
write_man(&mut counting).expect("a writer that never fails must succeed");
let writes = usize::MAX - counting.0;
assert!(
writes > 40,
"the page should take many writes, took {writes}"
);
for stop in 0..writes {
let error = write_man(&mut FailsAfter(stop))
.expect_err("a pipe closing mid-page must still be reported");
assert!(
error.to_string().starts_with("cannot write the man page: "),
"closing after {stop} of {writes} writes: {error}"
);
}
}
#[test]
fn the_bash_script_is_internally_consistent() {
let script = completions(Shell::Bash);
let assigned: Vec<&str> = script
.lines()
.filter_map(|line| line.trim().strip_prefix("cmd=\""))
.filter_map(|rest| rest.strip_suffix('"'))
.filter(|id| !id.is_empty())
.collect();
assert!(
assigned.len() > 50,
"expected the whole tree, found {} ids",
assigned.len()
);
let labels: std::collections::HashSet<&str> = script
.lines()
.map(str::trim)
.filter_map(|line| line.strip_suffix(')'))
.collect();
for id in assigned {
assert!(
labels.contains(id),
"the word loop builds `{id}`, which no `case` label matches: \
bash completion is dead below that point"
);
}
}
#[test]
fn the_command_tree_is_well_formed() {
Cli::command().debug_assert();
}
}