|
|
|
@@ -0,0 +1,177 @@
|
|
|
|
|
//! Generate manpages for the goose CLI.
|
|
|
|
|
//!
|
|
|
|
|
//! This binary generates ROFF-formatted manpages from the clap CLI definitions.
|
|
|
|
|
//! Manpages are an essential part of the Linux/Unix ecosystem, providing users with
|
|
|
|
|
//! offline documentation accessible via the `man` command (e.g., `man goose`).
|
|
|
|
|
//!
|
|
|
|
|
//! When goose is packaged for Linux distributions (deb, rpm, etc.), the generated
|
|
|
|
|
//! manpages should be installed to `/usr/share/man/man1/` so users can access help
|
|
|
|
|
//! without an internet connection, following Unix conventions that have existed
|
|
|
|
|
//! since the 1970s.
|
|
|
|
|
//!
|
|
|
|
|
//! Usage:
|
|
|
|
|
//! cargo run -p goose-cli --bin generate_manpages
|
|
|
|
|
//! # or
|
|
|
|
|
//! just generate-manpages
|
|
|
|
|
//!
|
|
|
|
|
//! Output: target/man/goose.1, target/man/goose-session.1, etc.
|
|
|
|
|
|
|
|
|
|
use clap::CommandFactory;
|
|
|
|
|
use clap_mangen::Man;
|
|
|
|
|
use goose_cli::Cli;
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::io::Result;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
|
// Manpages are a Unix/Linux convention - skip generation on Windows
|
|
|
|
|
if cfg!(target_os = "windows") {
|
|
|
|
|
eprintln!("Skipping manpage generation on Windows (manpages are a Unix/Linux convention)");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
|
|
|
let output_dir = PathBuf::from(package_dir)
|
|
|
|
|
.join("..")
|
|
|
|
|
.join("..")
|
|
|
|
|
.join("target")
|
|
|
|
|
.join("man");
|
|
|
|
|
|
|
|
|
|
fs::create_dir_all(&output_dir)?;
|
|
|
|
|
|
|
|
|
|
let cmd = Cli::command();
|
|
|
|
|
|
|
|
|
|
// First pass: collect all command names for SEE ALSO sections
|
|
|
|
|
let mut all_commands: Vec<String> = Vec::new();
|
|
|
|
|
collect_command_names(&cmd, &mut all_commands, None);
|
|
|
|
|
|
|
|
|
|
// Second pass: generate manpages with SEE ALSO sections
|
|
|
|
|
generate_manpages(&cmd, &output_dir, None, &all_commands)?;
|
|
|
|
|
|
|
|
|
|
let canonical_path = output_dir.canonicalize()?;
|
|
|
|
|
eprintln!(
|
|
|
|
|
"Successfully generated manpages at {}",
|
|
|
|
|
canonical_path.display()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_command_names(cmd: &clap::Command, names: &mut Vec<String>, parent_name: Option<&str>) {
|
|
|
|
|
let name = match parent_name {
|
|
|
|
|
Some(parent) => format!("{}-{}", parent, cmd.get_name()),
|
|
|
|
|
None => cmd.get_name().to_string(),
|
|
|
|
|
};
|
|
|
|
|
names.push(name.clone());
|
|
|
|
|
|
|
|
|
|
for subcmd in cmd.get_subcommands() {
|
|
|
|
|
if subcmd.get_name() == "help" || subcmd.is_hide_set() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
collect_command_names(subcmd, names, Some(&name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_manpages(
|
|
|
|
|
cmd: &clap::Command,
|
|
|
|
|
dir: &PathBuf,
|
|
|
|
|
parent_name: Option<&str>,
|
|
|
|
|
all_commands: &[String],
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let name = match parent_name {
|
|
|
|
|
Some(parent) => format!("{}-{}", parent, cmd.get_name()),
|
|
|
|
|
None => cmd.get_name().to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Generate the base manpage
|
|
|
|
|
let man = Man::new(cmd.clone());
|
|
|
|
|
let mut buffer = Vec::new();
|
|
|
|
|
man.render(&mut buffer)?;
|
|
|
|
|
|
|
|
|
|
// Add SEE ALSO section
|
|
|
|
|
let see_also = generate_see_also(&name, parent_name, cmd, all_commands);
|
|
|
|
|
buffer.extend_from_slice(see_also.as_bytes());
|
|
|
|
|
|
|
|
|
|
let manpage_path = dir.join(format!("{}.1", name));
|
|
|
|
|
fs::write(&manpage_path, buffer)?;
|
|
|
|
|
eprintln!(" Generated: {}.1", name);
|
|
|
|
|
|
|
|
|
|
for subcmd in cmd.get_subcommands() {
|
|
|
|
|
if subcmd.get_name() == "help" || subcmd.is_hide_set() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
generate_manpages(subcmd, dir, Some(&name), all_commands)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_see_also(
|
|
|
|
|
current_name: &str,
|
|
|
|
|
parent_name: Option<&str>,
|
|
|
|
|
cmd: &clap::Command,
|
|
|
|
|
all_commands: &[String],
|
|
|
|
|
) -> String {
|
|
|
|
|
let mut references: Vec<String> = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Always reference the main goose command if we're not it
|
|
|
|
|
if current_name != "goose" {
|
|
|
|
|
references.push("goose".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reference parent command if exists and not already added
|
|
|
|
|
if let Some(parent) = parent_name {
|
|
|
|
|
if parent != "goose" && !references.contains(&parent.to_string()) {
|
|
|
|
|
references.push(parent.to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For the main command, list immediate subcommands
|
|
|
|
|
// For subcommands, list sibling commands
|
|
|
|
|
if current_name == "goose" {
|
|
|
|
|
// Add all immediate subcommands (skip hidden ones)
|
|
|
|
|
for subcmd in cmd.get_subcommands() {
|
|
|
|
|
let subcmd_name = subcmd.get_name();
|
|
|
|
|
if subcmd_name != "help" && !subcmd.is_hide_set() {
|
|
|
|
|
let full_name = format!("goose-{}", subcmd_name);
|
|
|
|
|
if !references.contains(&full_name) {
|
|
|
|
|
references.push(full_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(parent) = parent_name {
|
|
|
|
|
// Add sibling commands (other commands with same parent)
|
|
|
|
|
let prefix = format!("{}-", parent);
|
|
|
|
|
for cmd_name in all_commands {
|
|
|
|
|
if cmd_name.starts_with(&prefix) && cmd_name != current_name {
|
|
|
|
|
// Only add immediate siblings, not nested subcommands
|
|
|
|
|
let suffix = &cmd_name.strip_prefix(&prefix).unwrap_or(cmd_name);
|
|
|
|
|
if !suffix.contains('-') && !references.contains(cmd_name) {
|
|
|
|
|
references.push(cmd_name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort references for consistent output
|
|
|
|
|
references.sort();
|
|
|
|
|
|
|
|
|
|
if references.is_empty() {
|
|
|
|
|
return String::new();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format as ROFF
|
|
|
|
|
let mut roff = String::from("\n.SH \"SEE ALSO\"\n");
|
|
|
|
|
let formatted_refs: Vec<String> = references
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|r| {
|
|
|
|
|
let escaped = r.replace('-', "\\-");
|
|
|
|
|
format!(".BR {} (1)", escaped)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
roff.push_str(&formatted_refs.join(",\n"));
|
|
|
|
|
roff.push('\n');
|
|
|
|
|
|
|
|
|
|
roff
|
|
|
|
|
}
|