| use std::any::type_name; |
| use std::ffi::OsStr; |
| use std::sync::Arc; |
| |
| use clap::builder::ArgExt; |
| use clap_lex::OsStrExt as _; |
| |
| use super::CompletionCandidate; |
| |
| /// Extend [`Arg`][clap::Arg] with a [`ValueCandidates`] |
| /// |
| /// # Example |
| /// |
| /// ```rust |
| /// use clap::Parser; |
| /// use clap_complete::engine::{ArgValueCandidates, CompletionCandidate}; |
| /// |
| /// #[derive(Debug, Parser)] |
| /// struct Cli { |
| /// #[arg(long, add = ArgValueCandidates::new(|| { vec![ |
| /// CompletionCandidate::new("foo"), |
| /// CompletionCandidate::new("bar"), |
| /// CompletionCandidate::new("baz")] }))] |
| /// custom: Option<String>, |
| /// } |
| /// ``` |
| #[derive(Clone)] |
| pub struct ArgValueCandidates(Arc<dyn ValueCandidates>); |
| |
| impl ArgValueCandidates { |
| /// Create a new `ArgValueCandidates` with a custom completer |
| pub fn new<C>(completer: C) -> Self |
| where |
| C: ValueCandidates + 'static, |
| { |
| Self(Arc::new(completer)) |
| } |
| |
| /// All potential candidates for an argument. |
| /// |
| /// See [`CompletionCandidate`] for more information. |
| pub fn candidates(&self) -> Vec<CompletionCandidate> { |
| self.0.candidates() |
| } |
| } |
| |
| impl std::fmt::Debug for ArgValueCandidates { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.write_str(type_name::<Self>()) |
| } |
| } |
| |
| impl ArgExt for ArgValueCandidates {} |
| |
| /// User-provided completion candidates for an [`Arg`][clap::Arg], see [`ArgValueCandidates`] |
| /// |
| /// This is useful when predefined value hints are not enough. |
| pub trait ValueCandidates: Send + Sync { |
| /// All potential candidates for an argument. |
| /// |
| /// See [`CompletionCandidate`] for more information. |
| fn candidates(&self) -> Vec<CompletionCandidate>; |
| } |
| |
| impl<F> ValueCandidates for F |
| where |
| F: Fn() -> Vec<CompletionCandidate> + Send + Sync, |
| { |
| fn candidates(&self) -> Vec<CompletionCandidate> { |
| self() |
| } |
| } |
| |
| /// Extend [`Arg`][clap::Arg] with a completer |
| /// |
| /// # Example |
| /// |
| /// ```rust |
| /// use clap::Parser; |
| /// use clap_complete::engine::{ArgValueCompleter, CompletionCandidate}; |
| /// |
| /// fn custom_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { |
| /// let mut completions = vec![]; |
| /// let Some(current) = current.to_str() else { |
| /// return completions; |
| /// }; |
| /// |
| /// if "foo".starts_with(current) { |
| /// completions.push(CompletionCandidate::new("foo")); |
| /// } |
| /// if "bar".starts_with(current) { |
| /// completions.push(CompletionCandidate::new("bar")); |
| /// } |
| /// if "baz".starts_with(current) { |
| /// completions.push(CompletionCandidate::new("baz")); |
| /// } |
| /// completions |
| /// } |
| /// |
| /// #[derive(Debug, Parser)] |
| /// struct Cli { |
| /// #[arg(long, add = ArgValueCompleter::new(custom_completer))] |
| /// custom: Option<String>, |
| /// } |
| /// ``` |
| #[derive(Clone)] |
| pub struct ArgValueCompleter(Arc<dyn ValueCompleter>); |
| |
| impl ArgValueCompleter { |
| /// Create a new `ArgValueCompleter` with a custom completer |
| pub fn new<C>(completer: C) -> Self |
| where |
| C: ValueCompleter + 'static, |
| { |
| Self(Arc::new(completer)) |
| } |
| |
| /// Candidates that match `current` |
| /// |
| /// See [`CompletionCandidate`] for more information. |
| pub fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> { |
| self.0.complete(current) |
| } |
| } |
| |
| impl std::fmt::Debug for ArgValueCompleter { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.write_str(type_name::<Self>()) |
| } |
| } |
| |
| impl ArgExt for ArgValueCompleter {} |
| |
| /// User-provided completion candidates for an [`Arg`][clap::Arg], see [`ArgValueCompleter`] |
| /// |
| /// This is useful when predefined value hints are not enough. |
| pub trait ValueCompleter: Send + Sync { |
| /// All potential candidates for an argument. |
| /// |
| /// See [`CompletionCandidate`] for more information. |
| fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate>; |
| } |
| |
| impl<F> ValueCompleter for F |
| where |
| F: Fn(&OsStr) -> Vec<CompletionCandidate> + Send + Sync, |
| { |
| fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> { |
| self(current) |
| } |
| } |
| |
| /// Complete a value as a [`std::path::Path`] |
| /// |
| /// # Example |
| /// |
| /// ```rust |
| /// use clap::Parser; |
| /// use clap_complete::engine::{ArgValueCompleter, PathCompleter}; |
| /// |
| /// #[derive(Debug, Parser)] |
| /// struct Cli { |
| /// #[arg(long, add = ArgValueCompleter::new(PathCompleter::file()))] |
| /// custom: Option<String>, |
| /// } |
| /// ``` |
| pub struct PathCompleter { |
| current_dir: Option<std::path::PathBuf>, |
| filter: Option<Box<dyn Fn(&std::path::Path) -> bool + Send + Sync>>, |
| stdio: bool, |
| } |
| |
| impl PathCompleter { |
| /// Any path is allowed |
| pub fn any() -> Self { |
| Self { |
| filter: None, |
| current_dir: None, |
| stdio: false, |
| } |
| } |
| |
| /// Complete only files |
| pub fn file() -> Self { |
| Self::any().filter(|p| p.is_file()) |
| } |
| |
| /// Complete only directories |
| pub fn dir() -> Self { |
| Self::any().filter(|p| p.is_dir()) |
| } |
| |
| /// Include stdio (`-`) |
| pub fn stdio(mut self) -> Self { |
| self.stdio = true; |
| self |
| } |
| |
| /// Select which paths should be completed |
| pub fn filter( |
| mut self, |
| filter: impl Fn(&std::path::Path) -> bool + Send + Sync + 'static, |
| ) -> Self { |
| self.filter = Some(Box::new(filter)); |
| self |
| } |
| |
| /// Override [`std::env::current_dir`] |
| pub fn current_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self { |
| self.current_dir = Some(path.into()); |
| self |
| } |
| } |
| |
| impl Default for PathCompleter { |
| fn default() -> Self { |
| Self::any() |
| } |
| } |
| |
| impl ValueCompleter for PathCompleter { |
| fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> { |
| let filter = self.filter.as_deref().unwrap_or(&|_| true); |
| let mut current_dir_actual = None; |
| let current_dir = self.current_dir.as_deref().or_else(|| { |
| current_dir_actual = std::env::current_dir().ok(); |
| current_dir_actual.as_deref() |
| }); |
| let mut candidates = complete_path(current, current_dir, filter); |
| if self.stdio && current.is_empty() { |
| candidates.push(CompletionCandidate::new("-").help(Some("stdio".into()))); |
| } |
| candidates |
| } |
| } |
| |
| pub(crate) fn complete_path( |
| value_os: &OsStr, |
| current_dir: Option<&std::path::Path>, |
| is_wanted: &dyn Fn(&std::path::Path) -> bool, |
| ) -> Vec<CompletionCandidate> { |
| let mut completions = Vec::new(); |
| let mut potential = Vec::new(); |
| |
| let value_path = std::path::Path::new(value_os); |
| let (prefix, current) = split_file_name(value_path); |
| let current = current.to_string_lossy(); |
| let search_root = if prefix.is_absolute() { |
| prefix.to_owned() |
| } else { |
| let current_dir = match current_dir { |
| Some(current_dir) => current_dir, |
| None => { |
| // Can't complete without a `current_dir` |
| return completions; |
| } |
| }; |
| current_dir.join(prefix) |
| }; |
| debug!("complete_path: search_root={search_root:?}, prefix={prefix:?}"); |
| |
| if value_os.is_empty() && is_wanted(&search_root) { |
| completions.push(".".into()); |
| } |
| |
| for entry in std::fs::read_dir(&search_root) |
| .ok() |
| .into_iter() |
| .flatten() |
| .filter_map(Result::ok) |
| { |
| let raw_file_name = entry.file_name(); |
| if !raw_file_name.starts_with(¤t) { |
| continue; |
| } |
| |
| if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) { |
| let mut suggestion = prefix.join(raw_file_name); |
| suggestion.push(""); // Ensure trailing `/` |
| let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned()); |
| |
| if is_wanted(&entry.path()) { |
| completions.push(candidate); |
| } else { |
| potential.push(candidate); |
| } |
| } else { |
| if is_wanted(&entry.path()) { |
| let suggestion = prefix.join(raw_file_name); |
| let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned()); |
| completions.push(candidate); |
| } |
| } |
| } |
| completions.sort(); |
| potential.sort(); |
| completions.extend(potential); |
| |
| completions |
| } |
| |
| fn split_file_name(path: &std::path::Path) -> (&std::path::Path, &OsStr) { |
| // Workaround that `Path::new("name/").file_name()` reports `"name"` |
| if path_has_name(path) { |
| ( |
| path.parent().unwrap_or_else(|| std::path::Path::new("")), |
| path.file_name().expect("not called with `..`"), |
| ) |
| } else { |
| (path, Default::default()) |
| } |
| } |
| |
| fn path_has_name(path: &std::path::Path) -> bool { |
| let path_bytes = path.as_os_str().as_encoded_bytes(); |
| let Some(trailing) = path_bytes.last() else { |
| return false; |
| }; |
| let trailing = *trailing as char; |
| !std::path::is_separator(trailing) && path.file_name().is_some() |
| } |