diff --git a/Cargo.lock b/Cargo.lock index 765d253..116e01a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "clap_mangen" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "color-print" version = "0.3.7" @@ -1337,6 +1347,7 @@ dependencies = [ "clap", "clap-verbosity-flag", "clap_complete", + "clap_mangen", "color-print", "const_format", "derive_more", @@ -1769,6 +1780,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rsa" version = "0.9.9" diff --git a/Cargo.toml b/Cargo.toml index 99b7504..32fd450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ bincode = "2.0.1" clap = { version = "4.5.53", features = ["cargo", "derive"] } clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] } clap_complete = { version = "4.5.62", features = ["unstable-dynamic"] } +clap_mangen = "0.2.31" color-print = "0.3.7" const_format = "0.2.35" derive_more = { version = "2.1.1", features = ["display", "error"] } diff --git a/src/bin/muscl.rs b/src/bin/muscl.rs index 897e4bc..04ef326 100644 --- a/src/bin/muscl.rs +++ b/src/bin/muscl.rs @@ -305,6 +305,10 @@ fn main() -> anyhow::Result<()> { return Ok(()); } + if handle_manpage_command()?.is_some() { + return Ok(()); + } + #[cfg(feature = "mysql-admutils-compatibility")] if handle_mysql_admutils_command()?.is_some() { return Ok(()); @@ -361,6 +365,48 @@ fn handle_dynamic_completion() -> anyhow::Result> { } } +/// **WARNING:** This function may be run with elevated privileges. +fn handle_manpage_command() -> anyhow::Result> { + let argv1: Option = std::env::args().nth(1); + + match argv1.as_deref() { + Some("generate-manpages") => { + #[cfg(feature = "suid-sgid-mode")] + if executing_in_suid_sgid_mode()? { + use muscl_lib::core::bootstrap::drop_privs; + drop_privs()? + } + + let output_dir = std::env::args().nth(2).ok_or(anyhow::anyhow!( + "Output directory argument missing for manpage generation" + ))?; + + let output_dir = PathBuf::from(&output_dir); + if !output_dir.is_dir() { + anyhow::bail!( + "Output directory `{:?}` does not exist or is not a directory", + output_dir, + ); + } + + let mut roff = clap_mangen::roff::Roff::new(); + let man = clap_mangen::Man::new(Args::command()); + man.render_title(&mut std::io::stdout())?; + man.render_name_section(&mut std::io::stdout())?; + man.render_synopsis_section(&mut std::io::stdout())?; + man.render_subcommands_section(&mut std::io::stdout())?; + man.render_options_section(&mut std::io::stdout())?; + + roff.control("SH", ["VERSION"]); + roff.text([clap_mangen::roff::roman(AFTER_LONG_HELP)]); + roff.to_writer(&mut std::io::stdout())?; + + Ok(Some(())) + } + _ => Ok(None), + } +} + /// **WARNING:** This function may be run with elevated privileges. fn handle_mysql_admutils_command() -> anyhow::Result> { let argv0 = std::env::args().next().and_then(|s| {