mirror of
https://github.com/clap-rs/clap
synced 2024-11-10 14:54:15 +00:00
Merge pull request #5018 from epage/dynamiite
feat(complete)!: Allow alternative shells for dynamic completions
This commit is contained in:
commit
0951f93467
7 changed files with 574 additions and 551 deletions
|
@ -16,14 +16,14 @@ fn command() -> clap::Command {
|
|||
.value_parser(["json", "yaml", "toml"]),
|
||||
)
|
||||
.args_conflicts_with_subcommands(true);
|
||||
clap_complete::dynamic::bash::CompleteCommand::augment_subcommands(cmd)
|
||||
clap_complete::dynamic::shells::CompleteCommand::augment_subcommands(cmd)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cmd = command();
|
||||
let matches = cmd.get_matches();
|
||||
if let Ok(completions) =
|
||||
clap_complete::dynamic::bash::CompleteCommand::from_arg_matches(&matches)
|
||||
clap_complete::dynamic::shells::CompleteCommand::from_arg_matches(&matches)
|
||||
{
|
||||
completions.complete(&mut command());
|
||||
} else {
|
||||
|
|
|
@ -1,549 +0,0 @@
|
|||
//! 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
|
||||
}
|
||||
}
|
281
clap_complete/src/dynamic/completer.rs
Normal file
281
clap_complete/src/dynamic/completer.rs
Normal file
|
@ -0,0 +1,281 @@
|
|||
use std::ffi::OsStr;
|
||||
use std::ffi::OsString;
|
||||
|
||||
use clap_lex::OsStrExt as _;
|
||||
|
||||
/// Shell-specific completions
|
||||
pub trait Completer {
|
||||
/// The recommended file name for the registration code
|
||||
fn file_name(&self, name: &str) -> String;
|
||||
/// Register for completions
|
||||
fn write_registration(
|
||||
&self,
|
||||
name: &str,
|
||||
bin: &str,
|
||||
completer: &str,
|
||||
buf: &mut dyn std::io::Write,
|
||||
) -> Result<(), std::io::Error>;
|
||||
/// Complete the command
|
||||
fn write_complete(
|
||||
&self,
|
||||
cmd: &mut clap::Command,
|
||||
args: Vec<std::ffi::OsString>,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
buf: &mut dyn std::io::Write,
|
||||
) -> Result<(), std::io::Error>;
|
||||
}
|
||||
|
||||
/// Complete the command specified
|
||||
pub fn complete(
|
||||
cmd: &mut clap::Command,
|
||||
args: Vec<std::ffi::OsString>,
|
||||
arg_index: usize,
|
||||
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
|
||||
}
|
7
clap_complete/src/dynamic/mod.rs
Normal file
7
clap_complete/src/dynamic/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
//! Complete commands within shells
|
||||
|
||||
mod completer;
|
||||
|
||||
pub mod shells;
|
||||
|
||||
pub use completer::*;
|
123
clap_complete/src/dynamic/shells/bash.rs
Normal file
123
clap_complete/src/dynamic/shells/bash.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use unicode_xid::UnicodeXID as _;
|
||||
|
||||
/// Bash completions
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Bash;
|
||||
|
||||
impl crate::dynamic::Completer for Bash {
|
||||
fn file_name(&self, name: &str) -> String {
|
||||
format!("{name}.bash")
|
||||
}
|
||||
fn write_registration(
|
||||
&self,
|
||||
name: &str,
|
||||
bin: &str,
|
||||
completer: &str,
|
||||
buf: &mut dyn std::io::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 completer = shlex::quote(completer);
|
||||
|
||||
let script = r#"
|
||||
_clap_complete_NAME() {
|
||||
export _CLAP_COMPLETE_INDEX=${COMP_CWORD}
|
||||
export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE}
|
||||
if compopt +o nospace 2> /dev/null; then
|
||||
export _CLAP_COMPLETE_SPACE=false
|
||||
else
|
||||
export _CLAP_COMPLETE_SPACE=true
|
||||
fi
|
||||
export _CLAP_COMPLETE_IFS=$'\013'
|
||||
COMPREPLY=( $("COMPLETER" complete --shell bash -- "${COMP_WORDS[@]}") )
|
||||
if [[ $? != 0 ]]; then
|
||||
unset COMPREPLY
|
||||
elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
|
||||
compopt -o nospace
|
||||
fi
|
||||
}
|
||||
complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
|
||||
"#
|
||||
.replace("NAME", &escaped_name)
|
||||
.replace("BIN", bin)
|
||||
.replace("COMPLETER", &completer)
|
||||
.replace("UPPER", &upper_name);
|
||||
|
||||
writeln!(buf, "{script}")?;
|
||||
Ok(())
|
||||
}
|
||||
fn write_complete(
|
||||
&self,
|
||||
cmd: &mut clap::Command,
|
||||
args: Vec<std::ffi::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_COMPLETE_IFS")
|
||||
.ok()
|
||||
.and_then(|i| i.parse().ok());
|
||||
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;
|
||||
|
||||
for (i, completion) in completions.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
|
||||
}
|
||||
write!(buf, "{}", completion.to_string_lossy())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of completion attempted that caused a completion function to be called
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
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 std::str::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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CompType {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
80
clap_complete/src/dynamic/shells/mod.rs
Normal file
80
clap_complete/src/dynamic/shells/mod.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
//! Shell support
|
||||
|
||||
mod bash;
|
||||
mod shell;
|
||||
|
||||
pub use bash::*;
|
||||
pub use shell::*;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write as _;
|
||||
|
||||
use crate::dynamic::Completer as _;
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CompleteCommand {
|
||||
/// Register shell completions for this program
|
||||
#[command(hide = true)]
|
||||
Complete(CompleteArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompleteArgs {
|
||||
/// Specify shell to complete for
|
||||
#[arg(long)]
|
||||
shell: Shell,
|
||||
|
||||
/// Path to write completion-registration to
|
||||
#[arg(long, required = true)]
|
||||
register: Option<std::path::PathBuf>,
|
||||
|
||||
#[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());
|
||||
args.shell.write_registration(name, bin, bin, &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(args.shell.file_name(name));
|
||||
std::fs::write(out_path, buf)?;
|
||||
} else {
|
||||
std::fs::write(out_path, buf)?;
|
||||
}
|
||||
} else {
|
||||
let current_dir = std::env::current_dir().ok();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
args.shell.write_complete(
|
||||
cmd,
|
||||
args.comp_words.clone(),
|
||||
current_dir.as_deref(),
|
||||
&mut buf,
|
||||
)?;
|
||||
std::io::stdout().write_all(&buf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
81
clap_complete/src/dynamic/shells/shell.rs
Normal file
81
clap_complete/src/dynamic/shells/shell.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::PossibleValue;
|
||||
use clap::ValueEnum;
|
||||
|
||||
/// Shell with auto-generated completion script available.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Shell {
|
||||
/// Bourne Again SHell (bash)
|
||||
Bash,
|
||||
}
|
||||
|
||||
impl Display for Shell {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_possible_value()
|
||||
.expect("no values are skipped")
|
||||
.get_name()
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Shell {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
for variant in Self::value_variants() {
|
||||
if variant.to_possible_value().unwrap().matches(s, false) {
|
||||
return Ok(*variant);
|
||||
}
|
||||
}
|
||||
Err(format!("invalid variant: {s}"))
|
||||
}
|
||||
}
|
||||
|
||||
// Hand-rolled so it can work even when `derive` feature is disabled
|
||||
impl ValueEnum for Shell {
|
||||
fn value_variants<'a>() -> &'a [Self] {
|
||||
&[Shell::Bash]
|
||||
}
|
||||
|
||||
fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
|
||||
Some(match self {
|
||||
Shell::Bash => PossibleValue::new("bash"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
fn completer(&self) -> &dyn crate::dynamic::Completer {
|
||||
match self {
|
||||
Self::Bash => &super::Bash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::dynamic::Completer for Shell {
|
||||
fn file_name(&self, name: &str) -> String {
|
||||
self.completer().file_name(name)
|
||||
}
|
||||
fn write_registration(
|
||||
&self,
|
||||
name: &str,
|
||||
bin: &str,
|
||||
completer: &str,
|
||||
buf: &mut dyn std::io::Write,
|
||||
) -> Result<(), std::io::Error> {
|
||||
self.completer()
|
||||
.write_registration(name, bin, completer, buf)
|
||||
}
|
||||
fn write_complete(
|
||||
&self,
|
||||
cmd: &mut clap::Command,
|
||||
args: Vec<std::ffi::OsString>,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
buf: &mut dyn std::io::Write,
|
||||
) -> Result<(), std::io::Error> {
|
||||
self.completer().write_complete(cmd, args, current_dir, buf)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue