From 0b045f5d0de9f6c97607be3276f529a14510e94e Mon Sep 17 00:00:00 2001 From: Sondre Nilsen Date: Fri, 28 Jan 2022 21:55:55 +0100 Subject: [PATCH] feat(man): Initial man generator (#3174) This is an initial implementation with plenty of room to grow, including - Allowing pulling out a subset of the generated man page for greater customization - Subcommand handling - Extra sections - Consolidate argument formatter after #2914 Fixes #552 --- Cargo.toml | 1 + clap_generate/Cargo.toml | 1 - clap_man/CONTRIBUTING.md | 3 + clap_man/Cargo.toml | 45 ++++++ clap_man/LICENSE-APACHE | 201 ++++++++++++++++++++++++++ clap_man/LICENSE-MIT | 21 +++ clap_man/README.md | 20 +++ clap_man/examples/man-builder.rs | 34 +++++ clap_man/examples/man.rs | 40 ++++++ clap_man/src/lib.rs | 135 ++++++++++++++++++ clap_man/src/render.rs | 238 +++++++++++++++++++++++++++++++ clap_man/tests/generate.rs | 29 ++++ src/build/app/mod.rs | 42 ++++++ 13 files changed, 809 insertions(+), 1 deletion(-) create mode 100644 clap_man/CONTRIBUTING.md create mode 100644 clap_man/Cargo.toml create mode 100644 clap_man/LICENSE-APACHE create mode 100644 clap_man/LICENSE-MIT create mode 100644 clap_man/README.md create mode 100644 clap_man/examples/man-builder.rs create mode 100644 clap_man/examples/man.rs create mode 100644 clap_man/src/lib.rs create mode 100644 clap_man/src/render.rs create mode 100644 clap_man/tests/generate.rs diff --git a/Cargo.toml b/Cargo.toml index bc08fa36e0e..917fc6bcc2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "clap_complete_fig", "clap_generate", "clap_generate_fig", + "clap_man", ] [package] diff --git a/clap_generate/Cargo.toml b/clap_generate/Cargo.toml index 28c5534fb52..c6e68989aa1 100644 --- a/clap_generate/Cargo.toml +++ b/clap_generate/Cargo.toml @@ -16,7 +16,6 @@ keywords = [ "cli", "generate", "completion", - "manpage", ] categories = ["command-line-interface"] license = "MIT OR Apache-2.0" diff --git a/clap_man/CONTRIBUTING.md b/clap_man/CONTRIBUTING.md new file mode 100644 index 00000000000..e54a51d0e2b --- /dev/null +++ b/clap_man/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# How to Contribute + +See the [clap-wide CONTRIBUTING.md](../CONTRIBUTING.md). This will contain `clap_man` specific notes. diff --git a/clap_man/Cargo.toml b/clap_man/Cargo.toml new file mode 100644 index 00000000000..01f7a36f070 --- /dev/null +++ b/clap_man/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "clap_man" +version = "0.1.0" +edition = "2018" +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE-*", + "README.md" +] +description = "A manpage generator for clap" +repository = "https://github.com/clap-rs/clap/tree/master/clap_man" +documentation = "https://docs.rs/clap_man" +keywords = [ + "clap", + "cli", + "generate", + "manpage", +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +readme = "README.md" + +[package.metadata.release] +pre-release-replacements = [ + {file="README.md", search="github.com/clap-rs/clap/blob/[^/]+/", replace="github.com/clap-rs/clap/blob/{{tag_name}}/", exactly=4, prerelease = true}, +] + +[lib] +bench = false + +[dependencies] +roff = "0.2.1" +clap = { path = "../", version = "3.0", default-features = false, features = ["std", "env"] } + +[dev-dependencies] +pretty_assertions = "1.0" +clap = { path = "../", version = "3.0", default-features = false, features = ["std"] } + +[features] +default = [] +debug = ["clap/debug"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/clap_man/LICENSE-APACHE b/clap_man/LICENSE-APACHE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/clap_man/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/clap_man/LICENSE-MIT b/clap_man/LICENSE-MIT new file mode 100644 index 00000000000..5acedf04122 --- /dev/null +++ b/clap_man/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016 Kevin B. Knapp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/clap_man/README.md b/clap_man/README.md new file mode 100644 index 00000000000..25e73110e2b --- /dev/null +++ b/clap_man/README.md @@ -0,0 +1,20 @@ + +# clap_man + +> **Manpage generation for `clap`** + +[![Crates.io](https://img.shields.io/crates/v/clap_man?style=flat-square)](https://crates.io/crates/clap_man) +[![Crates.io](https://img.shields.io/crates/d/clap_man?style=flat-square)](https://crates.io/crates/clap_man) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/master/LICENSE-APACHE) +[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/master/LICENSE-MIT) + +Dual-licensed under [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT). + +1. [About](#about) +2. [API Reference](https://docs.rs/clap_man) +3. [Questions & Discussions](https://github.com/clap-rs/clap/discussions) +4. [CONTRIBUTING](https://github.com/clap-rs/clap/blob/v3.0.0-rc.4/clap_generate/CCONTRIBUTING.md) +5. [Sponsors](https://github.com/clap-rs/clap/blob/v3.0.0-rc.4/README.md#sponsors) + +## About + diff --git a/clap_man/examples/man-builder.rs b/clap_man/examples/man-builder.rs new file mode 100644 index 00000000000..e93eeabc53d --- /dev/null +++ b/clap_man/examples/man-builder.rs @@ -0,0 +1,34 @@ +use clap::{arg, App}; +use clap_man::{Man, Meta}; + +// Run this example as `cargo run --example man-builder | man -l -`. + +fn main() -> Result<(), std::io::Error> { + let mut app = App::new("myapp") + .version("1.0") + .author("Kevin K. ") + .about("Does awesome things") + .subcommand_help_heading("Commands") + .arg( + arg!(-c --config "Sets a custom config file") + .long_help("Some more text about how to set a custom config file") + .required(false) + .takes_value(true), + ) + .arg(arg!([output] "Sets an optional output file").index(1)) + .arg(arg!(-d --debug ... "Turn debugging information on")) + .subcommand( + App::new("test") + .about("does testing things") + .arg(arg!(-l --list "Lists test values")), + ) + .after_help( + "\ +For more information about the config file syntax, look up the INI format. +To see the debug information, visit our website on github.com", + ); + + let meta = Meta::from_clap("1", "GNU", &app); + let man = Man::new(&meta, &mut app); + man.render(&mut std::io::stdout()) +} diff --git a/clap_man/examples/man.rs b/clap_man/examples/man.rs new file mode 100644 index 00000000000..3b0f3b631d6 --- /dev/null +++ b/clap_man/examples/man.rs @@ -0,0 +1,40 @@ +use clap::{arg, App}; +use clap_man::generate_manpage; +use std::io; + +// Run this example as `cargo run --example man | man -l -`. + +fn main() -> Result<(), std::io::Error> { + let mut app = App::new("myapp") + .version("1.0") + .author("Kevin K. :Ola Nordmann ") + .about("Does awesome things") + .long_about( + "With a longer description to help clarify some things. + +And a few newlines.", + ) + .after_help("This is an extra section added to the end of the manpage.") + .after_long_help("With even more text added.") + .arg( + arg!(-c --config "Sets a custom config file") + .long_help("Some more text about how to set a custom config file") + .required(false) + .takes_value(true) + .default_value("config.toml") + .env("CONFIG_FILE"), + ) + .arg(arg!([output] "Sets an output file").default_value("result.txt")) + .arg( + arg!(-d --debug ... "Turn debugging information on") + .env("DEBUG_ON") + .hide_env(true), + ) + .subcommand( + App::new("test") + .about("does testing things") + .arg(arg!(-l --list "Lists test values")), + ); + + generate_manpage(&mut app, &mut io::stdout()) +} diff --git a/clap_man/src/lib.rs b/clap_man/src/lib.rs new file mode 100644 index 00000000000..cda156957a4 --- /dev/null +++ b/clap_man/src/lib.rs @@ -0,0 +1,135 @@ +#![doc(html_logo_url = "https://raw.githubusercontent.com/clap-rs/clap/master/assets/clap.png")] +#![doc = include_str!("../README.md")] +#![warn(missing_docs, trivial_casts, unused_allocation, trivial_numeric_casts)] +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +mod render; + +pub use roff; + +use render::subcommand_heading; +use roff::{roman, Roff}; +use std::io::Write; + +/// Generate a manual page and write it out. +pub fn generate_manpage<'a>( + app: &mut clap::App<'a>, + buf: &mut dyn Write, +) -> Result<(), std::io::Error> { + let meta = Meta::from_clap("1", "", app); + let man = Man::new(&meta, app); + man.render(buf) +} + +/// Metadata about a manual page. +pub struct Meta { + title: String, + section: String, + date: String, + source: String, + manual: String, +} + +impl Meta { + /// Create metadata from a clap::App. + pub fn from_clap(section: &str, manual: &str, app: &clap::App) -> Self { + Self { + title: app.get_name().to_string(), + section: section.to_string(), + date: "".to_string(), // FIXME + source: format!( + "{} {}", + app.get_name(), + app.get_version().unwrap_or_default() + ), + manual: manual.to_string(), + } + } + + // Turn metadata into arguments for a .TH macro. + fn to_args(&self) -> Vec<&str> { + vec![ + &self.title, + &self.section, + &self.date, + &self.source, + &self.manual, + ] + } +} + +/// A manual page as constructed from a clap::App. +pub struct Man { + roff: Roff, +} + +impl Man { + /// Create a new manual page. + pub fn new(meta: &Meta, app: &mut clap::App) -> Self { + app._build_all(); + + let mut roff = Roff::default(); + roff.control("TH", meta.to_args()); + roff.control("SH", ["NAME"]); + render::about(&mut roff, app); + roff.control("SH", ["SYNOPSIS"]); + render::synopsis(&mut roff, app); + roff.control("SH", ["DESCRIPTION"]); + render::description(&mut roff, app); + + if app_has_arguments(app) { + roff.control("SH", ["OPTIONS"]); + render::options(&mut roff, app); + } + + if app_has_subcommands(app) { + let heading = subcommand_heading(app); + roff.control("SH", [heading.as_str()]); + render::subcommands(&mut roff, app, &meta.section); + } + + if app.get_after_long_help().is_some() || app.get_after_help().is_some() { + roff.control("SH", ["EXTRA"]); + render::after_help(&mut roff, app); + } + + if app_has_version(app) { + let version = roman(&render::version(app)); + roff.control("SH", ["VERSION"]); + roff.text([version]); + } + + if app.get_author().is_some() { + let author = roman(app.get_author().unwrap_or_default()); + roff.control("SH", ["AUTHORS"]); + roff.text([author]); + } + + Self { roff } + } + + /// Render a manual page into writer. + pub fn render(&self, w: &mut dyn Write) -> Result<(), std::io::Error> { + self.roff.to_writer(w) + } +} + +// Does the application have a version? +fn app_has_version(app: &clap::App) -> bool { + app.get_version() + .or_else(|| app.get_long_version()) + .is_some() +} + +// Does the application have any command line arguments? +fn app_has_arguments(app: &clap::App) -> bool { + app.get_arguments() + .any(|i| !i.is_set(clap::ArgSettings::Hidden)) +} + +// Does the application have any subcommands? +fn app_has_subcommands(app: &clap::App) -> bool { + app.get_subcommands() + .any(|i| !i.is_set(clap::AppSettings::Hidden)) +} diff --git a/clap_man/src/render.rs b/clap_man/src/render.rs new file mode 100644 index 00000000000..9ffb792d148 --- /dev/null +++ b/clap_man/src/render.rs @@ -0,0 +1,238 @@ +use clap::{AppSettings, ArgSettings}; +use roff::{bold, italic, roman, Inline, Roff}; + +pub(crate) fn subcommand_heading(app: &clap::App) -> String { + match app.get_subommand_help_heading() { + Some(title) => title.to_string(), + None => "SUBCOMMANDS".to_string(), + } +} + +pub(crate) fn about(roff: &mut Roff, app: &clap::App) { + let s = match app.get_about().or_else(|| app.get_long_about()) { + Some(about) => format!("{} - {}", app.get_name(), about), + None => app.get_name().to_string(), + }; + roff.text([roman(&s)]); +} + +pub(crate) fn description(roff: &mut Roff, app: &clap::App) { + if let Some(about) = app.get_long_about().or_else(|| app.get_about()) { + for line in about.lines() { + if line.trim().is_empty() { + roff.control("PP", []); + } else { + roff.text([roman(line)]); + } + } + } +} + +pub(crate) fn synopsis(roff: &mut Roff, app: &clap::App) { + let mut line = vec![bold(app.get_name()), roman(" ")]; + + for opt in app.get_arguments() { + let (lhs, rhs) = option_markers(opt); + match (opt.get_short(), opt.get_long()) { + (Some(short), Some(long)) => { + line.push(roman(lhs)); + line.push(bold(&format!("-{}", short))); + line.push(roman("|")); + line.push(bold(&format!("--{}", long))); + line.push(roman(rhs)); + line.push(roman(" ")); + } + (Some(short), None) => { + line.push(roman(lhs)); + line.push(bold(&format!("-{} ", short))); + line.push(roman(rhs)); + line.push(roman(" ")); + } + (None, Some(long)) => { + line.push(roman(lhs)); + line.push(bold(&format!("--{}", long))); + line.push(roman(rhs)); + line.push(roman(" ")); + } + (None, None) => (), + }; + } + + for arg in app.get_positionals() { + let (lhs, rhs) = option_markers(arg); + line.push(roman(lhs)); + line.push(italic(arg.get_name())); + line.push(roman(rhs)); + line.push(roman(" ")); + } + + if app.has_subcommands() { + let (lhs, rhs) = subcommand_markers(app); + line.push(roman(lhs)); + line.push(italic( + &app.get_subcommand_value_name() + .unwrap_or(&subcommand_heading(app)) + .to_lowercase(), + )); + line.push(roman(rhs)); + } + + roff.text(line); +} + +pub(crate) fn options(roff: &mut Roff, app: &clap::App) { + let items: Vec<_> = app + .get_arguments() + .filter(|i| !i.is_set(ArgSettings::Hidden)) + .collect(); + + for opt in items.iter().filter(|a| !a.is_positional()) { + let mut body = vec![]; + + let mut header = match (opt.get_short(), opt.get_long()) { + (Some(short), Some(long)) => { + vec![short_option(short), roman(", "), long_option(long)] + } + (Some(short), None) => vec![short_option(short)], + (None, Some(long)) => vec![long_option(long)], + (None, None) => vec![], + }; + + if let Some(value) = &opt.get_value_names() { + header.push(roman("=")); + header.push(italic(&value.join(" "))); + } + + if let Some(defs) = option_default_values(opt) { + header.push(roman(" ")); + header.push(roman(&defs)); + } + + if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) { + body.push(roman(help)); + } + + if let Some(mut env) = option_environment(opt) { + body.append(&mut env); + } + + roff.control("TP", []); + roff.text(header); + roff.text(body); + } + + for pos in items.iter().filter(|a| a.is_positional()) { + let (lhs, rhs) = option_markers(pos); + let name = format!("{}{}{}", lhs, pos.get_name(), rhs); + + let mut header = vec![bold(&name)]; + + let mut body = vec![]; + + if let Some(defs) = option_default_values(pos) { + header.push(roman(&format!(" {}", defs))); + } + + if let Some(help) = pos.get_long_help().or_else(|| pos.get_help()) { + body.push(roman(&help.to_string())); + } + + if let Some(mut env) = option_environment(pos) { + body.append(&mut env); + } + + roff.control("TP", []); + roff.text(body); + } +} + +pub(crate) fn subcommands(roff: &mut Roff, app: &clap::App, section: &str) { + for sub in app + .get_subcommands() + .filter(|s| !s.is_set(AppSettings::Hidden)) + { + roff.control("TP", []); + + let name = format!("{}-{}({})", app.get_name(), sub.get_name(), section); + roff.text([roman(&name)]); + + if let Some(about) = sub.get_about().or_else(|| sub.get_long_about()) { + for line in about.lines() { + roff.text([roman(line)]); + } + } + } +} + +pub(crate) fn version(app: &clap::App) -> String { + format!( + "v{}", + app.get_long_version() + .or_else(|| app.get_version()) + .unwrap() + ) +} + +pub(crate) fn after_help(roff: &mut Roff, app: &clap::App) { + if let Some(about) = app.get_after_long_help().or_else(|| app.get_after_help()) { + for line in about.lines() { + roff.text([roman(line)]); + } + } +} + +fn subcommand_markers(cmd: &clap::App) -> (&'static str, &'static str) { + markers( + cmd.is_set(AppSettings::SubcommandRequired) + || cmd.is_set(AppSettings::SubcommandRequiredElseHelp), + ) +} + +fn option_markers(opt: &clap::Arg) -> (&'static str, &'static str) { + markers(opt.is_set(ArgSettings::Required)) +} + +fn markers(required: bool) -> (&'static str, &'static str) { + if required { + ("<", ">") + } else { + ("[", "]") + } +} + +fn short_option(opt: char) -> Inline { + bold(&format!("-{}", opt)) +} + +fn long_option(opt: &str) -> Inline { + bold(&format!("--{}", opt)) +} + +fn option_environment(opt: &clap::Arg) -> Option> { + if opt.is_set(ArgSettings::HideEnv) { + return None; + } else if let Some(env) = opt.get_env() { + return Some(vec![ + roman("May also be specified with the "), + bold(env.to_string_lossy().to_owned()), + roman(" environment variable. "), + ]); + } + + None +} + +fn option_default_values(opt: &clap::Arg) -> Option { + if !opt.get_default_values().is_empty() { + let values = opt + .get_default_values() + .iter() + .map(|s| s.to_string_lossy()) + .collect::>() + .join(","); + + return Some(format!("[default: {}]", values)); + } + + None +} diff --git a/clap_man/tests/generate.rs b/clap_man/tests/generate.rs new file mode 100644 index 00000000000..422f7d3f323 --- /dev/null +++ b/clap_man/tests/generate.rs @@ -0,0 +1,29 @@ +use clap::{arg, App}; +use clap_man::generate_manpage; +use std::io; + +#[test] +fn render_manpage() { + let mut app = App::new("myapp") + .version("1.0") + .author("Kevin K. ") + .about("Does awesome things") + .long_about("With a longer description to help clarify some things.") + .after_help("This is an extra section added to the end of the manpage.") + .after_long_help("With even more text added.") + .arg( + arg!(-c --config "Sets a custom config file") + .long_help("Some more text about how to set a custom config file") + .required(false) + .takes_value(true), + ) + .arg(arg!([output] "Sets an optional output file").index(1)) + .arg(arg!(-d --debug ... "Turn debugging information on")) + .subcommand( + App::new("test") + .about("does testing things") + .arg(arg!(-l --list "Lists test values")), + ); + + generate_manpage(&mut app, &mut io::sink()).unwrap(); +} diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 366e18307a7..0047c1647af 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -2106,6 +2106,24 @@ impl<'help> App<'help> { &self.name } + /// Get the version of the app. + #[inline] + pub fn get_version(&self) -> Option<&'help str> { + self.version + } + + /// Get the long version of the app. + #[inline] + pub fn get_long_version(&self) -> Option<&'help str> { + self.long_version + } + + /// Get the authors of the app. + #[inline] + pub fn get_author(&self) -> Option<&'help str> { + self.author + } + /// Get the short flag of the subcommand. #[inline] pub fn get_short_flag(&self) -> Option { @@ -2235,6 +2253,30 @@ impl<'help> App<'help> { !self.subcommands.is_empty() } + /// Returns the help heading for listing subcommands. + #[inline] + pub fn get_subommand_help_heading(&self) -> Option<&str> { + self.subcommand_heading + } + + /// Returns the subcommand value name. + #[inline] + pub fn get_subcommand_value_name(&self) -> Option<&str> { + self.subcommand_value_name + } + + /// Returns the help heading for listing subcommands. + #[inline] + pub fn get_after_help(&self) -> Option<&str> { + self.after_help + } + + /// Returns the help heading for listing subcommands. + #[inline] + pub fn get_after_long_help(&self) -> Option<&str> { + self.after_long_help + } + /// Find subcommand such that its name or one of aliases equals `name`. /// /// This does not recurse through subcommands of subcommands.