mirror of
https://github.com/clap-rs/clap
synced 2024-11-10 06:44:16 +00:00
feat(complete)!: Open to new shells for dynamic completions
This commit is contained in:
parent
830dd740ef
commit
00e9217183
7 changed files with 256 additions and 266 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,262 +0,0 @@
|
|||
//! Complete commands within bash
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
|
||||
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(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: 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 current_dir = std::env::current_dir().ok();
|
||||
let completions =
|
||||
super::complete(cmd, args.comp_words.clone(), index, current_dir.as_deref())?;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
for (i, completion) in completions.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(&mut buf, "{}", 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() {
|
||||
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 -- "${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 std::str::FromStr for CompType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use clap::ValueEnum as _;
|
||||
for variant in Self::value_variants() {
|
||||
if variant.to_possible_value().unwrap().matches(s, false) {
|
||||
return Ok(*variant);
|
||||
}
|
||||
}
|
||||
Err(format!("invalid variant: {s}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CompType {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
|
@ -3,6 +3,28 @@ 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,
|
||||
|
@ -58,7 +80,7 @@ pub fn complete(
|
|||
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"No completion generated",
|
||||
"no completion generated",
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
mod completer;
|
||||
|
||||
pub mod bash;
|
||||
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
|
||||
}
|
||||
}
|
|
@ -1,5 +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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,3 +46,36 @@ impl ValueEnum for Shell {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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