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