Skip to content

Commit

Permalink
feat: make both commands available under CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
ysthakur committed Dec 24, 2023
1 parent ee69874 commit 013958d
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 99 deletions.
7 changes: 2 additions & 5 deletions src/gen/bash.rs
Original file line number Diff line number Diff line change
@@ -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"));
Expand All @@ -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) {
Expand Down
49 changes: 30 additions & 19 deletions src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path>,
) -> 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(),
),
}
}
10 changes: 2 additions & 8 deletions src/gen/nu.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
7 changes: 2 additions & 5 deletions src/gen/zsh.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::{fs, path::Path};

use crate::gen::{
util::{self, Output},
CommandInfo,
Expand Down Expand Up @@ -36,16 +34,15 @@ 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"));
res.writeln(format!("#compdef {}", cmd.name));
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
Expand Down
149 changes: 94 additions & 55 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<PathBuf>>,

/// 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<Regex>,

/// Commands to exclude (regex). To match the whole name, use "^...$"
#[arg(short = 'C', long, value_name = "regex")]
exclude_cmds: Option<Regex>,

/// 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<String>,

/// 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<String>)>,
#[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<Vec<PathBuf>>,

/// 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<Regex>,

/// Commands to exclude (regex). To match the whole name, use "^...$"
#[arg(short = 'C', long, value_name = "regex")]
exclude_cmds: Option<Regex>,

/// 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<String>,

/// 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<String>)>,
},
/// 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<PathBuf>,
},
}

fn subcmd_map_parser(
Expand All @@ -63,30 +83,49 @@ fn main() -> Result<()> {

let args = Cli::parse();

let search_dirs = match args.dirs {
Some(dirs) => dirs.into_iter().collect::<Vec<_>>(),
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::<Vec<_>>(),
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));
}
}
}

Expand Down
15 changes: 8 additions & 7 deletions tests/man_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down

0 comments on commit 013958d

Please sign in to comment.