use std::fmt;
use std::str::FromStr;
use crate::parser::ArgParser;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Shell {
Bash,
Zsh,
Fish,
PowerShell,
}
impl FromStr for Shell {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bash" => Ok(Shell::Bash),
"zsh" => Ok(Shell::Zsh),
"fish" => Ok(Shell::Fish),
"powershell" | "pwsh" => Ok(Shell::PowerShell),
_ => Err(format!("unsupported shell: {s}")),
}
}
}
impl fmt::Display for Shell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Shell::Bash => write!(f, "bash"),
Shell::Zsh => write!(f, "zsh"),
Shell::Fish => write!(f, "fish"),
Shell::PowerShell => write!(f, "powershell"),
}
}
}
impl ArgParser {
pub fn generate_completions(&self, shell: Shell) -> String {
match shell {
Shell::Bash => generate_bash(self),
Shell::Zsh => generate_zsh(self),
Shell::Fish => generate_fish(self),
Shell::PowerShell => generate_powershell(self),
}
}
}
fn generate_bash(parser: &ArgParser) -> String {
let name = parser.program_name().unwrap_or("program");
let func_name = name.replace('-', "_");
let mut out = String::new();
out.push_str(&format!("_{func_name}() {{\n"));
out.push_str(" local i cur prev opts cmd\n");
out.push_str(" COMPREPLY=()\n");
out.push_str(" if type _init_completion &>/dev/null; then\n");
out.push_str(" _init_completion || return\n");
out.push_str(" else\n");
out.push_str(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n");
out.push_str(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n");
out.push_str(" fi\n\n");
let subs = parser.subcommands();
if subs.is_empty() {
let words = bash_completable_words(parser);
out.push_str(&format!(" opts=\"{words}\"\n"));
out.push_str(" COMPREPLY=($(compgen -W \"${opts}\" -- \"${cur}\"))\n");
} else {
out.push_str(" cmd=\"\"\n");
out.push_str(" for i in \"${COMP_WORDS[@]}\"; do\n");
out.push_str(" case \"${i}\" in\n");
for sub in subs {
out.push_str(&format!(" {}) cmd=\"{}\";;\n", sub.name, sub.name));
}
out.push_str(" esac\n");
out.push_str(" done\n\n");
out.push_str(" case \"${cmd}\" in\n");
for sub in subs {
let sub_words = bash_completable_words(&sub.parser);
out.push_str(&format!(" {})\n", sub.name));
out.push_str(&format!(" opts=\"{sub_words}\"\n"));
out.push_str(" ;;\n");
}
let sub_names: Vec<&str> = subs.iter().map(|s| s.name.as_str()).collect();
let top_words = bash_completable_words(parser);
let all_top = format!("{} {}", sub_names.join(" "), top_words);
out.push_str(" *)\n");
out.push_str(&format!(" opts=\"{}\"\n", all_top.trim()));
out.push_str(" ;;\n");
out.push_str(" esac\n\n");
out.push_str(" COMPREPLY=($(compgen -W \"${opts}\" -- \"${cur}\"))\n");
}
out.push_str("}\n\n");
out.push_str(&format!("complete -F _{func_name} -o bashdefault -o default {name}\n"));
out
}
fn bash_escape(s: &str) -> String {
s.replace('\'', "'\\''")
}
fn bash_completable_words(parser: &ArgParser) -> String {
let mut words = Vec::new();
for flag in parser.flags() {
if flag.hidden {
continue;
}
words.push(format!("--{}", flag.long));
if let Some(c) = flag.short {
words.push(format!("-{c}"));
}
}
for opt in parser.options() {
if opt.hidden {
continue;
}
words.push(format!("--{}", opt.long));
if let Some(c) = opt.short {
words.push(format!("-{c}"));
}
}
bash_escape(&words.join(" "))
}
fn generate_zsh(parser: &ArgParser) -> String {
let name = parser.program_name().unwrap_or("program");
let func_name = name.replace('-', "_");
let mut out = String::new();
out.push_str(&format!("#compdef {name}\n\n"));
let subs = parser.subcommands();
if subs.is_empty() {
out.push_str(&format!("_{func_name}() {{\n"));
out.push_str(" _arguments \\\n");
zsh_push_args(parser, &mut out, " ");
out.push_str("}\n\n");
} else {
out.push_str(&format!("_{func_name}() {{\n"));
out.push_str(" local line state\n\n");
out.push_str(" _arguments \\\n");
zsh_push_args(parser, &mut out, " ");
out.push_str(" '*::subcmd:->subcmd' && return\n\n");
out.push_str(" case $state in\n");
out.push_str(" subcmd)\n");
out.push_str(" case $line[1] in\n");
for sub in subs {
let sub_func = format!("_{func_name}_{}", sub.name.replace('-', "_"));
out.push_str(&format!(" {})\n", sub.name));
out.push_str(&format!(" {sub_func}\n"));
out.push_str(" ;;\n");
}
out.push_str(" *)\n");
out.push_str(" local -a subcmds\n");
out.push_str(" subcmds=(\n");
for sub in subs {
let desc = zsh_escape(&sub.description);
out.push_str(&format!(" '{}:{}'\n", sub.name, desc));
}
out.push_str(" )\n");
out.push_str(" _describe 'subcommand' subcmds\n");
out.push_str(" ;;\n");
out.push_str(" esac\n");
out.push_str(" ;;\n");
out.push_str(" esac\n");
out.push_str("}\n\n");
for sub in subs {
let sub_func = format!("_{func_name}_{}", sub.name.replace('-', "_"));
out.push_str(&format!("{sub_func}() {{\n"));
out.push_str(" _arguments \\\n");
zsh_push_args(&sub.parser, &mut out, " ");
out.push_str("}\n\n");
}
}
out.push_str(&format!("compdef _{func_name} {name}\n"));
out
}
fn zsh_escape(s: &str) -> String {
s.replace('[', "\\[").replace(']', "\\]").replace(':', "\\:")
}
fn zsh_push_args(parser: &ArgParser, out: &mut String, indent: &str) {
for flag in parser.flags() {
if flag.hidden {
continue;
}
let desc = zsh_escape(&flag.description);
if let Some(c) = flag.short {
let excl = format!("-{c} --{}", flag.long);
out.push_str(&format!("{indent}'({excl})--{}[{desc}]' \\\n", flag.long));
out.push_str(&format!("{indent}'({excl})-{c}[{desc}]' \\\n"));
} else {
out.push_str(&format!("{indent}'--{}[{desc}]' \\\n", flag.long));
}
}
for opt in parser.options() {
if opt.hidden {
continue;
}
let desc = zsh_escape(&opt.description);
let ph = if opt.placeholder.is_empty() {
"value".to_string()
} else {
opt.placeholder.clone()
};
if let Some(c) = opt.short {
let excl = format!("-{c} --{}", opt.long);
out.push_str(&format!("{indent}'({excl})--{}[{desc}]:{ph}:' \\\n", opt.long));
out.push_str(&format!("{indent}'({excl})-{c}[{desc}]:{ph}:' \\\n"));
} else {
out.push_str(&format!("{indent}'--{}[{desc}]:{ph}:' \\\n", opt.long));
}
}
}
fn generate_fish(parser: &ArgParser) -> String {
let name = parser.program_name().unwrap_or("program");
let mut out = String::new();
let subs = parser.subcommands();
let has_subs = !subs.is_empty();
for flag in parser.flags() {
if flag.hidden {
continue;
}
let mut cmd = format!("complete -c {name}");
if has_subs {
cmd.push_str(" -n '__fish_use_subcommand'");
}
cmd.push_str(&format!(" -l {}", flag.long));
if let Some(c) = flag.short {
cmd.push_str(&format!(" -s {c}"));
}
if !flag.description.is_empty() {
cmd.push_str(&format!(" -d '{}'", fish_escape(&flag.description)));
}
out.push_str(&cmd);
out.push('\n');
}
for opt in parser.options() {
if opt.hidden {
continue;
}
let mut cmd = format!("complete -c {name}");
if has_subs {
cmd.push_str(" -n '__fish_use_subcommand'");
}
cmd.push_str(&format!(" -l {}", opt.long));
if let Some(c) = opt.short {
cmd.push_str(&format!(" -s {c}"));
}
cmd.push_str(" -r");
if !opt.description.is_empty() {
cmd.push_str(&format!(" -d '{}'", fish_escape(&opt.description)));
}
out.push_str(&cmd);
out.push('\n');
}
for sub in subs {
let mut cmd = format!("complete -c {name} -n '__fish_use_subcommand' -a {}", sub.name);
if !sub.description.is_empty() {
cmd.push_str(&format!(" -d '{}'", fish_escape(&sub.description)));
}
out.push_str(&cmd);
out.push('\n');
for flag in sub.parser.flags() {
if flag.hidden {
continue;
}
let mut cmd = format!(
"complete -c {name} -n '__fish_seen_subcommand_from {}' -l {}",
sub.name, flag.long
);
if let Some(c) = flag.short {
cmd.push_str(&format!(" -s {c}"));
}
if !flag.description.is_empty() {
cmd.push_str(&format!(" -d '{}'", fish_escape(&flag.description)));
}
out.push_str(&cmd);
out.push('\n');
}
for opt in sub.parser.options() {
if opt.hidden {
continue;
}
let mut cmd = format!(
"complete -c {name} -n '__fish_seen_subcommand_from {}' -l {}",
sub.name, opt.long
);
if let Some(c) = opt.short {
cmd.push_str(&format!(" -s {c}"));
}
cmd.push_str(" -r");
if !opt.description.is_empty() {
cmd.push_str(&format!(" -d '{}'", fish_escape(&opt.description)));
}
out.push_str(&cmd);
out.push('\n');
}
}
out
}
fn fish_escape(s: &str) -> String {
s.replace('\'', "'\\''")
}
fn generate_powershell(parser: &ArgParser) -> String {
let name = parser.program_name().unwrap_or("program");
let mut out = String::new();
out.push_str(&format!(
"Register-ArgumentCompleter -CommandName '{name}' -ScriptBlock {{\n"
));
out.push_str(" param($commandName, $wordToComplete, $cursorPosition)\n\n");
let subs = parser.subcommands();
if subs.is_empty() {
out.push_str(" $completions = @(\n");
ps_push_completions(parser, &mut out, " ");
out.push_str(" )\n\n");
out.push_str(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" }\n");
} else {
out.push_str(" # Determine the subcommand\n");
out.push_str(" $tokens = $commandName -split '\\s+'\n");
out.push_str(" $subcmd = $null\n");
out.push_str(" foreach ($t in $tokens[1..($tokens.Length - 1)]) {\n");
out.push_str(" switch ($t) {\n");
for sub in subs {
out.push_str(&format!(" '{}' {{ $subcmd = '{}' }}\n", sub.name, sub.name));
}
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" $completions = @()\n\n");
out.push_str(" switch ($subcmd) {\n");
for sub in subs {
out.push_str(&format!(" '{}' {{\n", sub.name));
out.push_str(" $completions = @(\n");
ps_push_completions(&sub.parser, &mut out, " ");
out.push_str(" )\n");
out.push_str(" }\n");
}
out.push_str(" default {\n");
out.push_str(" $completions = @(\n");
ps_push_completions(parser, &mut out, " ");
for sub in subs {
let desc = ps_escape(&sub.description);
out.push_str(&format!(
" [System.Management.Automation.CompletionResult]::new('{}', '{}', 'ParameterValue', '{}')\n",
sub.name, sub.name, desc
));
}
out.push_str(" )\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" }\n");
}
out.push_str("}\n");
out
}
fn ps_escape(s: &str) -> String {
s.replace('\'', "''")
}
fn ps_push_completions(parser: &ArgParser, out: &mut String, indent: &str) {
for flag in parser.flags() {
if flag.hidden {
continue;
}
let desc = ps_escape(&flag.description);
out.push_str(&format!(
"{indent}[System.Management.Automation.CompletionResult]::new('--{}', '--{}', 'ParameterName', '{}')\n",
flag.long, flag.long, desc
));
if let Some(c) = flag.short {
out.push_str(&format!(
"{indent}[System.Management.Automation.CompletionResult]::new('-{c}', '-{c}', 'ParameterName', '{}')\n",
desc
));
}
}
for opt in parser.options() {
if opt.hidden {
continue;
}
let desc = ps_escape(&opt.description);
out.push_str(&format!(
"{indent}[System.Management.Automation.CompletionResult]::new('--{}', '--{}', 'ParameterName', '{}')\n",
opt.long, opt.long, desc
));
if let Some(c) = opt.short {
out.push_str(&format!(
"{indent}[System.Management.Automation.CompletionResult]::new('-{c}', '-{c}', 'ParameterName', '{}')\n",
desc
));
}
}
}