diff --git a/README.md b/README.md index 5836b6b..a77cc23 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,18 @@ Generate structured man pages using ```rust extern crate man; -use man::Man; +use man::prelude::*; fn main() { - let page = Man::new("basic") - .description("A basic example") - .author("Alice", Some("alice@email.com")) - .author("Bob", Some("bob@email.com")) - .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(); + let page = Manual::new("basic") + .about("A basic example") + .author(Author::new("Alice Person").email("alice@person.com")) + .author(Author::new("Bob Human").email("bob@human.com")) + .flag(Flag::new().short("-d").long("--deubg").description("Enable debug mode")) + .flag(Flag::new().short("-v").long("--verbose").description("Enable verbose mode")) + .option(Opt::new("output").short("-o").long("--output").description("Output file")); + + let _string = page.render(); } ``` Preview by running: diff --git a/examples/demo.rs b/examples/demo.rs deleted file mode 100644 index e3306eb..0000000 --- a/examples/demo.rs +++ /dev/null @@ -1,49 +0,0 @@ -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 bfb3b69..98cf5d6 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,28 +1,40 @@ -extern crate clap; extern crate man; -use clap::{App, AppSettings, Arg, Man, SubCommand}; -use man::Manual; +use man::prelude::*; fn main() { - let a = App::new("testapp") - .about("Pointless application") - .setting(AppSettings::SubcommandRequiredElseHelp) - .author("Katharina Fey ") - // .author("Yosh Wuyts Self { + Self { name: name.into() } + } +} diff --git a/src/author.rs b/src/author.rs new file mode 100644 index 0000000..69aa0a2 --- /dev/null +++ b/src/author.rs @@ -0,0 +1,22 @@ +/// An author entry. +#[derive(Debug, Clone)] +pub struct Author { + pub(crate) name: String, + pub(crate) email: Option, +} + +impl Author { + /// Create a new instance. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + email: None, + } + } + + /// Set the email field. + pub fn email(mut self, email: &str) -> Self { + self.email = Some(email.into()); + self + } +} diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..0e51bce --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,30 @@ +/// Command line environment variable representation. +#[derive(Debug, Clone)] +pub struct Env { + pub(crate) name: String, + pub(crate) default: Option, + pub(crate) help: Option, +} + +impl Env { + /// Create a new instance. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + default: None, + help: None, + } + } + + /// Set the default value. + pub fn default_value(mut self, default: &str) -> Self { + self.default = Some(default.into()); + self + } + + /// Set the help. + pub fn help(mut self, help: &str) -> Self { + self.help = Some(help.into()); + self + } +} diff --git a/src/flag.rs b/src/flag.rs new file mode 100644 index 0000000..1463706 --- /dev/null +++ b/src/flag.rs @@ -0,0 +1,36 @@ +/// Command line flag representation. +#[derive(Debug, Clone)] +pub struct Flag { + pub(crate) short: Option, + pub(crate) long: Option, + pub(crate) help: Option, +} + +impl Flag { + /// Create a new instance. + pub fn new() -> Self { + Self { + short: None, + long: None, + help: None, + } + } + + /// Set the short value. + pub fn short(mut self, short: &str) -> Self { + self.short = Some(short.into()); + self + } + + /// Set the long value. + pub fn long(mut self, long: &str) -> Self { + self.long = Some(long.into()); + self + } + + /// Set the help value. + pub fn help(mut self, help: &str) -> Self { + self.help = Some(help.into()); + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 150369a..d3c3fd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,104 +3,20 @@ #![cfg_attr(feature = "nightly", doc(include = "../README.md"))] #![cfg_attr(test, deny(warnings))] -extern crate clap; extern crate roff; +mod arg; +mod author; +mod environment; +mod flag; mod man; +mod option; -use clap::{App, Arg, ArgSettings}; -pub use man::*; +pub mod prelude; -/// Describe an argument or option -#[derive(Debug)] -pub struct ManualArg { - short: Option, - long: Option, - descr: Option, -} - -#[derive(Debug)] -pub struct Manual { - /// Optionally children of this app - children: Vec<(String, Manual)>, - /// Application name - name: Option, - /// Type description - description: Option, - /// Type authors - authors: Option, - /// Type flags - flags: Vec, - /// Type options - options: Vec, -} - -impl<'a, 'b, 'c> From<&'c Arg<'a, 'b>> for ManualArg { - fn from(s: &'c Arg) -> Self { - ManualArg { - short: s.short, - long: match s.long { - Some(s) => Some(s.into()), - _ => None, - }, - descr: match s.help { - Some(s) => Some(s.into()), - _ => None, - }, - } - } -} - -impl Manual { - fn new() -> Self { - Manual { - children: Vec::new(), - name: None, - description: None, - authors: None, - flags: Vec::new(), - options: Vec::new(), - } - } - - // TODO: Make this less awful - fn add_empty_child(&mut self, name: &str) -> &mut Manual { - self.children.push((name.into(), Manual::new())); - let (_, ref mut manual) = self.children.last_mut().unwrap(); - manual - } - - fn recursive(manual: &mut Manual, app: &App) { - manual.name = app.name.clone().into(); - manual.description = app.about.map(Into::into); - manual.authors = app.author.map(Into::into); - - let (flags, options): (Vec, Vec) = app - .args - .iter() - .fold((Vec::new(), Vec::new()), |(mut f, mut o), i: &Arg| { - if i.is_set(ArgSettings::TakesValue) { - f.push(i.into()); - } else { - o.push(i.into()); - } - - (f, o) - }); - - manual.flags = flags; - manual.options = options; - - app.subcommands.iter().for_each(|app| { - let _inner_name: String = app.name.clone(); - let mut inner = manual.add_empty_child(&app.name); - Manual::recursive(&mut inner, app); - }); - } - - pub fn from_clap<'a, 'b>(app: &App<'a, 'b>) -> Manual { - let mut man = Manual::new(); - Manual::recursive(&mut man, app); - man - } -} +pub use arg::Arg; +pub use author::Author; +pub use environment::Env; +pub use flag::Flag; +pub use man::Manual; +pub use option::Opt; diff --git a/src/man.rs b/src/man.rs new file mode 100644 index 0000000..5357dc1 --- /dev/null +++ b/src/man.rs @@ -0,0 +1,327 @@ +use super::*; +use roff::{bold, italic, list, Roff, Troffable}; + +/// Man page struct. +#[derive(Debug, Clone)] +pub struct Manual { + name: String, + about: Option, + authors: Vec, + flags: Vec, + options: Vec, + environment: Vec, + arguments: Vec, +} + +impl Manual { + /// Create a new instance. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + about: None, + authors: vec![], + flags: vec![], + options: vec![], + arguments: vec![], + environment: vec![], + } + } + + /// Add a description. + pub fn about(mut self, about: String) -> Self { + self.about = Some(about); + self + } + + /// Add an author. + pub fn author(mut self, author: Author) -> Self { + self.authors.push(author); + self + } + + /// Add an environment variable. + pub fn env(mut self, env: Env) -> Self { + self.environment.push(env); + self + } + + /// Add an flag. + pub fn flag(mut self, flag: Flag) -> Self { + self.flags.push(flag); + self + } + + /// Add an option. + pub fn option(mut self, opt: Opt) -> Self { + self.options.push(opt); + 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 arg(mut self, arg: Arg) -> Self { + self.arguments.push(arg); + self + } + + /// Render to a string. + pub fn render(self) -> String { + let man_num = 1; + let mut page = Roff::new(&self.name, man_num); + page = about(page, &self.name, &self.about); + page = synopsis( + page, + &self.name, + &self.flags, + &self.options, + &self.arguments, + ); + page = flags(page, &self.flags); + page = options(page, &self.options); + page = env(page, &self.environment); + page = exit_status(page); + page = authors(page, &self.authors); + page.render() + } +} + +/// Create a `NAME` section. +/// +/// ## Formatting +/// ```txt +/// NAME +/// mycmd - brief help of the application +/// ``` +fn about(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: &[Arg], +) -> 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.name)); + } + + 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.help { + 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.name)); + 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.help { + 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 env(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.help { + 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.\n\n"]), + list(&[bold("1")], &["Unsuccessful program execution.\n\n"]), + list(&[bold("101")], &["The program panicked."]), + ], + ) +} + +// 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/author.rs b/src/man/author.rs deleted file mode 100644 index 84f3a3e..0000000 --- a/src/man/author.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// 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 deleted file mode 100644 index b2d1394..0000000 --- a/src/man/environment.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// 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 deleted file mode 100644 index 8314976..0000000 --- a/src/man/flag.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// 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/option.rs b/src/man/option.rs deleted file mode 100644 index cb477b1..0000000 --- a/src/man/option.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// 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, -} diff --git a/src/man/mod.rs b/src/mod.rs similarity index 92% rename from src/man/mod.rs rename to src/mod.rs index 0739720..cb7a1a5 100644 --- a/src/man/mod.rs +++ b/src/mod.rs @@ -14,7 +14,7 @@ use std::convert::AsRef; #[derive(Debug, Clone)] pub struct Man { name: String, - description: Option, + help: Option, authors: Vec, flags: Vec, options: Vec, @@ -27,7 +27,7 @@ impl Man { pub fn new(name: &str) -> Self { Self { name: name.into(), - description: None, + help: None, authors: vec![], flags: vec![], options: vec![], @@ -36,10 +36,10 @@ impl Man { } } - /// Add a description. - pub fn description(mut self, desc: &str) -> Self { + /// Add a help. + pub fn help(mut self, desc: &str) -> Self { let desc = desc.into(); - self.description = Some(desc); + self.help = Some(desc); self } @@ -61,12 +61,12 @@ impl Man { mut self, name: String, default: Option, - description: Option, + help: Option, ) -> Self { self.environment.push(Env { name, default, - description, + help, }); self } @@ -76,12 +76,12 @@ impl Man { mut self, short: Option, long: Option, - description: Option, + help: Option, ) -> Self { self.flags.push(Flag { short, long, - description, + help, }); self } @@ -91,14 +91,14 @@ impl Man { mut self, short: Option, long: Option, - description: Option, + help: Option, argument: String, default: Option, ) -> Self { self.options.push(Opt { short, long, - description, + help, argument, default, }); @@ -116,7 +116,7 @@ impl Man { 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 = help(page, &self.name, &self.help); page = synopsis( page, &self.name, @@ -138,9 +138,9 @@ impl Man { /// ## Formatting /// ```txt /// NAME -/// mycmd - brief description of the application +/// mycmd - brief help of the application /// ``` -fn description(page: Roff, name: &str, desc: &Option) -> Roff { +fn help(page: Roff, name: &str, desc: &Option) -> Roff { let desc = match desc { Some(ref desc) => format!("{} - {}", name, desc), None => name.to_owned(), @@ -235,7 +235,7 @@ fn flags(page: Roff, flags: &[Flag]) -> Roff { } args.push(bold(&long)); } - let desc = match flag.description { + let desc = match flag.help { Some(ref desc) => desc.to_string(), None => "".to_string(), }; @@ -284,7 +284,7 @@ fn options(page: Roff, options: &[Opt]) -> Roff { args.push(italic(&default)); args.push("]".into()); } - let desc = match opt.description { + let desc = match opt.help { Some(ref desc) => desc.to_string(), None => "".to_string(), }; @@ -323,7 +323,7 @@ fn environment(page: Roff, environment: &[Env]) -> Roff { args.push(italic(&default)); args.push("]".into()); } - let desc = match env.description { + let desc = match env.help { Some(ref desc) => desc.to_string(), None => "".to_string(), }; diff --git a/src/option.rs b/src/option.rs new file mode 100644 index 0000000..127227d --- /dev/null +++ b/src/option.rs @@ -0,0 +1,46 @@ +/// Option +#[derive(Debug, Clone)] +pub struct Opt { + pub(crate) name: String, + pub(crate) default: Option, + pub(crate) help: Option, + pub(crate) short: Option, + pub(crate) long: Option, +} + +impl Opt { + /// Create a new instance. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + default: None, + help: None, + short: None, + long: None, + } + } + + /// Set the default value. + pub fn default_value(mut self, default: &str) -> Self { + self.default = Some(default.into()); + self + } + + /// Set the help. + pub fn help(mut self, help: &str) -> Self { + self.help = Some(help.into()); + self + } + + /// Set the short value. + pub fn short(mut self, short: &str) -> Self { + self.short = Some(short.into()); + self + } + + /// Set the long value. + pub fn long(mut self, long: &str) -> Self { + self.long = Some(long.into()); + self + } +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..89a3b20 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,17 @@ +//! Convenience wrapper to import all the essential structs. +//! +//! ```rust +//! extern crate man; +//! +//! use man::prelude::*; +//! +//! fn main () { +//! let msg = Man::new("my-app").render(); +//! } +//! ``` +pub use arg::Arg; +pub use author::Author; +pub use environment::Env; +pub use flag::Flag; +pub use man::Manual; +pub use option::Opt;