From 543693d937484eab4d5f258fdcba8316c5602340 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 5 Jul 2018 00:39:51 +0200 Subject: [PATCH 01/12] init man generator --- Cargo.toml | 1 + README.md | 2 + examples/demo.rs | 15 +++++++ examples/main.rs | 2 +- src/lib.rs | 4 ++ src/man/author.rs | 6 +++ src/man/flag.rs | 3 ++ src/man/mod.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++ src/man/option.rs | 3 ++ 9 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 examples/demo.rs create mode 100644 src/man/author.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..4eba697 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ 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(); } ``` diff --git a/examples/demo.rs b/examples/demo.rs new file mode 100644 index 0000000..64235a3 --- /dev/null +++ b/examples/demo.rs @@ -0,0 +1,15 @@ +extern crate man; + +use man::Man; + +fn main() { + let mut page = Man::new("basic"); + page.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"); + + println!("{}", page.render()); +} 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..2686b4a --- /dev/null +++ b/src/man/author.rs @@ -0,0 +1,6 @@ +/// An author entry. +#[derive(Debug)] +pub struct Author { + pub(crate) name: String, + pub(crate) email: Option, +} diff --git a/src/man/flag.rs b/src/man/flag.rs new file mode 100644 index 0000000..748dce9 --- /dev/null +++ b/src/man/flag.rs @@ -0,0 +1,3 @@ +/// Command line flag representation. +#[derive(Debug)] +pub struct Flag {} diff --git a/src/man/mod.rs b/src/man/mod.rs new file mode 100644 index 0000000..09b8d66 --- /dev/null +++ b/src/man/mod.rs @@ -0,0 +1,107 @@ +mod author; +mod flag; +mod option; + +use self::author::Author; +use self::flag::Flag; +use self::option::Opt; +use roff::{self, Roff, Troffable}; +use std::convert::AsRef; + +/// Man page struct. +#[derive(Debug)] +pub struct Man { + name: String, + description: Option, + authors: Vec, + flags: Vec, + options: 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![], + } + } + + /// Add a description. + pub fn description(&mut self, desc: &str) { + let desc = desc.into(); + self.description = Some(desc); + } + + /// Add an author. + pub fn author(&mut self, name: impl AsRef, email: Option) { + self.authors.push(Author { + name: name.as_ref().to_owned(), + email, + }); + } + + /// Add an flag. + pub fn flag(&mut self, flag: Flag) { + self.flags.push(flag); + } + + /// Add an option. + pub fn option(&mut self, option: Opt) { + self.options.push(option); + } + + pub fn render(&mut self) -> String { + let man_num = 1; + let mut page = Roff::new(&self.name, man_num); + + page = description(page, &self.description); + page.render() + } +} + +/// Create a `NAME` section. +/// +/// ## Formatting +/// ```txt +/// NAME +/// mycmd - brief description of the application +/// ``` +pub fn description(page: Roff, desc: &Option) -> Roff { + let desc = match desc { + Some(ref desc) => format!("- {:?}", desc), + None => String::from(""), + }; + + page.section("NAME", &[desc]) +} + +/// Create a `AUTHOR` or `AUTHORS` section. +/// +/// ## Formatting +/// ```txt +/// AUTHORS +/// alice person +/// bob human +/// ``` +pub fn authors(page: Roff, authors: &[Author]) -> Roff { + let title = match authors.len() { + 0 => return page, + 1 => "AUTHOR", + _ => "AUTHORS", + }; + + let auth_values = vec![]; + for author in authors.iter() { + let email = match author.email { + Some(email) => format!("- {:?}", email), + None => "".into(), + }; + auth_values.push([roff::bold(&author.name), email]); + } + + page.section(title, &auth_values) +} diff --git a/src/man/option.rs b/src/man/option.rs new file mode 100644 index 0000000..6770fb6 --- /dev/null +++ b/src/man/option.rs @@ -0,0 +1,3 @@ +/// Option +#[derive(Debug)] +pub struct Opt {} From 4f713cc017d3844c12a511ec769531a6b6372bf9 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Sat, 14 Jul 2018 18:53:19 +0200 Subject: [PATCH 02/12] authors works! --- examples/demo.rs | 2 +- src/man/mod.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 64235a3..ccd903a 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -5,7 +5,7 @@ use man::Man; fn main() { let mut page = Man::new("basic"); page.description("A basic example"); - // .author("Alice", Some("alice@email.com")) + page.author(&"Alice", Some(String::from("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")); diff --git a/src/man/mod.rs b/src/man/mod.rs index 09b8d66..3d1c989 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -37,7 +37,7 @@ impl Man { } /// Add an author. - pub fn author(&mut self, name: impl AsRef, email: Option) { + pub fn author(&mut self, name: impl AsRef, email: Option) { self.authors.push(Author { name: name.as_ref().to_owned(), email, @@ -59,6 +59,7 @@ impl Man { let mut page = Roff::new(&self.name, man_num); page = description(page, &self.description); + page = authors(page, &self.authors); page.render() } } @@ -94,13 +95,13 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { _ => "AUTHORS", }; - let auth_values = vec![]; + let mut auth_values = vec![]; for author in authors.iter() { - let email = match author.email { - Some(email) => format!("- {:?}", email), - None => "".into(), + auth_values.push(roff::bold(&author.name)); + + if let Some(ref email) = author.email { + auth_values.push(format!("- {:?}", email)) }; - auth_values.push([roff::bold(&author.name), email]); } page.section(title, &auth_values) From dd7700e47a6e79ab5078c7f285645ae26b799d70 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 00:47:18 +0200 Subject: [PATCH 03/12] clean up author list --- README.md | 4 ++++ examples/demo.rs | 6 +++--- src/man/mod.rs | 36 ++++++++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4eba697..5836b6b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ fn main() { 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 index ccd903a..85110c3 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -4,9 +4,9 @@ use man::Man; fn main() { let mut page = Man::new("basic"); - page.description("A basic example"); - page.author(&"Alice", Some(String::from("alice@email.com"))); - // .author("Bob", Some("bob@email.com")) + page.description("perform basic operations"); + page.author(&"Alice Person", Some(String::from("alice@person.com"))); + page.author("Bob", Some(String::from("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"); diff --git a/src/man/mod.rs b/src/man/mod.rs index 3d1c989..62de02a 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -5,7 +5,7 @@ mod option; use self::author::Author; use self::flag::Flag; use self::option::Opt; -use roff::{self, Roff, Troffable}; +use roff::{Roff, Troffable}; use std::convert::AsRef; /// Man page struct. @@ -58,7 +58,7 @@ impl Man { let man_num = 1; let mut page = Roff::new(&self.name, man_num); - page = description(page, &self.description); + page = description(page, &self.name, &self.description); page = authors(page, &self.authors); page.render() } @@ -71,10 +71,11 @@ impl Man { /// NAME /// mycmd - brief description of the application /// ``` -pub fn description(page: Roff, desc: &Option) -> Roff { +#[inline] +pub fn description(page: Roff, name: &str, desc: &Option) -> Roff { let desc = match desc { - Some(ref desc) => format!("- {:?}", desc), - None => String::from(""), + Some(ref desc) => format!("{} - {}", name, desc), + None => name.to_owned(), }; page.section("NAME", &[desc]) @@ -88,6 +89,7 @@ pub fn description(page: Roff, desc: &Option) -> Roff { /// alice person /// bob human /// ``` +#[inline] pub fn authors(page: Roff, authors: &[Author]) -> Roff { let title = match authors.len() { 0 => return page, @@ -95,14 +97,32 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { _ => "AUTHORS", }; + let last = authors.len() - 1; let mut auth_values = vec![]; - for author in authors.iter() { - auth_values.push(roff::bold(&author.name)); + 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)) + auth_values.push(format!(" <{}>", email)) }; + + if index != last { + auth_values.push(format!("\n")); + } } page.section(title, &auth_values) } + +// 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 +// ``` +#[inline] +fn init_list() -> String { + format!(".P\n.RS 2\n.nf\n") +} From 5e9ac9d88bc500a982305e9681b0fb21db6bb17a Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 00:52:58 +0200 Subject: [PATCH 04/12] slightly better examples --- examples/demo.rs | 2 +- src/man/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 85110c3..b7440cf 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -6,7 +6,7 @@ fn main() { let mut page = Man::new("basic"); page.description("perform basic operations"); page.author(&"Alice Person", Some(String::from("alice@person.com"))); - page.author("Bob", Some(String::from("bob@email.com"))); + page.author("Bob Human", Some(String::from("bob@human.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"); diff --git a/src/man/mod.rs b/src/man/mod.rs index 62de02a..28c198f 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -86,8 +86,8 @@ pub fn description(page: Roff, name: &str, desc: &Option) -> Roff { /// ## Formatting /// ```txt /// AUTHORS -/// alice person -/// bob human +/// Alice Person +/// Bob Human /// ``` #[inline] pub fn authors(page: Roff, authors: &[Author]) -> Roff { From 2a712d9c06daf8ea09790b15fd9299ad09863eef Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 00:55:45 +0200 Subject: [PATCH 05/12] chaining API --- examples/demo.rs | 8 ++++---- src/man/mod.rs | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index b7440cf..21e7efa 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -3,10 +3,10 @@ extern crate man; use man::Man; fn main() { - let mut page = Man::new("basic"); - page.description("perform basic operations"); - page.author(&"Alice Person", Some(String::from("alice@person.com"))); - page.author("Bob Human", Some(String::from("bob@human.com"))); + let mut page = Man::new("basic") + .description("perform basic operations") + .author(&"Alice Person", Some(String::from("alice@person.com"))) + .author("Bob Human", Some(String::from("bob@human.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"); diff --git a/src/man/mod.rs b/src/man/mod.rs index 28c198f..71cb234 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -31,30 +31,38 @@ impl Man { } /// Add a description. - pub fn description(&mut self, desc: &str) { + 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) { + 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 flag. - pub fn flag(&mut self, flag: Flag) { + pub fn flag(mut self, flag: Flag) -> Self { self.flags.push(flag); + self } /// Add an option. - pub fn option(&mut self, option: Opt) { + pub fn option(mut self, option: Opt) -> Self { self.options.push(option); + self } - pub fn render(&mut self) -> String { + pub fn render(self) -> String { let man_num = 1; let mut page = Roff::new(&self.name, man_num); From 653436c46826efaf2c070fe14958ce964da9066b Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 01:29:17 +0200 Subject: [PATCH 06/12] exit status --- examples/demo.rs | 10 +++++++--- src/man/flag.rs | 6 +++++- src/man/mod.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 21e7efa..d5c44d5 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -3,11 +3,15 @@ extern crate man; use man::Man; fn main() { - let mut page = Man::new("basic") + let page = Man::new("basic") .description("perform basic operations") .author(&"Alice Person", Some(String::from("alice@person.com"))) - .author("Bob Human", Some(String::from("bob@human.com"))); - // .flag(Some("-d"), Some("--debug"), Some("Activate debug mode")) + .author("Bob Human", Some(String::from("bob@human.com"))) + .flag( + Some("-d".into()), + Some("--debug".into()), + Some("activate debug mode".into()), + ); // .flag(Some("-v"), Some("--verbose"), Some("Verbose mode")); // .option(Some("-o"), Some("--output"), "output", None, "Output file"); diff --git a/src/man/flag.rs b/src/man/flag.rs index 748dce9..570035d 100644 --- a/src/man/flag.rs +++ b/src/man/flag.rs @@ -1,3 +1,7 @@ /// Command line flag representation. #[derive(Debug)] -pub struct Flag {} +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 index 71cb234..0507b78 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -5,7 +5,7 @@ mod option; use self::author::Author; use self::flag::Flag; use self::option::Opt; -use roff::{Roff, Troffable}; +use roff::{bold, list, Roff, Troffable}; use std::convert::AsRef; /// Man page struct. @@ -51,8 +51,17 @@ impl Man { } /// Add an flag. - pub fn flag(mut self, flag: Flag) -> Self { - self.flags.push(flag); + pub fn flag( + mut self, + short: Option, + long: Option, + description: Option, + ) -> Self { + self.flags.push(Flag { + short, + long, + description, + }); self } @@ -65,8 +74,8 @@ 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 = exit_status(page); page = authors(page, &self.authors); page.render() } @@ -123,6 +132,41 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { page.section(title, &auth_values) } +/// Create a `FLAGS` section. +/// +/// ## Formatting +/// ```txt +/// FLAGS +/// Alice Person +/// Bob Human +/// ``` +pub fn flags(_page: Roff) -> Roff { + unimplemented!(); +} + +/// 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 +/// ``` +pub fn exit_status(page: Roff) -> Roff { + let section = "EXIT STATUS"; + page.section( + section, + &[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. From 51d7dac2a5e51e164ba6e357737dd63cc89b65b6 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 01:48:28 +0200 Subject: [PATCH 07/12] flags --- examples/demo.rs | 11 ++++++++--- src/man/mod.rs | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index d5c44d5..5b7d916 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -8,9 +8,14 @@ fn main() { .author(&"Alice Person", Some(String::from("alice@person.com"))) .author("Bob Human", Some(String::from("bob@human.com"))) .flag( - Some("-d".into()), - Some("--debug".into()), - Some("activate debug mode".into()), + Some("-h".into()), + Some("--help".into()), + Some("Print help information.".into()), + ) + .flag( + Some("-v".into()), + Some("--verbose".into()), + Some("Prints version information.".into()), ); // .flag(Some("-v"), Some("--verbose"), Some("Verbose mode")); // .option(Some("-o"), Some("--output"), "output", None, "Output file"); diff --git a/src/man/mod.rs b/src/man/mod.rs index 0507b78..e682f38 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -75,6 +75,7 @@ impl Man { let man_num = 1; let mut page = Roff::new(&self.name, man_num); page = description(page, &self.name, &self.description); + page = flags(page, &self.flags); page = exit_status(page); page = authors(page, &self.authors); page.render() @@ -140,8 +141,35 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { /// Alice Person /// Bob Human /// ``` -pub fn flags(_page: Roff) -> Roff { - unimplemented!(); +pub 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 `EXIT STATUS` section. @@ -160,9 +188,8 @@ pub fn flags(_page: Roff) -> Roff { /// 2 Optional error /// ``` pub fn exit_status(page: Roff) -> Roff { - let section = "EXIT STATUS"; page.section( - section, + "EXIT STATUS", &[list(&[bold("0")], &["Successful program execution."])], ) } From adbcf3823a0baa7f9e7b02a5fe0638004a129eef Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 01:50:51 +0200 Subject: [PATCH 08/12] brush up example --- examples/demo.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 5b7d916..5f5ee67 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -5,19 +5,18 @@ use man::Man; fn main() { let page = Man::new("basic") .description("perform basic operations") - .author(&"Alice Person", Some(String::from("alice@person.com"))) - .author("Bob Human", Some(String::from("bob@human.com"))) + .author("Alice Person", Some("alice@person.com".into())) + .author("Bob Human", Some("bob@human.com".into())) .flag( Some("-h".into()), Some("--help".into()), - Some("Print help information.".into()), + Some("Prints help information.".into()), ) .flag( Some("-v".into()), Some("--verbose".into()), Some("Prints version information.".into()), ); - // .flag(Some("-v"), Some("--verbose"), Some("Verbose mode")); // .option(Some("-o"), Some("--output"), "output", None, "Output file"); println!("{}", page.render()); From c99518c26842862eac5c9758bd4c6fd56df4d710 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 02:18:07 +0200 Subject: [PATCH 09/12] options --- examples/demo.rs | 36 ++++++++++++++++++------ src/man/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++++++---- src/man/option.rs | 8 +++++- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/examples/demo.rs b/examples/demo.rs index 5f5ee67..f35ccd3 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -3,21 +3,41 @@ extern crate man; use man::Man; fn main() { - let page = Man::new("basic") - .description("perform basic operations") - .author("Alice Person", Some("alice@person.com".into())) - .author("Bob Human", Some("bob@human.com".into())) + let msg = Man::new("auth-service") + .description("authorize & authenticate members") .flag( Some("-h".into()), Some("--help".into()), Some("Prints help information.".into()), ) .flag( - Some("-v".into()), - Some("--verbose".into()), + 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!("{}", page.render()); + println!("{}", msg); } diff --git a/src/man/mod.rs b/src/man/mod.rs index e682f38..f4c2eb3 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -5,7 +5,7 @@ mod option; use self::author::Author; use self::flag::Flag; use self::option::Opt; -use roff::{bold, list, Roff, Troffable}; +use roff::{bold, italic, list, Roff, Troffable}; use std::convert::AsRef; /// Man page struct. @@ -66,8 +66,21 @@ impl Man { } /// Add an option. - pub fn option(mut self, option: Opt) -> Self { - self.options.push(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 } @@ -76,6 +89,7 @@ impl Man { let mut page = Roff::new(&self.name, man_num); page = description(page, &self.name, &self.description); page = flags(page, &self.flags); + page = options(page, &self.options); page = exit_status(page); page = authors(page, &self.authors); page.render() @@ -138,8 +152,6 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { /// ## Formatting /// ```txt /// FLAGS -/// Alice Person -/// Bob Human /// ``` pub fn flags(page: Roff, flags: &[Flag]) -> Roff { if flags.is_empty() { @@ -172,6 +184,55 @@ pub fn flags(page: Roff, flags: &[Flag]) -> Roff { page.section("FLAGS", &arr) } +/// Create a `OPTIONS` section. +/// +/// ## Formatting +/// ```txt +/// OPTIONS +/// ``` +pub 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 `EXIT STATUS` section. /// /// ## Implementation Note diff --git a/src/man/option.rs b/src/man/option.rs index 6770fb6..dee74c4 100644 --- a/src/man/option.rs +++ b/src/man/option.rs @@ -1,3 +1,9 @@ /// Option #[derive(Debug)] -pub struct Opt {} +pub struct Opt { + pub(crate) short: Option, + pub(crate) long: Option, + pub(crate) description: Option, + pub(crate) argument: String, + pub(crate) default: Option, +} From a85cdb60a28d21de8ceda3f1c8b35665ac6a04bf Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 13:37:21 +0200 Subject: [PATCH 10/12] add arguments --- examples/demo.rs | 1 + src/man/mod.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/examples/demo.rs b/examples/demo.rs index f35ccd3..b589f59 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -5,6 +5,7 @@ use man::Man; fn main() { let msg = Man::new("auth-service") .description("authorize & authenticate members") + .argument("path".into()) .flag( Some("-h".into()), Some("--help".into()), diff --git a/src/man/mod.rs b/src/man/mod.rs index f4c2eb3..4c389b6 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -16,6 +16,7 @@ pub struct Man { authors: Vec, flags: Vec, options: Vec, + arguments: Vec, } impl Man { @@ -27,6 +28,7 @@ impl Man { authors: vec![], flags: vec![], options: vec![], + arguments: vec![], } } @@ -84,10 +86,25 @@ impl Man { 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 = exit_status(page); @@ -113,6 +130,35 @@ pub fn description(page: Roff, name: &str, desc: &Option) -> Roff { page.section("NAME", &[desc]) } +/// Create a `SYNOPSIS` section. +pub 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 From 77e75bb820bff51de3ff2c3472282740b7654579 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Jul 2018 14:19:58 +0200 Subject: [PATCH 11/12] environment --- examples/demo.rs | 5 ++++ src/man/environment.rs | 7 +++++ src/man/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/man/environment.rs diff --git a/examples/demo.rs b/examples/demo.rs index b589f59..e3306eb 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -6,6 +6,11 @@ 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()), diff --git a/src/man/environment.rs b/src/man/environment.rs new file mode 100644 index 0000000..75e0679 --- /dev/null +++ b/src/man/environment.rs @@ -0,0 +1,7 @@ +/// Command line environment variable representation. +#[derive(Debug)] +pub struct Env { + pub(crate) name: String, + pub(crate) default: Option, + pub(crate) description: Option, +} diff --git a/src/man/mod.rs b/src/man/mod.rs index 4c389b6..dca608a 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -1,8 +1,10 @@ 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}; @@ -16,6 +18,7 @@ pub struct Man { authors: Vec, flags: Vec, options: Vec, + environment: Vec, arguments: Vec, } @@ -29,6 +32,7 @@ impl Man { flags: vec![], options: vec![], arguments: vec![], + environment: vec![], } } @@ -52,6 +56,21 @@ impl Man { 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, @@ -107,6 +126,7 @@ impl Man { ); 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() @@ -279,6 +299,45 @@ pub fn options(page: Roff, options: &[Opt]) -> Roff { page.section("OPTIONS", &arr) } +/// Create a `ENVIRONMENT` section. +/// +/// ## Formatting +/// ```txt +/// ENVIRONMENT +/// ``` +pub 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 From 1ea4ef274e82e6c277a8fa88aa19e352c2ac86b2 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Tue, 24 Jul 2018 11:36:39 +0200 Subject: [PATCH 12/12] derive clone --- src/man/author.rs | 2 +- src/man/environment.rs | 2 +- src/man/flag.rs | 2 +- src/man/mod.rs | 19 ++++++++----------- src/man/option.rs | 2 +- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/man/author.rs b/src/man/author.rs index 2686b4a..84f3a3e 100644 --- a/src/man/author.rs +++ b/src/man/author.rs @@ -1,5 +1,5 @@ /// An author entry. -#[derive(Debug)] +#[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 index 75e0679..b2d1394 100644 --- a/src/man/environment.rs +++ b/src/man/environment.rs @@ -1,5 +1,5 @@ /// Command line environment variable representation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Env { pub(crate) name: String, pub(crate) default: Option, diff --git a/src/man/flag.rs b/src/man/flag.rs index 570035d..8314976 100644 --- a/src/man/flag.rs +++ b/src/man/flag.rs @@ -1,5 +1,5 @@ /// Command line flag representation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Flag { pub(crate) short: Option, pub(crate) long: Option, diff --git a/src/man/mod.rs b/src/man/mod.rs index dca608a..0739720 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -11,7 +11,7 @@ use roff::{bold, italic, list, Roff, Troffable}; use std::convert::AsRef; /// Man page struct. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Man { name: String, description: Option, @@ -140,8 +140,7 @@ impl Man { /// NAME /// mycmd - brief description of the application /// ``` -#[inline] -pub fn description(page: Roff, name: &str, desc: &Option) -> Roff { +fn description(page: Roff, name: &str, desc: &Option) -> Roff { let desc = match desc { Some(ref desc) => format!("{} - {}", name, desc), None => name.to_owned(), @@ -151,7 +150,7 @@ pub fn description(page: Roff, name: &str, desc: &Option) -> Roff { } /// Create a `SYNOPSIS` section. -pub fn synopsis( +fn synopsis( page: Roff, name: &str, flags: &[Flag], @@ -187,8 +186,7 @@ pub fn synopsis( /// Alice Person /// Bob Human /// ``` -#[inline] -pub fn authors(page: Roff, authors: &[Author]) -> Roff { +fn authors(page: Roff, authors: &[Author]) -> Roff { let title = match authors.len() { 0 => return page, 1 => "AUTHOR", @@ -219,7 +217,7 @@ pub fn authors(page: Roff, authors: &[Author]) -> Roff { /// ```txt /// FLAGS /// ``` -pub fn flags(page: Roff, flags: &[Flag]) -> Roff { +fn flags(page: Roff, flags: &[Flag]) -> Roff { if flags.is_empty() { return page; } @@ -256,7 +254,7 @@ pub fn flags(page: Roff, flags: &[Flag]) -> Roff { /// ```txt /// OPTIONS /// ``` -pub fn options(page: Roff, options: &[Opt]) -> Roff { +fn options(page: Roff, options: &[Opt]) -> Roff { if options.is_empty() { return page; } @@ -305,7 +303,7 @@ pub fn options(page: Roff, options: &[Opt]) -> Roff { /// ```txt /// ENVIRONMENT /// ``` -pub fn environment(page: Roff, environment: &[Env]) -> Roff { +fn environment(page: Roff, environment: &[Env]) -> Roff { if environment.is_empty() { return page; } @@ -353,7 +351,7 @@ pub fn environment(page: Roff, environment: &[Env]) -> Roff { /// /// 2 Optional error /// ``` -pub fn exit_status(page: Roff) -> Roff { +fn exit_status(page: Roff) -> Roff { page.section( "EXIT STATUS", &[list(&[bold("0")], &["Successful program execution."])], @@ -367,7 +365,6 @@ pub fn exit_status(page: Roff) -> Roff { // ```sh // $ less /usr/share/man/man1/npm-install.1 // ``` -#[inline] fn init_list() -> String { format!(".P\n.RS 2\n.nf\n") } diff --git a/src/man/option.rs b/src/man/option.rs index dee74c4..cb477b1 100644 --- a/src/man/option.rs +++ b/src/man/option.rs @@ -1,5 +1,5 @@ /// Option -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Opt { pub(crate) short: Option, pub(crate) long: Option,