Skip to content

Commit a61c53e

Browse files
authored
Merge pull request #6202 from iepathos/6201-symlink-path-completions
fix(complete): follow symlinks when completing paths
2 parents ca0aeba + c3b4405 commit a61c53e

File tree

2 files changed

+150
-1
lines changed

2 files changed

+150
-1
lines changed

clap_complete/src/engine/custom.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ pub(crate) fn complete_path(
332332
continue;
333333
}
334334

335-
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
335+
if entry.path().is_dir() {
336336
let mut suggestion = prefix.join(&raw_file_name);
337337
suggestion.push(""); // Ensure trailing `/`
338338
let candidate = CompletionCandidate::new(suggestion.as_os_str().to_owned())

clap_complete/tests/testsuite/engine.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,155 @@ d_dir/
671671
);
672672
}
673673

674+
#[cfg(unix)]
675+
#[test]
676+
fn suggest_value_hint_file_path_symlink_to_dir() {
677+
use std::os::unix::fs::symlink;
678+
679+
let mut cmd = Command::new("dynamic").arg(
680+
clap::Arg::new("input")
681+
.long("input")
682+
.short('i')
683+
.value_hint(clap::ValueHint::FilePath),
684+
);
685+
686+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
687+
let testdir_path = testdir.path().unwrap();
688+
689+
fs::create_dir_all(testdir_path.join("real_dir")).unwrap();
690+
fs::write(testdir_path.join("real_dir/file.txt"), "").unwrap();
691+
symlink("real_dir", testdir_path.join("link_dir")).unwrap();
692+
693+
// Symlink to directory should appear with trailing slash
694+
assert_data_eq!(
695+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
696+
snapbox::str![[r#"
697+
link_dir/
698+
real_dir/
699+
"#]],
700+
);
701+
702+
// Should be able to complete through the symlink
703+
assert_data_eq!(
704+
complete!(cmd, "--input link_dir/[TAB]", current_dir = Some(testdir_path)),
705+
snapbox::str!["link_dir/file.txt"],
706+
);
707+
}
708+
709+
#[cfg(unix)]
710+
#[test]
711+
fn suggest_value_hint_file_path_symlink_to_file() {
712+
use std::os::unix::fs::symlink;
713+
714+
let mut cmd = Command::new("dynamic").arg(
715+
clap::Arg::new("input")
716+
.long("input")
717+
.short('i')
718+
.value_hint(clap::ValueHint::FilePath),
719+
);
720+
721+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
722+
let testdir_path = testdir.path().unwrap();
723+
724+
fs::write(testdir_path.join("real_file.txt"), "").unwrap();
725+
symlink("real_file.txt", testdir_path.join("link_file.txt")).unwrap();
726+
727+
// Symlink to file should appear without trailing slash
728+
assert_data_eq!(
729+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
730+
snapbox::str![[r#"
731+
link_file.txt
732+
real_file.txt
733+
"#]],
734+
);
735+
}
736+
737+
#[cfg(unix)]
738+
#[test]
739+
fn suggest_value_hint_dir_path_symlink() {
740+
use std::os::unix::fs::symlink;
741+
742+
let mut cmd = Command::new("dynamic").arg(
743+
clap::Arg::new("input")
744+
.long("input")
745+
.short('i')
746+
.value_hint(clap::ValueHint::DirPath),
747+
);
748+
749+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
750+
let testdir_path = testdir.path().unwrap();
751+
752+
fs::create_dir_all(testdir_path.join("real_dir")).unwrap();
753+
fs::write(testdir_path.join("real_file.txt"), "").unwrap();
754+
symlink("real_dir", testdir_path.join("link_dir")).unwrap();
755+
symlink("real_file.txt", testdir_path.join("link_file.txt")).unwrap();
756+
757+
// Symlink to directory should have trailing slash
758+
assert_data_eq!(
759+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
760+
snapbox::str![[r#"
761+
.
762+
link_dir/
763+
real_dir/
764+
"#]],
765+
);
766+
}
767+
768+
#[cfg(unix)]
769+
#[test]
770+
fn suggest_value_hint_file_path_broken_symlink() {
771+
use std::os::unix::fs::symlink;
772+
773+
let mut cmd = Command::new("dynamic").arg(
774+
clap::Arg::new("input")
775+
.long("input")
776+
.short('i')
777+
.value_hint(clap::ValueHint::FilePath),
778+
);
779+
780+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
781+
let testdir_path = testdir.path().unwrap();
782+
783+
fs::write(testdir_path.join("real_file.txt"), "").unwrap();
784+
symlink("nonexistent", testdir_path.join("broken_link")).unwrap();
785+
786+
// Broken symlink should not appear for FilePath (target doesn't exist)
787+
// but should not cause a crash
788+
assert_data_eq!(
789+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
790+
snapbox::str!["real_file.txt"],
791+
);
792+
}
793+
794+
#[cfg(unix)]
795+
#[test]
796+
fn suggest_value_hint_any_path_broken_symlink() {
797+
use std::os::unix::fs::symlink;
798+
799+
let mut cmd = Command::new("dynamic").arg(
800+
clap::Arg::new("input")
801+
.long("input")
802+
.short('i')
803+
.value_hint(clap::ValueHint::AnyPath),
804+
);
805+
806+
let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap();
807+
let testdir_path = testdir.path().unwrap();
808+
809+
fs::write(testdir_path.join("real_file.txt"), "").unwrap();
810+
symlink("nonexistent", testdir_path.join("broken_link")).unwrap();
811+
812+
// Broken symlink should appear for AnyPath since filter is |_| true
813+
assert_data_eq!(
814+
complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)),
815+
snapbox::str![[r#"
816+
.
817+
broken_link
818+
real_file.txt
819+
"#]],
820+
);
821+
}
822+
674823
#[test]
675824
fn suggest_custom_arg_value() {
676825
fn custom_completer() -> Vec<CompletionCandidate> {

0 commit comments

Comments
 (0)