blob: f25b4a48df8f03e8473b2a6b9e47c73b201b672d [file] [log] [blame]
//! Complete commands within shells
/// Complete commands within bash
pub mod bash {
use std::ffi::OsStr;
use std::ffi::OsString;
use std::io::Write;
use clap_lex::OsStrExt as _;
use unicode_xid::UnicodeXID;
#[derive(clap::Subcommand)]
#[command(hide = true)]
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub enum CompleteCommand {
/// Register shell completions for this program
Complete(CompleteArgs),
}
#[derive(clap::Args)]
#[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))]
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct CompleteArgs {
/// Path to write completion-registration to
#[arg(long, required = true)]
register: Option<std::path::PathBuf>,
#[arg(
long,
required = true,
value_name = "COMP_CWORD",
hide_short_help = true,
group = "complete"
)]
index: Option<usize>,
#[arg(long, hide_short_help = true, group = "complete")]
ifs: Option<String>,
#[arg(
long = "type",
required = true,
hide_short_help = true,
group = "complete"
)]
comp_type: Option<CompType>,
#[arg(long, hide_short_help = true, group = "complete")]
space: bool,
#[arg(
long,
conflicts_with = "space",
hide_short_help = true,
group = "complete"
)]
no_space: bool,
#[arg(raw = true, hide_short_help = true, group = "complete")]
comp_words: Vec<OsString>,
}
impl CompleteCommand {
/// Process the completion request
pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
self.try_complete(cmd).unwrap_or_else(|e| e.exit());
std::process::exit(0)
}
/// Process the completion request
pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
debug!("CompleteCommand::try_complete: {self:?}");
let CompleteCommand::Complete(args) = self;
if let Some(out_path) = args.register.as_deref() {
let mut buf = Vec::new();
let name = cmd.get_name();
let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
register(name, [bin], bin, &Behavior::default(), &mut buf)?;
if out_path == std::path::Path::new("-") {
std::io::stdout().write_all(&buf)?;
} else if out_path.is_dir() {
let out_path = out_path.join(file_name(name));
std::fs::write(out_path, buf)?;
} else {
std::fs::write(out_path, buf)?;
}
} else {
let index = args.index.unwrap_or_default();
let comp_type = args.comp_type.unwrap_or_default();
let space = match (args.space, args.no_space) {
(true, false) => Some(true),
(false, true) => Some(false),
(true, true) => {
unreachable!("`--space` and `--no-space` set, clap should prevent this")
}
(false, false) => None,
}
.unwrap();
let current_dir = std::env::current_dir().ok();
let completions = complete(
cmd,
args.comp_words.clone(),
index,
comp_type,
space,
current_dir.as_deref(),
)?;
let mut buf = Vec::new();
for (i, completion) in completions.iter().enumerate() {
if i != 0 {
write!(&mut buf, "{}", args.ifs.as_deref().unwrap_or("\n"))?;
}
write!(&mut buf, "{}", completion.to_string_lossy())?;
}
std::io::stdout().write_all(&buf)?;
}
Ok(())
}
}
/// The recommended file name for the registration code
pub fn file_name(name: &str) -> String {
format!("{name}.bash")
}
/// Define the completion behavior
pub enum Behavior {
/// Bare bones behavior
Minimal,
/// Fallback to readline behavior when no matches are generated
Readline,
/// Customize bash's completion behavior
Custom(String),
}
impl Default for Behavior {
fn default() -> Self {
Self::Readline
}
}
/// Generate code to register the dynamic completion
pub fn register(
name: &str,
executables: impl IntoIterator<Item = impl AsRef<str>>,
completer: &str,
behavior: &Behavior,
buf: &mut dyn Write,
) -> Result<(), std::io::Error> {
let escaped_name = name.replace('-', "_");
debug_assert!(
escaped_name.chars().all(|c| c.is_xid_continue()),
"`name` must be an identifier, got `{escaped_name}`"
);
let mut upper_name = escaped_name.clone();
upper_name.make_ascii_uppercase();
let executables = executables
.into_iter()
.map(|s| shlex::quote(s.as_ref()).into_owned())
.collect::<Vec<_>>()
.join(" ");
let options = match behavior {
Behavior::Minimal => "-o nospace -o bashdefault",
Behavior::Readline => "-o nospace -o default -o bashdefault",
Behavior::Custom(c) => c.as_str(),
};
let completer = shlex::quote(completer);
let script = r#"
_clap_complete_NAME() {
local IFS=$'\013'
local SUPPRESS_SPACE=0
if compopt +o nospace 2> /dev/null; then
SUPPRESS_SPACE=1
fi
if [[ ${SUPPRESS_SPACE} == 1 ]]; then
SPACE_ARG="--no-space"
else
SPACE_ARG="--space"
fi
COMPREPLY=( $("COMPLETER" complete --index ${COMP_CWORD} --type ${COMP_TYPE} ${SPACE_ARG} --ifs="$IFS" -- "${COMP_WORDS[@]}") )
if [[ $? != 0 ]]; then
unset COMPREPLY
elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
compopt -o nospace
fi
}
complete OPTIONS -F _clap_complete_NAME EXECUTABLES
"#
.replace("NAME", &escaped_name)
.replace("EXECUTABLES", &executables)
.replace("OPTIONS", options)
.replace("COMPLETER", &completer)
.replace("UPPER", &upper_name);
writeln!(buf, "{script}")?;
Ok(())
}
/// Type of completion attempted that caused a completion function to be called
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompType {
/// Normal completion
Normal,
/// List completions after successive tabs
Successive,
/// List alternatives on partial word completion
Alternatives,
/// List completions if the word is not unmodified
Unmodified,
/// Menu completion
Menu,
}
impl clap::ValueEnum for CompType {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Normal,
Self::Successive,
Self::Alternatives,
Self::Unmodified,
Self::Menu,
]
}
fn to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue> {
match self {
Self::Normal => {
let value = "9";
debug_assert_eq!(b'\t'.to_string(), value);
Some(
clap::builder::PossibleValue::new(value)
.alias("normal")
.help("Normal completion"),
)
}
Self::Successive => {
let value = "63";
debug_assert_eq!(b'?'.to_string(), value);
Some(
clap::builder::PossibleValue::new(value)
.alias("successive")
.help("List completions after successive tabs"),
)
}
Self::Alternatives => {
let value = "33";
debug_assert_eq!(b'!'.to_string(), value);
Some(
clap::builder::PossibleValue::new(value)
.alias("alternatives")
.help("List alternatives on partial word completion"),
)
}
Self::Unmodified => {
let value = "64";
debug_assert_eq!(b'@'.to_string(), value);
Some(
clap::builder::PossibleValue::new(value)
.alias("unmodified")
.help("List completions if the word is not unmodified"),
)
}
Self::Menu => {
let value = "37";
debug_assert_eq!(b'%'.to_string(), value);
Some(
clap::builder::PossibleValue::new(value)
.alias("menu")
.help("Menu completion"),
)
}
}
}
}
impl Default for CompType {
fn default() -> Self {
Self::Normal
}
}
/// Complete the command specified
pub fn complete(
cmd: &mut clap::Command,
args: Vec<std::ffi::OsString>,
arg_index: usize,
_comp_type: CompType,
_trailing_space: bool,
current_dir: Option<&std::path::Path>,
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
cmd.build();
let raw_args = clap_lex::RawArgs::new(args.into_iter());
let mut cursor = raw_args.cursor();
let mut target_cursor = raw_args.cursor();
raw_args.seek(
&mut target_cursor,
clap_lex::SeekFrom::Start(arg_index as u64),
);
// As we loop, `cursor` will always be pointing to the next item
raw_args.next_os(&mut target_cursor);
// TODO: Multicall support
if !cmd.is_no_binary_name_set() {
raw_args.next_os(&mut cursor);
}
let mut current_cmd = &*cmd;
let mut pos_index = 1;
let mut is_escaped = false;
while let Some(arg) = raw_args.next(&mut cursor) {
if cursor == target_cursor {
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
}
debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),);
if let Ok(value) = arg.to_value() {
if let Some(next_cmd) = current_cmd.find_subcommand(value) {
current_cmd = next_cmd;
pos_index = 0;
continue;
}
}
if is_escaped {
pos_index += 1;
} else if arg.is_escape() {
is_escaped = true;
} else if let Some(_long) = arg.to_long() {
} else if let Some(_short) = arg.to_short() {
} else {
pos_index += 1;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"No completion generated",
))
}
fn complete_arg(
arg: &clap_lex::ParsedArg<'_>,
cmd: &clap::Command,
current_dir: Option<&std::path::Path>,
pos_index: usize,
is_escaped: bool,
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
debug!(
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
arg,
cmd.get_name(),
current_dir,
pos_index,
is_escaped
);
let mut completions = Vec::new();
if !is_escaped {
if let Some((flag, value)) = arg.to_long() {
if let Ok(flag) = flag {
if let Some(value) = value {
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag))
{
completions.extend(
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
.into_iter()
.map(|os| {
// HACK: Need better `OsStr` manipulation
format!("--{}={}", flag, os.to_string_lossy()).into()
}),
)
}
} else {
completions.extend(
crate::generator::utils::longs_and_visible_aliases(cmd)
.into_iter()
.filter_map(|f| {
f.starts_with(flag).then(|| format!("--{f}").into())
}),
);
}
}
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
// HACK: Assuming knowledge of is_escape / is_stdio
completions.extend(
crate::generator::utils::longs_and_visible_aliases(cmd)
.into_iter()
.map(|f| format!("--{f}").into()),
);
}
if arg.is_empty() || arg.is_stdio() || arg.is_short() {
// HACK: Assuming knowledge of is_stdio
completions.extend(
crate::generator::utils::shorts_and_visible_aliases(cmd)
.into_iter()
// HACK: Need better `OsStr` manipulation
.map(|f| format!("{}{}", arg.to_value_os().to_string_lossy(), f).into()),
);
}
}
if let Some(positional) = cmd
.get_positionals()
.find(|p| p.get_index() == Some(pos_index))
{
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
}
if let Ok(value) = arg.to_value() {
completions.extend(complete_subcommand(value, cmd));
}
Ok(completions)
}
fn complete_arg_value(
value: Result<&str, &OsStr>,
arg: &clap::Arg,
current_dir: Option<&std::path::Path>,
) -> Vec<OsString> {
let mut values = Vec::new();
debug!("complete_arg_value: arg={arg:?}, value={value:?}");
if let Some(possible_values) = crate::generator::utils::possible_values(arg) {
if let Ok(value) = value {
values.extend(possible_values.into_iter().filter_map(|p| {
let name = p.get_name();
name.starts_with(value).then(|| name.into())
}));
}
} else {
let value_os = match value {
Ok(value) => OsStr::new(value),
Err(value_os) => value_os,
};
match arg.get_value_hint() {
clap::ValueHint::Other => {
// Should not complete
}
clap::ValueHint::Unknown | clap::ValueHint::AnyPath => {
values.extend(complete_path(value_os, current_dir, |_| true));
}
clap::ValueHint::FilePath => {
values.extend(complete_path(value_os, current_dir, |p| p.is_file()));
}
clap::ValueHint::DirPath => {
values.extend(complete_path(value_os, current_dir, |p| p.is_dir()));
}
clap::ValueHint::ExecutablePath => {
use is_executable::IsExecutable;
values.extend(complete_path(value_os, current_dir, |p| p.is_executable()));
}
clap::ValueHint::CommandName
| clap::ValueHint::CommandString
| clap::ValueHint::CommandWithArguments
| clap::ValueHint::Username
| clap::ValueHint::Hostname
| clap::ValueHint::Url
| clap::ValueHint::EmailAddress => {
// No completion implementation
}
_ => {
// Safe-ish fallback
values.extend(complete_path(value_os, current_dir, |_| true));
}
}
values.sort();
}
values
}
fn complete_path(
value_os: &OsStr,
current_dir: Option<&std::path::Path>,
is_wanted: impl Fn(&std::path::Path) -> bool,
) -> Vec<OsString> {
let mut completions = Vec::new();
let current_dir = match current_dir {
Some(current_dir) => current_dir,
None => {
// Can't complete without a `current_dir`
return Vec::new();
}
};
let (existing, prefix) = value_os
.split_once("\\")
.unwrap_or((OsStr::new(""), value_os));
let root = current_dir.join(existing);
debug!("complete_path: root={root:?}, prefix={prefix:?}");
let prefix = prefix.to_string_lossy();
for entry in std::fs::read_dir(&root)
.ok()
.into_iter()
.flatten()
.filter_map(Result::ok)
{
let raw_file_name = OsString::from(entry.file_name());
if !raw_file_name.starts_with(&prefix) {
continue;
}
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
let path = entry.path();
let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
suggestion.push(""); // Ensure trailing `/`
completions.push(suggestion.as_os_str().to_owned());
} else {
let path = entry.path();
if is_wanted(&path) {
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
completions.push(suggestion.as_os_str().to_owned());
}
}
}
completions
}
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
cmd.get_name(),
value
);
let mut scs = crate::generator::utils::all_subcommands(cmd)
.into_iter()
.filter(|x| x.0.starts_with(value))
.map(|x| OsString::from(&x.0))
.collect::<Vec<_>>();
scs.sort();
scs.dedup();
scs
}
}