Skip to content

Commit

Permalink
fix: Make multiple subcommands work
Browse files Browse the repository at this point in the history
  • Loading branch information
ysthakur committed Aug 18, 2023
1 parent 7005c67 commit 72d1c20
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 122 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Things to do:
- Port darwin and degroff parsers
- Test the type 4 parser
- Find samples of type 4, Darwin, and Degroff to test
- Ensure nested subcommands and multiple subcommands work
- Ensure nested subcommands work
- Add .gz files to the tests folder
- Test excluding/including commands and directories
- Figure out why fish only seems to use man1, man6, and man8
40 changes: 21 additions & 19 deletions src/gen/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,66 @@ use anyhow::Result;
use crate::{gen::util::Output, parse::CommandInfo};

/// Generate a completion file for Bash
pub fn generate(
cmd_name: &str,
cmd_info: &CommandInfo,
out_dir: &Path,
) -> Result<()> {
let comp_name = format!("_comp_cmd_{cmd_name}");
pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> Result<()> {
let comp_name = format!("_comp_cmd_{}", cmd.name);

let mut out = Output::new(String::from("\t"));
out.writeln("#!/usr/bin/env bash\n");
out.writeln(format!("function {comp_name} {{"));
out.indent();
out.writeln("COMPREPLY=()");

generate_cmd(cmd_info, 1, &mut out);
generate_cmd(cmd, 1, &mut out);

out.writeln("return 0");
out.dedent();
out.writeln("}\n");

out.writeln(format!("complete -F _comp_cmd_{cmd_name} {cmd_name}"));
out.writeln(format!("complete -F _comp_cmd_{} {}", cmd.name, cmd.name));

fs::write(out_dir.join(format!("_{cmd_name}.bash")), out.text())?;
fs::write(out_dir.join(format!("_{}.bash", cmd.name)), out.text())?;
Ok(())
}

fn generate_cmd(cmd_info: &CommandInfo, pos: usize, out: &mut Output) {
fn generate_cmd(cmd: &CommandInfo, pos: usize, out: &mut Output) {
out.writeln("case $COMP_CWORD in");
out.indent();

let flags = cmd_info
let flags = cmd
.flags
.iter()
.map(|f| f.forms.join(" "))
.collect::<Vec<_>>()
.join(" ");
let subcmds = cmd_info
let subcmds = cmd
.subcommands
.keys()
.map(String::from)
.iter()
.map(|c| c.name.to_string())
.collect::<Vec<_>>()
.join(" ");
let completions = format!("{flags} {subcmds}");
let completions = if flags.is_empty() {
subcmds
} else if subcmds.is_empty() {
flags
} else {
format!("{flags} {subcmds}")
};
// This case is for when the subcommand we're processing is the one to
// complete
out.writeln(format!(
"{pos}) COMPREPLY=($(compgen -W '{completions}' -- $2)) ;;"
));

// This case is in case we need to go further to a deeper subcommand
if !cmd_info.subcommands.is_empty() {
if !cmd.subcommands.is_empty() {
out.writeln("*)");
out.indent();
out.writeln(format!("case ${{COMP_WORDS[{pos}]}} in"));
out.indent();
for (cmd_name, cmd_info) in &cmd_info.subcommands {
out.writeln(format!("{cmd_name})"));
for sub_cmd in &cmd.subcommands {
out.writeln(format!("{})", sub_cmd.name));
out.indent();
generate_cmd(cmd_info, pos + 1, out);
generate_cmd(sub_cmd, pos + 1, out);
out.writeln(";;");
out.dedent();
}
Expand Down
44 changes: 16 additions & 28 deletions src/gen/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@ use crate::{gen::util::Output, parse::CommandInfo};
/// Generate JSON representing the parsed options
///
/// This should probably use a real JSON library but whatever
pub fn generate(
cmd_name: &str,
cmd_info: &CommandInfo,
out_dir: &Path,
) -> Result<()> {
pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> Result<()> {
let mut res = Output::new(String::from(" "));
res.writeln("{");
res.indent();
generate_cmd(cmd_name, cmd_info, true, &mut res);
generate_cmd(cmd, true, &mut res);
res.dedent();
res.writeln("}");
fs::write(out_dir.join(format!("{cmd_name}.json")), res.text())?;
fs::write(out_dir.join(format!("{}.json", cmd.name)), res.text())?;
Ok(())
}

Expand All @@ -28,18 +24,13 @@ pub fn generate(
/// * `indent` - The indentation level (how many subcommands in we are)
/// * `last` - Whether this is the last command at this level. Used for deciding
/// whether or not to put a trailing comma
fn generate_cmd(
cmd: &str,
cmd_info: &CommandInfo,
last: bool,
out: &mut Output,
) {
let cmd = quote(cmd);
fn generate_cmd(cmd: &CommandInfo, last: bool, out: &mut Output) {
let cmd_name = quote(&cmd.name);
// Avoid trailing commas
let end = if last { "}" } else { "}," };
let mut flags = cmd_info.flags.iter();
let mut flags = cmd.flags.iter();
if let Some(mut flag) = flags.next() {
out.writeln(format!("{cmd}: {{"));
out.writeln(format!("{cmd_name}: {{"));
out.indent();
out.writeln("\"flags\": [");
out.indent();
Expand Down Expand Up @@ -76,20 +67,17 @@ fn generate_cmd(
out.dedent();
out.writeln("],");

let mut subcmds = cmd_info.subcommands.iter();
if let Some((mut name, mut info)) = subcmds.next() {
let mut subcmds = cmd.subcommands.iter();
if let Some(mut sub_cmd) = subcmds.next() {
out.writeln("\"subcommands\": {");
out.indent();
loop {
if let Some(next) = subcmds.next() {
generate_cmd(name, info, false, out);
name = next.0;
info = next.1;
} else {
generate_cmd(name, info, true, out);
break;
}

while let Some(next) = subcmds.next() {
generate_cmd(sub_cmd, false, out);
sub_cmd = next;
}
generate_cmd(sub_cmd, true, out);

out.dedent();
out.writeln("}");
} else {
Expand All @@ -100,7 +88,7 @@ fn generate_cmd(
out.writeln(end);
} else {
// If no arguments, print `"cmd": {}` on a single line
out.writeln(format!("{cmd}: {{{end}"));
out.writeln(format!("{cmd_name}: {{{end}"));
}
}

Expand Down
37 changes: 20 additions & 17 deletions src/gen/nu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@ use anyhow::Result;
use crate::{gen::util::Output, parse::CommandInfo};

/// Generate completions for Nushell
pub fn generate(
cmd_name: &str,
cmd_info: &CommandInfo,
out_dir: &Path,
) -> Result<()> {
pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> Result<()> {
let mut res = Output::new(String::from(" "));
generate_cmd(cmd_name, cmd_info, &mut res, true);
fs::write(out_dir.join(format!("{cmd_name}.nu")), res.text())?;
generate_cmd(&cmd.name, cmd, &mut res, true);
fs::write(out_dir.join(format!("{}.nu", cmd.name)), res.text())?;
Ok(())
}

fn generate_cmd(
cmd_name: &str,
cmd_info: &CommandInfo,
cmd: &CommandInfo,
out: &mut Output,
first: bool,
) {
Expand All @@ -29,8 +25,8 @@ fn generate_cmd(
out.writeln(format!("export extern \"{cmd_name}\" ["));
out.indent();

for flag in &cmd_info.flags {
let (mut short, mut long): (Vec<_>, Vec<_>) =
for flag in &cmd.flags {
let (short, long): (Vec<_>, Vec<_>) =
flag.forms.iter().partition(|f| f.len() == 2);

let desc_str = if let Some(desc) = &flag.desc {
Expand All @@ -42,24 +38,31 @@ fn generate_cmd(
// Pair off as many long and short forms as possible
// It's unlikely there'll be both long and short forms of the same flag, but
// you never know what kind of horrors a man page may hold
while !long.is_empty() && !short.is_empty() {
let short_str = format!("({})", short.pop().unwrap());
out.writeln(format!("{}{}{}", long.pop().unwrap(), short_str, desc_str));
let mut short = short.into_iter();
let mut long = long.into_iter();
while short.len() > 0 && long.len() > 0 {
let short_str = format!("({})", short.next().unwrap());
out.writeln(format!("{}{}{}", long.next().unwrap(), short_str, desc_str));
}

while let Some(flag) = long.pop() {
while let Some(flag) = long.next() {
out.writeln(format!("{flag}{desc_str}"));
}

while let Some(flag) = short.pop() {
while let Some(flag) = short.next() {
out.writeln(format!("{flag}{desc_str}"));
}
}

out.dedent();
out.writeln("]");

for (subname, subcmd) in &cmd_info.subcommands {
generate_cmd(&format!("{cmd_name} {subname}"), subcmd, out, false);
for sub_cmd in &cmd.subcommands {
generate_cmd(
&format!("{} {}", cmd.name, sub_cmd.name),
sub_cmd,
out,
false,
);
}
}
46 changes: 18 additions & 28 deletions src/gen/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,12 @@ use crate::{gen::util, parse::CommandInfo};
/// '-b[Make new branch]'
/// }
/// ```
pub fn generate(
cmd_name: &str,
cmd_info: &CommandInfo,
out_dir: &Path,
) -> Result<()> {
pub fn generate(cmd: &CommandInfo, out_dir: &Path) -> Result<()> {
// TODO make option to not overwrite file
let comp_name = format!("_{cmd_name}");
let comp_name = format!("_{}", cmd.name);
let mut res = Output::new(String::from("\t"));
res.writeln(format!("#compdef {comp_name} {cmd_name}"));
generate_fn(cmd_name, cmd_info, &mut res, 0, &comp_name);
res.writeln(format!("#compdef {comp_name} {}", cmd.name));
generate_fn(cmd, &mut res, 0, &comp_name);
fs::write(out_dir.join(format!("{comp_name}.zsh")), res.text())?;
Ok(())
}
Expand All @@ -58,28 +54,22 @@ pub fn generate(
/// * `fn` - What to name the completion function. If you have a command `foo`
/// with subcommand `bar`, the completion function for `foo bar` would be
/// named `_foo_bar`
fn generate_fn(
_cmd_name: &str,
cmd_info: &CommandInfo,
out: &mut Output,
pos: usize,
fn_name: &str,
) {
out.write("\n");
fn generate_fn(cmd: &CommandInfo, out: &mut Output, pos: usize, fn_name: &str) {
out.writeln("");
out.writeln(format!("function {fn_name} {{"));
out.indent();

if !cmd_info.subcommands.is_empty() {
if !cmd.subcommands.is_empty() {
out.writeln("local line");
}
if cmd_info.subcommands.is_empty() {
if cmd.subcommands.is_empty() {
out.write("_arguments");
} else {
out.write("_arguments -C");
}

out.indent();
for flag in &cmd_info.flags {
for flag in &cmd.flags {
let desc = if let Some(desc) = &flag.desc {
desc
} else {
Expand All @@ -92,13 +82,14 @@ fn generate_fn(
}
}

if cmd_info.subcommands.is_empty() {
if cmd.subcommands.is_empty() {
out.dedent();
out.write("\n");
} else {
let sub_cmds = cmd_info
let sub_cmds = cmd
.subcommands
.keys()
.map(|s| s.to_string())
.iter()
.map(|c| c.name.to_string())
.collect::<Vec<_>>()
.join(" ");
out.writeln(" \\");
Expand All @@ -108,8 +99,8 @@ fn generate_fn(

out.writeln(format!("case $line[{}] in", pos + 1));
out.indent();
for sub_cmd in cmd_info.subcommands.keys() {
out.writeln(format!("{sub_cmd}) {fn_name}_{sub_cmd};;"));
for sub_cmd in &cmd.subcommands {
out.writeln(format!("{}) {fn_name}_{};;", sub_cmd.name, sub_cmd.name));
}
out.dedent();
out.writeln("esac");
Expand All @@ -118,13 +109,12 @@ fn generate_fn(
out.dedent();
out.writeln("}");

for (sub_cmd, sub_cmd_info) in &cmd_info.subcommands {
for sub_cmd in &cmd.subcommands {
generate_fn(
sub_cmd,
sub_cmd_info,
out,
pos + 1,
&format!("{fn_name}_{sub_cmd}"),
&format!("{fn_name}_{}", sub_cmd.name),
);
}
}
11 changes: 5 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,14 @@ fn subcmd_map_parser(

fn gen_shell(
shell: &Shell,
cmd_name: &str,
parsed: &CommandInfo,
out_dir: &Path,
) -> Result<()> {
match shell {
Shell::Zsh => gen::zsh::generate(cmd_name, parsed, out_dir),
Shell::Json => gen::json::generate(cmd_name, parsed, out_dir),
Shell::Bash => gen::bash::generate(cmd_name, parsed, out_dir),
Shell::Nu => gen::nu::generate(cmd_name, parsed, out_dir),
Shell::Zsh => gen::zsh::generate(parsed, out_dir),
Shell::Json => gen::json::generate(parsed, out_dir),
Shell::Bash => gen::bash::generate(parsed, out_dir),
Shell::Nu => gen::nu::generate(parsed, out_dir),
}
}

Expand Down Expand Up @@ -115,7 +114,7 @@ fn main() -> Result<()> {

if let Some(cmd_info) = res {
info!("Generating completions for {cmd_name}");
gen_shell(&args.shell, &cmd_name, &cmd_info, &args.out)?;
gen_shell(&args.shell, &cmd_info, &args.out)?;
} else {
warn!("Could not parse man page for {cmd_name}");
}
Expand Down
Loading

0 comments on commit 72d1c20

Please sign in to comment.