1use nu_glob::MatchOptions;
2use nu_path::{absolute_with, expand_path_with};
3use nu_protocol::{
4 NuGlob, ShellError, Signals, Span, Spanned, shell_error::generic::GenericError,
5 shell_error::io::IoError,
6};
7use std::{
8 fs, io,
9 path::{Component, Path, PathBuf},
10};
11
12#[allow(clippy::type_complexity)]
20pub fn glob_from(
21 pattern: &Spanned<NuGlob>,
22 cwd: &Path,
23 span: Span,
24 options: Option<MatchOptions>,
25 signals: Signals,
26) -> Result<
27 (
28 Option<PathBuf>,
29 Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + Send>,
30 ),
31 ShellError,
32> {
33 let no_glob_for_pattern = matches!(pattern.item, NuGlob::DoNotExpand(_));
34 let pattern_span = pattern.span;
35 let (prefix, pattern) = if nu_glob::is_glob_with_backend(pattern.item.as_ref()) {
36 let mut p = PathBuf::new();
38 let path = PathBuf::from(&pattern.item.as_ref());
39 let components = path.components();
40 let mut counter = 0;
41
42 for c in components {
43 if let Component::Normal(os) = c
44 && nu_glob::is_glob_with_backend(os.to_string_lossy().as_ref())
45 {
46 break;
47 }
48 p.push(c);
49 counter += 1;
50 }
51
52 let mut just_pattern = PathBuf::new();
53 for c in counter..path.components().count() {
54 if let Some(comp) = path.components().nth(c) {
55 just_pattern.push(comp);
56 }
57 }
58 if no_glob_for_pattern {
59 just_pattern = PathBuf::from(nu_glob::escape_with_backend(
60 &just_pattern.to_string_lossy(),
61 ));
62 }
63
64 let path = expand_path_with(p, cwd, pattern.item.is_expand());
66 let escaped_prefix = PathBuf::from(nu_glob::escape_with_backend(&path.to_string_lossy()));
67
68 (Some(path), escaped_prefix.join(just_pattern))
69 } else {
70 let path = PathBuf::from(&pattern.item.as_ref());
71 let path = expand_path_with(path, cwd, pattern.item.is_expand());
72 let is_symlink = match fs::symlink_metadata(&path) {
73 Ok(attr) => attr.file_type().is_symlink(),
74 Err(_) => false,
75 };
76
77 if is_symlink {
78 (path.parent().map(|parent| parent.to_path_buf()), path)
79 } else {
80 let path = match absolute_with(path.clone(), cwd) {
81 Ok(p) if p.exists() => {
82 if nu_glob::is_glob_with_backend(p.to_string_lossy().as_ref()) {
83 PathBuf::from(nu_glob::escape_with_backend(&p.to_string_lossy()))
87 } else {
88 p
89 }
90 }
91 Ok(_) => {
92 return Err(IoError::new(
93 io::Error::from(io::ErrorKind::NotFound),
94 pattern_span,
95 path,
96 )
97 .into());
98 }
99 Err(err) => {
100 return Err(IoError::new(err, pattern_span, path).into());
101 }
102 };
103 (path.parent().map(|parent| parent.to_path_buf()), path)
104 }
105 };
106
107 let pattern = pattern.to_string_lossy().to_string();
108
109 if nu_experimental::DC_GLOB.get() {
110 let pattern_path = PathBuf::from(&pattern);
111 if pattern_path.exists() && !nu_glob::is_glob_with_backend(&pattern) {
122 return Ok((prefix, Box::new(std::iter::once(Ok(pattern_path)))));
123 }
124
125 let iter =
126 nu_glob::dc_glob::glob_from_interruptible(cwd, &pattern, signals.interrupt_flag())
127 .map_err(|e| {
128 ShellError::Generic(GenericError::new(
129 "Error extracting glob pattern",
130 e.to_string(),
131 span,
132 ))
133 })?;
134
135 let prefix_for_map = prefix.clone();
139 let mapped = iter.map(move |x| match x {
140 Ok(v) => {
141 let v = match &prefix_for_map {
142 Some(p) if v.is_relative() => p.join(&v),
143 _ => v,
144 };
145 Ok(v)
146 }
147 Err(e) => Err(ShellError::Generic(GenericError::new(
148 "Error extracting glob pattern",
149 e.to_string(),
150 span,
151 ))),
152 });
153
154 Ok((prefix, Box::new(mapped)))
155 } else {
156 let glob_options = options.unwrap_or_default();
157 let glob = nu_glob::glob_with(&pattern, glob_options, signals).map_err(|e| {
158 ShellError::Generic(GenericError::new(
159 "Error extracting glob pattern",
160 e.to_string(),
161 span,
162 ))
163 })?;
164
165 let mapped = glob.map(move |x| match x {
166 Ok(v) => Ok(v),
167 Err(e) => Err(ShellError::Generic(GenericError::new(
168 "Error extracting glob pattern",
169 e.error().to_string(),
170 span,
171 ))),
172 });
173
174 Ok((prefix, Box::new(mapped)))
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::glob_from;
181 use nu_protocol::{NuGlob, Signals, Span, Spanned};
182 use std::fs;
183 use std::path::{Path, PathBuf};
184 use std::sync::Arc;
185 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
186 use std::time::{SystemTime, UNIX_EPOCH};
187
188 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
189
190 fn unique_test_dir(prefix: &str) -> PathBuf {
191 let ts = SystemTime::now()
192 .duration_since(UNIX_EPOCH)
193 .map(|d| d.as_nanos())
194 .unwrap_or(0);
195
196 std::env::temp_dir().join(format!(
197 "nu_engine_glob_from_{prefix}_{}_{}",
198 std::process::id(),
199 ts + u128::from(NEXT_ID.fetch_add(1, Ordering::Relaxed))
200 ))
201 }
202
203 fn write_file(path: &PathBuf) {
204 let create_result = fs::create_dir_all(path.parent().unwrap_or(path));
205 assert!(
206 create_result.is_ok(),
207 "failed to create parent dir for {}: {:?}",
208 path.display(),
209 create_result
210 );
211
212 let write_result = fs::write(path, b"x");
213 assert!(
214 write_result.is_ok(),
215 "failed to write test file {}: {:?}",
216 path.display(),
217 write_result
218 );
219 }
220
221 #[test]
222 #[exp(nu_experimental::DC_GLOB)]
223 fn glob_from_dc_glob_remains_lazy_for_first_item() {
224 let root = unique_test_dir("lazy_first_item");
225 let root_create_result = fs::create_dir_all(&root);
226 assert!(
227 root_create_result.is_ok(),
228 "failed to create root test directory {}: {:?}",
229 root.display(),
230 root_create_result
231 );
232
233 write_file(&root.join("top.rs"));
235
236 let nested_count = 9000usize;
238 for idx in 0..nested_count {
239 write_file(&root.join(format!("deep/dir_{idx}/file_{idx}.rs")));
240 }
241
242 let ctrlc = Arc::new(AtomicBool::new(false));
243 let signals = Signals::new(ctrlc);
244 let pattern = Spanned {
245 item: NuGlob::Expand("**/*.rs".to_string()),
246 span: Span::test_data(),
247 };
248
249 let result = glob_from(&pattern, &root, Span::test_data(), None, signals.clone());
250 assert!(result.is_ok(), "glob_from failed");
251
252 let (_, mut iter) = match result {
253 Ok(v) => v,
254 Err(err) => panic!("glob_from failed unexpectedly: {err}"),
255 };
256
257 let first = iter.next();
258 assert!(
259 matches!(first, Some(Ok(_))),
260 "expected first iterator item to be a match, got: {first:?}"
261 );
262
263 signals.trigger();
266
267 let remaining = iter.count();
268 assert!(
269 remaining < 6000,
270 "expected interrupt to stop iteration before full drain; remaining={remaining}"
271 );
272
273 let _ = fs::remove_dir_all(&root);
274 }
275
276 #[test]
277 #[exp(nu_experimental::DC_GLOB)]
278 fn glob_from_dc_glob_matches_literal_file() {
279 let root = unique_test_dir("literal_file");
280 fs::create_dir_all(&root).expect("failed to create root");
281 let file = root.join("test.txt");
282 write_file(&file);
283
284 let ctrlc = Arc::new(AtomicBool::new(false));
285 let signals = Signals::new(ctrlc);
286 let pattern = Spanned {
287 item: NuGlob::Expand(file.to_string_lossy().to_string()),
288 span: Span::test_data(),
289 };
290
291 let result = glob_from(&pattern, Path::new("/"), Span::test_data(), None, signals);
292 assert!(result.is_ok(), "glob_from failed");
293
294 let (_, mut iter) = result.unwrap();
295 let first = iter.next();
296 assert!(
297 matches!(first, Some(Ok(ref p)) if *p == file),
298 "expected file path itself, got: {first:?}"
299 );
300 assert!(iter.next().is_none(), "expected exactly one result");
301
302 let _ = fs::remove_dir_all(&root);
303 }
304
305 #[test]
306 #[exp(nu_experimental::DC_GLOB)]
307 fn glob_from_dc_glob_matches_literal_directory() {
308 let root = unique_test_dir("literal_dir");
309 fs::create_dir_all(&root).expect("failed to create root");
310
311 let ctrlc = Arc::new(AtomicBool::new(false));
312 let signals = Signals::new(ctrlc);
313 let pattern = Spanned {
314 item: NuGlob::Expand(root.to_string_lossy().to_string()),
315 span: Span::test_data(),
316 };
317
318 let result = glob_from(&pattern, Path::new("/"), Span::test_data(), None, signals);
319 assert!(result.is_ok(), "glob_from failed");
320
321 let (_, mut iter) = result.unwrap();
322 let first = iter.next();
323 assert!(
324 matches!(first, Some(Ok(ref p)) if *p == root),
325 "expected directory path itself, got: {first:?}"
326 );
327 assert!(iter.next().is_none(), "expected exactly one result");
328
329 let _ = fs::remove_dir_all(&root);
330 }
331
332 #[cfg(not(windows))]
334 #[test]
335 #[exp(nu_experimental::DC_GLOB)]
336 fn glob_from_dc_glob_star_with_literal_star_file() {
337 let root = unique_test_dir("star_file");
340 fs::create_dir_all(&root).expect("failed to create root");
341 write_file(&root.join("a"));
342 write_file(&root.join("b"));
343 write_file(&root.join("*"));
344
345 let ctrlc = Arc::new(AtomicBool::new(false));
346 let signals = Signals::new(ctrlc);
347 let pattern = Spanned {
348 item: NuGlob::Expand("*".to_string()),
349 span: Span::test_data(),
350 };
351
352 let result = glob_from(&pattern, &root, Span::test_data(), None, signals);
353 assert!(result.is_ok(), "glob_from failed");
354
355 let (_, iter) = match result {
356 Ok(v) => v,
357 Err(err) => panic!("glob_from failed unexpectedly: {err}"),
358 };
359 let mut names: Vec<String> = iter
360 .map(|r| {
361 r.expect("glob path ok")
362 .file_name()
363 .expect("basename")
364 .to_string_lossy()
365 .into_owned()
366 })
367 .collect();
368 names.sort();
369
370 assert_eq!(
371 names,
372 vec!["*".to_string(), "a".to_string(), "b".to_string()]
373 );
374
375 let _ = fs::remove_dir_all(&root);
376 }
377
378 #[cfg(not(windows))]
380 #[test]
381 #[exp(nu_experimental::DC_GLOB)]
382 fn glob_from_dc_glob_prefix_wildcard_with_literal_match_name() {
383 let root = unique_test_dir("foo_star");
385 fs::create_dir_all(&root).expect("failed to create root");
386 write_file(&root.join("foo1"));
387 write_file(&root.join("foo2"));
388 write_file(&root.join("foo*"));
389 write_file(&root.join("other"));
390
391 let ctrlc = Arc::new(AtomicBool::new(false));
392 let signals = Signals::new(ctrlc);
393 let pattern = Spanned {
394 item: NuGlob::Expand("foo*".to_string()),
395 span: Span::test_data(),
396 };
397
398 let result = glob_from(&pattern, &root, Span::test_data(), None, signals);
399 assert!(result.is_ok(), "glob_from failed");
400
401 let (_, iter) = match result {
402 Ok(v) => v,
403 Err(err) => panic!("glob_from failed unexpectedly: {err}"),
404 };
405 let mut names: Vec<String> = iter
406 .map(|r| {
407 r.expect("glob path ok")
408 .file_name()
409 .expect("basename")
410 .to_string_lossy()
411 .into_owned()
412 })
413 .collect();
414 names.sort();
415
416 assert_eq!(
417 names,
418 vec!["foo*".to_string(), "foo1".to_string(), "foo2".to_string()]
419 );
420
421 let _ = fs::remove_dir_all(&root);
422 }
423}