···1+use std::sync::Arc;
2+3+use crate::globals::{get_pwd, get_vfs};
4+use nu_engine::CallExt;
5+use nu_glob::Pattern;
6+use nu_protocol::{
7+ Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
8+ engine::{Command, EngineState, Stack},
9+};
10+use vfs::VfsFileType;
11+12+/// Options for glob matching
13+pub struct GlobOptions {
14+ pub max_depth: Option<usize>,
15+ pub no_dirs: bool,
16+ pub no_files: bool,
17+}
18+19+impl Default for GlobOptions {
20+ fn default() -> Self {
21+ Self {
22+ max_depth: None,
23+ no_dirs: false,
24+ no_files: false,
25+ }
26+ }
27+}
28+29+/// Match files and directories using a glob pattern.
30+/// Returns a vector of relative paths (relative to the base path) that match the pattern.
31+pub fn glob_match(
32+ pattern_str: &str,
33+ base_path: Arc<vfs::VfsPath>,
34+ options: GlobOptions,
35+) -> Result<Vec<String>, ShellError> {
36+ if pattern_str.is_empty() {
37+ return Err(ShellError::GenericError {
38+ error: "glob pattern must not be empty".into(),
39+ msg: "glob pattern is empty".into(),
40+ span: None,
41+ help: Some("add characters to the glob pattern".into()),
42+ inner: vec![],
43+ });
44+ }
45+46+ // Parse the pattern
47+ let pattern = Pattern::new(pattern_str).map_err(|e| ShellError::GenericError {
48+ error: "error with glob pattern".into(),
49+ msg: format!("{}", e),
50+ span: None,
51+ help: None,
52+ inner: vec![],
53+ })?;
54+55+ // Determine max depth
56+ let max_depth = if let Some(d) = options.max_depth {
57+ d
58+ } else if pattern_str.contains("**") {
59+ usize::MAX
60+ } else {
61+ // Count number of / in pattern to determine depth
62+ pattern_str.split('/').count()
63+ };
64+65+ // Normalize pattern: remove leading / for relative matching
66+ let normalized_pattern = pattern_str.trim_start_matches('/');
67+ let is_recursive = normalized_pattern.contains("**");
68+69+ // Collect matching paths
70+ let mut matches = Vec::new();
71+72+ fn walk_directory(
73+ current_path: Arc<vfs::VfsPath>,
74+ current_relative_path: String,
75+ pattern: &Pattern,
76+ normalized_pattern: &str,
77+ current_depth: usize,
78+ max_depth: usize,
79+ matches: &mut Vec<String>,
80+ no_dirs: bool,
81+ no_files: bool,
82+ is_recursive: bool,
83+ ) -> Result<(), ShellError> {
84+ if current_depth > max_depth {
85+ return Ok(());
86+ }
87+88+ // Walk through directory entries
89+ if let Ok(entries) = current_path.read_dir() {
90+ for entry in entries {
91+ let filename = entry.filename();
92+ let entry_path = current_path.join(&filename)
93+ .map_err(|e| ShellError::GenericError {
94+ error: "path error".into(),
95+ msg: e.to_string(),
96+ span: None,
97+ help: None,
98+ inner: vec![],
99+ })?;
100+101+ // Build relative path from base
102+ let new_relative = if current_relative_path.is_empty() {
103+ filename.clone()
104+ } else {
105+ format!("{}/{}", current_relative_path, filename)
106+ };
107+108+ let metadata = entry_path.metadata().map_err(|e| ShellError::GenericError {
109+ error: "path error".into(),
110+ msg: e.to_string(),
111+ span: None,
112+ help: None,
113+ inner: vec![],
114+ })?;
115+116+ // Check if this path matches the pattern
117+ // For patterns without path separators, match just the filename
118+ // For patterns with path separators, match the full relative path
119+ let path_to_match = if normalized_pattern.contains('/') {
120+ &new_relative
121+ } else {
122+ &filename
123+ };
124+125+ if pattern.matches(path_to_match) {
126+ let should_include = match metadata.file_type {
127+ VfsFileType::Directory => !no_dirs,
128+ VfsFileType::File => !no_files,
129+ };
130+ if should_include {
131+ matches.push(new_relative.clone());
132+ }
133+ }
134+135+ // Recursively walk into subdirectories
136+ if metadata.file_type == VfsFileType::Directory {
137+ // Continue if: recursive pattern, or we haven't reached max depth, or pattern has more components
138+ let should_recurse = is_recursive
139+ || current_depth < max_depth
140+ || (normalized_pattern.contains('/') && current_depth < normalized_pattern.split('/').count());
141+142+ if should_recurse {
143+ walk_directory(
144+ Arc::new(entry_path),
145+ new_relative,
146+ pattern,
147+ normalized_pattern,
148+ current_depth + 1,
149+ max_depth,
150+ matches,
151+ no_dirs,
152+ no_files,
153+ is_recursive,
154+ )?;
155+ }
156+ }
157+ }
158+ }
159+160+ Ok(())
161+ }
162+163+ // Start walking from base path
164+ walk_directory(
165+ base_path,
166+ String::new(),
167+ &pattern,
168+ normalized_pattern,
169+ 0,
170+ max_depth,
171+ &mut matches,
172+ options.no_dirs,
173+ options.no_files,
174+ is_recursive,
175+ )?;
176+177+ Ok(matches)
178+}
179+180+#[derive(Clone)]
181+pub struct Glob;
182+183+impl Command for Glob {
184+ fn name(&self) -> &str {
185+ "glob"
186+ }
187+188+ fn signature(&self) -> Signature {
189+ Signature::build("glob")
190+ .required(
191+ "pattern",
192+ SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]),
193+ "The glob expression.",
194+ )
195+ .named(
196+ "depth",
197+ SyntaxShape::Int,
198+ "directory depth to search",
199+ Some('d'),
200+ )
201+ .switch(
202+ "no-dir",
203+ "Whether to filter out directories from the returned paths",
204+ Some('D'),
205+ )
206+ .switch(
207+ "no-file",
208+ "Whether to filter out files from the returned paths",
209+ Some('F'),
210+ )
211+ .input_output_type(Type::Nothing, Type::List(Box::new(Type::String)))
212+ .category(Category::FileSystem)
213+ }
214+215+ fn description(&self) -> &str {
216+ "Creates a list of files and/or folders based on the glob pattern provided."
217+ }
218+219+ fn run(
220+ &self,
221+ engine_state: &EngineState,
222+ stack: &mut Stack,
223+ call: &nu_protocol::engine::Call,
224+ _input: PipelineData,
225+ ) -> Result<PipelineData, ShellError> {
226+ let span = call.head;
227+ let pattern_value: Value = call.req(engine_state, stack, 0)?;
228+ let pattern_span = pattern_value.span();
229+ let depth: Option<i64> = call.get_flag(engine_state, stack, "depth")?;
230+ let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
231+ let no_files = call.has_flag(engine_state, stack, "no-file")?;
232+233+ let pattern_str = match pattern_value {
234+ Value::String { val, .. } | Value::Glob { val, .. } => val,
235+ _ => {
236+ return Err(ShellError::IncorrectValue {
237+ msg: "Incorrect glob pattern supplied to glob. Please use string or glob only."
238+ .to_string(),
239+ val_span: call.head,
240+ call_span: pattern_span,
241+ });
242+ }
243+ };
244+245+ if pattern_str.is_empty() {
246+ return Err(ShellError::GenericError {
247+ error: "glob pattern must not be empty".into(),
248+ msg: "glob pattern is empty".into(),
249+ span: Some(pattern_span),
250+ help: Some("add characters to the glob pattern".into()),
251+ inner: vec![],
252+ });
253+ }
254+255+ // Determine if pattern is absolute (starts with /)
256+ let is_absolute = pattern_str.starts_with('/');
257+ let base_path = if is_absolute {
258+ get_vfs()
259+ } else {
260+ get_pwd()
261+ };
262+263+ // Use the glob_match function
264+ let options = GlobOptions {
265+ max_depth: depth.map(|d| d as usize),
266+ no_dirs,
267+ no_files,
268+ };
269+270+ let matches = glob_match(&pattern_str, base_path, options)?;
271+272+ // Convert matches to Value stream
273+ let signals = engine_state.signals().clone();
274+ let values = matches.into_iter().map(move |path| Value::string(path, span));
275+276+ Ok(PipelineData::list_stream(
277+ ListStream::new(values, span, signals.clone()),
278+ None,
279+ ))
280+ }
281+}
282+
+2
src/cmd/mod.rs
···1pub mod cd;
2pub mod eval;
3pub mod fetch;
04pub mod job;
5pub mod job_kill;
6pub mod job_list;
···19pub use cd::Cd;
20pub use eval::Eval;
21pub use fetch::Fetch;
022pub use job::Job;
23pub use job_kill::JobKill;
24pub use job_list::JobList;
···1pub mod cd;
2pub mod eval;
3pub mod fetch;
4+pub mod glob;
5pub mod job;
6pub mod job_kill;
7pub mod job_list;
···20pub use cd::Cd;
21pub use eval::Eval;
22pub use fetch::Fetch;
23+pub use glob::Glob;
24pub use job::Job;
25pub use job_kill::JobKill;
26pub use job_list::JobList;