1use nu_engine::command_prelude::*;
2use nu_protocol::{ListStream, Signals, shell_error::generic::GenericError};
3use wax::{
4 Glob as WaxGlob, any, walk::DepthBehavior, walk::DepthMax, walk::Entry, walk::FileIterator,
5 walk::GlobEntry, walk::LinkBehavior, walk::WalkBehavior,
6};
7
8#[derive(Clone)]
9pub struct Glob;
10
11impl Command for Glob {
12 fn name(&self) -> &str {
13 "glob"
14 }
15
16 fn signature(&self) -> Signature {
17 let signature = Signature::build("glob")
18 .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::String)))])
19 .required(
20 "glob",
21 SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]),
22 "The glob expression.",
23 )
24 .named(
25 "depth",
26 SyntaxShape::Int,
27 "Directory depth to search.",
28 Some('d'),
29 )
30 .switch(
31 "no-dir",
32 "Whether to filter out directories from the returned paths.",
33 Some('D'),
34 )
35 .switch(
36 "no-file",
37 "Whether to filter out files from the returned paths.",
38 Some('F'),
39 )
40 .switch(
41 "no-symlink",
42 "Whether to filter out symlinks from the returned paths.",
43 Some('S'),
44 )
45 .switch(
46 "follow-symlinks",
47 "Whether to follow symbolic links to their targets.",
48 Some('l'),
49 )
50 .named(
51 "exclude",
52 SyntaxShape::List(Box::new(SyntaxShape::String)),
53 "Patterns to exclude from the search: `glob` will not walk the inside of directories matching the excluded patterns.",
54 Some('e'),
55 )
56 .category(Category::FileSystem);
57
58 if !nu_experimental::DC_GLOB.get() {
59 return signature;
60 }
61
62 signature
63 .switch(
64 "ignore-case",
65 "Whether to ignore case when matching the glob pattern.",
66 Some('i'),
67 )
68 .rest(
69 "debug-args",
70 SyntaxShape::String,
71 "Additional positional args used by --dbg-matches/--dbg-glob.",
72 )
73 .switch(
74 "dbg-parse",
75 "Use dc-glob debug parser mode. Requires one positional pattern.",
76 None,
77 )
78 .switch(
79 "dbg-compile",
80 "Use dc-glob debug compile mode. Requires one positional pattern.",
81 None,
82 )
83 .switch(
84 "dbg-matches",
85 "Use dc-glob debug match mode. Requires pattern and optional path positional args.",
86 None,
87 )
88 .switch(
89 "dbg-glob",
90 "Use dc-glob debug glob mode. Requires pattern and optional relative-to positional args.",
91 None,
92 )
93 }
94
95 fn description(&self) -> &str {
96 "Creates a list of files and/or folders based on the glob pattern provided."
97 }
98
99 fn search_terms(&self) -> Vec<&str> {
100 vec!["pattern", "files", "folders", "list", "ls"]
101 }
102
103 fn examples(&self) -> Vec<Example<'_>> {
104 vec![
105 Example {
106 description: "Search for *.rs files.",
107 example: "glob *.rs",
108 result: None,
109 },
110 Example {
111 description: "Search for *.rs and *.toml files recursively up to 2 folders deep.",
112 example: "glob **/*.{rs,toml} --depth 2",
113 result: None,
114 },
115 Example {
116 description: "Search for files and folders that begin with uppercase C or lowercase c.",
117 example: r#"glob "[Cc]*""#,
118 result: None,
119 },
120 Example {
121 description: "Search for files and folders like abc or xyz substituting a character for ?.",
122 example: r#"glob "{a?c,x?z}""#,
123 result: None,
124 },
125 Example {
126 description: "A case-insensitive search for files and folders that begin with c.",
127 example: if nu_experimental::DC_GLOB.get() {
128 "glob c* --ignore-case"
129 } else {
130 r#"glob "(?i)c*""#
131 },
132 result: None,
133 },
134 Example {
135 description: "Search for files or folders that do not begin with c, C, b, M, or s.",
136 example: r#"glob "[!cCbMs]*""#,
137 result: None,
138 },
139 Example {
140 description: "Search for files or folders with 3 a's in a row in the name.",
141 example: "glob <a*:3>",
142 result: None,
143 },
144 Example {
145 description: "Search for files or folders with only a, b, c, or d in the file name between 1 and 10 times.",
146 example: "glob <[a-d]:1,10>",
147 result: None,
148 },
149 Example {
150 description: "Search for folders that begin with an uppercase ASCII letter, ignoring files and symlinks.",
151 example: r#"glob "[A-Z]*" --no-file --no-symlink"#,
152 result: None,
153 },
154 Example {
155 description: "Search for files named tsconfig.json that are not in node_modules directories.",
156 example: "glob **/tsconfig.json --exclude [**/node_modules/**]",
157 result: None,
158 },
159 Example {
160 description: "Search for all files that are not in the target nor .git directories.",
161 example: "glob **/* --exclude [**/target/** **/.git/** */]",
162 result: None,
163 },
164 Example {
165 description: "Search for files following symbolic links to their targets.",
166 example: r#"glob "**/*.txt" --follow-symlinks"#,
167 result: None,
168 },
169 ]
170 }
171
172 fn extra_description(&self) -> &str {
173 if nu_experimental::DC_GLOB.get() {
174 ""
175 } else {
176 "For more glob pattern help, please refer to https://docs.rs/crate/wax/latest."
177 }
178 }
179
180 fn run(
181 &self,
182 engine_state: &EngineState,
183 stack: &mut Stack,
184 call: &Call,
185 _input: PipelineData,
186 ) -> Result<PipelineData, ShellError> {
187 let span = call.head;
188 let glob_pattern_input: Value = call.req(engine_state, stack, 0)?;
189
190 if nu_experimental::DC_GLOB.get() {
191 let has_dbg_flags = call.get_flag_span(stack, "dbg-parse").is_some()
192 || call.get_flag_span(stack, "dbg-compile").is_some()
193 || call.get_flag_span(stack, "dbg-matches").is_some()
194 || call.get_flag_span(stack, "dbg-glob").is_some();
195
196 if has_dbg_flags {
197 let dbg_parse = call.has_flag(engine_state, stack, "dbg-parse")?;
198 let dbg_compile = call.has_flag(engine_state, stack, "dbg-compile")?;
199 let dbg_matches = call.has_flag(engine_state, stack, "dbg-matches")?;
200 let dbg_glob = call.has_flag(engine_state, stack, "dbg-glob")?;
201
202 let dbg_modes = [dbg_parse, dbg_compile, dbg_matches, dbg_glob]
203 .into_iter()
204 .filter(|set| *set)
205 .count();
206
207 if dbg_modes > 1 {
208 return Err(ShellError::IncompatibleParametersSingle {
209 msg:
210 "use only one of --dbg-parse, --dbg-compile, --dbg-matches, --dbg-glob"
211 .to_string(),
212 span,
213 });
214 }
215
216 if dbg_modes == 1 {
217 let args = call.rest::<Spanned<String>>(engine_state, stack, 0)?;
218 let subcommand = if dbg_parse {
219 "dbg-parse"
220 } else if dbg_compile {
221 "dbg-compile"
222 } else if dbg_matches {
223 "dbg-matches"
224 } else {
225 "dbg-glob"
226 };
227
228 return run_debug_subcommand(
229 engine_state,
230 stack,
231 subcommand.to_string(),
232 args,
233 span,
234 );
235 }
236 }
237 }
238
239 let glob_span = glob_pattern_input.span();
240 let depth = call.get_flag(engine_state, stack, "depth")?;
241 let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
242 let no_files = call.has_flag(engine_state, stack, "no-file")?;
243 let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?;
244 let follow_symlinks = call.has_flag(engine_state, stack, "follow-symlinks")?;
245 let paths_to_exclude: Option<Value> = call.get_flag(engine_state, stack, "exclude")?;
246
247 let (not_patterns, not_pattern_span): (Vec<String>, Span) = match paths_to_exclude {
248 None => (vec![], span),
249 Some(f) => {
250 let pat_span = f.span();
251 match f {
252 Value::List { vals: pats, .. } => {
253 let p = convert_patterns(pats.as_slice())?;
254 (p, pat_span)
255 }
256 _ => (vec![], span),
257 }
258 }
259 };
260
261 let glob_pattern =
262 match glob_pattern_input {
263 Value::String { val, .. } | Value::Glob { val, .. } => val,
264 _ => return Err(ShellError::IncorrectValue {
265 msg: "Incorrect glob pattern supplied to glob. Please use string or glob only."
266 .to_string(),
267 val_span: call.head,
268 call_span: glob_span,
269 }),
270 };
271
272 let extra_args = call.rest::<Spanned<String>>(engine_state, stack, 1)?;
273 if !extra_args.is_empty() {
274 return Err(ShellError::IncompatibleParametersSingle {
275 msg: "extra positional argument".to_string(),
276 span: extra_args[0].span,
277 });
278 }
279
280 if glob_pattern.is_empty() {
281 return Err(ShellError::Generic(
282 GenericError::new(
283 "glob pattern must not be empty",
284 "glob pattern is empty",
285 glob_span,
286 )
287 .with_help("add characters to the glob pattern"),
288 ));
289 }
290
291 match nu_experimental::DC_GLOB.get() {
292 true => {
293 let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
294 run_dc_glob(
295 engine_state,
296 stack,
297 &glob_pattern,
298 depth,
299 follow_symlinks,
300 not_patterns,
301 glob_span,
302 no_dirs,
303 no_files,
304 no_symlinks,
305 ignore_case,
306 span,
307 )
308 }
309 false => {
310 #[cfg(windows)]
312 let glob_pattern = patch_windows_glob_pattern(glob_pattern, glob_span)?;
313
314 run_legacy_glob(
315 engine_state,
316 stack,
317 &glob_pattern,
318 depth,
319 follow_symlinks,
320 not_patterns,
321 glob_span,
322 not_pattern_span,
323 no_dirs,
324 no_files,
325 no_symlinks,
326 span,
327 )
328 }
329 }
330 }
331}
332
333fn infer_folder_depth(glob_pattern: &str, depth: Option<usize>) -> usize {
334 if let Some(depth) = depth {
335 depth
336 } else if glob_pattern.contains("**") {
337 usize::MAX
338 } else if glob_pattern.contains('/') {
339 glob_pattern.split('/').count() + 1
340 } else {
341 1
342 }
343}
344
345#[allow(clippy::too_many_arguments)]
346fn run_dc_glob(
347 engine_state: &EngineState,
348 stack: &Stack,
349 glob_pattern: &str,
350 depth: Option<usize>,
351 follow_symlinks: bool,
352 not_patterns: Vec<String>,
353 glob_span: Span,
354 no_dirs: bool,
355 no_files: bool,
356 no_symlinks: bool,
357 ignore_case: bool,
358 span: Span,
359) -> Result<PipelineData, ShellError> {
360 let folder_depth = infer_folder_depth(glob_pattern, depth);
361 let cwd = engine_state.cwd(Some(stack))?;
362 let options = nu_glob::dc_glob::GlobWalkOptions {
363 max_depth: (folder_depth != usize::MAX).then_some(folder_depth),
364 follow_symlinks,
365 excludes: not_patterns,
366 interrupt: engine_state.signals().interrupt_flag(),
367 ignore_case,
368 };
369 let cwd_for_matches = cwd.as_std_path().to_path_buf();
370 let glob_pattern = nu_path::expand_tilde(glob_pattern)
371 .to_string_lossy()
372 .to_string();
373
374 let matches =
375 nu_glob::dc_glob::glob_with(cwd.as_std_path(), &glob_pattern, &options).map_err(|err| {
376 ShellError::Generic(GenericError::new(
377 "error with glob pattern",
378 err.to_string(),
379 glob_span,
380 ))
381 })?;
382
383 let matches = matches.map(move |item| {
384 item.map(|path| {
385 if path.is_absolute() {
386 path
387 } else {
388 cwd_for_matches.join(path)
389 }
390 })
391 .map_err(|err| {
392 ShellError::Generic(GenericError::new(
393 "error with glob pattern",
394 err.to_string(),
395 glob_span,
396 ))
397 })
398 });
399
400 let values = glob_paths_to_value(
401 engine_state.signals(),
402 matches,
403 no_dirs,
404 no_files,
405 no_symlinks,
406 span,
407 );
408
409 Ok(values.into_pipeline_data(span, engine_state.signals().clone()))
410}
411
412#[allow(clippy::too_many_arguments)]
413fn run_legacy_glob(
414 engine_state: &EngineState,
415 stack: &Stack,
416 glob_pattern: &str,
417 depth: Option<usize>,
418 follow_symlinks: bool,
419 not_patterns: Vec<String>,
420 glob_span: Span,
421 not_pattern_span: Span,
422 no_dirs: bool,
423 no_files: bool,
424 no_symlinks: bool,
425 span: Span,
426) -> Result<PipelineData, ShellError> {
427 let folder_depth = infer_folder_depth(glob_pattern, depth);
430
431 let (prefix, glob) = match WaxGlob::new(glob_pattern) {
432 Ok(p) => p.partition_or_empty(),
433 Err(e) => {
434 return Err(ShellError::Generic(GenericError::new(
435 "error with glob pattern",
436 format!("{e}"),
437 glob_span,
438 )));
439 }
440 };
441
442 let path = engine_state.cwd_as_string(Some(stack))?;
443 let path = nu_path::absolute_with(prefix, path).map_err(|e| {
444 ShellError::Generic(GenericError::new("invalid path", format!("{e}"), glob_span))
445 })?;
446 let path = match path.try_exists() {
447 Ok(true) => path,
448 Ok(false) => std::path::PathBuf::new(), Err(e) => {
450 return Err(ShellError::Generic(GenericError::new(
451 "error accessing path",
452 format!("{e}"),
453 glob_span,
454 )));
455 }
456 };
457
458 let link_behavior = match follow_symlinks {
459 true => LinkBehavior::ReadTarget,
460 false => LinkBehavior::ReadFile,
461 };
462
463 let make_walk_behavior = |depth: usize| WalkBehavior {
464 depth: DepthBehavior::Max(DepthMax(depth)),
465 link: link_behavior,
466 };
467
468 let result = if !not_patterns.is_empty() {
469 let patterns: Vec<WaxGlob<'static>> = not_patterns
470 .into_iter()
471 .map(|pattern| {
472 WaxGlob::new(&pattern)
473 .map_err(|err| {
474 ShellError::Generic(GenericError::new(
475 "error with glob's not pattern",
476 format!("{err}"),
477 not_pattern_span,
478 ))
479 })
480 .map(|g| g.into_owned())
481 })
482 .collect::<Result<_, _>>()?;
483
484 let any_pattern = any(patterns).map_err(|err| {
485 ShellError::Generic(GenericError::new(
486 "error with glob's not pattern",
487 format!("{err}"),
488 not_pattern_span,
489 ))
490 })?;
491
492 let glob_results = glob
493 .walk_with_behavior(path, make_walk_behavior(folder_depth))
494 .not(any_pattern)
495 .map_err(|err| {
496 ShellError::Generic(GenericError::new(
497 "error with glob's not pattern",
498 format!("{err}"),
499 not_pattern_span,
500 ))
501 })?
502 .flatten();
503
504 glob_to_value(
505 engine_state.signals(),
506 glob_results,
507 no_dirs,
508 no_files,
509 no_symlinks,
510 span,
511 )
512 } else {
513 let glob_results = glob
514 .walk_with_behavior(path, make_walk_behavior(folder_depth))
515 .flatten();
516 glob_to_value(
517 engine_state.signals(),
518 glob_results,
519 no_dirs,
520 no_files,
521 no_symlinks,
522 span,
523 )
524 };
525
526 Ok(result.into_pipeline_data(span, engine_state.signals().clone()))
527}
528
529#[cfg(windows)]
530fn patch_windows_glob_pattern(glob_pattern: String, glob_span: Span) -> Result<String, ShellError> {
531 let mut chars = glob_pattern.chars();
532 match (chars.next(), chars.next(), chars.next()) {
533 (Some(drive), Some(':'), Some('/' | '\\')) if drive.is_ascii_alphabetic() => {
534 Ok(format!("{drive}\\:/{}", chars.as_str()))
535 }
536 (Some(drive), Some(':'), Some(_)) if drive.is_ascii_alphabetic() => {
537 Err(ShellError::Generic(
538 GenericError::new(
539 "invalid Windows path format",
540 "Windows paths with drive letters must include a path separator (/) after the colon",
541 glob_span,
542 )
543 .with_help("use format like 'C:/' instead of 'C:'"),
544 ))
545 }
546 _ => Ok(glob_pattern),
547 }
548}
549
550fn convert_patterns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
551 let res = columns
552 .iter()
553 .map(|value| match &value {
554 Value::String { val: s, .. } => Ok(s.clone()),
555 _ => Err(ShellError::IncompatibleParametersSingle {
556 msg: "Incorrect column format, Only string as column name".to_string(),
557 span: value.span(),
558 }),
559 })
560 .collect::<Result<Vec<String>, _>>()?;
561
562 Ok(res)
563}
564
565fn glob_to_value(
566 signals: &Signals,
567 glob_results: impl Iterator<Item = GlobEntry> + Send + 'static,
568 no_dirs: bool,
569 no_files: bool,
570 no_symlinks: bool,
571 span: Span,
572) -> ListStream {
573 let map_signals = signals.clone();
574 let result = glob_results.filter_map(move |entry| {
575 if let Err(err) = map_signals.check(&span) {
576 return Some(Value::error(err, span));
577 };
578 let file_type = entry.file_type();
579
580 if !(no_dirs && file_type.is_dir()
581 || no_files && file_type.is_file()
582 || no_symlinks && file_type.is_symlink())
583 {
584 Some(Value::string(
585 entry.into_path().to_string_lossy().into_owned(),
586 span,
587 ))
588 } else {
589 None
590 }
591 });
592
593 ListStream::new(result, span, signals.clone())
594}
595
596fn glob_paths_to_value(
597 signals: &Signals,
598 glob_results: impl Iterator<Item = Result<std::path::PathBuf, ShellError>> + Send + 'static,
599 no_dirs: bool,
600 no_files: bool,
601 no_symlinks: bool,
602 span: Span,
603) -> ListStream {
604 let map_signals = signals.clone();
605 let needs_file_type = no_dirs || no_files || no_symlinks;
606 let result = glob_results.filter_map(move |entry| {
607 if let Err(err) = map_signals.check(&span) {
608 return Some(Value::error(err, span));
609 }
610
611 let path = match entry {
612 Ok(path) => path,
613 Err(err) => return Some(Value::error(err, span)),
614 };
615
616 if !needs_file_type {
617 return Some(Value::string(path.to_string_lossy().into_owned(), span));
618 }
619
620 let file_type = match std::fs::symlink_metadata(&path) {
621 Ok(meta) => meta.file_type(),
622 Err(_) => {
623 return Some(Value::string(path.to_string_lossy().into_owned(), span));
624 }
625 };
626
627 if !(no_dirs && file_type.is_dir()
628 || no_files && file_type.is_file()
629 || no_symlinks && file_type.is_symlink())
630 {
631 Some(Value::string(path.to_string_lossy().into_owned(), span))
632 } else {
633 None
634 }
635 });
636
637 ListStream::new(result, span, signals.clone())
638}
639
640fn run_debug_subcommand(
641 engine_state: &EngineState,
642 stack: &Stack,
643 subcommand: String,
644 args: Vec<Spanned<String>>,
645 span: Span,
646) -> Result<PipelineData, ShellError> {
647 let expected = match subcommand.as_str() {
648 "dbg-parse" | "dbg-compile" => 1,
649 "dbg-matches" | "dbg-glob" => 2,
650 _ => 0,
651 };
652
653 if expected > 0 && args.len() > expected {
654 return Err(ShellError::IncompatibleParametersSingle {
655 msg: "extra positional argument".to_string(),
656 span: args[expected].span,
657 });
658 }
659
660 match (subcommand.as_str(), args.first()) {
661 ("dbg-parse", Some(pattern)) => {
662 let text = nu_glob::dc_glob::debug_parse(&pattern.item);
663
664 Ok(Value::string(text, span).into_pipeline_data())
665 }
666 ("dbg-compile", Some(pattern)) => {
667 let text = nu_glob::dc_glob::debug_compile(&pattern.item).map_err(|err| {
668 ShellError::Generic(GenericError::new(
669 "failed to compile debug glob pattern",
670 err.to_string(),
671 pattern.span,
672 ))
673 })?;
674 Ok(Value::string(text, span).into_pipeline_data())
675 }
676 ("dbg-matches", Some(pattern)) => {
677 let path = args.get(1).map(|p| p.item.as_str()).unwrap_or(".");
678 let matches = nu_glob::dc_glob::debug_matches(&pattern.item, path).map_err(|err| {
679 ShellError::Generic(GenericError::new(
680 "failed to run debug match",
681 err.to_string(),
682 pattern.span,
683 ))
684 })?;
685 Ok(Value::bool(matches, span).into_pipeline_data())
686 }
687 ("dbg-glob", Some(pattern)) => {
688 let pattern_span = pattern.span;
689 let relative_to = args.get(1).map(|p| p.item.as_str()).unwrap_or(".");
690 let cwd = engine_state.cwd(Some(stack))?;
691 let relative_to =
692 nu_path::absolute_with(relative_to, cwd.as_std_path()).map_err(|err| {
693 ShellError::Generic(GenericError::new(
694 "invalid debug glob path",
695 err.to_string(),
696 span,
697 ))
698 })?;
699 let expanded_pattern = nu_path::expand_tilde(&pattern.item)
700 .to_string_lossy()
701 .to_string();
702 let out = nu_glob::dc_glob::glob_with(
703 relative_to,
704 &expanded_pattern,
705 &nu_glob::dc_glob::GlobWalkOptions::default(),
706 )
707 .map_err(|err| {
708 ShellError::Generic(GenericError::new(
709 "failed to run debug glob",
710 err.to_string(),
711 pattern.span,
712 ))
713 })?;
714
715 let values = out.map(move |path| match path {
716 Ok(path) => Value::string(path.to_string_lossy().into_owned(), span),
717 Err(err) => Value::error(
718 ShellError::Generic(GenericError::new(
719 "failed to run debug glob",
720 err.to_string(),
721 pattern_span,
722 )),
723 span,
724 ),
725 });
726 Ok(
727 ListStream::new(values, span, engine_state.signals().clone())
728 .into_pipeline_data(span, engine_state.signals().clone()),
729 )
730 }
731 ("dbg-parse" | "dbg-compile" | "dbg-matches" | "dbg-glob", None) => {
732 Err(ShellError::MissingParameter {
733 param_name: "pattern".to_string(),
734 span,
735 })
736 }
737 (unknown, _) => Err(ShellError::IncompatibleParametersSingle {
738 msg: format!("unknown debug subcommand '{unknown}'"),
739 span,
740 }),
741 }
742}
743
744#[cfg(windows)]
745#[cfg(test)]
746mod windows_tests {
747 use super::*;
748
749 #[test]
750 fn glob_pattern_with_drive_letter() {
751 let pattern = "D:/*.mp4".to_string();
752 let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
753 assert!(WaxGlob::new(&result).is_ok());
754
755 let pattern = "Z:/**/*.md".to_string();
756 let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
757 assert!(WaxGlob::new(&result).is_ok());
758
759 let pattern = "C:/nested/**/escaped/path/<[_a-zA-Z\\-]>.md".to_string();
760 let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
761 assert!(dbg!(WaxGlob::new(&result)).is_ok());
762 }
763
764 #[test]
765 fn glob_pattern_without_drive_letter() {
766 let pattern = "/usr/bin/*.sh".to_string();
767 let result = patch_windows_glob_pattern(pattern.clone(), Span::test_data()).unwrap();
768 assert_eq!(result, pattern);
769 assert!(WaxGlob::new(&result).is_ok());
770
771 let pattern = "a".to_string();
772 let result = patch_windows_glob_pattern(pattern.clone(), Span::test_data()).unwrap();
773 assert_eq!(result, pattern);
774 assert!(WaxGlob::new(&result).is_ok());
775 }
776
777 #[test]
778 fn invalid_path_format() {
779 let invalid = "C:lol".to_string();
780 let result = patch_windows_glob_pattern(invalid, Span::test_data());
781 assert!(result.is_err());
782 }
783
784 #[test]
785 fn unpatched_patterns() {
786 let unpatched = "C:/Users/*.txt".to_string();
787 assert!(WaxGlob::new(&unpatched).is_err());
788
789 let patched = patch_windows_glob_pattern(unpatched, Span::test_data()).unwrap();
790 assert!(WaxGlob::new(&patched).is_ok());
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
799 fn signature_mentions_dbg_subcommands_and_ignore_case() {
800 let signature = Glob.signature();
801 let rendered = format!("{signature:#?}");
802
803 if nu_experimental::DC_GLOB.get() {
804 assert!(
805 rendered.contains("dbg-parse") && rendered.contains("dbg-glob"),
806 "glob signature should mention dbg-* subcommands when dc-glob is enabled"
807 );
808 assert!(
809 rendered.contains("ignore-case"),
810 "glob signature should mention --ignore-case when dc-glob is enabled"
811 );
812 } else {
813 assert!(
814 !rendered.contains("dbg-parse") && !rendered.contains("dbg-glob"),
815 "glob signature should hide dbg-* subcommands when dc-glob is disabled"
816 );
817 assert!(
818 !rendered.contains("ignore-case"),
819 "glob signature should hide --ignore-case when dc-glob is disabled"
820 );
821 }
822 }
823}