Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Apr 25, 2022
1 parent 406bfbd commit 6d926c1
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 0 deletions.
5 changes: 5 additions & 0 deletions clap_complete/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ bench = false

[dependencies]
clap = { path = "../", version = "3.1.10", default-features = false, features = ["std"] }
clap_lex = { path = "../clap_lex", version = "0.1.0", optional = true }
os_str_bytes = { version = "6.0", default-features = false, features = ["raw_os_str"], optional = true }
shlex = { version = "1.1.0", optional = true }
unicode-xid = { version = "0.2.2", optional = true }

[dev-dependencies]
pretty_assertions = "1.0"
Expand All @@ -44,6 +48,7 @@ clap = { path = "../", version = "3.1.10", default-features = false, features =

[features]
default = []
unstable-dynamic = ["clap_lex", "shlex", "unicode-xid", "os_str_bytes", "clap/derive"]
debug = ["clap/debug"]

[package.metadata.docs.rs]
Expand Down
252 changes: 252 additions & 0 deletions clap_complete/src/dynamic.rs
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![])
}
}
3 changes: 3 additions & 0 deletions clap_complete/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ pub use generator::generate;
pub use generator::generate_to;
pub use generator::Generator;
pub use shells::Shell;

#[cfg(feature = "unstable-dynamic")]
pub mod dynamic;
16 changes: 16 additions & 0 deletions clap_complete/tests/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,19 @@ fn value_hint() {
name,
);
}

#[cfg(feature = "unstable-dynamic")]
#[test]
fn register_minimal() {
let name = "my-app";
let executables = [name];
let completer = name;
let behavior = clap_complete::dynamic::bash::Behavior::Minimal;

let mut buf = Vec::new();
clap_complete::dynamic::bash::register(name, executables, completer, &behavior, &mut buf)
.unwrap();
snapbox::Assert::new()
.action_env("SNAPSHOTS")
.matches_path("tests/snapshots/register_minimal.bash", buf);
}
21 changes: 21 additions & 0 deletions clap_complete/tests/snapshots/register_minimal.bash
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

0 comments on commit 6d926c1

Please sign in to comment.