use std::ffi::OsString;
use std::str::FromStr;
use super::EnvCompleter;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Bash;
impl EnvCompleter for Bash {
fn name(&self) -> &'static str {
"bash"
}
fn is(&self, name: &str) -> bool {
name == "bash"
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let escaped_name = name.replace('-', "_");
let completer =
shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer));
let script = r#"
_clap_complete_NAME() {
local IFS=$'\013'
local _CLAP_COMPLETE_INDEX=${COMP_CWORD}
local _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE}
if compopt +o nospace 2> /dev/null; then
local _CLAP_COMPLETE_SPACE=false
else
local _CLAP_COMPLETE_SPACE=true
fi
local words=("${COMP_WORDS[@]}")
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
words[COMP_CWORD]="$2"
fi
COMPREPLY=( $( \
_CLAP_IFS="$IFS" \
_CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \
_CLAP_COMPLETE_COMP_TYPE="$_CLAP_COMPLETE_COMP_TYPE" \
_CLAP_COMPLETE_SPACE="$_CLAP_COMPLETE_SPACE" \
VAR="bash" \
"COMPLETER" -- "${words[@]}" \
) )
if [[ $? != 0 ]]; then
unset COMPREPLY
elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
compopt -o nospace
fi
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -o nospace -o bashdefault -o nosort -F _clap_complete_NAME BIN
else
complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
fi
"#
.replace("NAME", &escaped_name)
.replace("BIN", bin)
.replace("COMPLETER", &completer)
.replace("VAR", var);
writeln!(buf, "{script}")?;
Ok(())
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index: usize = std::env::var("_CLAP_COMPLETE_INDEX")
.ok()
.and_then(|i| i.parse().ok())
.unwrap_or_default();
let _comp_type: CompType = std::env::var("_CLAP_COMPLETE_COMP_TYPE")
.ok()
.and_then(|i| i.parse().ok())
.unwrap_or_default();
let _space: Option<bool> = std::env::var("_CLAP_COMPLETE_SPACE")
.ok()
.and_then(|i| i.parse().ok());
let ifs: Option<String> = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok());
let completions = crate::engine::complete(cmd, args, index, current_dir)?;
for (i, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", candidate.get_value().to_string_lossy())?;
}
Ok(())
}
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
enum CompType {
#[default]
Normal,
Successive,
Alternatives,
Unmodified,
Menu,
}
impl FromStr for CompType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"9" => Ok(Self::Normal),
"63" => Ok(Self::Successive),
"33" => Ok(Self::Alternatives),
"64" => Ok(Self::Unmodified),
"37" => Ok(Self::Menu),
_ => Err(format!("unsupported COMP_TYPE `{s}`")),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Elvish;
impl EnvCompleter for Elvish {
fn name(&self) -> &'static str {
"elvish"
}
fn is(&self, name: &str) -> bool {
name == "elvish"
}
fn write_registration(
&self,
var: &str,
_name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin));
let completer =
shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer));
let script = r#"
set edit:completion:arg-completer[BIN] = { |@words|
var index = (count $words)
set index = (- $index 1)
put (env _CLAP_IFS="\n" _CLAP_COMPLETE_INDEX=(to-string $index) VAR="elvish" COMPLETER -- $@words) | to-lines
}
"#
.replace("COMPLETER", &completer)
.replace("BIN", &bin)
.replace("VAR", var);
writeln!(buf, "{script}")?;
Ok(())
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index: usize = std::env::var("_CLAP_COMPLETE_INDEX")
.ok()
.and_then(|i| i.parse().ok())
.unwrap_or_default();
let ifs: Option<String> = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok());
let completions = crate::engine::complete(cmd, args, index, current_dir)?;
for (i, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", candidate.get_value().to_string_lossy())?;
}
Ok(())
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Fish;
impl EnvCompleter for Fish {
fn name(&self) -> &'static str {
"fish"
}
fn is(&self, name: &str) -> bool {
name == "fish"
}
fn write_registration(
&self,
var: &str,
_name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let bin = fish_quote(bin);
let completer = fish_quote_for_eval(completer);
writeln!(
buf,
r#"complete --keep-order --exclusive --command {bin} --arguments "({var}=fish {completer} -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))""#
)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index = args.len() - 1;
let completions = crate::engine::complete(cmd, args, index, current_dir)?;
for candidate in completions {
write!(buf, "{}", candidate.get_value().to_string_lossy())?;
if let Some(help) = candidate.get_help() {
write!(
buf,
"\t{}",
help.to_string().lines().next().unwrap_or_default()
)?;
}
writeln!(buf)?;
}
Ok(())
}
}
fn fish_quote_for_eval(s: &str) -> std::borrow::Cow<'_, str> {
if !fish_needs_quoting(s) {
return std::borrow::Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
match c {
'\\' => out.push_str(r"\\\\"),
'\'' => out.push_str(r"\\'"),
'"' => out.push_str(r#"\""#),
'$' => out.push_str(r"\$"),
_ => out.push(c),
}
}
out.push('\'');
std::borrow::Cow::Owned(out)
}
fn fish_quote(s: &str) -> std::borrow::Cow<'_, str> {
if !fish_needs_quoting(s) {
return std::borrow::Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
match c {
'\\' => out.push_str(r"\\"),
'\'' => out.push_str(r"\'"),
_ => out.push(c),
}
}
out.push('\'');
std::borrow::Cow::Owned(out)
}
fn fish_needs_quoting(s: &str) -> bool {
s.is_empty()
|| s.chars().any(|c| {
!(c.is_ascii_alphanumeric()
|| matches!(c, '/' | '_' | '-' | '.' | ',' | '+' | '=' | ':' | '@'))
})
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Powershell;
impl EnvCompleter for Powershell {
fn name(&self) -> &'static str {
"powershell"
}
fn is(&self, name: &str) -> bool {
name == "powershell" || name == "powershell_ise"
}
fn write_registration(
&self,
var: &str,
_name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin));
let completer =
shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer));
writeln!(
buf,
r#"
Register-ArgumentCompleter -Native -CommandName {bin} -ScriptBlock {{
param($wordToComplete, $commandAst, $cursorPosition)
$prev = $env:{var};
$env:{var} = "powershell";
$args = $commandAst.Extent.Text
$args = $args.Substring(0, [math]::Min($cursorPosition, $args.Length));
if ($wordToComplete -eq "") {{
$args += " ''";
}}
$results = Invoke-Expression @"
& {completer} -- $args
"@;
if ($null -eq $prev) {{
Remove-Item Env:\{var};
}} else {{
$env:{var} = $prev;
}}
$results | ForEach-Object {{
$split = $_.Split("`t");
$cmd = $split[0];
if ($split.Length -eq 2) {{
$help = $split[1];
}}
else {{
$help = $split[0];
}}
[System.Management.Automation.CompletionResult]::new($cmd, $cmd, 'ParameterValue', $help)
}}
}};
"#
)
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index = args.len() - 1;
let completions = crate::engine::complete(cmd, args, index, current_dir)?;
for candidate in completions {
write!(buf, "{}", candidate.get_value().to_string_lossy())?;
if let Some(help) = candidate.get_help() {
write!(
buf,
"\t{}",
help.to_string().lines().next().unwrap_or_default()
)?;
}
writeln!(buf)?;
}
Ok(())
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Zsh;
impl EnvCompleter for Zsh {
fn name(&self) -> &'static str {
"zsh"
}
fn is(&self, name: &str) -> bool {
name == "zsh"
}
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let escaped_name = name.replace('-', "_");
let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin));
let completer =
shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer));
let script = r#"#compdef BIN
function _clap_dynamic_completer_NAME() {
local _CLAP_COMPLETE_INDEX=$(expr $CURRENT - 1)
local _CLAP_IFS=$'\n'
local completions=("${(@f)$( \
_CLAP_IFS="$_CLAP_IFS" \
_CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \
VAR="zsh" \
COMPLETER -- "${words[@]}" 2>/dev/null \
)}")
if [[ -n $completions ]]; then
local -a dirs=()
local -a other=()
local completion
for completion in $completions; do
local value="${completion%%:*}"
if [[ "$value" == */ ]]; then
local dir_no_slash="${value%/}"
if [[ "$completion" == *:* ]]; then
local desc="${completion#*:}"
dirs+=("$dir_no_slash:$desc")
else
dirs+=("$dir_no_slash")
fi
else
other+=("$completion")
fi
done
[[ -n $dirs ]] && _describe -V 'values' dirs -S '/' -r '/'
[[ -n $other ]] && _describe -V 'values' other
fi
}
compdef _clap_dynamic_completer_NAME BIN"#
.replace("NAME", &escaped_name)
.replace("COMPLETER", &completer)
.replace("BIN", &bin)
.replace("VAR", var);
writeln!(buf, "{script}")?;
Ok(())
}
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index: usize = std::env::var("_CLAP_COMPLETE_INDEX")
.ok()
.and_then(|i| i.parse().ok())
.unwrap_or_default();
let ifs: Option<String> = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok());
let mut args = args.clone();
if args.len() == index {
args.push("".into());
}
let completions = crate::engine::complete(cmd, args, index, current_dir)?;
for (i, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(
buf,
"{}",
Self::escape_value(&candidate.get_value().to_string_lossy())
)?;
if let Some(help) = candidate.get_help() {
write!(
buf,
":{}",
Self::escape_help(help.to_string().lines().next().unwrap_or_default())
)?;
}
}
Ok(())
}
}
impl Zsh {
fn escape_value(string: &str) -> String {
string.replace('\\', "\\\\").replace(':', "\\:")
}
fn escape_help(string: &str) -> String {
string.replace('\\', "\\\\")
}
}
#[cfg(test)]
mod tests {
use super::*;
use snapbox::IntoData as _;
use snapbox::assert_data_eq;
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn fish_env_completer_path_quoting_works() {
let get_fish_registration = |completer_bin: &str| {
let mut buf = Vec::new();
let fish = Fish;
fish.write_registration(
"IGNORED_VAR",
"ignored-name",
"/ignored/bin",
completer_bin,
&mut buf,
)
.expect("write_registration failed");
String::from_utf8(buf).expect("Invalid UTF-8")
};
let script = get_fish_registration("completer");
assert_data_eq!(
script.trim(),
snapbox::str![r#"complete [..] "([..] completer [..])""#]
);
let script = get_fish_registration("/path/completer");
assert_data_eq!(
script.trim(),
snapbox::str![r#"complete [..] "([..] /path/completer [..])""#]
);
let script = get_fish_registration("/path with a space/completer");
assert_data_eq!(
script.trim(),
snapbox::str![r#"complete [..] "([..] '/path with a space/completer' [..])""#]
);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
fn fish_env_completer_path_with_backslash() {
let mut buf = Vec::new();
Fish.write_registration("V", "n", "/ignored/bin", "/p/dyn\\amic/foo", &mut buf)
.expect("write_registration failed");
let script = String::from_utf8(buf).expect("Invalid UTF-8");
assert_data_eq!(
script,
snapbox::str![[r#"
complete --keep-order --exclusive --command /ignored/bin --arguments "(V=fish '/p/dyn\\\\amic/foo' -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
"#]]
.raw()
);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
fn fish_env_command_name_with_backslash() {
let mut buf = Vec::new();
Fish.write_registration("V", "n", "dyn\\amic", "/p/completer", &mut buf)
.expect("write_registration failed");
let script = String::from_utf8(buf).expect("Invalid UTF-8");
assert_data_eq!(
script,
snapbox::str![[r#"
complete --keep-order --exclusive --command 'dyn\\amic' --arguments "(V=fish /p/completer -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
"#]]
.raw()
);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
fn fish_env_completer_path_with_dollar() {
let mut buf = Vec::new();
Fish.write_registration("V", "n", "/ignored/bin", "/p/$var/c", &mut buf)
.expect("write_registration failed");
let script = String::from_utf8(buf).expect("Invalid UTF-8");
assert_data_eq!(
script,
snapbox::str![[r#"
complete --keep-order --exclusive --command /ignored/bin --arguments "(V=fish '/p/\$var/c' -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
"#]]
.raw()
);
}
}