From 4bbcf6088fe852b15e1c6a3dbb64b4dd290feceb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 20:37:51 -0500 Subject: [PATCH 1/7] refactor(complete): Split dynamic into a directory --- clap_complete/src/{dynamic.rs => dynamic/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename clap_complete/src/{dynamic.rs => dynamic/mod.rs} (100%) diff --git a/clap_complete/src/dynamic.rs b/clap_complete/src/dynamic/mod.rs similarity index 100% rename from clap_complete/src/dynamic.rs rename to clap_complete/src/dynamic/mod.rs From e8622c5391deaf3f751ebca16fbbcaef6979868e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 20:39:34 -0500 Subject: [PATCH 2/7] refactor(complete): Split out bash dynamic code --- clap_complete/src/dynamic/bash.rs | 543 +++++++++++++++++++++++++++++ clap_complete/src/dynamic/mod.rs | 548 +----------------------------- 2 files changed, 544 insertions(+), 547 deletions(-) create mode 100644 clap_complete/src/dynamic/bash.rs diff --git a/clap_complete/src/dynamic/bash.rs b/clap_complete/src/dynamic/bash.rs new file mode 100644 index 00000000000..fbad9e6d9d4 --- /dev/null +++ b/clap_complete/src/dynamic/bash.rs @@ -0,0 +1,543 @@ +//! Complete commands within 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, + + #[arg( + long, + required = true, + value_name = "COMP_CWORD", + hide_short_help = true, + group = "complete" + )] + index: Option, + + #[arg(long, hide_short_help = true, group = "complete")] + ifs: Option, + + #[arg( + long = "type", + required = true, + hide_short_help = true, + group = "complete" + )] + comp_type: Option, + + #[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, +} + +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>, + 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::>() + .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 { + 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, + arg_index: usize, + _comp_type: CompType, + _trailing_space: bool, + current_dir: Option<&std::path::Path>, +) -> Result, 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, 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 { + 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 { + 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 { + 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::>(); + scs.sort(); + scs.dedup(); + scs +} diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index f25b4a48df8..b385cbf3bb8 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -1,549 +1,3 @@ //! 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, - - #[arg( - long, - required = true, - value_name = "COMP_CWORD", - hide_short_help = true, - group = "complete" - )] - index: Option, - - #[arg(long, hide_short_help = true, group = "complete")] - ifs: Option, - - #[arg( - long = "type", - required = true, - hide_short_help = true, - group = "complete" - )] - comp_type: Option, - - #[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, - } - - 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>, - 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::>() - .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 { - 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, - arg_index: usize, - _comp_type: CompType, - _trailing_space: bool, - current_dir: Option<&std::path::Path>, - ) -> Result, 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, 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 { - 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 { - 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 { - 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::>(); - scs.sort(); - scs.dedup(); - scs - } -} +pub mod bash; From 8e9ded2f6a484cd55142ce255677e9ce4578e02e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 20:42:46 -0500 Subject: [PATCH 3/7] fix(complete)!: Pull out generic completion code --- clap_complete/src/dynamic/bash.rs | 273 +----------------------------- clap_complete/src/dynamic/mod.rs | 260 ++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 269 deletions(-) diff --git a/clap_complete/src/dynamic/bash.rs b/clap_complete/src/dynamic/bash.rs index fbad9e6d9d4..39278239c43 100644 --- a/clap_complete/src/dynamic/bash.rs +++ b/clap_complete/src/dynamic/bash.rs @@ -1,10 +1,8 @@ //! Complete commands within 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)] @@ -86,8 +84,8 @@ impl CompleteCommand { } } 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) { + 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) => { @@ -97,14 +95,8 @@ impl CompleteCommand { } .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 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() { @@ -284,260 +276,3 @@ impl Default for CompType { Self::Normal } } - -/// Complete the command specified -pub fn complete( - cmd: &mut clap::Command, - args: Vec, - arg_index: usize, - _comp_type: CompType, - _trailing_space: bool, - current_dir: Option<&std::path::Path>, -) -> Result, 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, 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 { - 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 { - 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 { - 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::>(); - scs.sort(); - scs.dedup(); - scs -} diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index b385cbf3bb8..d9f28ac701e 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -1,3 +1,263 @@ //! Complete commands within shells +use std::ffi::OsStr; +use std::ffi::OsString; + +use clap_lex::OsStrExt as _; + pub mod bash; + +/// Complete the command specified +pub fn complete( + cmd: &mut clap::Command, + args: Vec, + arg_index: usize, + current_dir: Option<&std::path::Path>, +) -> Result, 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, 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 { + 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 { + 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 { + 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::>(); + scs.sort(); + scs.dedup(); + scs +} From e3f1ad932b5176bde534d1b350444a53bb25a48c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 21:06:29 -0500 Subject: [PATCH 4/7] refactor(complete): Generalize dynamic CLI interface --- clap_complete/src/dynamic/bash.rs | 96 +++++++++++++------------------ 1 file changed, 40 insertions(+), 56 deletions(-) diff --git a/clap_complete/src/dynamic/bash.rs b/clap_complete/src/dynamic/bash.rs index 39278239c43..b66e9da77ad 100644 --- a/clap_complete/src/dynamic/bash.rs +++ b/clap_complete/src/dynamic/bash.rs @@ -23,37 +23,6 @@ pub struct CompleteArgs { #[arg(long, required = true)] register: Option, - #[arg( - long, - required = true, - value_name = "COMP_CWORD", - hide_short_help = true, - group = "complete" - )] - index: Option, - - #[arg(long, hide_short_help = true, group = "complete")] - ifs: Option, - - #[arg( - long = "type", - required = true, - hide_short_help = true, - group = "complete" - )] - comp_type: Option, - - #[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, } @@ -83,17 +52,20 @@ impl CompleteCommand { 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 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 = std::env::var("_CLAP_COMPLETE_SPACE") + .ok() + .and_then(|i| i.parse().ok()); + let ifs: Option = 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())?; @@ -101,7 +73,7 @@ impl CompleteCommand { 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, "{}", ifs.as_deref().unwrap_or("\n"))?; } write!(&mut buf, "{}", completion.to_string_lossy())?; } @@ -165,17 +137,15 @@ pub fn register( let script = r#" _clap_complete_NAME() { - local IFS=$'\013' - local SUPPRESS_SPACE=0 + export _CLAP_COMPLETE_INDEX=${COMP_CWORD} + export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE} if compopt +o nospace 2> /dev/null; then - SUPPRESS_SPACE=1 - fi - if [[ ${SUPPRESS_SPACE} == 1 ]]; then - SPACE_ARG="--no-space" + export _CLAP_COMPLETE_SPACE=false else - SPACE_ARG="--space" + export _CLAP_COMPLETE_SPACE=true fi - COMPREPLY=( $("COMPLETER" complete --index ${COMP_CWORD} --type ${COMP_TYPE} ${SPACE_ARG} --ifs="$IFS" -- "${COMP_WORDS[@]}") ) + export _CLAP_COMPLETE_IFS=$'\013' + COMPREPLY=( $("COMPLETER" complete -- "${COMP_WORDS[@]}") ) if [[ $? != 0 ]]; then unset COMPREPLY elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then @@ -184,11 +154,11 @@ _clap_complete_NAME() { } complete OPTIONS -F _clap_complete_NAME EXECUTABLES "# - .replace("NAME", &escaped_name) - .replace("EXECUTABLES", &executables) - .replace("OPTIONS", options) - .replace("COMPLETER", &completer) - .replace("UPPER", &upper_name); + .replace("NAME", &escaped_name) + .replace("EXECUTABLES", &executables) + .replace("OPTIONS", options) + .replace("COMPLETER", &completer) + .replace("UPPER", &upper_name); writeln!(buf, "{script}")?; Ok(()) @@ -271,6 +241,20 @@ impl clap::ValueEnum for CompType { } } +impl std::str::FromStr for CompType { + type Err = String; + + fn from_str(s: &str) -> Result { + 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 From 278ae3ec635f970140f07b8ee460906ac61e7b52 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 21:14:48 -0500 Subject: [PATCH 5/7] refactor(complete): Pull out completer --- clap_complete/src/dynamic/completer.rs | 259 ++++++++++++++++++++++++ clap_complete/src/dynamic/mod.rs | 261 +------------------------ 2 files changed, 261 insertions(+), 259 deletions(-) create mode 100644 clap_complete/src/dynamic/completer.rs diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs new file mode 100644 index 00000000000..1a817b75705 --- /dev/null +++ b/clap_complete/src/dynamic/completer.rs @@ -0,0 +1,259 @@ +use std::ffi::OsStr; +use std::ffi::OsString; + +use clap_lex::OsStrExt as _; + +/// Complete the command specified +pub fn complete( + cmd: &mut clap::Command, + args: Vec, + arg_index: usize, + current_dir: Option<&std::path::Path>, +) -> Result, 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, 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 { + 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 { + 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 { + 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::>(); + scs.sort(); + scs.dedup(); + scs +} diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index d9f28ac701e..da62dd5970c 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -1,263 +1,6 @@ //! Complete commands within shells -use std::ffi::OsStr; -use std::ffi::OsString; - -use clap_lex::OsStrExt as _; - pub mod bash; +mod completer; -/// Complete the command specified -pub fn complete( - cmd: &mut clap::Command, - args: Vec, - arg_index: usize, - current_dir: Option<&std::path::Path>, -) -> Result, 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, 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 { - 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 { - 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 { - 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::>(); - scs.sort(); - scs.dedup(); - scs -} +pub use completer::*; From 830dd740ef1ae1f83b9887e107a15e5aef75d5f4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 21:19:54 -0500 Subject: [PATCH 6/7] feat(complete): Add dynamic-support shell enum --- clap_complete/src/dynamic/mod.rs | 4 +- clap_complete/src/dynamic/shells/mod.rs | 5 +++ clap_complete/src/dynamic/shells/shell.rs | 48 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 clap_complete/src/dynamic/shells/mod.rs create mode 100644 clap_complete/src/dynamic/shells/shell.rs diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index da62dd5970c..445295690e4 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -1,6 +1,8 @@ //! Complete commands within shells -pub mod bash; mod completer; +pub mod bash; +pub mod shells; + pub use completer::*; diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs new file mode 100644 index 00000000000..a56cbba0f10 --- /dev/null +++ b/clap_complete/src/dynamic/shells/mod.rs @@ -0,0 +1,5 @@ +//! Shell support + +mod shell; + +pub use shell::*; diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs new file mode 100644 index 00000000000..bde2757a5d0 --- /dev/null +++ b/clap_complete/src/dynamic/shells/shell.rs @@ -0,0 +1,48 @@ +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 { + 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 { + Some(match self { + Shell::Bash => PossibleValue::new("bash"), + }) + } +} From 00e92171832238e85ab7336a700598a6fb33effe Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 21:50:34 -0500 Subject: [PATCH 7/7] feat(complete)!: Open to new shells for dynamic completions --- clap_complete/examples/dynamic.rs | 4 +- clap_complete/src/dynamic/bash.rs | 262 ---------------------- clap_complete/src/dynamic/completer.rs | 24 +- clap_complete/src/dynamic/mod.rs | 1 - clap_complete/src/dynamic/shells/bash.rs | 123 ++++++++++ clap_complete/src/dynamic/shells/mod.rs | 75 +++++++ clap_complete/src/dynamic/shells/shell.rs | 33 +++ 7 files changed, 256 insertions(+), 266 deletions(-) delete mode 100644 clap_complete/src/dynamic/bash.rs create mode 100644 clap_complete/src/dynamic/shells/bash.rs diff --git a/clap_complete/examples/dynamic.rs b/clap_complete/examples/dynamic.rs index 5f1f30acd24..ccaf7d8ad2d 100644 --- a/clap_complete/examples/dynamic.rs +++ b/clap_complete/examples/dynamic.rs @@ -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 { diff --git a/clap_complete/src/dynamic/bash.rs b/clap_complete/src/dynamic/bash.rs deleted file mode 100644 index b66e9da77ad..00000000000 --- a/clap_complete/src/dynamic/bash.rs +++ /dev/null @@ -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, - - #[arg(raw = true, hide_short_help = true, group = "complete")] - comp_words: Vec, -} - -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 = std::env::var("_CLAP_COMPLETE_SPACE") - .ok() - .and_then(|i| i.parse().ok()); - let ifs: Option = 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>, - 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::>() - .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 { - 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 { - 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 - } -} diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 1a817b75705..1f03843ccdf 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -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, + 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", )) } diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index 445295690e4..f7c985704c9 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -2,7 +2,6 @@ mod completer; -pub mod bash; pub mod shells; pub use completer::*; diff --git a/clap_complete/src/dynamic/shells/bash.rs b/clap_complete/src/dynamic/shells/bash.rs new file mode 100644 index 00000000000..e6276eb0ce0 --- /dev/null +++ b/clap_complete/src/dynamic/shells/bash.rs @@ -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, + 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 = std::env::var("_CLAP_COMPLETE_SPACE") + .ok() + .and_then(|i| i.parse().ok()); + let ifs: Option = 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 { + 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 + } +} diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs index a56cbba0f10..f0ecc8c2629 100644 --- a/clap_complete/src/dynamic/shells/mod.rs +++ b/clap_complete/src/dynamic/shells/mod.rs @@ -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, + + #[arg(raw = true, hide_short_help = true, group = "complete")] + comp_words: Vec, +} + +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(()) + } +} diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs index bde2757a5d0..3de8c5c6842 100644 --- a/clap_complete/src/dynamic/shells/shell.rs +++ b/clap_complete/src/dynamic/shells/shell.rs @@ -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, + 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) + } +}