diff --git a/src/gen/bash.rs b/src/gen/bash.rs index b27a7ff..0a25abc 100644 --- a/src/gen/bash.rs +++ b/src/gen/bash.rs @@ -1,9 +1,7 @@ -use std::{fs, path::Path}; - use crate::gen::{util::Output, CommandInfo}; /// Generate a completion file for Bash -pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> std::io::Result<()> { +pub fn generate(cmd: &CommandInfo) -> (String, String) { let comp_name = format!("_comp_cmd_{}", cmd.name); let mut out = Output::new(String::from("\t")); @@ -20,8 +18,7 @@ pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> std::io::Result<()> { out.writeln(format!("complete -F _comp_cmd_{} {}", cmd.name, cmd.name)); - fs::write(out_dir.join(format!("_{}.bash", cmd.name)), out.text())?; - Ok(()) + (format!("_{}.bash", cmd.name), out.text()) } fn generate_cmd(cmd: &CommandInfo, pos: usize, out: &mut Output) { diff --git a/src/gen/mod.rs b/src/gen/mod.rs index e646c99..faa3c72 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -27,32 +27,43 @@ pub enum OutputFormat { Yaml, } +/// Generate completion for the given shell and write to a file +/// /// # Errors /// /// Fails if it can't write to a file, or if serde can't serialize the command /// info (the second case should never happen). -pub fn generate( +pub fn generate_to_file( cmd: &CommandInfo, format: OutputFormat, out_dir: impl AsRef, -) -> anyhow::Result<()> { +) -> std::io::Result<()> { let out_dir = out_dir.as_ref(); + let (file_name, text) = generate(cmd, format); + fs::write(out_dir.join(file_name), text) +} + +/// Generate completion for the given shell as a string +pub fn generate_to_str(cmd: &CommandInfo, format: OutputFormat) -> String { + let (_, text) = generate(cmd, format); + text +} + +fn generate(cmd: &CommandInfo, format: OutputFormat) -> (String, String) { match format { - OutputFormat::Bash => bash::generate(cmd, out_dir)?, - OutputFormat::Zsh => zsh::generate(cmd, out_dir)?, - OutputFormat::Nu => nu::generate(cmd, out_dir)?, - OutputFormat::Kdl => fs::write( - out_dir.join(format!("{}.kdl", cmd.name)), - to_kdl_node(cmd).to_string(), - )?, - OutputFormat::Json => fs::write( - out_dir.join(format!("{}.json", cmd.name)), - serde_json::to_string(cmd)?, - )?, - OutputFormat::Yaml => fs::write( - out_dir.join(format!("{}.yaml", cmd.name)), - serde_yaml::to_string(cmd)?, - )?, - }; - Ok(()) + OutputFormat::Bash => bash::generate(cmd), + OutputFormat::Zsh => zsh::generate(cmd), + OutputFormat::Nu => nu::generate(cmd), + OutputFormat::Kdl => { + (format!("{}.kdl", cmd.name), to_kdl_node(cmd).to_string()) + } + OutputFormat::Json => ( + format!("{}.json", cmd.name), + serde_json::to_string(cmd).unwrap(), + ), + OutputFormat::Yaml => ( + format!("{}.yaml", cmd.name), + serde_yaml::to_string(cmd).unwrap(), + ), + } } diff --git a/src/gen/nu.rs b/src/gen/nu.rs index cd0169d..9bac46c 100644 --- a/src/gen/nu.rs +++ b/src/gen/nu.rs @@ -1,16 +1,10 @@ -use std::{fs, path::Path}; - use crate::gen::{util::Output, CommandInfo}; /// Generate completions for Nushell -pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> std::io::Result<()> { +pub fn generate(cmd: &CommandInfo) -> (String, String) { let mut res = Output::new(String::from(" ")); generate_cmd(&cmd.name, cmd, &mut res, true); - fs::write( - out_dir.join(format!("{}-completions.nu", cmd.name)), - res.text(), - )?; - Ok(()) + (format!("{}-completions.nu", cmd.name), res.text()) } fn generate_cmd( diff --git a/src/gen/zsh.rs b/src/gen/zsh.rs index b4e6f73..43b7889 100644 --- a/src/gen/zsh.rs +++ b/src/gen/zsh.rs @@ -1,5 +1,3 @@ -use std::{fs, path::Path}; - use crate::gen::{ util::{self, Output}, CommandInfo, @@ -36,7 +34,7 @@ use crate::gen::{ /// '-b[Make new branch]' /// } /// ``` -pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> std::io::Result<()> { +pub fn generate(cmd: &CommandInfo) -> (String, String) { // TODO make option to not overwrite file let comp_name = format!("_{}", cmd.name); let mut res = Output::new(String::from("\t")); @@ -44,8 +42,7 @@ pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> std::io::Result<()> { generate_fn(cmd, &mut res, &comp_name); res.writeln(""); res.writeln(format!(r#"{comp_name} "$@""#)); - fs::write(out_dir.join(format!("{comp_name}.zsh")), res.text())?; - Ok(()) + (format!("{comp_name}.zsh"), res.text()) } /// Generate a completion function for a command/subcommand diff --git a/src/main.rs b/src/main.rs index 1b15759..21611a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,69 @@ use std::{path::PathBuf, process::Command}; use anyhow::{anyhow, Result}; -use clap::Parser; +use clap::{Parser, Subcommand}; use log::{debug, error, info, warn}; use man_completions::{ gen::{self, OutputFormat}, + parse_deser, parse_man::{detect_subcommands, get_cmd_name, parse_from}, }; use regex::Regex; -/// Generate completions from manpages +/// Generate completions from either manpages or KDL/JSON/YAML files #[derive(Debug, Parser)] #[command(version, about, long_about)] struct Cli { - /// Directory to output completions to - #[arg(short, long, value_name = "path")] - out: PathBuf, - - /// Shell(s) to generate completions for - #[arg(short, long, value_name = "shell")] - shell: OutputFormat, - - /// Directories to search for man pages in, e.g. - /// `--dirs=/usr/share/man/man1,/usr/share/man/man6` - #[arg(short, long, value_delimiter = ',', value_name = "path,...")] - dirs: Option>, - - /// Commands to generate completions for. If omitted, generates completions - /// for all found commands. To match the whole name, use "^...$" - #[arg(short, long, value_name = "regex")] - cmds: Option, - - /// Commands to exclude (regex). To match the whole name, use "^...$" - #[arg(short = 'C', long, value_name = "regex")] - exclude_cmds: Option, - - /// Commands that should not be treated as subcommands, to help deal - /// with false positives when detecting subcommands. - #[arg(long, value_name = "command_name,...", value_delimiter = ',')] - not_subcmds: Vec, - - /// Explicitly list which man pages are for which subcommands. e.g. - /// `git-commit=git commit,foobar=foo bar` - #[arg(long, value_name = "man-page=sub cmd,...", value_parser=subcmd_map_parser, value_delimiter = ',')] - subcmds: Vec<(String, Vec)>, + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + /// Generate completions from manpages + Man { + /// Shell(s) to generate completions for + shell: OutputFormat, + + /// Directory to output completions to + out: PathBuf, + + /// Directories to search for man pages in, e.g. + /// `--dirs=/usr/share/man/man1,/usr/share/man/man6` + #[arg(short, long, value_delimiter = ',', value_name = "path,...")] + dirs: Option>, + + /// Commands to generate completions for. If omitted, generates completions + /// for all found commands. To match the whole name, use "^...$" + #[arg(short, long, value_name = "regex")] + cmds: Option, + + /// Commands to exclude (regex). To match the whole name, use "^...$" + #[arg(short = 'C', long, value_name = "regex")] + exclude_cmds: Option, + + /// Commands that should not be treated as subcommands, to help deal + /// with false positives when detecting subcommands. + #[arg(long, value_name = "command_name,...", value_delimiter = ',')] + not_subcmds: Vec, + + /// Explicitly list which man pages are for which subcommands. e.g. + /// `git-commit=git commit,foobar=foo bar` + #[arg(long, value_name = "man-page=sub cmd,...", value_parser=subcmd_map_parser, value_delimiter = ',')] + subcmds: Vec<(String, Vec)>, + }, + /// Generate completions from a file + For { + /// Shell(s) to generate completions for + #[arg(short, long, value_name = "shell")] + shell: OutputFormat, + + /// File to generate completions from + conf: PathBuf, + + /// File to generate completions to. Outputted to stdout if not given. + out: Option, + }, } fn subcmd_map_parser( @@ -63,30 +83,49 @@ fn main() -> Result<()> { let args = Cli::parse(); - let search_dirs = match args.dirs { - Some(dirs) => dirs.into_iter().collect::>(), - None => enumerate_dirs(get_manpath()?), - }; - - let manpages = - enumerate_manpages(search_dirs, &args.cmds, &args.exclude_cmds); - - let all_cmds = detect_subcommands(manpages, args.subcmds); - let total = all_cmds.len(); - for (i, (cmd_name, cmd_info)) in all_cmds.into_iter().enumerate() { - info!("Parsing {cmd_name} ({}/{})", i + 1, total); - - let (res, errors) = parse_from(&cmd_name, cmd_info); + match args.command { + Commands::Man { + shell, + out, + dirs, + cmds, + exclude_cmds, + not_subcmds: _, // todo actually use this + subcmds, + } => { + let search_dirs = match dirs { + Some(dirs) => dirs.into_iter().collect::>(), + None => enumerate_dirs(get_manpath()?), + }; + + let manpages = enumerate_manpages(search_dirs, &cmds, &exclude_cmds); + + let all_cmds = detect_subcommands(manpages, subcmds); + let total = all_cmds.len(); + for (i, (cmd_name, cmd_info)) in all_cmds.into_iter().enumerate() { + info!("Parsing {cmd_name} ({}/{})", i + 1, total); + + let (res, errors) = parse_from(&cmd_name, cmd_info); + + for error in errors { + error!("{}", error); + } - for error in errors { - error!("{}", error); + if let Some(cmd_info) = res { + info!("Generating completions for {cmd_name}"); + gen::generate_to_file(&cmd_info, shell, &out)?; + } else { + warn!("Could not parse man page for {cmd_name}"); + } + } } - - if let Some(cmd_info) = res { - info!("Generating completions for {cmd_name}"); - gen::generate(&cmd_info, args.shell, &args.out)?; - } else { - warn!("Could not parse man page for {cmd_name}"); + Commands::For { shell, conf, out } => { + let cmd = parse_deser::parse(conf)?; + if let Some(out) = out { + gen::generate_to_file(&cmd, shell, out)?; + } else { + println!("{}", gen::generate_to_str(&cmd, shell)); + } } } diff --git a/tests/man_integration_tests.rs b/tests/man_integration_tests.rs index 530eb4e..834e528 100644 --- a/tests/man_integration_tests.rs +++ b/tests/man_integration_tests.rs @@ -22,13 +22,14 @@ fn run_test(shell: &str, outputs: &[&str], args: &[&str]) { // The man-completions binary to test let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); - let cmd = cmd.env("MANPATH", &in_dir).args(args).args([ - "--out", - &out_dir.path().display().to_string(), - "--shell", - shell, - ]); - // So we can explicitly ask for logging + let cmd = cmd + .env("MANPATH", &in_dir) + .arg("man") + .args(args) + .arg(shell) + .arg(&out_dir.path().display().to_string()); + + // So we can explicitly ask for logging if let Ok(log_level) = env::var("RUST_LOG") { cmd.env("RUST_LOG", log_level).stderr(Stdio::inherit()); }