1use nu_engine::command_prelude::*;
2use nu_protocol::{ListStream, Signals};
3use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry};
4
5#[derive(Clone)]
6pub struct Glob;
7
8impl Command for Glob {
9 fn name(&self) -> &str {
10 "glob"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("glob")
15 .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::String)))])
16 .required("glob", SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]), "The glob expression.")
17 .named(
18 "depth",
19 SyntaxShape::Int,
20 "directory depth to search",
21 Some('d'),
22 )
23 .switch(
24 "no-dir",
25 "Whether to filter out directories from the returned paths",
26 Some('D'),
27 )
28 .switch(
29 "no-file",
30 "Whether to filter out files from the returned paths",
31 Some('F'),
32 )
33 .switch(
34 "no-symlink",
35 "Whether to filter out symlinks from the returned paths",
36 Some('S'),
37 )
38 .switch(
39 "follow-symlinks",
40 "Whether to follow symbolic links to their targets",
41 Some('l'),
42 )
43 .named(
44 "exclude",
45 SyntaxShape::List(Box::new(SyntaxShape::String)),
46 "Patterns to exclude from the search: `glob` will not walk the inside of directories matching the excluded patterns.",
47 Some('e'),
48 )
49 .category(Category::FileSystem)
50 }
51
52 fn description(&self) -> &str {
53 "Creates a list of files and/or folders based on the glob pattern provided."
54 }
55
56 fn search_terms(&self) -> Vec<&str> {
57 vec!["pattern", "files", "folders", "list", "ls"]
58 }
59
60 fn examples(&self) -> Vec<Example> {
61 vec![
62 Example {
63 description: "Search for *.rs files",
64 example: "glob *.rs",
65 result: None,
66 },
67 Example {
68 description: "Search for *.rs and *.toml files recursively up to 2 folders deep",
69 example: "glob **/*.{rs,toml} --depth 2",
70 result: None,
71 },
72 Example {
73 description:
74 "Search for files and folders that begin with uppercase C or lowercase c",
75 example: r#"glob "[Cc]*""#,
76 result: None,
77 },
78 Example {
79 description:
80 "Search for files and folders like abc or xyz substituting a character for ?",
81 example: r#"glob "{a?c,x?z}""#,
82 result: None,
83 },
84 Example {
85 description: "A case-insensitive search for files and folders that begin with c",
86 example: r#"glob "(?i)c*""#,
87 result: None,
88 },
89 Example {
90 description: "Search for files for folders that do not begin with c, C, b, M, or s",
91 example: r#"glob "[!cCbMs]*""#,
92 result: None,
93 },
94 Example {
95 description: "Search for files or folders with 3 a's in a row in the name",
96 example: "glob <a*:3>",
97 result: None,
98 },
99 Example {
100 description: "Search for files or folders with only a, b, c, or d in the file name between 1 and 10 times",
101 example: "glob <[a-d]:1,10>",
102 result: None,
103 },
104 Example {
105 description: "Search for folders that begin with an uppercase ASCII letter, ignoring files and symlinks",
106 example: r#"glob "[A-Z]*" --no-file --no-symlink"#,
107 result: None,
108 },
109 Example {
110 description: "Search for files named tsconfig.json that are not in node_modules directories",
111 example: r#"glob **/tsconfig.json --exclude [**/node_modules/**]"#,
112 result: None,
113 },
114 Example {
115 description: "Search for all files that are not in the target nor .git directories",
116 example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#,
117 result: None,
118 },
119 Example {
120 description: "Search for files following symbolic links to their targets",
121 example: r#"glob "**/*.txt" --follow-symlinks"#,
122 result: None,
123 },
124 ]
125 }
126
127 fn extra_description(&self) -> &str {
128 r#"For more glob pattern help, please refer to https://docs.rs/crate/wax/latest"#
129 }
130
131 fn run(
132 &self,
133 engine_state: &EngineState,
134 stack: &mut Stack,
135 call: &Call,
136 _input: PipelineData,
137 ) -> Result<PipelineData, ShellError> {
138 let span = call.head;
139 let glob_pattern_input: Value = call.req(engine_state, stack, 0)?;
140 let glob_span = glob_pattern_input.span();
141 let depth = call.get_flag(engine_state, stack, "depth")?;
142 let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
143 let no_files = call.has_flag(engine_state, stack, "no-file")?;
144 let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?;
145 let follow_symlinks = call.has_flag(engine_state, stack, "follow-symlinks")?;
146 let paths_to_exclude: Option<Value> = call.get_flag(engine_state, stack, "exclude")?;
147
148 let (not_patterns, not_pattern_span): (Vec<String>, Span) = match paths_to_exclude {
149 None => (vec![], span),
150 Some(f) => {
151 let pat_span = f.span();
152 match f {
153 Value::List { vals: pats, .. } => {
154 let p = convert_patterns(pats.as_slice())?;
155 (p, pat_span)
156 }
157 _ => (vec![], span),
158 }
159 }
160 };
161
162 let glob_pattern =
163 match glob_pattern_input {
164 Value::String { val, .. } | Value::Glob { val, .. } => val,
165 _ => return Err(ShellError::IncorrectValue {
166 msg: "Incorrect glob pattern supplied to glob. Please use string or glob only."
167 .to_string(),
168 val_span: call.head,
169 call_span: glob_span,
170 }),
171 };
172
173 if glob_pattern.is_empty() {
174 return Err(ShellError::GenericError {
175 error: "glob pattern must not be empty".into(),
176 msg: "glob pattern is empty".into(),
177 span: Some(glob_span),
178 help: Some("add characters to the glob pattern".into()),
179 inner: vec![],
180 });
181 }
182
183 let folder_depth = if let Some(depth) = depth {
186 depth
187 } else if glob_pattern.contains("**") {
188 usize::MAX
189 } else if glob_pattern.contains('/') {
190 glob_pattern.split('/').count() + 1
191 } else {
192 1
193 };
194
195 let (prefix, glob) = match WaxGlob::new(&glob_pattern) {
196 Ok(p) => p.partition(),
197 Err(e) => {
198 return Err(ShellError::GenericError {
199 error: "error with glob pattern".into(),
200 msg: format!("{e}"),
201 span: Some(glob_span),
202 help: None,
203 inner: vec![],
204 })
205 }
206 };
207
208 let path = engine_state.cwd_as_string(Some(stack))?;
209 let path = match nu_path::canonicalize_with(prefix, path) {
210 Ok(path) => path,
211 Err(e) if e.to_string().contains("os error 2") =>
212 {
214 std::path::PathBuf::new() }
216 Err(e) => {
217 return Err(ShellError::GenericError {
218 error: "error in canonicalize".into(),
219 msg: format!("{e}"),
220 span: Some(glob_span),
221 help: None,
222 inner: vec![],
223 })
224 }
225 };
226
227 let link_behavior = match follow_symlinks {
228 true => wax::LinkBehavior::ReadTarget,
229 false => wax::LinkBehavior::ReadFile,
230 };
231
232 let result = if !not_patterns.is_empty() {
233 let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect();
234 let glob_results = glob
235 .walk_with_behavior(
236 path,
237 WalkBehavior {
238 depth: folder_depth,
239 link: link_behavior,
240 },
241 )
242 .into_owned()
243 .not(np)
244 .map_err(|err| ShellError::GenericError {
245 error: "error with glob's not pattern".into(),
246 msg: format!("{err}"),
247 span: Some(not_pattern_span),
248 help: None,
249 inner: vec![],
250 })?
251 .flatten();
252 glob_to_value(
253 engine_state.signals(),
254 glob_results,
255 no_dirs,
256 no_files,
257 no_symlinks,
258 span,
259 )
260 } else {
261 let glob_results = glob
262 .walk_with_behavior(
263 path,
264 WalkBehavior {
265 depth: folder_depth,
266 link: link_behavior,
267 },
268 )
269 .into_owned()
270 .flatten();
271 glob_to_value(
272 engine_state.signals(),
273 glob_results,
274 no_dirs,
275 no_files,
276 no_symlinks,
277 span,
278 )
279 };
280
281 Ok(result.into_pipeline_data(span, engine_state.signals().clone()))
282 }
283}
284
285fn convert_patterns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
286 let res = columns
287 .iter()
288 .map(|value| match &value {
289 Value::String { val: s, .. } => Ok(s.clone()),
290 _ => Err(ShellError::IncompatibleParametersSingle {
291 msg: "Incorrect column format, Only string as column name".to_string(),
292 span: value.span(),
293 }),
294 })
295 .collect::<Result<Vec<String>, _>>()?;
296
297 Ok(res)
298}
299
300fn glob_to_value(
301 signals: &Signals,
302 glob_results: impl Iterator<Item = WalkEntry<'static>> + Send + 'static,
303 no_dirs: bool,
304 no_files: bool,
305 no_symlinks: bool,
306 span: Span,
307) -> ListStream {
308 let map_signals = signals.clone();
309 let result = glob_results.filter_map(move |entry| {
310 if let Err(err) = map_signals.check(span) {
311 return Some(Value::error(err, span));
312 };
313 let file_type = entry.file_type();
314
315 if !(no_dirs && file_type.is_dir()
316 || no_files && file_type.is_file()
317 || no_symlinks && file_type.is_symlink())
318 {
319 Some(Value::string(
320 entry.into_path().to_string_lossy().to_string(),
321 span,
322 ))
323 } else {
324 None
325 }
326 });
327
328 ListStream::new(result, span, signals.clone())
329}