nu_command/filesystem/
glob.rs

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        // below we have to check / instead of MAIN_SEPARATOR because glob uses / as separator
184        // using a glob like **\*.rs should fail because it's not a valid glob pattern
185        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            // path we're trying to glob doesn't exist,
213            {
214                std::path::PathBuf::new() // user should get empty list not an error
215            }
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}