Skip to content

Commit

Permalink
doc: generate man page
Browse files Browse the repository at this point in the history
This commit uses the recent refactoring for defining flags to
automatically generate a man page. This finally allows us to define the
documentation for each flag in a single place.

The man page is generated on every build, if and only if `asciidoc` is
installed. When generated, it is placed in Cargo's `OUT_DIR` directory,
which is the same place that shell completions live.
  • Loading branch information
BurntSushi committed Feb 6, 2018
1 parent b874835 commit a2fc8d8
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 1,174 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ addons:
- zsh
# Needed for testing decompression search.
- xz-utils
# For generating man page.
- asciidoc
matrix:
fast_finish: true
include:
Expand Down
128 changes: 124 additions & 4 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ extern crate clap;
extern crate lazy_static;

use std::env;
use std::fs;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::Path;
use std::process;

use clap::Shell;

use app::{RGArg, RGArgKind};

#[allow(dead_code)]
#[path = "src/app.rs"]
mod app;
Expand All @@ -27,6 +31,9 @@ fn main() {
}
};
fs::create_dir_all(&outdir).unwrap();
if let Err(err) = generate_man_page(&outdir) {
eprintln!("failed to generate man page: {}", err);
}

// Use clap to build completion files.
let mut app = app::app();
Expand All @@ -37,11 +44,124 @@ fn main() {
// are manually maintained in `complete/_rg`.

// Make the current git hash available to the build.
if let Some(rev) = git_revision_hash() {
println!("cargo:rustc-env=RIPGREP_BUILD_GIT_HASH={}", rev);
}
}

fn git_revision_hash() -> Option<String> {
let result = process::Command::new("git")
.args(&["rev-parse", "--short=10", "HEAD"])
.output();
if let Ok(output) = result {
let hash = String::from_utf8_lossy(&output.stdout);
println!("cargo:rustc-env=RIPGREP_BUILD_GIT_HASH={}", hash);
result.ok().map(|output| {
String::from_utf8_lossy(&output.stdout).trim().to_string()
})
}

fn generate_man_page<P: AsRef<Path>>(outdir: P) -> io::Result<()> {
// If asciidoc isn't installed, then don't do anything.
if let Err(err) = process::Command::new("a2x").output() {
eprintln!("Could not run 'a2x' binary, skipping man page generation.");
eprintln!("Error from running 'a2x': {}", err);
return Ok(());
}
let outdir = outdir.as_ref();
let cwd = env::current_dir()?;
let tpl_path = cwd.join("doc").join("rg.1.txt.tpl");
let txt_path = outdir.join("rg.1.txt");

let mut tpl = String::new();
File::open(&tpl_path)?.read_to_string(&mut tpl)?;
tpl = tpl.replace("{OPTIONS}", &formatted_options()?);

let githash = git_revision_hash();
let githash = githash.as_ref().map(|x| &**x);
tpl = tpl.replace("{VERSION}", &app::long_version(githash));

File::create(&txt_path)?.write_all(tpl.as_bytes())?;
let result = process::Command::new("a2x")
.arg("--no-xmllint")
.arg("--doctype").arg("manpage")
.arg("--format").arg("manpage")
.arg(&txt_path)
.spawn()?
.wait()?;
if !result.success() {
let msg = format!("'a2x' failed with exit code {:?}", result.code());
return Err(ioerr(msg));
}
Ok(())
}

fn formatted_options() -> io::Result<String> {
let mut args = app::all_args_and_flags();
args.sort_by(|x1, x2| x1.name.cmp(&x2.name));

let mut formatted = vec![];
for arg in args {
// ripgrep only has two positional arguments, and probably will only
// ever have two positional arguments, so we just hardcode them into
// the template.
if let app::RGArgKind::Positional{..} = arg.kind {
continue;
}
formatted.push(formatted_arg(&arg)?);
}
Ok(formatted.join("\n\n"))
}

fn formatted_arg(arg: &RGArg) -> io::Result<String> {
match arg.kind {
RGArgKind::Positional{..} => panic!("unexpected positional argument"),
RGArgKind::Switch { long, short, multiple } => {
let mut out = vec![];

let mut header = format!("--{}", long);
if let Some(short) = short {
header = format!("-{}, {}", short, header);
}
if multiple {
header = format!("*{}* ...::", header);
} else {
header = format!("*{}*::", header);
}
writeln!(out, "{}", header)?;
writeln!(out, "{}", formatted_doc_txt(arg)?)?;

Ok(String::from_utf8(out).unwrap())
}
RGArgKind::Flag { long, short, value_name, multiple, .. } => {
let mut out = vec![];

let mut header = format!("--{}", long);
if let Some(short) = short {
header = format!("-{}, {}", short, header);
}
if multiple {
header = format!("*{}* _{}_ ...::", header, value_name);
} else {
header = format!("*{}* _{}_::", header, value_name);
}
writeln!(out, "{}", header)?;
writeln!(out, "{}", formatted_doc_txt(arg)?)?;

Ok(String::from_utf8(out).unwrap())
}
}
}

fn formatted_doc_txt(arg: &RGArg) -> io::Result<String> {
let paragraphs: Vec<&str> = arg.doc_long.split("\n\n").collect();
if paragraphs.is_empty() {
return Err(ioerr(format!("missing docs for --{}", arg.name)));
}
let first = format!(" {}", paragraphs[0].replace("\n", "\n "));
if paragraphs.len() == 1 {
return Ok(first);
}
Ok(format!("{}\n+\n{}", first, paragraphs[1..].join("\n+\n")))
}

fn ioerr(msg: String) -> io::Error {
io::Error::new(io::ErrorKind::Other, msg)
}
22 changes: 15 additions & 7 deletions ci/before_deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ mk_tarball() {
local gcc_prefix="$(gcc_prefix)"
local td="$(mktemp -d)"
local name="${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}"
mkdir -p "$td/$name/complete"
mkdir deployment
local staging="$td/$name"
mkdir -p "$staging/complete"
local out_dir="$(pwd)/deployment"

cp target/$TARGET/release/rg "$td/$name/rg"
"${gcc_prefix}strip" "$td/$name/rg"
cp {doc/rg.1,README.md,UNLICENSE,COPYING,LICENSE-MIT} "$td/$name/"
mkdir -p "$out_dir"

# Copy the ripgrep binary and strip it.
cp target/$TARGET/release/rg "$staging/rg"
"${gcc_prefix}strip" "$staging/rg"
# Copy the README and licenses.
cp {README.md,UNLICENSE,COPYING,LICENSE-MIT} "$staging/"
# Copy shell completion files.
cp \
target/"$TARGET"/release/build/ripgrep-*/out/{rg.bash,rg.fish,_rg.ps1} \
"$td/$name/complete/"
"$staging/complete/"
cp complete/_rg "$td/$name/complete/"
# Copy man page.
cp \
target/"$TARGET"/release/build/ripgrep-*/out/rg.1 \
"$td/$name/"

(cd "$td" && tar czf "$out_dir/$name.tar.gz" *)
rm -rf "$td"
Expand Down
5 changes: 0 additions & 5 deletions doc/convert-to-man

This file was deleted.

Loading

0 comments on commit a2fc8d8

Please sign in to comment.