Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Apr 22, 2022
1 parent 406bfbd commit 1b6ecd7
Show file tree
Hide file tree
Showing 5 changed files with 209 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"]
debug = ["clap/debug"]

[package.metadata.docs.rs]
Expand Down
140 changes: 140 additions & 0 deletions clap_complete/src/dynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! Complete commands within shells
/// Complete commands within bash
pub mod bash {
use std::io::Write;
use unicode_xid::UnicodeXID;

/// 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),
}

/// 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#"
# Run something, muting output or redirecting it to the debug stream
# depending on the value of _ARC_DEBUG.
# If UPPER_COMPLETE_USE_TEMPFILES is set, use tempfiles for IPC.
__clap_complete_run_NAME() {
if [[ -z "${UPPER_COMPLETE_USE_TEMPFILES-}" ]]; then
__clap_complete_run_inner_NAME "$@"
return
fi
local tmpfile="$(mktemp)"
CLAP_COMPLETE_STDOUT_FILE="$tmpfile" __clap_complete_run_inner_NAME "$@"
local code=$?
cat "$tmpfile"
rm "$tmpfile"
return $code
}
__clap_complete_run_inner_NAME() {
if [[ -z "${_ARC_DEBUG-}" ]]; then
"$@" 8>&1 9>&2 1>/dev/null 2>&1
else
"$@" 8>&1 9>&2 1>&9 2>&1
fi
}
_clap_complete_NAME() {
local IFS=$'\013'
local SUPPRESS_SPACE=0
if compopt +o nospace 2> /dev/null; then
SUPPRESS_SPACE=1
fi
COMPREPLY=( $(IFS="$IFS" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
COMP_TYPE="$COMP_TYPE" \
CLAP_COMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" \
CLAP_COMPLETE=1 \
CLAP_COMPLETE_SUPPRESS_SPACE=$SUPPRESS_SPACE \
__clap_complete_run_NAME COMPLETER) )
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(())
}

/// Complete the command specified by env setup by [`register`]
pub fn complete_env(buf: &mut dyn Write) -> Result<(), std::io::Error> {
let comp_line = std::env::var_os("COMP_LINE")
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "`COMP_LINE` not set"))?;
let comp_point = std::env::var_os("COMP_POINT").ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::Other, "`COMP_POINT` not set")
})?;
let comp_point = comp_point.to_str().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::Other, "`COMP_POINT` format is invalid")
})?;
let comp_point = comp_point.parse::<usize>().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("`COMP_POINT` format is invalid: {}", e),
)
})?;
complete(&comp_line, comp_point, buf)?;
Ok(())
}

/// Complete the command specified
pub fn complete(
comp_line: &std::ffi::OsStr,
comp_point: usize,
_buf: &mut dyn Write,
) -> Result<(), std::io::Error> {
let comp_line_raw = os_str_bytes::RawOsStr::new(comp_line);
let comp_line_bytes = comp_line_raw.as_raw_bytes();
let _comp_line_prefix_bytes = comp_line_bytes.get(..comp_point).unwrap_or(comp_line_bytes);
Ok(())
}
}
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);
}
45 changes: 45 additions & 0 deletions clap_complete/tests/snapshots/register_minimal.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

# Run something, muting output or redirecting it to the debug stream
# depending on the value of _ARC_DEBUG.
# If MY_APP_COMPLETE_USE_TEMPFILES is set, use tempfiles for IPC.
__clap_complete_run_my_app() {
if [[ -z "${MY_APP_COMPLETE_USE_TEMPFILES-}" ]]; then
__clap_complete_run_inner_my_app "$@"
return
fi
local tmpfile="$(mktemp)"
CLAP_COMPLETE_STDOUT_FILE="$tmpfile" __clap_complete_run_inner_my_app "$@"
local code=$?
cat "$tmpfile"
rm "$tmpfile"
return $code
}
__clap_complete_run_inner_my_app() {
if [[ -z "${_ARC_DEBUG-}" ]]; then
"$@" 8>&1 9>&2 1>/dev/null 2>&1
else
"$@" 8>&1 9>&2 1>&9 2>&1
fi
}
_clap_complete_my_app() {
local IFS=$'/013'
local SUPPRESS_SPACE=0
if compopt +o nospace 2> /dev/null; then
SUPPRESS_SPACE=1
fi
COMPREPLY=( $(IFS="$IFS" /
COMP_LINE="$COMP_LINE" /
COMP_POINT="$COMP_POINT" /
COMP_TYPE="$COMP_TYPE" /
CLAP_COMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" /
CLAP_COMPLETE=1 /
CLAP_COMPLETE_SUPPRESS_SPACE=$SUPPRESS_SPACE /
__clap_complete_run_my_app my-app) )
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 1b6ecd7

Please sign in to comment.