From 8e9ded2f6a484cd55142ce255677e9ce4578e02e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 18 Jul 2023 20:42:46 -0500 Subject: [PATCH] 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 +}