endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 313 lines 10 kB view raw
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}