sequoia-man 0.2.0

Man page generation for Sequoia PGP CLIs
Documentation
/// 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 std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Context;

pub mod man;

type Result<T, E=anyhow::Error> = std::result::Result<T, E>;

/// Variable name to control the asset out directory with.
pub const ASSET_OUT_DIR: &str = "ASSET_OUT_DIR";

/// Returns the directory to write the given assets to.
///
/// For man pages, this would usually be `man-pages`.
///
/// The base directory is takens from the environment variable
/// [`ASSET_OUT_DIR`] or, if that is not set, cargo's [`OUT_DIR`]:
///
/// > OUT_DIR — If the package has a build script, this is set to the
/// > folder where the build script should place its output. See below
/// > for more information. (Only set during compilation.)
///
/// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
///
/// This function panics if neither environment variable is set.
pub fn asset_out_dir(asset: &str) -> Result<PathBuf> {
    println!("cargo:rerun-if-env-changed={}", ASSET_OUT_DIR);
    let outdir: PathBuf =
        env::var_os(ASSET_OUT_DIR).unwrap_or_else(
            || env::var_os("OUT_DIR").expect("OUT_DIR not set")).into();
    if outdir.exists() && ! outdir.is_dir() {
        return Err(anyhow::anyhow!("{}={:?} is not a directory",
                                   ASSET_OUT_DIR, outdir));
    }

    let path = outdir.join(asset);
    fs::create_dir_all(&path)?;
    Ok(path)
}

/// pandoc helper file to convert a man page to HTML.
pub const MAN_PANDOC_LUA: &[u8] = include_bytes!("man-pandoc.lua");

/// pandoc helper file to convert a man page to HTML.
pub const MAN_PANDOC_INC_HTML: &[u8] = include_bytes!("man-pandoc.inc.html");

/// Generates man pages.
///
/// `asset_dir` is the directory where the man pages will be written.
///
/// `version` is the bare version string, which is usually obtained
/// from `env!("CARGO_PKG_VERSION")`.
///
/// If `extra_version` is `Some`, then the version is created `version
/// (extra_version)`.
///
/// The helper files `man-pandoc.lua`, `man-pandoc.inc.html` and
/// `man2html.sh`, will also be written to the directory.
///
/// If you define a data type `Cli`, then you would do:
///
/// ```no_run
/// use clap::CommandFactory;
/// use clap::Parser;
/// #
///
/// #[derive(Parser, Debug)]
/// #[clap(
///    name = "sq",
///    about = "A command-line frontend for Sequoia, an implementation of OpenPGP")]
/// struct Cli {
///     // ...
/// }
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let dir = sequoia_man::asset_out_dir("man-pages")?;
/// let cli = Cli::command();
/// sequoia_man::generate_man_pages(&dir, &cli, env!("CARGO_PKG_VERSION"), None)?;
/// # Ok(())
/// # }
/// ```
///
/// To convert the man pages to HTML, run the `man2html.sh` script.
///
/// ```shell
/// bash .../man-pages/man2html.sh
/// ```
pub fn generate_man_pages(asset_dir: &Path, cli: &clap::Command,
                          version: &str, extra_version: Option<&str>)
    -> Result<()>
{
    let mut man2html = String::new();

    man2html.push_str("#! /bin/bash\n");
    man2html.push_str("# Convert the man pages to HTML using pandoc.\n");
    man2html.push_str("\n");
    man2html.push_str("set -e\n\n");
    man2html.push_str("cd $(dirname $0)\n\n");

    man2html.push_str("for man_page in");

    for man in man::manpages(cli, version, extra_version) {
        man2html.push_str(&format!(" {}", man.filename().display()));
        std::fs::write(asset_dir.join(man.filename()), man.troff_source())?;
    }

    man2html.push_str("\ndo\n");
    man2html.push_str("    pandoc -s $man_page -L man-pandoc.lua -H man-pandoc.inc.html -o $man_page.html\n");
    man2html.push_str("done\n");

    let target = asset_dir.join("man-pandoc.lua");
    std::fs::write(&target, MAN_PANDOC_LUA)
        .with_context(|| format!("Writing {}", target.display()))?;

    let target = asset_dir.join("man-pandoc.inc.html");
    std::fs::write(&target, MAN_PANDOC_INC_HTML)
        .with_context(|| format!("Writing {}", target.display()))?;

    let target = asset_dir.join("man2html.sh");
    std::fs::write(&target, man2html.as_bytes())
        .with_context(|| format!("Writing {}", target.display()))?;

    println!("cargo:warning=man pages written to {}", asset_dir.display());

    Ok(())
}