-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
//! Complete commands within shells | ||
/// Complete commands within bash | ||
pub mod bash { | ||
use std::ffi::OsString; | ||
use std::io::Write; | ||
|
||
use unicode_xid::UnicodeXID; | ||
|
||
#[derive(clap::Subcommand)] | ||
#[clap(hide = true)] | ||
#[allow(missing_docs)] | ||
pub enum CompleteCommand { | ||
/// Register shell completions for this program | ||
Complete(CompleteArgs), | ||
} | ||
|
||
#[derive(clap::Args)] | ||
#[clap(group = clap::ArgGroup::new("complete").multiple(true))] | ||
#[allow(missing_docs)] | ||
pub struct CompleteArgs { | ||
/// Path to write completion-registration to | ||
#[clap(long, required = true, conflicts_with = "complete", parse(from_os_str))] | ||
register: Option<std::path::PathBuf>, | ||
|
||
#[clap( | ||
long, | ||
required = true, | ||
value_name = "COMP_CWORD", | ||
hide_short_help = true, | ||
group = "complete" | ||
)] | ||
index: Option<usize>, | ||
|
||
#[clap( | ||
long, | ||
required = true, | ||
value_name = "COMP_CWORD", | ||
hide_short_help = true, | ||
group = "complete" | ||
)] | ||
ifs: Option<String>, | ||
|
||
#[clap( | ||
long = "type", | ||
required = true, | ||
arg_enum, | ||
hide_short_help = true, | ||
group = "complete" | ||
)] | ||
comp_type: Option<CompType>, | ||
|
||
#[clap(long, hide_short_help = true, group = "complete")] | ||
space: bool, | ||
|
||
#[clap( | ||
long, | ||
conflicts_with = "space", | ||
hide_short_help = true, | ||
group = "complete" | ||
)] | ||
no_space: bool, | ||
|
||
#[clap(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::Result<()> { | ||
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(cmd.get_name()); | ||
register(name, [bin], bin, &Behavior::default(), &mut buf)?; | ||
if out_path == std::path::Path::new("-") { | ||
std::io::stdout().write(&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 completions = complete(cmd, &args.comp_words, index, comp_type, space)?; | ||
|
||
let mut buf = Vec::new(); | ||
for completion in &completions { | ||
write!(&mut buf, "{}", completion.to_string_lossy())?; | ||
write!(&mut buf, "{}", args.ifs.as_deref().unwrap_or("\n"))?; | ||
} | ||
std::io::stdout().write(&buf)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
/// The recommended file name for the registration code | ||
pub fn file_name(name: &str) -> String { | ||
format!("{}.bash", name) | ||
} | ||
|
||
/// 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, PartialEq, Eq, clap::ArgEnum)] | ||
#[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 CompType { | ||
/// Parse bash-provided command-line argument value into a `CompType` | ||
pub fn from_arg(arg: &std::ffi::OsStr) -> Option<Self> { | ||
let t = match arg.to_str()?.parse::<u8>().ok()? { | ||
b'\t' => Self::Normal, | ||
b'?' => Self::Successive, | ||
b'!' => Self::Alternatives, | ||
b'@' => Self::Unmodified, | ||
b'%' => Self::Menu, | ||
_ => { | ||
return None; | ||
} | ||
}; | ||
Some(t) | ||
} | ||
} | ||
|
||
impl Default for CompType { | ||
fn default() -> Self { | ||
Self::Normal | ||
} | ||
} | ||
|
||
/// Complete the command specified | ||
pub fn complete( | ||
cmd: &mut clap::Command, | ||
_args: &[std::ffi::OsString], | ||
_arg_index: usize, | ||
_comp_type: CompType, | ||
_trailing_space: bool, | ||
) -> Result<Vec<std::ffi::OsString>, std::io::Error> { | ||
cmd.build(); | ||
Ok(vec![]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
|
||
_clap_complete_my_app() { | ||
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=( $("my-app" 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 -o nospace -o bashdefault -F _clap_complete_my_app my-app | ||
|