diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:19:13 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:19:13 +0000 |
commit | 218caa410aa38c29984be31a5229b9fa717560ee (patch) | |
tree | c54bd55eeb6e4c508940a30e94c0032fbd45d677 /vendor/clap_complete/src/dynamic.rs | |
parent | Releasing progress-linux version 1.67.1+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-218caa410aa38c29984be31a5229b9fa717560ee.tar.xz rustc-218caa410aa38c29984be31a5229b9fa717560ee.zip |
Merging upstream version 1.68.2+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/clap_complete/src/dynamic.rs')
-rw-r--r-- | vendor/clap_complete/src/dynamic.rs | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/vendor/clap_complete/src/dynamic.rs b/vendor/clap_complete/src/dynamic.rs new file mode 100644 index 000000000..929841ec8 --- /dev/null +++ b/vendor/clap_complete/src/dynamic.rs @@ -0,0 +1,551 @@ +//! 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)] + #[command(hide = true)] + #[allow(missing_docs)] + #[derive(Clone, Debug)] + pub enum CompleteCommand { + /// Register shell completions for this program + Complete(CompleteArgs), + } + + #[derive(clap::Args)] + #[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))] + #[allow(missing_docs)] + #[derive(Clone, Debug)] + pub struct CompleteArgs { + /// Path to write completion-registration to + #[arg(long, required = true)] + register: Option<std::path::PathBuf>, + + #[arg( + long, + required = true, + value_name = "COMP_CWORD", + hide_short_help = true, + group = "complete" + )] + index: Option<usize>, + + #[arg(long, hide_short_help = true, group = "complete")] + ifs: Option<String>, + + #[arg( + long = "type", + required = true, + hide_short_help = true, + group = "complete" + )] + comp_type: Option<CompType>, + + #[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<OsString>, + } + + impl CompleteCommand { + /// Process the completion request + pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible { + self.try_complete(cmd).unwrap_or_else(|e| e.exit()); + std::process::exit(0) + } + + /// Process the completion request + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + debug!("CompleteCommand::try_complete: {:?}", self); + let CompleteCommand::Complete(args) = self; + if let Some(out_path) = args.register.as_deref() { + let mut buf = Vec::new(); + let name = cmd.get_name(); + let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); + register(name, [bin], bin, &Behavior::default(), &mut buf)?; + if out_path == std::path::Path::new("-") { + std::io::stdout().write_all(&buf)?; + } else if out_path.is_dir() { + let out_path = out_path.join(file_name(name)); + std::fs::write(out_path, buf)?; + } else { + std::fs::write(out_path, buf)?; + } + } else { + let index = 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!("{}.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, Debug, PartialEq, Eq)] + #[non_exhaustive] + pub enum CompType { + /// Normal completion + Normal, + /// List completions after successive tabs + Successive, + /// List alternatives on partial word completion + Alternatives, + /// List completions if the word is not unmodified + Unmodified, + /// Menu completion + Menu, + } + + impl clap::ValueEnum for CompType { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::Normal, + Self::Successive, + Self::Alternatives, + Self::Unmodified, + Self::Menu, + ] + } + fn to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue> { + match self { + Self::Normal => { + let value = "9"; + debug_assert_eq!(b'\t'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("normal") + .help("Normal completion"), + ) + } + Self::Successive => { + let value = "63"; + debug_assert_eq!(b'?'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("successive") + .help("List completions after successive tabs"), + ) + } + Self::Alternatives => { + let value = "33"; + debug_assert_eq!(b'!'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("alternatives") + .help("List alternatives on partial word completion"), + ) + } + Self::Unmodified => { + let value = "64"; + debug_assert_eq!(b'@'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("unmodified") + .help("List completions if the word is not unmodified"), + ) + } + Self::Menu => { + let value = "37"; + debug_assert_eq!(b'%'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("menu") + .help("Menu completion"), + ) + } + } + } + } + + impl Default for CompType { + fn default() -> Self { + Self::Normal + } + } + + /// Complete the command specified + pub fn complete( + cmd: &mut clap::Command, + args: Vec<std::ffi::OsString>, + arg_index: usize, + _comp_type: CompType, + _trailing_space: bool, + current_dir: Option<&std::path::Path>, + ) -> Result<Vec<std::ffi::OsString>, 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(), + arg.to_value_os().as_raw_bytes() + ); + + 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<Vec<std::ffi::OsString>, 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_str_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, &clap_lex::RawOsStr>, + arg: &clap::Arg, + current_dir: Option<&std::path::Path>, + ) -> Vec<OsString> { + let mut values = Vec::new(); + debug!("complete_arg_value: arg={:?}, value={:?}", arg, 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) => clap_lex::RawOsStr::from_str(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: &clap_lex::RawOsStr, + current_dir: Option<&std::path::Path>, + is_wanted: impl Fn(&std::path::Path) -> bool, + ) -> Vec<OsString> { + 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((clap_lex::RawOsStr::from_str(""), value_os)); + let root = current_dir.join(existing.to_os_str()); + debug!("complete_path: root={:?}, prefix={:?}", root, prefix); + + for entry in std::fs::read_dir(&root) + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + { + let raw_file_name = clap_lex::RawOsString::new(entry.file_name()); + if !raw_file_name.starts_with_os(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<OsString> { + 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::<Vec<_>>(); + scs.sort(); + scs.dedup(); + scs + } +} |