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() {
117 return Ok((prefix, Box::new(std::iter::once(Ok(pattern_path)))));
118 }
119
120 let iter =
121 nu_glob::dc_glob::glob_from_interruptible(cwd, &pattern, signals.interrupt_flag())
122 .map_err(|e| {
123 ShellError::Generic(GenericError::new(
124 "Error extracting glob pattern",
125 e.to_string(),
126 span,
127 ))
128 })?;
129
130 let prefix_for_map = prefix.clone();
134 let mapped = iter.map(move |x| match x {
135 Ok(v) => {
136 let v = match &prefix_for_map {
137 Some(p) if v.is_relative() => p.join(&v),
138 _ => v,
139 };
140 Ok(v)
141 }
142 Err(e) => Err(ShellError::Generic(GenericError::new(
143 "Error extracting glob pattern",
144 e.to_string(),
145 span,
146 ))),
147 });
148
149 Ok((prefix, Box::new(mapped)))
150 } else {
151 let glob_options = options.unwrap_or_default();
152 let glob = nu_glob::glob_with(&pattern, glob_options, signals).map_err(|e| {
153 ShellError::Generic(GenericError::new(
154 "Error extracting glob pattern",
155 e.to_string(),
156 span,
157 ))
158 })?;
159
160 let mapped = glob.map(move |x| match x {
161 Ok(v) => Ok(v),
162 Err(e) => Err(ShellError::Generic(GenericError::new(
163 "Error extracting glob pattern",
164 e.error().to_string(),
165 span,
166 ))),
167 });
168
169 Ok((prefix, Box::new(mapped)))
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::glob_from;
176 use nu_protocol::{NuGlob, Signals, Span, Spanned};
177 use std::fs;
178 use std::path::{Path, PathBuf};
179 use std::sync::Arc;
180 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
181 use std::time::{SystemTime, UNIX_EPOCH};
182
183 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
184
185 fn unique_test_dir(prefix: &str) -> PathBuf {
186 let ts = SystemTime::now()
187 .duration_since(UNIX_EPOCH)
188 .map(|d| d.as_nanos())
189 .unwrap_or(0);
190
191 std::env::temp_dir().join(format!(
192 "nu_engine_glob_from_{prefix}_{}_{}",
193 std::process::id(),
194 ts + u128::from(NEXT_ID.fetch_add(1, Ordering::Relaxed))
195 ))
196 }
197
198 fn write_file(path: &PathBuf) {
199 let create_result = fs::create_dir_all(path.parent().unwrap_or(path));
200 assert!(
201 create_result.is_ok(),
202 "failed to create parent dir for {}: {:?}",
203 path.display(),
204 create_result
205 );
206
207 let write_result = fs::write(path, b"x");
208 assert!(
209 write_result.is_ok(),
210 "failed to write test file {}: {:?}",
211 path.display(),
212 write_result
213 );
214 }
215
216 #[test]
217 #[exp(nu_experimental::DC_GLOB)]
218 fn glob_from_dc_glob_remains_lazy_for_first_item() {
219 let root = unique_test_dir("lazy_first_item");
220 let root_create_result = fs::create_dir_all(&root);
221 assert!(
222 root_create_result.is_ok(),
223 "failed to create root test directory {}: {:?}",
224 root.display(),
225 root_create_result
226 );
227
228 write_file(&root.join("top.rs"));
230
231 let nested_count = 9000usize;
233 for idx in 0..nested_count {
234 write_file(&root.join(format!("deep/dir_{idx}/file_{idx}.rs")));
235 }
236
237 let ctrlc = Arc::new(AtomicBool::new(false));
238 let signals = Signals::new(ctrlc);
239 let pattern = Spanned {
240 item: NuGlob::Expand("**/*.rs".to_string()),
241 span: Span::test_data(),
242 };
243
244 let result = glob_from(&pattern, &root, Span::test_data(), None, signals.clone());
245 assert!(result.is_ok(), "glob_from failed");
246
247 let (_, mut iter) = match result {
248 Ok(v) => v,
249 Err(err) => panic!("glob_from failed unexpectedly: {err}"),
250 };
251
252 let first = iter.next();
253 assert!(
254 matches!(first, Some(Ok(_))),
255 "expected first iterator item to be a match, got: {first:?}"
256 );
257
258 signals.trigger();
261
262 let remaining = iter.count();
263 assert!(
264 remaining < 6000,
265 "expected interrupt to stop iteration before full drain; remaining={remaining}"
266 );
267
268 let _ = fs::remove_dir_all(&root);
269 }
270
271 #[test]
272 #[exp(nu_experimental::DC_GLOB)]
273 fn glob_from_dc_glob_matches_literal_file() {
274 let root = unique_test_dir("literal_file");
275 fs::create_dir_all(&root).expect("failed to create root");
276 let file = root.join("test.txt");
277 write_file(&file);
278
279 let ctrlc = Arc::new(AtomicBool::new(false));
280 let signals = Signals::new(ctrlc);
281 let pattern = Spanned {
282 item: NuGlob::Expand(file.to_string_lossy().to_string()),
283 span: Span::test_data(),
284 };
285
286 let result = glob_from(&pattern, Path::new("/"), Span::test_data(), None, signals);
287 assert!(result.is_ok(), "glob_from failed");
288
289 let (_, mut iter) = result.unwrap();
290 let first = iter.next();
291 assert!(
292 matches!(first, Some(Ok(ref p)) if *p == file),
293 "expected file path itself, got: {first:?}"
294 );
295 assert!(iter.next().is_none(), "expected exactly one result");
296
297 let _ = fs::remove_dir_all(&root);
298 }
299
300 #[test]
301 #[exp(nu_experimental::DC_GLOB)]
302 fn glob_from_dc_glob_matches_literal_directory() {
303 let root = unique_test_dir("literal_dir");
304 fs::create_dir_all(&root).expect("failed to create root");
305
306 let ctrlc = Arc::new(AtomicBool::new(false));
307 let signals = Signals::new(ctrlc);
308 let pattern = Spanned {
309 item: NuGlob::Expand(root.to_string_lossy().to_string()),
310 span: Span::test_data(),
311 };
312
313 let result = glob_from(&pattern, Path::new("/"), Span::test_data(), None, signals);
314 assert!(result.is_ok(), "glob_from failed");
315
316 let (_, mut iter) = result.unwrap();
317 let first = iter.next();
318 assert!(
319 matches!(first, Some(Ok(ref p)) if *p == root),
320 "expected directory path itself, got: {first:?}"
321 );
322 assert!(iter.next().is_none(), "expected exactly one result");
323
324 let _ = fs::remove_dir_all(&root);
325 }
326}