add glob command and use in eval file

ptr.pet 4f3ad21f 02326f7e

verified
+1
Cargo.lock
··· 1409 1409 "nu-cmd-lang", 1410 1410 "nu-command", 1411 1411 "nu-engine", 1412 + "nu-glob", 1412 1413 "nu-parser", 1413 1414 "nu-path", 1414 1415 "nu-protocol",
+1
Cargo.toml
··· 17 17 nu-parser = { git = "https://github.com/90-008/nushell", default-features = false } 18 18 nu-protocol = { git = "https://github.com/90-008/nushell", default-features = false } 19 19 nu-path = { git = "https://github.com/90-008/nushell", default-features = false } 20 + nu-glob = { git = "https://github.com/90-008/nushell", default-features = false } 20 21 nu-cmd-base = { git = "https://github.com/90-008/nushell", default-features = false } 21 22 nu-cmd-lang = { git = "https://github.com/90-008/nushell", default-features = false } 22 23 nu-cmd-extra = { git = "https://github.com/90-008/nushell", default-features = false }
+282
src/cmd/glob.rs
··· 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
··· 1 1 pub mod cd; 2 2 pub mod eval; 3 3 pub mod fetch; 4 + pub mod glob; 4 5 pub mod job; 5 6 pub mod job_kill; 6 7 pub mod job_list; ··· 19 20 pub use cd::Cd; 20 21 pub use eval::Eval; 21 22 pub use fetch::Fetch; 23 + pub use glob::Glob; 22 24 pub use job::Job; 23 25 pub use job_kill::JobKill; 24 26 pub use job_list::JobList;
+79 -21
src/cmd/source_file.rs
··· 1 1 use crate::{ 2 + cmd::glob::glob_match, 2 3 error::{CommandError, to_shell_err}, 3 - globals::{get_pwd, print_to_console, set_pwd}, 4 + globals::{get_pwd, get_vfs, print_to_console, set_pwd}, 4 5 }; 6 + use std::sync::Arc; 5 7 use nu_engine::{CallExt, get_eval_block_with_early_return}; 6 8 use nu_parser::parse; 7 9 use nu_protocol::{ 8 - Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 10 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 9 11 engine::{Command, EngineState, Stack, StateWorkingSet}, 10 12 }; 11 13 ··· 19 21 20 22 fn signature(&self) -> Signature { 21 23 Signature::build(self.name()) 22 - .required("path", SyntaxShape::Filepath, "the file to source") 24 + .required( 25 + "path", 26 + SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 27 + "the file to source", 28 + ) 23 29 .input_output_type(Type::Nothing, Type::Nothing) 24 30 .category(Category::Core) 25 31 } ··· 36 42 _input: PipelineData, 37 43 ) -> Result<PipelineData, ShellError> { 38 44 let span = call.arguments_span(); 39 - let path: String = call.req(engine_state, stack, 0)?; 45 + let path: Value = call.req(engine_state, stack, 0)?; 46 + 47 + // Check if path is a glob pattern 48 + let path_str = match &path { 49 + Value::String { val, .. } | Value::Glob { val, .. } => val.clone(), 50 + _ => { 51 + return Err(ShellError::GenericError { 52 + error: "not a path or glob pattern".into(), 53 + msg: String::new(), 54 + span: Some(span), 55 + help: None, 56 + inner: vec![], 57 + }); 58 + } 59 + }; 40 60 41 61 let pwd = get_pwd(); 62 + let is_absolute = path_str.starts_with('/'); 63 + let base_path: Arc<vfs::VfsPath> = if is_absolute { 64 + get_vfs() 65 + } else { 66 + pwd.clone() 67 + }; 42 68 43 - let path = pwd.join(&path).map_err(to_shell_err(span))?; 44 - let contents = path.read_to_string().map_err(to_shell_err(span))?; 69 + // Check if it's a glob pattern (contains *, ?, [, or **) 70 + let is_glob = path_str.contains('*') 71 + || path_str.contains('?') 72 + || path_str.contains('[') 73 + || path_str.contains("**"); 45 74 46 - set_pwd(path.parent().into()); 47 - let res = eval(engine_state, stack, &contents, Some(&path.filename())); 48 - set_pwd(pwd); 75 + let paths_to_source = if is_glob { 76 + // Expand glob pattern 77 + let options = crate::cmd::glob::GlobOptions { 78 + max_depth: None, 79 + no_dirs: true, // Only source files, not directories 80 + no_files: false, 81 + }; 82 + glob_match(&path_str, base_path.clone(), options)? 83 + } else { 84 + // Single file path 85 + vec![path_str] 86 + }; 49 87 50 - match res { 51 - Ok(d) => Ok(d), 52 - Err(err) => { 53 - let msg: String = err.into(); 54 - print_to_console(&msg, true); 55 - Err(ShellError::GenericError { 56 - error: "source error".into(), 57 - msg: "can't source file".into(), 58 - span: Some(span), 59 - help: None, 60 - inner: vec![], 61 - }) 88 + // Source each matching file 89 + for rel_path in paths_to_source { 90 + let full_path = base_path.join(&rel_path).map_err(to_shell_err(span))?; 91 + 92 + let metadata = full_path.metadata().map_err(to_shell_err(span))?; 93 + if metadata.file_type != vfs::VfsFileType::File { 94 + continue; 95 + } 96 + 97 + let contents = full_path.read_to_string().map_err(to_shell_err(span))?; 98 + 99 + set_pwd(full_path.parent().into()); 100 + let res = eval(engine_state, stack, &contents, Some(&full_path.filename())); 101 + set_pwd(pwd.clone()); 102 + 103 + match res { 104 + Ok(p) => { 105 + print_to_console(&p.collect_string("\n", &engine_state.config)?, true); 106 + } 107 + Err(err) => { 108 + let msg: String = err.into(); 109 + print_to_console(&msg, true); 110 + return Err(ShellError::GenericError { 111 + error: "source error".into(), 112 + msg: format!("can't source file: {}", rel_path), 113 + span: Some(span), 114 + help: None, 115 + inner: vec![], 116 + }); 117 + } 62 118 } 63 119 } 120 + 121 + Ok(PipelineData::Empty) 64 122 } 65 123 } 66 124
+3 -2
src/lib.rs
··· 28 28 29 29 use crate::{ 30 30 cmd::{ 31 - Cd, Eval, Fetch, Job, JobKill, JobList, Ls, Mkdir, Mv, Open, Print, Pwd, Random, Rm, Save, 31 + Cd, Eval, Fetch, Glob, Job, JobKill, JobList, Ls, Mkdir, Mv, Open, Print, Pwd, Random, Rm, Save, 32 32 SourceFile, Sys, 33 33 }, 34 34 default_context::add_shell_command_context, ··· 102 102 engine_state = add_extra_command_context(engine_state); 103 103 104 104 let mut working_set = StateWorkingSet::new(&engine_state); 105 - let decls: [Box<dyn Command>; 17] = [ 105 + let decls: [Box<dyn Command>; 18] = [ 106 106 Box::new(Ls), 107 107 Box::new(Open), 108 108 Box::new(Save), ··· 120 120 Box::new(Sys), 121 121 Box::new(Random), 122 122 Box::new(Print), 123 + Box::new(Glob), 123 124 ]; 124 125 for decl in decls { 125 126 working_set.add_decl(decl);