diff options
Diffstat (limited to 'src/shells/bash.rs')
-rw-r--r-- | src/shells/bash.rs | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/src/shells/bash.rs b/src/shells/bash.rs new file mode 100644 index 0000000..2a97e1d --- /dev/null +++ b/src/shells/bash.rs @@ -0,0 +1,243 @@ +use std::{fmt::Write as _, io::Write}; + +use clap::*; + +use crate::generator::{utils, Generator}; + +/// Generate bash completion file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Bash; + +impl Generator for Bash { + fn file_name(&self, name: &str) -> String { + format!("{name}.bash") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + w!( + buf, + format!( + "_{name}() {{ + local i cur prev opts cmd + COMPREPLY=() + cur=\"${{COMP_WORDS[COMP_CWORD]}}\" + prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\" + cmd=\"\" + opts=\"\" + + for i in ${{COMP_WORDS[@]}} + do + case \"${{cmd}},${{i}}\" in + \",$1\") + cmd=\"{cmd}\" + ;;{subcmds} + *) + ;; + esac + done + + case \"${{cmd}}\" in + {cmd}) + opts=\"{name_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + fi + case \"${{prev}}\" in{name_opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + ;;{subcmd_details} + esac +}} + +complete -F _{name} -o nosort -o bashdefault -o default {name} +", + name = bin_name, + cmd = bin_name.replace('-', "__"), + name_opts = all_options_for_path(cmd, bin_name), + name_opts_details = option_details_for_path(cmd, bin_name), + subcmds = all_subcommands(cmd), + subcmd_details = subcommand_details(cmd) + ) + .as_bytes() + ); + } +} + +fn all_subcommands(cmd: &Command) -> String { + debug!("all_subcommands"); + + fn add_command( + parent_fn_name: &str, + cmd: &Command, + subcmds: &mut Vec<(String, String, String)>, + ) { + let fn_name = format!( + "{parent_fn_name}__{cmd_name}", + parent_fn_name = parent_fn_name, + cmd_name = cmd.get_name().to_string().replace('-', "__") + ); + subcmds.push(( + parent_fn_name.to_string(), + cmd.get_name().to_string(), + fn_name.clone(), + )); + for alias in cmd.get_visible_aliases() { + subcmds.push(( + parent_fn_name.to_string(), + alias.to_string(), + fn_name.clone(), + )); + } + for subcmd in cmd.get_subcommands() { + add_command(&fn_name, subcmd, subcmds); + } + } + let mut subcmds = vec![]; + let fn_name = cmd.get_name().replace('-', "__"); + for subcmd in cmd.get_subcommands() { + add_command(&fn_name, subcmd, &mut subcmds); + } + subcmds.sort(); + + let mut cases = vec![String::new()]; + for (parent_fn_name, name, fn_name) in subcmds { + cases.push(format!( + "{parent_fn_name},{name}) + cmd=\"{fn_name}\" + ;;", + )); + } + + cases.join("\n ") +} + +fn subcommand_details(cmd: &Command) -> String { + debug!("subcommand_details"); + + let mut subcmd_dets = vec![String::new()]; + let mut scs = utils::all_subcommands(cmd) + .iter() + .map(|x| x.1.replace(' ', "__")) + .collect::<Vec<_>>(); + + scs.sort(); + + subcmd_dets.extend(scs.iter().map(|sc| { + format!( + "{subcmd}) + opts=\"{sc_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + fi + case \"${{prev}}\" in{opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + ;;", + subcmd = sc.replace('-', "__"), + sc_opts = all_options_for_path(cmd, sc), + level = sc.split("__").map(|_| 1).sum::<u64>(), + opts_details = option_details_for_path(cmd, sc) + ) + })); + + subcmd_dets.join("\n ") +} + +fn option_details_for_path(cmd: &Command, path: &str) -> String { + debug!("option_details_for_path: path={path}"); + + let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); + let mut opts = vec![String::new()]; + + for o in p.get_opts() { + if let Some(longs) = o.get_long_and_visible_aliases() { + opts.extend(longs.iter().map(|long| { + format!( + "--{}) + COMPREPLY=({}) + return 0 + ;;", + long, + vals_for(o) + ) + })); + } + + if let Some(shorts) = o.get_short_and_visible_aliases() { + opts.extend(shorts.iter().map(|short| { + format!( + "-{}) + COMPREPLY=({}) + return 0 + ;;", + short, + vals_for(o) + ) + })); + } + } + + opts.join("\n ") +} + +fn vals_for(o: &Arg) -> String { + debug!("vals_for: o={}", o.get_id()); + + if let Some(vals) = crate::generator::utils::possible_values(o) { + format!( + "$(compgen -W \"{}\" -- \"${{cur}}\")", + vals.iter() + .filter(|pv| !pv.is_hide_set()) + .map(|n| n.get_name()) + .collect::<Vec<_>>() + .join(" ") + ) + } else if o.get_value_hint() == ValueHint::Other { + String::from("\"${cur}\"") + } else { + String::from("$(compgen -f \"${cur}\")") + } +} + +fn all_options_for_path(cmd: &Command, path: &str) -> String { + debug!("all_options_for_path: path={path}"); + + let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); + + let mut opts = String::new(); + for short in utils::shorts_and_visible_aliases(p) { + write!(&mut opts, "-{short} ").unwrap(); + } + for long in utils::longs_and_visible_aliases(p) { + write!(&mut opts, "--{long} ").unwrap(); + } + for pos in p.get_positionals() { + if let Some(vals) = utils::possible_values(pos) { + for value in vals { + write!(&mut opts, "{} ", value.get_name()).unwrap(); + } + } else { + write!(&mut opts, "{pos} ").unwrap(); + } + } + for (sc, _) in utils::subcommands(p) { + write!(&mut opts, "{sc} ").unwrap(); + } + opts.pop(); + + opts +} |