From 814af0e94d44311345da37d1699277e09799340b Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 13 Jul 2024 15:13:38 +0300 Subject: [PATCH] meli/args: add --gzipped flag to man subcommand Add --gzipped flag to print a manpage without decompressing it, for piping the output to a file. Because why not. Signed-off-by: Manos Pitsidianakis --- meli/src/args.rs | 31 ++++++++++++---- meli/src/manpages.rs | 38 ++++++++++++-------- meli/tests/test_cli_subcommands.rs | 57 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 22 deletions(-) diff --git a/meli/src/args.rs b/meli/src/args.rs index 46d3e321..a183268b 100644 --- a/meli/src/args.rs +++ b/meli/src/args.rs @@ -91,17 +91,18 @@ pub enum SubCommand { #[derive(Debug, StructOpt)] pub struct ManOpt { + /// If set, output text in stdout instead of spawning `$PAGER`. + #[cfg(feature = "cli-docs")] + #[cfg_attr(feature = "cli-docs", structopt(long = "no-raw", alias = "no-raw"))] + pub no_raw: bool, + /// If set, output compressed gzip manpage in binary form in stdout. + #[cfg(feature = "cli-docs")] + #[cfg_attr(feature = "cli-docs", structopt(long = "gzipped"))] + pub gzipped: bool, #[cfg(feature = "cli-docs")] #[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))] /// Name of manual page. pub page: manpages::ManPages, - /// If true, output text in stdout instead of spawning `$PAGER`. - #[cfg(feature = "cli-docs")] - #[cfg_attr( - feature = "cli-docs", - structopt(long = "no-raw", alias = "no-raw", value_name = "bool") - )] - pub no_raw: Option>, } #[derive(Debug, StructOpt)] @@ -161,6 +162,22 @@ impl Opt { SubCommand::Man(ManOpt { page, no_raw, + gzipped: true, + }) => { + use std::io::Write; + + ret_err!(std::io::stdout().write_all(if no_raw { + page.text_gz() + } else { + page.mdoc_gz() + })); + Ok(()) + } + #[cfg(feature = "cli-docs")] + SubCommand::Man(ManOpt { + page, + no_raw, + gzipped: false, }) => { subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw)) } diff --git a/meli/src/manpages.rs b/meli/src/manpages.rs index eed066a7..635c14a6 100644 --- a/meli/src/manpages.rs +++ b/meli/src/manpages.rs @@ -84,6 +84,19 @@ impl std::fmt::Display for ManPages { } impl ManPages { + const MANPAGES: [&'static [u8]; 4] = [ + include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")), + ]; + const MANPAGES_MDOC: [&'static [u8]; 4] = [ + include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")), + ]; + pub fn install(destination: Option) -> Result { fn path_valid(p: &Path, tries: &mut Vec) -> bool { tries.push(p.into()); @@ -145,24 +158,19 @@ impl ManPages { Ok(path) } - pub fn read(self, source: bool) -> Result { - const MANPAGES: [&[u8]; 4] = [ - include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")), - ]; - const MANPAGES_MDOC: [&[u8]; 4] = [ - include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")), - include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")), - ]; + pub fn mdoc_gz(self) -> &'static [u8] { + Self::MANPAGES_MDOC[self as usize] + } + pub fn text_gz(self) -> &'static [u8] { + Self::MANPAGES[self as usize] + } + + pub fn read(self, source: bool) -> Result { let mut gz = GzDecoder::new(if source { - MANPAGES_MDOC[self as usize] + self.mdoc_gz() } else { - MANPAGES[self as usize] + self.text_gz() }); let mut v = String::with_capacity( str::parse::(unsafe { diff --git a/meli/tests/test_cli_subcommands.rs b/meli/tests/test_cli_subcommands.rs index f9d576ca..1357c236 100644 --- a/meli/tests/test_cli_subcommands.rs +++ b/meli/tests/test_cli_subcommands.rs @@ -114,11 +114,68 @@ fn test_cli_subcommands() { } } + fn test_subcommand_man() { + for (man, title) in [ + ("meli.1", "MELI(1)"), + ("meli.conf.5", "MELI.CONF(5)"), + ("meli-themes.5", "MELI-THEMES(5)"), + ("meli.7", "MELI(7)"), + ] { + for gzipped in [true, false] { + for no_raw in [true, false] { + let mut cmd = Command::cargo_bin("meli").unwrap(); + let args = match (no_raw, gzipped) { + (true, true) => &["man", "--no-raw", "--gzipped", man][..], + (true, false) => &["man", "--no-raw", man], + (false, false) => &["man", man], + (false, true) => &["man", "--gzipped", man], + }; + let output = cmd.args(args).output().unwrap().assert(); + output.code(0).stdout(predicate::function(|x: &[u8]| { + use std::io::Read; + + use flate2::bufread::GzDecoder; + + let mut gz = GzDecoder::new(x); + let content = if gzipped { + let size = gz.header().unwrap().comment().unwrap(); + + let mut v = String::with_capacity( + str::parse::( + std::str::from_utf8(size) + .expect("was not compressed with size comment header"), + ) + .expect("was not compressed with size comment header"), + ); + gz.read_to_string(&mut v) + .expect("expected gzipped output but could not decode it."); + v + } else { + assert_eq!(gz.header(), None); + let mut v = String::with_capacity(0); + gz.read_to_string(&mut v).unwrap_err(); + String::from_utf8(x.to_vec()).expect("invalid utf-8 content") + }; + if !no_raw && gzipped { + assert!(content.contains(man)); + } else { + assert!(content.contains('\u{8}')); + assert!(content.contains(title)); + } + + true + })); + } + } + } + } + version(); help(); test_subcommand_succeeds("help"); test_subcommand_succeeds("compiled-with"); test_subcommand_succeeds("man"); + test_subcommand_man(); let tmp_dir = TempDir::new().unwrap();