sequoia-man 0.3.2

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::io::Write;
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 mut cli = Cli::command();
/// let mut builder = sequoia_man::man::Builder::new(&mut cli, env!("CARGO_PKG_VERSION"), None);
/// builder.see_also(&[ "For the full documentation see <https://...>." ]);
/// sequoia_man::generate_man_pages(&dir, &builder)?;
/// # 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, builder: &man::Builder)
    -> 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("FILES=\"");
    for man in builder.build() {
        man2html.push_str(&format!(" {}", man.filename().display()));
        std::fs::write(asset_dir.join(man.filename()), man.troff_source())?;
    }
    man2html.push_str("\"\n");
    man2html.push_str("\n");

    man2html.push_str(&format!("\
case \"$1\" in
  --generate)
    for man_page in $FILES
    do
      BINARY={} pandoc -s $man_page -L man-pandoc.lua -H man-pandoc.inc.html -o $man_page.html
    done
    ;;
  --man-files)
    for man_page in $FILES
    do
      echo $man_page
    done
    ;;
  --man-root)
    for man_page in $FILES
    do
      echo $man_page
      break
    done
    ;;
  --html-files)
    for man_page in $FILES
    do
      echo $man_page.html
    done
    ;;
  --html-root)
    for man_page in $FILES
    do
      echo $man_page.html
      break
    done
    ;;
  *)
    echo \"Usage: $0 --generate|--man-files|--man-root|--html-files|--html-root\"
    exit 1
    ;;
esac
", builder.binary()));

    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");
    let mut f = std::fs::File::create(&target)
        .with_context(|| format!("Crating {}", target.display()))?;
    f.write_all(man2html.as_bytes())?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        // Make it executable for the owner.
        let metadata = f.metadata()?;
        let mut permissions = metadata.permissions();
        permissions.set_mode(permissions.mode() | 0o100);
        f.set_permissions(permissions)?;
    }

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

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    use clap::CommandFactory;
    use clap::Parser;

    #[derive(Parser, Debug)]
    #[clap(
        name = "frob",
        about = "A tool to help with frobnication.")]
    struct Cli {
        /// How intense to frobnicate.
        #[arg(long="intensity")]
        intensity: usize,
    }

    #[test]
    fn build() {
        let dir = tempfile::TempDir::new().unwrap();
        let mut cli = Cli::command();
        let mut builder = man::Builder::new(
            &mut cli, env!("CARGO_PKG_VERSION"), None);
        builder.see_also(&[ "For the full documentation see <https://...>." ]);
        generate_man_pages(dir.path(), &builder).unwrap();

        // Persist the state:
        if false {
            let p = dir.into_path();
            eprintln!("Persisted output to: {}", p.display());
        }
    }
}