From a6271b8ddee411f7b6fcb7f296de2f95c0ddaab6 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Tue, 24 Jul 2018 11:43:33 +0200 Subject: [PATCH] Man gen (#6) * init man generator * authors works! * clean up author list * slightly better examples * chaining API * exit status * flags * brush up example * options * add arguments * environment * derive clone --- Cargo.toml | 1 + README.md | 6 + examples/demo.rs | 49 ++++++ examples/main.rs | 2 +- src/lib.rs | 4 + src/man/author.rs | 6 + src/man/environment.rs | 7 + src/man/flag.rs | 7 + src/man/mod.rs | 370 +++++++++++++++++++++++++++++++++++++++++ src/man/option.rs | 9 + 10 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 examples/demo.rs create mode 100644 src/man/author.rs create mode 100644 src/man/environment.rs create mode 100644 src/man/flag.rs create mode 100644 src/man/mod.rs create mode 100644 src/man/option.rs diff --git a/Cargo.toml b/Cargo.toml index accfe81..265bbcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,6 @@ readme = "README.md" [dependencies] clap = { git = "https://github.com/kbknapp/clap-rs", branch = "v3-dev" } +roff = "0.1.0" [dev-dependencies] diff --git a/README.md b/README.md index 8f74dcc..5836b6b 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,14 @@ fn main() { .flag(Some("-d"), Some("--debug"), Some("Activate debug mode")) .flag(Some("-v"), Some("--verbose"), Some("Verbose mode")); .option(Some("-o"), Some("--output"), "output", None, "Output file"); + + let _string = page.to_string(); } ``` +Preview by running: +```sh +$ cargo run > /tmp/app.man; man /tmp/app.man +``` ## Installation ```sh diff --git a/examples/demo.rs b/examples/demo.rs new file mode 100644 index 0000000..e3306eb --- /dev/null +++ b/examples/demo.rs @@ -0,0 +1,49 @@ +extern crate man; + +use man::Man; + +fn main() { + let msg = Man::new("auth-service") + .description("authorize & authenticate members") + .argument("path".into()) + .environment( + "PORT".into(), + None, + Some("The network port to listen to.".into()), + ) + .flag( + Some("-h".into()), + Some("--help".into()), + Some("Prints help information.".into()), + ) + .flag( + Some("-V".into()), + Some("--version".into()), + Some("Prints version information.".into()), + ) + .flag( + Some("-v".into()), + Some("--verbosity".into()), + Some("Pass multiple times to print more information.".into()), + ) + .option( + Some("-a".into()), + Some("--address".into()), + Some("The network address to listen to.".into()), + "address".into(), + Some("127.0.0.1".into()), + ) + .option( + Some("-p".into()), + Some("--port".into()), + Some("The network port to listen to.".into()), + "port".into(), + None, + ) + .author("Alice Person", Some("alice@person.com".into())) + .author("Bob Human", Some("bob@human.com".into())) + .render(); + // .option(Some("-o"), Some("--output"), "output", None, "Output file"); + + println!("{}", msg); +} diff --git a/examples/main.rs b/examples/main.rs index 3319f62..bfb3b69 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,7 +1,7 @@ extern crate clap; extern crate man; -use clap::{App, AppSettings, Arg, SubCommand}; +use clap::{App, AppSettings, Arg, Man, SubCommand}; use man::Manual; fn main() { diff --git a/src/lib.rs b/src/lib.rs index 45801ad..150369a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,12 @@ #![cfg_attr(test, deny(warnings))] extern crate clap; +extern crate roff; + +mod man; use clap::{App, Arg, ArgSettings}; +pub use man::*; /// Describe an argument or option #[derive(Debug)] diff --git a/src/man/author.rs b/src/man/author.rs new file mode 100644 index 0000000..84f3a3e --- /dev/null +++ b/src/man/author.rs @@ -0,0 +1,6 @@ +/// An author entry. +#[derive(Debug, Clone)] +pub struct Author { + pub(crate) name: String, + pub(crate) email: Option, +} diff --git a/src/man/environment.rs b/src/man/environment.rs new file mode 100644 index 0000000..b2d1394 --- /dev/null +++ b/src/man/environment.rs @@ -0,0 +1,7 @@ +/// Command line environment variable representation. +#[derive(Debug, Clone)] +pub struct Env { + pub(crate) name: String, + pub(crate) default: Option, + pub(crate) description: Option, +} diff --git a/src/man/flag.rs b/src/man/flag.rs new file mode 100644 index 0000000..8314976 --- /dev/null +++ b/src/man/flag.rs @@ -0,0 +1,7 @@ +/// Command line flag representation. +#[derive(Debug, Clone)] +pub struct Flag { + pub(crate) short: Option, + pub(crate) long: Option, + pub(crate) description: Option, +} diff --git a/src/man/mod.rs b/src/man/mod.rs new file mode 100644 index 0000000..0739720 --- /dev/null +++ b/src/man/mod.rs @@ -0,0 +1,370 @@ +mod author; +mod environment; +mod flag; +mod option; + +use self::author::Author; +use self::environment::Env; +use self::flag::Flag; +use self::option::Opt; +use roff::{bold, italic, list, Roff, Troffable}; +use std::convert::AsRef; + +/// Man page struct. +#[derive(Debug, Clone)] +pub struct Man { + name: String, + description: Option, + authors: Vec, + flags: Vec, + options: Vec, + environment: Vec, + arguments: Vec, +} + +impl Man { + /// Create a new instance. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + description: None, + authors: vec![], + flags: vec![], + options: vec![], + arguments: vec![], + environment: vec![], + } + } + + /// Add a description. + pub fn description(mut self, desc: &str) -> Self { + let desc = desc.into(); + self.description = Some(desc); + self + } + + /// Add an author. + pub fn author( + mut self, + name: impl AsRef, + email: Option, + ) -> Self { + self.authors.push(Author { + name: name.as_ref().to_owned(), + email, + }); + self + } + + /// Add an environment variable. + pub fn environment( + mut self, + name: String, + default: Option, + description: Option, + ) -> Self { + self.environment.push(Env { + name, + default, + description, + }); + self + } + + /// Add an flag. + pub fn flag( + mut self, + short: Option, + long: Option, + description: Option, + ) -> Self { + self.flags.push(Flag { + short, + long, + description, + }); + self + } + + /// Add an option. + pub fn option( + mut self, + short: Option, + long: Option, + description: Option, + argument: String, + default: Option, + ) -> Self { + self.options.push(Opt { + short, + long, + description, + argument, + default, + }); + self + } + + /// Add a positional argument. The items are displayed in the order they're + /// pushed. + // TODO: make this accept argument vecs / optional args too. `arg...`, `arg?` + pub fn argument(mut self, arg: String) -> Self { + self.arguments.push(arg); + self + } + + pub fn render(self) -> String { + let man_num = 1; + let mut page = Roff::new(&self.name, man_num); + page = description(page, &self.name, &self.description); + page = synopsis( + page, + &self.name, + &self.flags, + &self.options, + &self.arguments, + ); + page = flags(page, &self.flags); + page = options(page, &self.options); + page = environment(page, &self.environment); + page = exit_status(page); + page = authors(page, &self.authors); + page.render() + } +} + +/// Create a `NAME` section. +/// +/// ## Formatting +/// ```txt +/// NAME +/// mycmd - brief description of the application +/// ``` +fn description(page: Roff, name: &str, desc: &Option) -> Roff { + let desc = match desc { + Some(ref desc) => format!("{} - {}", name, desc), + None => name.to_owned(), + }; + + page.section("NAME", &[desc]) +} + +/// Create a `SYNOPSIS` section. +fn synopsis( + page: Roff, + name: &str, + flags: &[Flag], + options: &[Opt], + args: &[String], +) -> Roff { + let flags = match flags.len() { + 0 => "".into(), + _ => " [FLAGS]".into(), + }; + let options = match options.len() { + 0 => "".into(), + _ => " [OPTIONS]".into(), + }; + + let mut msg = vec![]; + msg.push(bold(name)); + msg.push(flags); + msg.push(options); + + for arg in args { + msg.push(format!(" {}", arg)); + } + + page.section("SYNOPSIS", &msg) +} + +/// Create a `AUTHOR` or `AUTHORS` section. +/// +/// ## Formatting +/// ```txt +/// AUTHORS +/// Alice Person +/// Bob Human +/// ``` +fn authors(page: Roff, authors: &[Author]) -> Roff { + let title = match authors.len() { + 0 => return page, + 1 => "AUTHOR", + _ => "AUTHORS", + }; + + let last = authors.len() - 1; + let mut auth_values = vec![]; + auth_values.push(init_list()); + for (index, author) in authors.iter().enumerate() { + auth_values.push(author.name.to_owned()); + + if let Some(ref email) = author.email { + auth_values.push(format!(" <{}>", email)) + }; + + if index != last { + auth_values.push(format!("\n")); + } + } + + page.section(title, &auth_values) +} + +/// Create a `FLAGS` section. +/// +/// ## Formatting +/// ```txt +/// FLAGS +/// ``` +fn flags(page: Roff, flags: &[Flag]) -> Roff { + if flags.is_empty() { + return page; + } + + let last = flags.len() - 1; + let mut arr: Vec = vec![]; + for (index, flag) in flags.iter().enumerate() { + let mut args: Vec = vec![]; + if let Some(ref short) = flag.short { + args.push(bold(&short)); + } + if let Some(ref long) = flag.long { + if !args.is_empty() { + args.push(", ".to_string()); + } + args.push(bold(&long)); + } + let desc = match flag.description { + Some(ref desc) => desc.to_string(), + None => "".to_string(), + }; + arr.push(list(&args, &[desc])); + + if index != last { + arr.push(format!("\n\n")); + } + } + page.section("FLAGS", &arr) +} + +/// Create a `OPTIONS` section. +/// +/// ## Formatting +/// ```txt +/// OPTIONS +/// ``` +fn options(page: Roff, options: &[Opt]) -> Roff { + if options.is_empty() { + return page; + } + + let last = options.len() - 1; + let mut arr: Vec = vec![]; + for (index, opt) in options.iter().enumerate() { + let mut args: Vec = vec![]; + if let Some(ref short) = opt.short { + args.push(bold(&short)); + } + if let Some(ref long) = opt.long { + if !args.is_empty() { + args.push(", ".to_string()); + } + args.push(bold(&long)); + } + args.push("=".into()); + args.push(italic(&opt.argument)); + if let Some(ref default) = opt.default { + if !args.is_empty() { + args.push(" ".to_string()); + } + args.push("[".into()); + args.push("default:".into()); + args.push(" ".into()); + args.push(italic(&default)); + args.push("]".into()); + } + let desc = match opt.description { + Some(ref desc) => desc.to_string(), + None => "".to_string(), + }; + arr.push(list(&args, &[desc])); + + if index != last { + arr.push(format!("\n\n")); + } + } + page.section("OPTIONS", &arr) +} + +/// Create a `ENVIRONMENT` section. +/// +/// ## Formatting +/// ```txt +/// ENVIRONMENT +/// ``` +fn environment(page: Roff, environment: &[Env]) -> Roff { + if environment.is_empty() { + return page; + } + + let last = environment.len() - 1; + let mut arr: Vec = vec![]; + for (index, env) in environment.iter().enumerate() { + let mut args: Vec = vec![]; + args.push(bold(&env.name)); + if let Some(ref default) = env.default { + if !args.is_empty() { + args.push(" ".to_string()); + } + args.push("[".into()); + args.push("default:".into()); + args.push(" ".into()); + args.push(italic(&default)); + args.push("]".into()); + } + let desc = match env.description { + Some(ref desc) => desc.to_string(), + None => "".to_string(), + }; + arr.push(list(&args, &[desc])); + + if index != last { + arr.push(format!("\n\n")); + } + } + page.section("ENVIRONMENT", &arr) +} + +/// Create a `EXIT STATUS` section. +/// +/// ## Implementation Note +/// This currently only returns the status code `0`, and takes no arguments. We +/// should let it take arguments. +/// +/// ## Formatting +/// ```txt +/// EXIT STATUS +/// 0 Successful program execution +/// +/// 1 Usage, syntax or configuration file error +/// +/// 2 Optional error +/// ``` +fn exit_status(page: Roff) -> Roff { + page.section( + "EXIT STATUS", + &[list(&[bold("0")], &["Successful program execution."])], + ) +} + +// NOTE(yw): This code was taken from the npm-install(1) command. The location +// on your system may vary. In all honesty I just copy-pasted this. We should +// probably port this to troff-rs at some point. +// +// ```sh +// $ less /usr/share/man/man1/npm-install.1 +// ``` +fn init_list() -> String { + format!(".P\n.RS 2\n.nf\n") +} diff --git a/src/man/option.rs b/src/man/option.rs new file mode 100644 index 0000000..cb477b1 --- /dev/null +++ b/src/man/option.rs @@ -0,0 +1,9 @@ +/// Option +#[derive(Debug, Clone)] +pub struct Opt { + pub(crate) short: Option, + pub(crate) long: Option, + pub(crate) description: Option, + pub(crate) argument: String, + pub(crate) default: Option, +}