// Adapted from https://github.com/BurntSushi/ripgrep/blob/master/build.rs use std::env; use std::fs::{self, File}; use std::io::{self, Read, Write}; use std::path::Path; use std::process; use clap::Shell; #[path = "src/app.rs"] mod app; use app::{CustomArg, CustomArgKind}; fn main() { // OUT_DIR is set by Cargo and it's where any additional build artifacts // are written. let outdir = if let Some(outdir) = env::var_os("OUT_DIR") { outdir } else { eprintln!( "OUT_DIR environment variable not defined. \ Please file a bug: \ https://github.com/IgnisDa/printr/issues/new" ); process::exit(1); }; if fs::remove_dir_all(&outdir).is_ok() {}; fs::create_dir_all(&outdir).unwrap(); let stamp_path = Path::new(&outdir).join("printr-stamp"); if let Err(err) = File::create(&stamp_path) { panic!("failed to write {}: {}", stamp_path.display(), err); } if let Err(err) = generate_man_page(&outdir) { eprintln!("failed to generate man page: {}", err); } // generate completions let mut app = app::app(); app.gen_completions("printr", Shell::Bash, &outdir); app.gen_completions("printr", Shell::Fish, &outdir); app.gen_completions("printr", Shell::Zsh, &outdir); } fn generate_man_page>(outdir: P) -> io::Result<()> { // 1. Read asciidoctor template. // 2. Interpolate template with auto-generated docs. // 3. Save interpolation to disk. // 4. Use asciidoctor to convert to man page. let outdir = outdir.as_ref(); let cwd = env::current_dir()?; let tpl_path = cwd.join("docs").join("printr.1.txt.tpl"); let txt_path = outdir.join("printr.1.txt"); let mut tpl = String::new(); File::open(&tpl_path)?.read_to_string(&mut tpl)?; let options = formatted_options()? .replace("{", "{") .replace("}", "}"); tpl = tpl.replace("{{OPTIONS}}", &options); tpl = tpl.replace("{{VERSION}}", clap::crate_version!()); tpl = tpl.replace("{{AUTHORS}}", clap::crate_authors!(",\n")); File::create(&txt_path)?.write_all(tpl.as_bytes())?; let result = process::Command::new("asciidoctor") .arg("--doctype") .arg("manpage") .arg("--backend") .arg("manpage") .arg(&txt_path) .spawn()? .wait()?; if !result.success() { let msg = format!("'asciidoctor' failed with exit code {:?}", result.code()); return Err(ioerr(msg)); } Ok(()) } fn formatted_options() -> io::Result { 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 { if arg.hidden { continue; } if let app::CustomArgKind::Positional { .. } = arg.kind { continue; } formatted.push(formatted_arg(&arg)?); } Ok(formatted.join("\n\n")) } fn formatted_arg(arg: &CustomArg) -> io::Result { match arg.kind { CustomArgKind::Positional { .. } => { panic!("unexpected positional argument") } CustomArgKind::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()) } CustomArgKind::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: &CustomArg) -> io::Result { let paragraphs: Vec = arg .doc_long .replace("{", "{") .replace("}", r"}") // Hack to render ** literally in man page correctly. We can't put // these crazy +++ in the help text directly, since that shows // literally in --help output. .replace("*-g 'foo/**'*", "*-g +++'foo/**'+++*") .split("\n\n") .map(|s| s.to_string()) .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) }