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