nushell on your web browser
nushell wasm terminal
2
fork

Configure Feed

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

use glob matching in other commands

dawn 9f22274a 4f3ad21f

+348 -122
+22
src/cmd/glob.rs
··· 26 26 } 27 27 } 28 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). 32 + pub 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 + 29 51 /// Match files and directories using a glob pattern. 30 52 /// Returns a vector of relative paths (relative to the base path) that match the pattern. 31 53 pub fn glob_match(
+74 -51
src/cmd/ls.rs
··· 1 - use std::{ 2 - sync::Arc, 3 - time::{SystemTime, UNIX_EPOCH}, 4 - }; 1 + use std::time::{SystemTime, UNIX_EPOCH}; 5 2 6 - use crate::{error::to_shell_err, globals::get_pwd}; 3 + use crate::{ 4 + cmd::glob::{expand_path, GlobOptions}, 5 + error::to_shell_err, 6 + globals::{get_pwd, get_vfs}, 7 + }; 8 + use std::sync::Arc; 7 9 use jacquard::chrono; 8 10 use nu_engine::CallExt; 9 11 use nu_protocol::{ ··· 21 23 22 24 fn signature(&self) -> Signature { 23 25 Signature::build("ls") 24 - .optional("path", SyntaxShape::Filepath, "the path to list") 26 + .optional( 27 + "path", 28 + SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 29 + "the path to list", 30 + ) 25 31 .switch( 26 32 "all", 27 33 "include hidden paths (that start with a dot)", ··· 48 54 call: &nu_protocol::engine::Call, 49 55 _input: PipelineData, 50 56 ) -> Result<PipelineData, ShellError> { 51 - let path_arg: Option<String> = call.opt(engine_state, stack, 0)?; 57 + let path_arg: Option<Value> = call.opt(engine_state, stack, 0)?; 52 58 let all = call.has_flag(engine_state, stack, "all")?; 53 59 let long = call.has_flag(engine_state, stack, "long")?; 54 60 let full_paths = call.has_flag(engine_state, stack, "full-paths")?; 55 61 56 62 let pwd = get_pwd(); 57 - // web_sys::console::log_1(&JsValue::from_str(&format!("{pwd:?}"))); 58 - // web_sys::console::log_1(&JsValue::from_str(&format!( 59 - // "{:?}", 60 - // pwd.read_dir().map(|a| a.collect::<Vec<_>>()) 61 - // ))); 62 - let mut target_dir = pwd.clone(); 63 - if let Some(path) = path_arg { 64 - target_dir = Arc::new( 65 - target_dir 66 - .join(path.trim_end_matches('/')) 67 - .map_err(to_shell_err(call.arguments_span()))?, 68 - ); 69 - } 63 + let span = call.head; 64 + 65 + // If no path provided, list current directory 66 + let (matches, base_path) = if let Some(path_val) = &path_arg { 67 + let path_str = match path_val { 68 + Value::String { val, .. } | Value::Glob { val, .. } => val, 69 + _ => { 70 + return Err(ShellError::GenericError { 71 + error: "invalid path".into(), 72 + msg: "path must be a string or glob pattern".into(), 73 + span: Some(call.arguments_span()), 74 + help: None, 75 + inner: vec![], 76 + }); 77 + } 78 + }; 79 + 80 + let is_absolute = path_str.starts_with('/'); 81 + let base_path: Arc<vfs::VfsPath> = if is_absolute { 82 + get_vfs() 83 + } else { 84 + pwd.clone() 85 + }; 86 + 87 + let options = GlobOptions { 88 + max_depth: None, 89 + no_dirs: false, 90 + no_files: false, 91 + }; 92 + 93 + let matches = expand_path(path_str, base_path.clone(), options)?; 94 + (matches, base_path) 95 + } else { 96 + // No path: list current directory entries 97 + let entries = pwd.read_dir().map_err(to_shell_err(span))?; 98 + let matches: Vec<String> = entries.map(|e| e.filename()).collect(); 99 + (matches, pwd.clone()) 100 + }; 101 + 102 + let make_record = move |rel_path: &str| { 103 + let full_path = base_path.join(rel_path).map_err(to_shell_err(span))?; 104 + let metadata = full_path.metadata().map_err(to_shell_err(span))?; 70 105 71 - let span = call.head; 72 - let entries = target_dir.read_dir().map_err(to_shell_err(span))?; 106 + // Filter hidden files if --all is not set 107 + let filename = rel_path.split('/').last().unwrap_or(rel_path); 108 + if filename.starts_with('.') && !all { 109 + return Ok(None); 110 + } 73 111 74 - let make_record = move |name: &str, metadata: &vfs::VfsMetadata| { 75 112 let type_str = match metadata.file_type { 76 113 vfs::VfsFileType::Directory => "dir", 77 114 vfs::VfsFileType::File => "file", 78 115 }; 79 116 80 117 let mut record = Record::new(); 81 - record.push("name", Value::string(name, span)); 118 + let display_name = if full_paths { 119 + full_path.as_str().to_string() 120 + } else { 121 + rel_path.to_string() 122 + }; 123 + record.push("name", Value::string(display_name, span)); 82 124 record.push("type", Value::string(type_str, span)); 83 125 record.push("size", Value::filesize(metadata.len as i64, span)); 84 126 let mut add_timestamp = |field: &str, timestamp: Option<SystemTime>| { ··· 100 142 add_timestamp("created", metadata.created); 101 143 add_timestamp("accessed", metadata.accessed); 102 144 } 103 - Value::record(record, span) 145 + Ok(Some(Value::record(record, span))) 104 146 }; 105 147 106 - let entries = entries.into_iter().flat_map(move |entry| { 107 - let do_map = || { 108 - let name = entry.filename(); 109 - if name.starts_with('.') && !all { 110 - return Ok(None); 111 - } 112 - let metadata = entry.metadata().map_err(to_shell_err(span))?; 113 - 114 - let name = if full_paths { 115 - format!("{path}/{name}", path = target_dir.as_str()) 116 - } else { 117 - let path = target_dir 118 - .as_str() 119 - .trim_start_matches(pwd.as_str()) 120 - .trim_start_matches("/"); 121 - format!( 122 - "{path}{sep}{name}", 123 - sep = path.is_empty().then_some("").unwrap_or("/"), 124 - ) 125 - }; 126 - Ok(Some(make_record(&name, &metadata))) 127 - }; 128 - do_map() 129 - .transpose() 130 - .map(|res| res.unwrap_or_else(|err| Value::error(err, span))) 131 - }); 148 + let entries = matches 149 + .into_iter() 150 + .flat_map(move |rel_path| { 151 + make_record(&rel_path) 152 + .transpose() 153 + .map(|res| res.unwrap_or_else(|err| Value::error(err, span))) 154 + }); 132 155 133 156 let signals = engine_state.signals().clone(); 134 157 Ok(PipelineData::list_stream(
+72 -17
src/cmd/mv.rs
··· 1 1 use std::io::{Read, Write}; 2 2 3 - use crate::{error::to_shell_err, globals::get_pwd}; 3 + use crate::{ 4 + cmd::glob::{expand_path, GlobOptions}, 5 + error::to_shell_err, 6 + globals::{get_pwd, get_vfs}, 7 + }; 8 + use std::sync::Arc; 4 9 use nu_engine::CallExt; 5 10 use nu_protocol::{ 6 - Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 11 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 7 12 engine::{Command, EngineState, Stack}, 8 13 }; 9 14 use vfs::{VfsError, VfsFileType}; ··· 20 25 Signature::build("mv") 21 26 .required( 22 27 "source", 23 - SyntaxShape::Filepath, 28 + SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 24 29 "path to the file or directory to move", 25 30 ) 26 31 .required( ··· 43 48 call: &nu_protocol::engine::Call, 44 49 _input: PipelineData, 45 50 ) -> Result<PipelineData, ShellError> { 46 - let source_path: String = call.req(engine_state, stack, 0)?; 51 + let source_value: Value = call.req(engine_state, stack, 0)?; 47 52 let dest_path: String = call.req(engine_state, stack, 1)?; 48 53 54 + let source_str = match source_value { 55 + Value::String { val, .. } | Value::Glob { val, .. } => val, 56 + _ => { 57 + return Err(ShellError::GenericError { 58 + error: "invalid source path".into(), 59 + msg: "source must be a string or glob pattern".into(), 60 + span: Some(call.arguments_span()), 61 + help: None, 62 + inner: vec![], 63 + }); 64 + } 65 + }; 66 + 49 67 // Prevent moving root 50 - if source_path == "/" { 68 + if source_str == "/" { 51 69 return Err(ShellError::GenericError { 52 70 error: "cannot move root".to_string(), 53 71 msg: "refusing to move root directory".to_string(), ··· 57 75 }); 58 76 } 59 77 60 - // Resolve source relative to PWD (or absolute if path starts with '/') 61 - let source = get_pwd() 62 - .join(source_path.trim_end_matches('/')) 63 - .map_err(to_shell_err(call.arguments_span()))?; 78 + // Expand source path (glob or single) into list of paths 79 + let is_absolute = source_str.starts_with('/'); 80 + let base_path: Arc<vfs::VfsPath> = if is_absolute { 81 + get_vfs() 82 + } else { 83 + get_pwd() 84 + }; 85 + 86 + let options = GlobOptions { 87 + max_depth: None, 88 + no_dirs: false, 89 + no_files: false, 90 + }; 91 + 92 + let matches = expand_path(&source_str, base_path.clone(), options)?; 93 + let is_glob = matches.len() > 1 || source_str.contains('*') || source_str.contains('?') || source_str.contains('[') || source_str.contains("**"); 64 94 65 - // Resolve destination relative to PWD (or absolute if path starts with '/') 95 + // Resolve destination 66 96 let dest = get_pwd() 67 97 .join(dest_path.trim_end_matches('/')) 68 98 .map_err(to_shell_err(call.arguments_span()))?; 69 99 70 - // Check that source exists 71 - let meta = source 72 - .metadata() 73 - .map_err(to_shell_err(call.arguments_span()))?; 100 + // For glob patterns, destination must be a directory 101 + if is_glob { 102 + let dest_meta = dest.metadata().map_err(to_shell_err(call.arguments_span()))?; 103 + if dest_meta.file_type != VfsFileType::Directory { 104 + return Err(ShellError::GenericError { 105 + error: "destination must be a directory".to_string(), 106 + msg: "when using glob patterns, destination must be a directory".to_string(), 107 + span: Some(call.arguments_span()), 108 + help: None, 109 + inner: vec![], 110 + }); 111 + } 112 + } 74 113 75 - match meta.file_type { 76 - VfsFileType::File => move_file(&source, &dest, call.arguments_span())?, 77 - VfsFileType::Directory => move_directory(&source, &dest, call.arguments_span())?, 114 + // Move each matching file/directory 115 + for rel_path in matches { 116 + let source = base_path.join(&rel_path).map_err(to_shell_err(call.arguments_span()))?; 117 + let source_meta = source.metadata().map_err(to_shell_err(call.arguments_span()))?; 118 + 119 + // Determine destination path 120 + let dest_entry = if is_glob { 121 + // For glob patterns, use filename in destination directory 122 + let filename = rel_path.split('/').last().unwrap_or(&rel_path); 123 + dest.join(filename).map_err(to_shell_err(call.arguments_span()))? 124 + } else { 125 + // For single path, use destination as-is 126 + dest.clone() 127 + }; 128 + 129 + match source_meta.file_type { 130 + VfsFileType::File => move_file(&source, &dest_entry, call.arguments_span())?, 131 + VfsFileType::Directory => move_directory(&source, &dest_entry, call.arguments_span())?, 132 + } 78 133 } 79 134 80 135 Ok(PipelineData::Empty)
+124 -30
src/cmd/open.rs
··· 1 1 use std::ops::Not; 2 2 3 - use crate::{error::to_shell_err, globals::get_pwd}; 3 + use crate::{ 4 + cmd::glob::{expand_path, GlobOptions}, 5 + globals::{get_pwd, get_vfs}, 6 + }; 7 + use std::sync::Arc; 4 8 use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml}; 5 9 use nu_engine::CallExt; 6 10 use nu_protocol::{ 7 - ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 11 + ByteStream, Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 8 12 engine::{Command, EngineState, Stack}, 9 13 }; 10 14 ··· 18 22 19 23 fn signature(&self) -> Signature { 20 24 Signature::build("open") 21 - .required("path", SyntaxShape::Filepath, "path to the file") 25 + .required( 26 + "path", 27 + SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 28 + "path to the file", 29 + ) 22 30 .switch( 23 31 "raw", 24 32 "output content as raw string/binary without parsing", ··· 39 47 call: &nu_protocol::engine::Call, 40 48 _input: PipelineData, 41 49 ) -> Result<PipelineData, ShellError> { 42 - let path: String = call.req(engine_state, stack, 0)?; 50 + let path_value: Value = call.req(engine_state, stack, 0)?; 43 51 let raw_flag = call.has_flag(engine_state, stack, "raw")?; 44 52 45 - let target_file = get_pwd().join(&path).map_err(to_shell_err(call.head))?; 53 + let path_str = match path_value { 54 + Value::String { val, .. } | Value::Glob { val, .. } => val, 55 + _ => { 56 + return Err(ShellError::GenericError { 57 + error: "invalid path".into(), 58 + msg: "path must be a string or glob pattern".into(), 59 + span: Some(call.head), 60 + help: None, 61 + inner: vec![], 62 + }); 63 + } 64 + }; 65 + 66 + // Expand path (glob or single) into list of paths 67 + let is_absolute = path_str.starts_with('/'); 68 + let base_path: Arc<vfs::VfsPath> = if is_absolute { 69 + get_vfs() 70 + } else { 71 + get_pwd() 72 + }; 73 + 74 + let options = GlobOptions { 75 + max_depth: None, 76 + no_dirs: true, // Only open files, not directories 77 + no_files: false, 78 + }; 46 79 47 - let parse_cmd = raw_flag 48 - .not() 49 - .then(|| { 50 - target_file 51 - .extension() 52 - .and_then(|ext| get_cmd_for_ext(&ext)) 53 - }) 54 - .flatten(); 80 + let matches = expand_path(&path_str, base_path.clone(), options)?; 55 81 56 - target_file 57 - .open_file() 58 - .map_err(to_shell_err(call.head)) 59 - .and_then(|f| { 60 - let data = PipelineData::ByteStream( 61 - ByteStream::read( 62 - f, 63 - call.head, 64 - engine_state.signals().clone(), 65 - nu_protocol::ByteStreamType::String, 66 - ), 67 - None, 68 - ); 69 - if let Some(cmd) = parse_cmd { 70 - return cmd.run(engine_state, stack, call, data); 82 + let span = call.head; 83 + let signals = engine_state.signals().clone(); 84 + 85 + // Open each matching file 86 + let mut results = Vec::new(); 87 + for rel_path in matches { 88 + let target_file = match base_path.join(&rel_path) { 89 + Ok(p) => p, 90 + Err(e) => { 91 + results.push(Value::error( 92 + ShellError::GenericError { 93 + error: "path error".into(), 94 + msg: e.to_string(), 95 + span: Some(span), 96 + help: None, 97 + inner: vec![], 98 + }, 99 + span, 100 + )); 101 + continue; 71 102 } 72 - Ok(data) 73 - }) 103 + }; 104 + 105 + let parse_cmd = raw_flag 106 + .not() 107 + .then(|| { 108 + target_file 109 + .extension() 110 + .and_then(|ext| get_cmd_for_ext(&ext)) 111 + }) 112 + .flatten(); 113 + 114 + match target_file.open_file() { 115 + Ok(f) => { 116 + let data = PipelineData::ByteStream( 117 + ByteStream::read( 118 + f, 119 + span, 120 + signals.clone(), 121 + nu_protocol::ByteStreamType::String, 122 + ), 123 + None, 124 + ); 125 + 126 + let value = if let Some(cmd) = parse_cmd { 127 + match cmd.run(engine_state, stack, call, data) { 128 + Ok(pipeline_data) => { 129 + // Convert pipeline data to value 130 + pipeline_data.into_value(span).unwrap_or_else(|e| { 131 + Value::error(e, span) 132 + }) 133 + } 134 + Err(e) => Value::error(e, span), 135 + } 136 + } else { 137 + data.into_value(span).unwrap_or_else(|e| Value::error(e, span)) 138 + }; 139 + results.push(value); 140 + } 141 + Err(e) => { 142 + results.push(Value::error( 143 + ShellError::GenericError { 144 + error: "io error".into(), 145 + msg: format!("failed to open file {}: {}", rel_path, e), 146 + span: Some(span), 147 + help: None, 148 + inner: vec![], 149 + }, 150 + span, 151 + )); 152 + } 153 + } 154 + } 155 + 156 + // If single file, return the single result directly (for backward compatibility) 157 + if results.len() == 1 && !path_str.contains('*') && !path_str.contains('?') && !path_str.contains('[') && !path_str.contains("**") { 158 + match results.into_iter().next().unwrap() { 159 + Value::Error { error, .. } => Err(*error), 160 + val => Ok(PipelineData::Value(val, None)), 161 + } 162 + } else { 163 + Ok(PipelineData::list_stream( 164 + ListStream::new(results.into_iter(), span, signals.clone()), 165 + None, 166 + )) 167 + } 74 168 } 75 169 } 76 170
+56 -24
src/cmd/rm.rs
··· 1 - use crate::{error::to_shell_err, globals::get_pwd}; 1 + use crate::{ 2 + cmd::glob::{expand_path, GlobOptions}, 3 + error::to_shell_err, 4 + globals::{get_pwd, get_vfs}, 5 + }; 6 + use std::sync::Arc; 2 7 use nu_engine::CallExt; 3 8 use nu_protocol::{ 4 - Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 9 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 5 10 engine::{Command, EngineState, Stack}, 6 11 }; 7 12 use vfs::VfsFileType; ··· 18 23 Signature::build("rm") 19 24 .required( 20 25 "path", 21 - SyntaxShape::String, 26 + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]), 22 27 "path to file or directory to remove", 23 28 ) 24 29 .switch( ··· 41 46 call: &nu_protocol::engine::Call, 42 47 _input: PipelineData, 43 48 ) -> Result<PipelineData, ShellError> { 44 - let path: String = call.req(engine_state, stack, 0)?; 49 + let path_value: Value = call.req(engine_state, stack, 0)?; 45 50 let recursive = call.has_flag(engine_state, stack, "recursive")?; 46 51 52 + let path_str = match path_value { 53 + Value::String { val, .. } | Value::Glob { val, .. } => val, 54 + _ => { 55 + return Err(ShellError::GenericError { 56 + error: "invalid path".into(), 57 + msg: "path must be a string or glob pattern".into(), 58 + span: Some(call.head), 59 + help: None, 60 + inner: vec![], 61 + }); 62 + } 63 + }; 64 + 47 65 // Prevent removing root 48 - if path == "/" { 66 + if path_str == "/" { 49 67 return Err(ShellError::GenericError { 50 68 error: "cannot remove root".to_string(), 51 69 msg: "refusing to remove root directory".to_string(), ··· 55 73 }); 56 74 } 57 75 58 - // Resolve target relative to PWD (or absolute if path starts with '/') 59 - let target = get_pwd() 60 - .join(path.trim_end_matches('/')) 61 - .map_err(to_shell_err(call.head))?; 76 + // Expand path (glob or single) into list of paths 77 + let is_absolute = path_str.starts_with('/'); 78 + let base_path: Arc<vfs::VfsPath> = if is_absolute { 79 + get_vfs() 80 + } else { 81 + get_pwd() 82 + }; 62 83 63 - let meta = target.metadata().map_err(to_shell_err(call.head))?; 64 - match meta.file_type { 65 - VfsFileType::File => { 66 - target.remove_file().map_err(to_shell_err(call.head))?; 67 - Ok(PipelineData::Empty) 68 - } 69 - VfsFileType::Directory => { 70 - (if recursive { 71 - target.remove_dir_all() 72 - } else { 73 - // non-recursive: attempt to remove directory (will fail if not empty) 74 - target.remove_dir() 75 - }) 76 - .map_err(to_shell_err(call.head)) 77 - .map(|_| PipelineData::Empty) 84 + let options = GlobOptions { 85 + max_depth: None, 86 + no_dirs: false, 87 + no_files: false, 88 + }; 89 + 90 + let matches = expand_path(&path_str, base_path.clone(), options)?; 91 + 92 + // Remove all matching paths 93 + for rel_path in matches { 94 + let target = base_path.join(&rel_path).map_err(to_shell_err(call.head))?; 95 + let meta = target.metadata().map_err(to_shell_err(call.head))?; 96 + match meta.file_type { 97 + VfsFileType::File => { 98 + target.remove_file().map_err(to_shell_err(call.head))?; 99 + } 100 + VfsFileType::Directory => { 101 + (if recursive { 102 + target.remove_dir_all() 103 + } else { 104 + target.remove_dir() 105 + }) 106 + .map_err(to_shell_err(call.head))?; 107 + } 78 108 } 79 109 } 110 + 111 + Ok(PipelineData::Empty) 80 112 } 81 113 }