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 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 {
203 std::path::PathBuf::new() }
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}