Skip to content

Commit 49b8108

Browse files
committed
feat(complete): Add PathCompleter
1 parent 82a360a commit 49b8108

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

clap_complete/src/engine/custom.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,78 @@ where
150150
}
151151
}
152152

153+
/// Complete a value as a [`std::path::Path`]
154+
///
155+
/// # Example
156+
///
157+
/// ```rust
158+
/// use clap::Parser;
159+
/// use clap_complete::engine::{ArgValueCompleter, PathCompleter};
160+
///
161+
/// #[derive(Debug, Parser)]
162+
/// struct Cli {
163+
/// #[arg(long, add = ArgValueCompleter::new(PathCompleter::file()))]
164+
/// custom: Option<String>,
165+
/// }
166+
/// ```
167+
pub struct PathCompleter {
168+
current_dir: Option<std::path::PathBuf>,
169+
filter: Option<Box<dyn Fn(&std::path::Path) -> bool + Send + Sync>>,
170+
}
171+
172+
impl PathCompleter {
173+
/// Any path is allowed
174+
pub fn any() -> Self {
175+
Self {
176+
filter: None,
177+
current_dir: None,
178+
}
179+
}
180+
181+
/// Complete only files
182+
pub fn file() -> Self {
183+
Self::any().filter(|p| p.is_file())
184+
}
185+
186+
/// Complete only directories
187+
pub fn dir() -> Self {
188+
Self::any().filter(|p| p.is_dir())
189+
}
190+
191+
/// Select which paths should be completed
192+
pub fn filter(
193+
mut self,
194+
filter: impl Fn(&std::path::Path) -> bool + Send + Sync + 'static,
195+
) -> Self {
196+
self.filter = Some(Box::new(filter));
197+
self
198+
}
199+
200+
/// Override [`std::env::current_dir`]
201+
pub fn current_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
202+
self.current_dir = Some(path.into());
203+
self
204+
}
205+
}
206+
207+
impl Default for PathCompleter {
208+
fn default() -> Self {
209+
Self::any()
210+
}
211+
}
212+
213+
impl ValueCompleter for PathCompleter {
214+
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
215+
let filter = self.filter.as_deref().unwrap_or(&|_| true);
216+
let mut current_dir_actual = None;
217+
let current_dir = self.current_dir.as_deref().or_else(|| {
218+
current_dir_actual = std::env::current_dir().ok();
219+
current_dir_actual.as_deref()
220+
});
221+
complete_path(current, current_dir, filter)
222+
}
223+
}
224+
153225
pub(crate) fn complete_path(
154226
value_os: &OsStr,
155227
current_dir: Option<&std::path::Path>,

clap_complete/src/engine/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ pub use candidate::CompletionCandidate;
1010
pub use complete::complete;
1111
pub use custom::ArgValueCandidates;
1212
pub use custom::ArgValueCompleter;
13+
pub use custom::PathCompleter;
1314
pub use custom::ValueCandidates;
1415
pub use custom::ValueCompleter;

clap_complete/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ pub use engine::ArgValueCompleter;
8787
#[cfg(feature = "unstable-dynamic")]
8888
pub use engine::CompletionCandidate;
8989
#[cfg(feature = "unstable-dynamic")]
90+
pub use engine::PathCompleter;
91+
#[cfg(feature = "unstable-dynamic")]
9092
pub use env::CompleteEnv;
9193

9294
/// Deprecated, see [`aot`]

clap_complete/tests/testsuite/engine.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::fs;
44
use std::path::Path;
55

66
use clap::{builder::PossibleValue, Command};
7-
use clap_complete::engine::{ArgValueCandidates, ArgValueCompleter, CompletionCandidate};
7+
use clap_complete::engine::{
8+
ArgValueCandidates, ArgValueCompleter, CompletionCandidate, PathCompleter,
9+
};
810
use snapbox::assert_data_eq;
911

1012
macro_rules! complete {
@@ -575,6 +577,42 @@ d_dir/
575577
);
576578
}
577579

580+
#[test]
581+
fn suggest_value_path_file() {
582+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
583+
let testdir_path = testdir.path().unwrap();
584+
fs::write(testdir_path.join("a_file"), "").unwrap();
585+
fs::write(testdir_path.join("b_file"), "").unwrap();
586+
fs::create_dir_all(testdir_path.join("c_dir")).unwrap();
587+
fs::create_dir_all(testdir_path.join("d_dir")).unwrap();
588+
589+
let mut cmd = Command::new("dynamic")
590+
.arg(
591+
clap::Arg::new("input")
592+
.long("input")
593+
.short('i')
594+
.add(ArgValueCompleter::new(
595+
PathCompleter::file().current_dir(testdir_path.to_owned()),
596+
)),
597+
)
598+
.args_conflicts_with_subcommands(true);
599+
600+
assert_data_eq!(
601+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
602+
snapbox::str![[r#"
603+
a_file
604+
b_file
605+
c_dir/
606+
d_dir/
607+
"#]],
608+
);
609+
610+
assert_data_eq!(
611+
complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)),
612+
snapbox::str!["a_file"],
613+
);
614+
}
615+
578616
#[test]
579617
fn suggest_custom_arg_value() {
580618
fn custom_completer() -> Vec<CompletionCandidate> {

0 commit comments

Comments
 (0)