1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::sync::atomic::AtomicBool;
6
7use crate::Pattern;
8
9mod compiler;
10mod globber;
11mod matcher;
12mod parser;
13
14const GLOB_CHARS: &[char] = &['*', '?', '['];
15
16#[derive(Debug, Clone, Default)]
18pub struct GlobWalkOptions {
19 pub max_depth: Option<usize>,
21 pub follow_symlinks: bool,
23 pub excludes: Vec<String>,
25 pub interrupt: Option<Arc<AtomicBool>>,
28 pub ignore_case: bool,
30}
31
32pub fn is_glob(pattern: &str) -> bool {
34 pattern.contains(GLOB_CHARS)
35}
36
37pub fn escape(pattern: &str) -> String {
39 crate::Pattern::escape(pattern)
40}
41
42pub fn glob_from(
44 relative_to: impl AsRef<Path>,
45 pattern: impl AsRef<str>,
46) -> anyhow::Result<Box<dyn Iterator<Item = anyhow::Result<PathBuf>> + Send>> {
47 let pattern = pattern.as_ref().to_owned();
48 let parsed = parser::parse(&pattern);
49 let fast_path = detect_recursive_suffix_fast_path(&parsed);
50 let compiled = compiler::compile(&parsed)?;
51 Ok(Box::new(globber::glob(
52 relative_to.as_ref().to_path_buf(),
53 Arc::new(compiled),
54 fast_path,
55 )))
56}
57
58pub fn glob_from_interruptible(
60 relative_to: impl AsRef<Path>,
61 pattern: impl AsRef<str>,
62 interrupt: Option<Arc<AtomicBool>>,
63) -> anyhow::Result<Box<dyn Iterator<Item = anyhow::Result<PathBuf>> + Send>> {
64 let pattern = pattern.as_ref().to_owned();
65 let parsed = parser::parse(&pattern);
66 let fast_path = detect_recursive_suffix_fast_path(&parsed);
67 let compiled = compiler::compile(&parsed)?;
68 Ok(Box::new(globber::glob_with_options(
69 relative_to.as_ref().to_path_buf(),
70 Arc::new(compiled),
71 globber::TraversalOptions::default(),
72 vec![],
73 globber::InterruptFlag(interrupt),
74 fast_path,
75 )))
76}
77
78pub fn glob_with(
80 relative_to: impl AsRef<Path>,
81 pattern: impl AsRef<str>,
82 options: &GlobWalkOptions,
83) -> anyhow::Result<Box<dyn Iterator<Item = anyhow::Result<PathBuf>> + Send>> {
84 let pattern = pattern.as_ref().to_owned();
85 let parsed = parser::parse(&pattern);
86 let fast_path = detect_recursive_suffix_fast_path(&parsed);
87 let include_program = compiler::compile_with_options(&parsed, options.ignore_case)?;
88
89 let exclude_programs = options
90 .excludes
91 .iter()
92 .map(|exclude| Pattern::new(exclude).map_err(anyhow::Error::from))
93 .collect::<Vec<_>>();
94 let exclude_programs = exclude_programs
95 .into_iter()
96 .collect::<anyhow::Result<Vec<_>>>()?;
97
98 Ok(Box::new(globber::glob_with_options(
99 relative_to.as_ref().to_path_buf(),
100 Arc::new(include_program),
101 globber::TraversalOptions {
102 max_depth: options.max_depth,
103 follow_symlinks: options.follow_symlinks,
104 },
105 exclude_programs,
106 globber::InterruptFlag(options.interrupt.clone()),
107 fast_path,
108 )))
109}
110
111fn detect_recursive_suffix_fast_path(
112 pattern: &parser::Pattern,
113) -> Option<globber::RecursiveFastPath> {
114 use parser::AstNode;
115
116 let nodes = pattern.nodes.as_slice();
117 if nodes.len() < 3 {
118 return None;
119 }
120
121 let recurse_index = nodes
122 .windows(2)
123 .position(|window| matches!(window, [AstNode::Recurse, AstNode::Separator]))?;
124
125 let tail = &nodes[(recurse_index + 2)..];
126 if tail.is_empty() {
127 return None;
128 }
129
130 let static_prefix_only = nodes[..recurse_index].iter().all(|node| {
131 matches!(
132 node,
133 AstNode::Prefix(_)
134 | AstNode::RootDir
135 | AstNode::CurDir
136 | AstNode::ParentDir
137 | AstNode::LiteralString(_)
138 | AstNode::Separator
139 )
140 });
141
142 if !static_prefix_only || tail.iter().any(|node| matches!(node, AstNode::Separator)) {
143 return None;
144 }
145
146 if let [AstNode::Wildcard] = tail {
147 return Some(globber::RecursiveFastPath::Suffix(Box::<[u8]>::default()));
148 }
149
150 if let [AstNode::Wildcard, AstNode::LiteralString(bytes)] = tail {
151 return Some(globber::RecursiveFastPath::Suffix(
152 bytes.clone().into_boxed_slice(),
153 ));
154 }
155
156 let tokens = tail
157 .iter()
158 .map(|node| match node {
159 AstNode::LiteralString(bytes) => Some(globber::BasenameToken::Literal(
160 bytes.clone().into_boxed_slice(),
161 )),
162 AstNode::Wildcard => Some(globber::BasenameToken::Wildcard),
163 AstNode::AnyCharacter => Some(globber::BasenameToken::AnyCharacter),
164 _ => None,
165 })
166 .collect::<Option<Vec<_>>>()?;
167
168 Some(globber::RecursiveFastPath::BasenamePattern(
169 tokens.into_boxed_slice(),
170 ))
171}
172
173pub fn debug_parse(pattern: impl AsRef<str>) -> String {
175 let parsed = parser::parse(pattern.as_ref());
176 format!("{parsed:#?}")
177}
178
179pub fn debug_compile(pattern: impl AsRef<str>) -> anyhow::Result<String> {
181 let parsed = parser::parse(pattern.as_ref());
182 let compiled = compiler::compile(&parsed)?;
183 Ok(format!("{compiled:#?}"))
184}
185
186pub fn debug_matches(pattern: impl AsRef<str>, path: impl AsRef<Path>) -> anyhow::Result<bool> {
188 let parsed = parser::parse(pattern.as_ref());
189 let compiled = compiler::compile(&parsed)?;
190 Ok(matcher::path_matches(path.as_ref(), &compiled).valid_as_complete_match)
191}
192
193#[derive(Clone)]
198pub struct DcPattern {
199 pattern: String,
200 compiled: std::sync::Arc<compiler::Program>,
201}
202
203impl std::fmt::Debug for DcPattern {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("DcPattern")
206 .field("pattern", &self.pattern)
207 .finish()
208 }
209}
210
211impl DcPattern {
212 pub fn new(pattern: &str) -> anyhow::Result<Self> {
214 let parsed = parser::parse(pattern);
215 let compiled = compiler::compile(&parsed)?;
216 Ok(DcPattern {
217 pattern: pattern.to_owned(),
218 compiled: std::sync::Arc::new(compiled),
219 })
220 }
221
222 pub fn with_ignore_case(pattern: &str) -> anyhow::Result<Self> {
224 let parsed = parser::parse(pattern);
225 let compiled = compiler::compile_with_options(&parsed, true)?;
226 Ok(DcPattern {
227 pattern: pattern.to_owned(),
228 compiled: std::sync::Arc::new(compiled),
229 })
230 }
231
232 pub fn matches_path(&self, path: &Path) -> bool {
234 matcher::path_matches(path, &self.compiled).valid_as_complete_match
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::compiler::{Instruction, Program};
241 use super::matcher::path_matches;
242 use super::parser::{AstNode, CharacterClass};
243 use super::*;
244 use std::fs;
245 use std::path::Path;
246 use std::sync::atomic::{AtomicU64, Ordering};
247 use std::time::{SystemTime, UNIX_EPOCH};
248
249 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
250
251 fn expected_path(parts: &[&str]) -> String {
252 parts.join(std::path::MAIN_SEPARATOR_STR)
253 }
254
255 fn unique_test_dir(prefix: &str) -> PathBuf {
256 let ts = SystemTime::now()
257 .duration_since(UNIX_EPOCH)
258 .map(|d| d.as_nanos())
259 .unwrap_or(0);
260 std::env::temp_dir().join(format!(
261 "nu_dc_glob_{prefix}_{}_{}",
262 std::process::id(),
263 ts + u128::from(NEXT_ID.fetch_add(1, Ordering::Relaxed))
264 ))
265 }
266
267 fn write_file(path: &Path) {
268 fs::create_dir_all(path.parent().expect("file path must have parent"))
269 .expect("failed to create parent directory");
270 fs::write(path, b"x").expect("failed to write test file");
271 }
272
273 fn collect_ok_paths(
274 iter: impl Iterator<Item = anyhow::Result<PathBuf>>,
275 ) -> anyhow::Result<Vec<String>> {
276 let mut out = Vec::new();
277 for item in iter {
278 let path_str = item?.to_string_lossy().into_owned();
279 #[cfg(windows)]
281 let normalized = path_str.replace('/', "\\");
282 #[cfg(not(windows))]
283 let normalized = path_str.replace('\\', "/");
284 out.push(normalized);
285 }
286 out.sort();
287 Ok(out)
288 }
289
290 #[test]
291 fn glob_with_streams_and_matches_simple_pattern() {
292 let root = unique_test_dir("basic");
293 fs::create_dir_all(&root).expect("failed to create root test directory");
294 write_file(&root.join("five.txt"));
295 write_file(&root.join("six.md"));
296
297 let result = glob_with(root.as_path(), "*.txt", &GlobWalkOptions::default())
298 .expect("glob_with should succeed");
299 let paths = collect_ok_paths(result).expect("failed to collect streamed paths");
300
301 assert_eq!(paths, vec!["five.txt"]);
302
303 let _ = fs::remove_dir_all(&root);
304 }
305
306 #[test]
307 fn glob_with_respects_depth_limit() {
308 let root = unique_test_dir("depth");
309 fs::create_dir_all(&root).expect("failed to create root test directory");
310 write_file(&root.join("a.txt"));
311 write_file(&root.join("nested/inner.txt"));
312
313 let options = GlobWalkOptions {
314 max_depth: Some(1),
315 ..Default::default()
316 };
317 let result =
318 glob_with(root.as_path(), "**/*.txt", &options).expect("glob_with should succeed");
319 let paths = collect_ok_paths(result).expect("failed to collect streamed paths");
320
321 assert_eq!(paths, vec!["a.txt"]);
322
323 let _ = fs::remove_dir_all(&root);
324 }
325
326 #[test]
327 fn glob_with_respects_excludes_and_prunes_nested_dirs() {
328 let root = unique_test_dir("exclude_prune");
329 fs::create_dir_all(&root).expect("failed to create root test directory");
330 write_file(&root.join("src/keep/main.rs"));
331 write_file(&root.join("src/target/skip.rs"));
332 write_file(&root.join("src/.git/config"));
333 write_file(&root.join("src/.git/hooks/pre-commit"));
334 write_file(&root.join("src/node_modules/pkg/index.js"));
335
336 let options = GlobWalkOptions {
337 excludes: vec![
338 "**/target/**".to_string(),
339 "**/.git/**".to_string(),
340 "**/node_modules/**".to_string(),
341 ],
342 ..Default::default()
343 };
344
345 let result = glob_with(root.as_path(), "**/*", &options).expect("glob_with should succeed");
346 let paths = collect_ok_paths(result).expect("failed to collect streamed paths");
347
348 assert!(paths.contains(&expected_path(&["src", "keep", "main.rs"])));
349 assert!(
350 !paths
351 .iter()
352 .any(|p| p.contains(&format!("target{}skip.rs", std::path::MAIN_SEPARATOR)))
353 );
354 assert!(
355 !paths
356 .iter()
357 .any(|p| p.contains(&format!(".git{}config", std::path::MAIN_SEPARATOR)))
358 );
359 assert!(!paths.iter().any(|p| p.contains(&format!(
360 ".git{}hooks{}pre-commit",
361 std::path::MAIN_SEPARATOR,
362 std::path::MAIN_SEPARATOR
363 ))));
364 assert!(!paths.iter().any(|p| p.contains(&format!(
365 "node_modules{}pkg{}index.js",
366 std::path::MAIN_SEPARATOR,
367 std::path::MAIN_SEPARATOR
368 ))));
369
370 let _ = fs::remove_dir_all(&root);
371 }
372
373 #[test]
374 fn glob_with_absolute_recursive_pattern_finds_nested_files() {
375 let root = unique_test_dir("absolute_recursive");
376 fs::create_dir_all(&root).expect("failed to create root test directory");
377 write_file(&root.join("src/lib.rs"));
378 write_file(&root.join("README.md"));
379
380 let pattern = format!("{}/**/*", root.to_string_lossy());
381 let result = glob_with(root.as_path(), &pattern, &GlobWalkOptions::default())
382 .expect("glob_with should succeed");
383 let paths = collect_ok_paths(result).expect("failed to collect streamed paths");
384
385 assert!(
386 paths.iter().any(|p| {
387 Path::new(p).is_absolute() && p.ends_with(&expected_path(&["src", "lib.rs"]))
388 }),
389 "absolute recursive pattern should include nested absolute files"
390 );
391
392 let _ = fs::remove_dir_all(&root);
393 }
394
395 #[test]
396 fn debug_helpers_behave_as_expected() {
397 let parse = debug_parse("**/*.txt");
398 assert!(parse.contains("Recurse"));
399
400 let compile = debug_compile("**/*.txt").expect("debug_compile should succeed");
401 assert!(compile.contains("Complete"));
402
403 assert!(debug_matches("*.txt", "file.txt").expect("debug_matches should succeed"));
404 assert!(!debug_matches("*.txt", "file.md").expect("debug_matches should succeed"));
405 }
406
407 #[test]
408 fn detect_fast_path_for_recursive_richer_basename_pattern() {
409 let parsed = parser::parse("crates/**/mod*.rs");
410 let fast_path = detect_recursive_suffix_fast_path(&parsed);
411
412 assert!(matches!(
413 fast_path,
414 Some(globber::RecursiveFastPath::BasenamePattern(_))
415 ));
416 }
417
418 #[test]
419 fn glob_with_matches_recursive_richer_basename_pattern() {
420 let root = unique_test_dir("richer_tail");
421 fs::create_dir_all(&root).expect("failed to create root test directory");
422 write_file(&root.join("crates/nu-glob/src/mod.rs"));
423 write_file(&root.join("crates/nu-glob/src/index.rs"));
424 write_file(&root.join("crates/nu-protocol/src/mod_helpers.rs"));
425
426 let result = glob_with(
427 root.as_path(),
428 "crates/**/mod*.rs",
429 &GlobWalkOptions::default(),
430 )
431 .expect("glob_with should succeed");
432 let paths = collect_ok_paths(result).expect("failed to collect streamed paths");
433
434 assert!(paths.contains(&expected_path(&["crates", "nu-glob", "src", "mod.rs"])));
435 assert!(paths.contains(&expected_path(&[
436 "crates",
437 "nu-protocol",
438 "src",
439 "mod_helpers.rs"
440 ])));
441 assert!(!paths.contains(&expected_path(&["crates", "nu-glob", "src", "index.rs"])));
442
443 let _ = fs::remove_dir_all(&root);
444 }
445
446 fn node_kinds(pattern: &str) -> Vec<std::mem::Discriminant<AstNode>> {
449 parser::parse(pattern)
450 .nodes
451 .iter()
452 .map(std::mem::discriminant)
453 .collect()
454 }
455
456 fn discriminant<T>(val: &T) -> std::mem::Discriminant<T> {
457 std::mem::discriminant(val)
458 }
459
460 #[test]
461 fn parser_literal_string() {
462 let p = parser::parse("hello");
463 assert_eq!(p.nodes.len(), 1);
464 match &p.nodes[0] {
465 AstNode::LiteralString(bytes) => assert_eq!(bytes, b"hello"),
466 other => panic!("expected LiteralString, got {other:?}"),
467 }
468 }
469
470 #[test]
471 fn parser_wildcard() {
472 let p = parser::parse("*");
473 assert!(p.nodes.iter().any(|n| matches!(n, AstNode::Wildcard)));
474 }
475
476 #[test]
477 fn parser_recurse() {
478 let p = parser::parse("**");
479 assert!(p.nodes.iter().any(|n| matches!(n, AstNode::Recurse)));
480 assert!(!p.nodes.iter().any(|n| matches!(n, AstNode::Wildcard)));
482 }
483
484 #[test]
485 fn parser_any_character() {
486 let p = parser::parse("?");
487 assert_eq!(p.nodes.len(), 1);
488 assert!(matches!(p.nodes[0], AstNode::AnyCharacter));
489 }
490
491 #[test]
492 fn parser_separator_between_components() {
493 let p = parser::parse("a/b");
494 assert!(p.nodes.iter().any(|n| matches!(n, AstNode::Separator)));
495 assert!(
496 p.nodes
497 .iter()
498 .any(|n| matches!(n, AstNode::LiteralString(_)))
499 );
500 }
501
502 #[test]
503 fn parser_cur_dir() {
504 let p = parser::parse("./foo");
505 assert!(p.nodes.iter().any(|n| matches!(n, AstNode::CurDir)));
506 }
507
508 #[test]
509 fn parser_parent_dir() {
510 let p = parser::parse("../foo");
511 assert!(p.nodes.iter().any(|n| matches!(n, AstNode::ParentDir)));
512 }
513
514 #[test]
515 fn parser_alternatives() {
516 let p = parser::parse("{a,b,c}");
517 assert_eq!(p.nodes.len(), 1);
518 match &p.nodes[0] {
519 AstNode::Alternatives { choices } => assert_eq!(choices.len(), 3),
520 other => panic!("expected Alternatives, got {other:?}"),
521 }
522 }
523
524 #[test]
525 fn parser_character_class_range() {
526 let p = parser::parse("[a-z]");
527 assert_eq!(p.nodes.len(), 1);
528 match &p.nodes[0] {
529 AstNode::Characters(classes) => {
530 assert_eq!(classes.len(), 1);
531 assert_eq!(classes[0], CharacterClass::Range('a', 'z'));
532 }
533 other => panic!("expected Characters, got {other:?}"),
534 }
535 }
536
537 #[test]
538 fn parser_character_class_single() {
539 let p = parser::parse("[abc]");
540 assert_eq!(p.nodes.len(), 1);
541 match &p.nodes[0] {
542 AstNode::Characters(classes) => {
543 assert_eq!(classes.len(), 3);
544 assert!(classes.contains(&CharacterClass::Single('a')));
545 assert!(classes.contains(&CharacterClass::Single('b')));
546 assert!(classes.contains(&CharacterClass::Single('c')));
547 }
548 other => panic!("expected Characters, got {other:?}"),
549 }
550 }
551
552 #[test]
553 fn parser_repeat_exact() {
554 let p = parser::parse("<*:3>");
555 assert_eq!(p.nodes.len(), 1);
556 match &p.nodes[0] {
557 AstNode::Repeat { min, max, .. } => {
558 assert_eq!(*min, 3);
559 assert_eq!(*max, 3);
560 }
561 other => panic!("expected Repeat, got {other:?}"),
562 }
563 }
564
565 #[test]
566 fn parser_repeat_range() {
567 let p = parser::parse("<*:1,4>");
568 assert_eq!(p.nodes.len(), 1);
569 match &p.nodes[0] {
570 AstNode::Repeat { min, max, .. } => {
571 assert_eq!(*min, 1);
572 assert_eq!(*max, 4);
573 }
574 other => panic!("expected Repeat, got {other:?}"),
575 }
576 }
577
578 #[test]
579 fn parser_glob_pattern_recurse_then_literal() {
580 let p = parser::parse("**/*.rs");
581 let kinds = node_kinds("**/*.rs");
583 assert!(
584 kinds.contains(&discriminant(&AstNode::Recurse)),
585 "must have Recurse"
586 );
587 assert!(
588 kinds.contains(&discriminant(&AstNode::Separator)),
589 "must have Separator"
590 );
591 assert!(
592 kinds.contains(&discriminant(&AstNode::Wildcard)),
593 "must have Wildcard"
594 );
595 let _ = p; }
597
598 fn compile_pattern(pattern: &str) -> Program {
601 let parsed = parser::parse(pattern);
602 compiler::compile(&parsed).expect("compile should not fail for valid pattern")
603 }
604
605 fn last_instruction(prog: &Program) -> &Instruction {
606 prog.instructions
607 .last()
608 .expect("program must have at least one instruction")
609 }
610
611 #[test]
612 fn compiler_program_ends_with_complete() {
613 let prog = compile_pattern("*.txt");
614 assert_eq!(last_instruction(&prog), &Instruction::Complete);
615 }
616
617 #[test]
618 fn compiler_literal_produces_literal_string_instruction() {
619 let prog = compile_pattern("hello");
620 assert!(
621 prog.instructions
622 .iter()
623 .any(|i| matches!(i, Instruction::LiteralString(b) if &**b == b"hello")),
624 "expected LiteralString(hello) in {:?}",
625 prog.instructions
626 );
627 }
628
629 #[test]
630 fn compiler_wildcard_produces_alternative_jump_gadget() {
631 let prog = compile_pattern("*");
632 assert!(
634 prog.instructions
635 .iter()
636 .any(|i| matches!(i, Instruction::Alternative(_))),
637 "wildcard must produce Alternative instruction"
638 );
639 assert!(
640 prog.instructions
641 .iter()
642 .any(|i| matches!(i, Instruction::AnyCharacter)),
643 "wildcard must produce AnyCharacter instruction"
644 );
645 }
646
647 #[test]
648 fn compiler_recurse_produces_anystring_separator_gadget() {
649 let prog = compile_pattern("**");
650 assert!(
651 prog.instructions
652 .iter()
653 .any(|i| matches!(i, Instruction::AnyString)),
654 "recurse must produce AnyString instruction"
655 );
656 assert!(
657 prog.instructions
658 .iter()
659 .any(|i| matches!(i, Instruction::Separator)),
660 "recurse must produce Separator instruction"
661 );
662 assert!(
663 prog.trailing_recursive,
664 "bare ** must set trailing_recursive"
665 );
666 }
667
668 #[test]
669 fn compiler_star_star_slash_star_is_not_trailing_recursive() {
670 let prog = compile_pattern("**/*");
671 assert!(
672 !prog.trailing_recursive,
673 "**/* must not be treated as directory-only trailing **"
674 );
675 }
676
677 #[test]
678 fn compiler_prefixed_trailing_recursive_sets_flag() {
679 let prog = compile_pattern("foo/**");
680 assert!(
681 prog.trailing_recursive,
682 "foo/** must set trailing_recursive"
683 );
684 }
685
686 #[test]
687 fn compiler_multi_component_trailing_recursive_keeps_intermediate_separators() {
688 let prog = compile_pattern("a/b/**");
692 assert!(
693 prog.trailing_recursive,
694 "a/b/** must set trailing_recursive"
695 );
696
697 let mut saw_a = false;
698 let mut saw_sep_after_a = false;
699 let mut saw_b = false;
700 let mut saw_boundary = false;
701 for inst in &prog.instructions {
702 match inst {
703 Instruction::LiteralString(bytes) if &**bytes == b"a" => {
704 saw_a = true;
705 }
706 Instruction::Separator if saw_a && !saw_b => {
707 saw_sep_after_a = true;
708 }
709 Instruction::LiteralString(bytes) if &**bytes == b"b" => {
710 assert!(
711 saw_sep_after_a,
712 "a/b/** must keep Separator between a and b, got {prog}"
713 );
714 saw_b = true;
715 }
716 Instruction::ComponentBoundary if saw_b => {
717 saw_boundary = true;
718 }
719 _ => {}
720 }
721 }
722 assert!(saw_a && saw_b && saw_boundary, "unexpected program: {prog}");
723 }
724
725 #[test]
726 fn compiler_nested_terminal_recurse_in_alternatives() {
727 let prog = compile_pattern("{**}");
729 assert!(
730 prog.trailing_recursive,
731 "{{**}} must set trailing_recursive"
732 );
733 let mixed = compile_pattern("{**,README.md}");
735 assert!(
736 !mixed.trailing_recursive,
737 "mixed {{**,file}} must not force directory-only expansion"
738 );
739 let nested = compile_pattern("foo/{**}");
741 assert!(
742 nested.trailing_recursive,
743 "foo/{{**}} must set trailing_recursive"
744 );
745 }
746
747 #[test]
748 fn compiler_any_character_produces_any_character_instruction() {
749 let prog = compile_pattern("?");
750 assert!(
751 prog.instructions
752 .iter()
753 .any(|i| matches!(i, Instruction::AnyCharacter)),
754 "? must produce AnyCharacter instruction"
755 );
756 }
757
758 #[test]
759 fn compiler_alternatives_produce_alternative_instruction() {
760 let prog = compile_pattern("{foo,bar}");
761 assert!(
762 prog.instructions
763 .iter()
764 .any(|i| matches!(i, Instruction::Alternative(_))),
765 "{{foo,bar}} must produce Alternative instruction"
766 );
767 }
768
769 #[test]
770 fn compiler_absolute_path_sets_absolute_prefix_on_unix() {
771 #[cfg(unix)]
773 {
774 let prog = compile_pattern("/foo/bar");
775 assert!(
776 prog.absolute_prefix.is_some(),
777 "absolute path should set absolute_prefix"
778 );
779 }
780 }
781
782 #[test]
783 fn compiler_relative_path_leaves_absolute_prefix_empty() {
784 let prog = compile_pattern("foo/bar");
785 assert!(
786 prog.absolute_prefix.is_none(),
787 "relative path must not set absolute_prefix"
788 );
789 }
790
791 #[test]
792 fn compiler_repeat_produces_increment_and_branch_instructions() {
793 let prog = compile_pattern("<*:2,4>");
794 assert!(
795 prog.instructions
796 .iter()
797 .any(|i| matches!(i, Instruction::Increment(_))),
798 "repeat must produce Increment instruction"
799 );
800 assert!(
801 prog.instructions
802 .iter()
803 .any(|i| matches!(i, Instruction::BranchIfLessThan(..))),
804 "repeat must produce BranchIfLessThan instruction"
805 );
806 }
807
808 fn compile_for_match(pattern: &str) -> Program {
811 let parsed = parser::parse(pattern);
812 compiler::compile(&parsed).expect("compile should succeed")
813 }
814
815 fn matches_complete(pattern: &str, path: &str) -> bool {
816 let prog = compile_for_match(pattern);
817 path_matches(Path::new(path), &prog).valid_as_complete_match
818 }
819
820 fn matches_prefix(pattern: &str, path: &str) -> bool {
821 let prog = compile_for_match(pattern);
822 path_matches(Path::new(path), &prog).valid_as_prefix
823 }
824
825 #[test]
826 fn matcher_literal_exact_match() {
827 assert!(matches_complete("hello", "hello"));
828 assert!(!matches_complete("hello", "world"));
829 }
830
831 #[test]
832 fn matcher_wildcard_matches_any_string_without_separator() {
833 assert!(matches_complete("*.txt", "file.txt"));
834 assert!(matches_complete("*.txt", "my_file.txt"));
835 assert!(!matches_complete("*.txt", "file.md"));
836 assert!(!matches_complete("*.txt", "dir/file.txt"));
838 }
839
840 #[test]
841 fn matcher_any_character_matches_single_char() {
842 assert!(matches_complete("f?o", "foo"));
843 assert!(matches_complete("f?o", "fXo"));
844 assert!(!matches_complete("o?f", "of"));
845 assert!(!matches_complete("f?o", "fXXo"));
846 }
847
848 #[test]
849 fn matcher_recurse_matches_across_separators() {
850 assert!(matches_complete("**/*.txt", "a/b/c/file.txt"));
851 assert!(matches_complete("**/*.txt", "file.txt"));
852 assert!(!matches_complete("**/*.txt", "a/b/c/file.md"));
853 }
854
855 #[test]
856 fn matcher_character_class_range() {
857 assert!(matches_complete("[a-z]", "a"));
858 assert!(matches_complete("[a-z]", "m"));
859 assert!(matches_complete("[a-z]", "z"));
860 assert!(!matches_complete("[a-z]", "A"));
861 assert!(!matches_complete("[a-z]", "1"));
862 }
863
864 #[test]
865 fn matcher_character_class_single_chars() {
866 assert!(matches_complete("[abc]", "a"));
867 assert!(matches_complete("[abc]", "b"));
868 assert!(matches_complete("[abc]", "c"));
869 assert!(!matches_complete("[abc]", "d"));
870 }
871
872 #[test]
873 fn matcher_alternatives() {
874 assert!(matches_complete("{foo,bar}", "foo"));
875 assert!(matches_complete("{foo,bar}", "bar"));
876 assert!(!matches_complete("{foo,bar}", "baz"));
877 }
878
879 #[test]
880 fn matcher_alternatives_in_path() {
881 assert!(matches_complete("src/{lib,main}.rs", "src/lib.rs"));
882 assert!(matches_complete("src/{lib,main}.rs", "src/main.rs"));
883 assert!(!matches_complete("src/{lib,main}.rs", "src/other.rs"));
884 }
885
886 #[test]
887 fn matcher_valid_as_prefix_with_short_path() {
888 assert!(matches_prefix("a/b/c.txt", "a/b"));
890 assert!(!matches_prefix("a/b/c.txt", "x/y"));
892 }
893
894 #[test]
895 fn matcher_recurse_double_star_matches_deep_paths() {
896 assert!(matches_complete("**/*.rs", "src/lib.rs"));
898 assert!(matches_complete("**/*.rs", "a/b/c/deep.rs"));
899 assert!(!matches_complete("**/*.rs", "src/lib.txt"));
900 assert!(matches_prefix("**/foo", "a/b"));
902 }
903
904 #[test]
905 fn matcher_bare_double_star_matches_any_depth() {
906 assert!(matches_complete("**", ""));
908 assert!(matches_complete("**", "1"));
909 assert!(matches_complete("**", "1/2"));
910 assert!(matches_complete("**", "1/2/3"));
911 }
912
913 #[test]
914 fn matcher_star_slash_star_requires_two_components() {
915 assert!(!matches_complete("*/*", "1"));
917 assert!(matches_complete("*/*", "1/2"));
918 assert!(!matches_complete("*/*", "1/2/3"));
919 }
920
921 #[test]
922 fn matcher_prefixed_trailing_double_star_matches_directory_itself() {
923 assert!(matches_complete("foo/**", "foo"));
925 assert!(matches_complete("foo/**", "foo/bar"));
926 assert!(matches_complete("foo/**", "foo/bar/baz"));
927 assert!(!matches_complete("foo/**", "bar"));
928 assert!(!matches_complete("foo/**", "foobar"));
929 assert!(!matches_complete("*/*", "1"));
931 }
932
933 #[test]
934 fn matcher_nested_terminal_double_star_in_alternatives() {
935 assert!(matches_complete("{**}", ""));
936 assert!(matches_complete("{**}", "a/b"));
937 assert!(matches_complete("foo/{**}", "foo"));
938 assert!(matches_complete("foo/{**}", "foo/bar"));
939 assert!(!matches_complete("foo/{**}", "bar"));
940 }
941
942 #[test]
943 fn matcher_nested_recursive_stars_enforce_min_depth() {
944 assert!(matches_complete("**/*", "1"));
947 assert!(matches_complete("**/*", "1/2"));
948 assert!(matches_complete("**/*", "1/2/3"));
949
950 assert!(!matches_complete("**/*/*", "1"));
952 assert!(matches_complete("**/*/*", "1/2"));
953 assert!(matches_complete("**/*/*", "1/2/3"));
954
955 assert!(!matches_complete("**/*/*/*", "1"));
957 assert!(!matches_complete("**/*/*/*", "1/2"));
958 assert!(matches_complete("**/*/*/*", "1/2/3"));
959 assert!(matches_complete("**/*/*/*", "1/2/3/4"));
960 }
961
962 #[test]
963 fn matcher_double_star_slash_literal_matches_at_any_depth() {
964 assert!(matches_complete("**/foo", "foo"));
965 assert!(matches_complete("**/foo", "a/foo"));
966 assert!(matches_complete("**/foo", "a/b/foo"));
967 assert!(!matches_complete("**/foo", "foo/bar"));
968 assert!(!matches_complete("**/foo", "bar"));
969 }
970
971 #[test]
972 fn matcher_literal_multi_component_path() {
973 assert!(matches_complete("foo/bar/baz", "foo/bar/baz"));
974 assert!(!matches_complete("foo/bar/baz", "foo/bar"));
975 assert!(!matches_complete("foo/bar/baz", "foo/bar/baz/extra"));
976 }
977
978 #[test]
979 fn matcher_complete_match_requires_full_consumption() {
980 assert!(!matches_complete("*.txt", "file.txt.bak"));
982 }
983
984 #[test]
985 fn matcher_repeat_exact_count() {
986 assert!(matches_complete("<a:3>", "aaa"));
988 assert!(!matches_complete("<a:3>", "aa"));
989 assert!(!matches_complete("<a:3>", "aaaa"));
990 }
991
992 #[test]
998 fn glob_with_issue_18600_nested_depth_and_bare_double_star() {
999 let root = unique_test_dir("issue_18600");
1002 fs::create_dir_all(root.join("1/2/3")).expect("failed to create nested dirs");
1003 write_file(&root.join("1/2/3/file.txt"));
1004
1005 let options = GlobWalkOptions::default();
1006
1007 let paths = collect_ok_paths(
1009 glob_with(root.as_path(), "**", &options).expect("glob ** should succeed"),
1010 )
1011 .expect("collect **");
1012 assert!(
1013 paths.iter().any(|p| p.is_empty()),
1014 "bare ** should include the start directory, got {paths:?}"
1015 );
1016 assert!(paths.contains(&expected_path(&["1"])));
1017 assert!(paths.contains(&expected_path(&["1", "2"])));
1018 assert!(paths.contains(&expected_path(&["1", "2", "3"])));
1019 assert!(
1020 !paths.iter().any(|p| p.ends_with("file.txt")),
1021 "bare ** must not list files, got {paths:?}"
1022 );
1023
1024 let paths = collect_ok_paths(
1026 glob_with(root.as_path(), "**/*", &options).expect("glob **/* should succeed"),
1027 )
1028 .expect("collect **/*");
1029 assert!(
1030 !paths.iter().any(|p| p.is_empty()),
1031 "**/* must not include the start directory, got {paths:?}"
1032 );
1033 assert!(paths.contains(&expected_path(&["1"])));
1034 assert!(paths.contains(&expected_path(&["1", "2"])));
1035 assert!(paths.contains(&expected_path(&["1", "2", "3"])));
1036 assert!(paths.contains(&expected_path(&["1", "2", "3", "file.txt"])));
1037
1038 let paths = collect_ok_paths(
1040 glob_with(root.as_path(), "**/*/*", &options).expect("glob **/*/* should succeed"),
1041 )
1042 .expect("collect **/*/*");
1043 assert!(!paths.contains(&expected_path(&["1"])));
1044 assert!(paths.contains(&expected_path(&["1", "2"])));
1045 assert!(paths.contains(&expected_path(&["1", "2", "3"])));
1046 assert!(paths.contains(&expected_path(&["1", "2", "3", "file.txt"])));
1047
1048 let paths = collect_ok_paths(
1050 glob_with(root.as_path(), "**/*/*/*", &options).expect("glob **/*/*/* should succeed"),
1051 )
1052 .expect("collect **/*/*/*");
1053 assert!(!paths.contains(&expected_path(&["1"])));
1054 assert!(!paths.contains(&expected_path(&["1", "2"])));
1055 assert!(paths.contains(&expected_path(&["1", "2", "3"])));
1056 assert!(paths.contains(&expected_path(&["1", "2", "3", "file.txt"])));
1057
1058 let _ = fs::remove_dir_all(&root);
1059 }
1060
1061 #[test]
1062 fn glob_with_prefixed_trailing_double_star_emits_prefix_dir() {
1063 let root = unique_test_dir("prefixed_trailing");
1065 fs::create_dir_all(root.join("foo/bar")).expect("failed to create nested dirs");
1066 write_file(&root.join("foo/bar/file.txt"));
1067 write_file(&root.join("foo/sibling.txt"));
1068
1069 let paths = collect_ok_paths(
1070 glob_with(root.as_path(), "foo/**", &GlobWalkOptions::default())
1071 .expect("glob foo/** should succeed"),
1072 )
1073 .expect("collect foo/**");
1074
1075 assert!(
1076 paths.contains(&expected_path(&["foo"])),
1077 "foo/** should include the prefix directory itself, got {paths:?}"
1078 );
1079 assert!(paths.contains(&expected_path(&["foo", "bar"])));
1080 assert!(
1081 !paths
1082 .iter()
1083 .any(|p| p.ends_with("file.txt") || p.ends_with("sibling.txt")),
1084 "foo/** must not list files, got {paths:?}"
1085 );
1086
1087 let _ = fs::remove_dir_all(&root);
1088 }
1089
1090 #[test]
1091 fn glob_with_multi_component_trailing_double_star() {
1092 let root = unique_test_dir("multi_component_trailing");
1094 fs::create_dir_all(root.join("a/b/c")).expect("failed to create nested dirs");
1095 write_file(&root.join("a/b/c/file.txt"));
1096 write_file(&root.join("a/sibling.txt"));
1097
1098 let paths = collect_ok_paths(
1099 glob_with(root.as_path(), "a/b/**", &GlobWalkOptions::default())
1100 .expect("glob a/b/** should succeed"),
1101 )
1102 .expect("collect a/b/**");
1103
1104 assert!(
1105 paths.contains(&expected_path(&["a", "b"])),
1106 "a/b/** should include the prefix directory itself, got {paths:?}"
1107 );
1108 assert!(paths.contains(&expected_path(&["a", "b", "c"])));
1109 assert!(
1110 !paths
1111 .iter()
1112 .any(|p| p.ends_with("file.txt") || p.ends_with("sibling.txt")),
1113 "a/b/** must not list files, got {paths:?}"
1114 );
1115 assert!(
1116 !paths.contains(&expected_path(&["a"])),
1117 "a/b/** must not list the parent of the prefix, got {paths:?}"
1118 );
1119
1120 let _ = fs::remove_dir_all(&root);
1121 }
1122
1123 #[test]
1124 fn glob_with_absolute_trailing_double_star() {
1125 let root = unique_test_dir("absolute_trailing");
1127 fs::create_dir_all(root.join("1/2")).expect("failed to create nested dirs");
1128 write_file(&root.join("1/2/file.txt"));
1129
1130 let pattern = format!("{}/**", root.to_string_lossy());
1131 let paths = collect_ok_paths(
1132 glob_with(root.as_path(), &pattern, &GlobWalkOptions::default())
1133 .expect("glob absolute /** should succeed"),
1134 )
1135 .expect("collect absolute /**");
1136
1137 let root_str = root.to_string_lossy().to_string();
1138 assert!(
1139 paths.iter().any(|p| {
1140 let p = p.trim_end_matches(['/', '\\']);
1141 p == root_str.trim_end_matches(['/', '\\'])
1142 }),
1143 "absolute /** should include the start directory, got {paths:?}"
1144 );
1145 assert!(
1146 paths
1147 .iter()
1148 .any(|p| Path::new(p).ends_with(expected_path(&["1"]))),
1149 "absolute /** should include nested dir 1, got {paths:?}"
1150 );
1151 assert!(
1152 paths
1153 .iter()
1154 .any(|p| Path::new(p).ends_with(expected_path(&["1", "2"]))),
1155 "absolute /** should include nested dir 1/2, got {paths:?}"
1156 );
1157 assert!(
1158 !paths.iter().any(|p| p.ends_with("file.txt")),
1159 "absolute /** must not list files, got {paths:?}"
1160 );
1161
1162 let _ = fs::remove_dir_all(&root);
1163 }
1164
1165 #[test]
1166 fn glob_with_trailing_double_star_respects_excludes_on_start() {
1167 let root = unique_test_dir("trailing_exclude");
1169 fs::create_dir_all(root.join("foo/bar")).expect("create foo/bar");
1170 fs::create_dir_all(root.join("other")).expect("create other");
1171 fs::create_dir_all(root.join("1")).expect("create 1");
1172
1173 let options = GlobWalkOptions {
1175 excludes: vec!["foo".to_string(), "foo/**".to_string()],
1176 ..Default::default()
1177 };
1178 let paths = collect_ok_paths(
1179 glob_with(root.as_path(), "foo/**", &options).expect("glob foo/** should succeed"),
1180 )
1181 .expect("collect foo/** with excludes");
1182
1183 assert!(
1184 !paths.iter().any(|p| {
1185 p == &expected_path(&["foo"])
1186 || p.starts_with(&format!("foo{}", std::path::MAIN_SEPARATOR))
1187 }),
1188 "excluded foo/** must not emit foo or descendants, got {paths:?}"
1189 );
1190
1191 let options = GlobWalkOptions {
1193 excludes: vec!["1".to_string(), "1/**".to_string()],
1194 ..Default::default()
1195 };
1196 let paths = collect_ok_paths(
1197 glob_with(root.as_path(), "**", &options).expect("glob ** should succeed"),
1198 )
1199 .expect("collect ** with excludes");
1200 assert!(
1201 paths.iter().any(|p| p.is_empty()),
1202 "bare ** should still emit start dir when only nested paths are excluded, got {paths:?}"
1203 );
1204 assert!(!paths.contains(&expected_path(&["1"])));
1205 assert!(paths.contains(&expected_path(&["other"])));
1206 assert!(paths.contains(&expected_path(&["foo"])));
1207
1208 let _ = fs::remove_dir_all(&root);
1209 }
1210
1211 #[test]
1212 fn glob_with_dir_prefix_literal_then_wildcard() {
1213 let root = unique_test_dir("literal_wildcard");
1216 fs::create_dir_all(&root).expect("failed to create root");
1217 write_file(&root.join("dir/nu_test1"));
1218 write_file(&root.join("dir/nu_test2"));
1219 write_file(&root.join("dir/other"));
1220
1221 let result = glob_with(root.as_path(), "dir/nu*", &GlobWalkOptions::default())
1222 .expect("glob_with should succeed");
1223 let paths = collect_ok_paths(result).expect("failed to collect paths");
1224
1225 assert_eq!(paths.len(), 2);
1226 assert!(paths.contains(&expected_path(&["dir", "nu_test1"])));
1227 assert!(paths.contains(&expected_path(&["dir", "nu_test2"])));
1228
1229 let _ = fs::remove_dir_all(&root);
1230 }
1231
1232 #[test]
1233 fn glob_with_dir_prefix_wildcard_then_literal() {
1234 let root = unique_test_dir("wildcard_literal");
1237 fs::create_dir_all(&root).expect("failed to create root");
1238 write_file(&root.join("dir/nu_test1"));
1239 write_file(&root.join("dir/nu_test2"));
1240 write_file(&root.join("dir/other"));
1241
1242 let result = glob_with(root.as_path(), "dir/*nu*", &GlobWalkOptions::default())
1243 .expect("glob_with should succeed");
1244 let paths = collect_ok_paths(result).expect("failed to collect paths");
1245
1246 assert_eq!(paths.len(), 2);
1247 assert!(paths.contains(&expected_path(&["dir", "nu_test1"])));
1248 assert!(paths.contains(&expected_path(&["dir", "nu_test2"])));
1249
1250 let _ = fs::remove_dir_all(&root);
1251 }
1252
1253 #[test]
1254 fn glob_with_nested_dir_prefix_literal_then_wildcard() {
1255 let root = unique_test_dir("nested_literal_wildcard");
1258 fs::create_dir_all(&root).expect("failed to create root");
1259 write_file(&root.join("a/b/nu_test1"));
1260 write_file(&root.join("a/b/nu_test2"));
1261 write_file(&root.join("a/b/other"));
1262
1263 let result = glob_with(root.as_path(), "a/b/nu*", &GlobWalkOptions::default())
1264 .expect("glob_with should succeed");
1265 let paths = collect_ok_paths(result).expect("failed to collect paths");
1266
1267 assert_eq!(paths.len(), 2);
1268 assert!(paths.contains(&expected_path(&["a", "b", "nu_test1"])));
1269 assert!(paths.contains(&expected_path(&["a", "b", "nu_test2"])));
1270
1271 let _ = fs::remove_dir_all(&root);
1272 }
1273
1274 #[test]
1275 fn glob_with_dir_prefix_literal_then_any_char() {
1276 let root = unique_test_dir("literal_any_char");
1279 fs::create_dir_all(&root).expect("failed to create root");
1280 write_file(&root.join("dir/nu1"));
1281 write_file(&root.join("dir/nu2"));
1282 write_file(&root.join("dir/not"));
1283
1284 let result = glob_with(root.as_path(), "dir/nu?", &GlobWalkOptions::default())
1285 .expect("glob_with should succeed");
1286 let paths = collect_ok_paths(result).expect("failed to collect paths");
1287
1288 assert_eq!(paths.len(), 2);
1289 assert!(paths.contains(&expected_path(&["dir", "nu1"])));
1290 assert!(paths.contains(&expected_path(&["dir", "nu2"])));
1291
1292 let _ = fs::remove_dir_all(&root);
1293 }
1294
1295 #[test]
1296 fn glob_with_absolute_pattern_literal_then_wildcard() {
1297 let root = unique_test_dir("absolute_literal_wildcard");
1300 fs::create_dir_all(&root).expect("failed to create root");
1301 write_file(&root.join("dir/nu_test1"));
1302 write_file(&root.join("dir/other"));
1303
1304 let pattern = format!("{}/dir/nu*", root.to_string_lossy());
1305 let result = glob_with(root.as_path(), &pattern, &GlobWalkOptions::default())
1306 .expect("glob_with should succeed");
1307 let paths = collect_ok_paths(result).expect("failed to collect paths");
1308
1309 assert_eq!(paths.len(), 1);
1310 assert!(Path::new(&paths[0]).is_absolute());
1311
1312 #[cfg(windows)]
1313 let expected = root
1314 .join("dir")
1315 .join("nu_test1")
1316 .to_string_lossy()
1317 .replace('/', "\\");
1318 #[cfg(not(windows))]
1319 let expected = root
1320 .join("dir")
1321 .join("nu_test1")
1322 .to_string_lossy()
1323 .into_owned();
1324
1325 assert_eq!(paths[0], expected);
1326
1327 let _ = fs::remove_dir_all(&root);
1328 }
1329}