···11+use std::sync::Arc;
22+33+use crate::globals::{get_pwd, get_vfs};
44+use nu_engine::CallExt;
55+use nu_glob::Pattern;
66+use nu_protocol::{
77+ Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
88+ engine::{Command, EngineState, Stack},
99+};
1010+use vfs::VfsFileType;
1111+1212+/// Options for glob matching
1313+pub struct GlobOptions {
1414+ pub max_depth: Option<usize>,
1515+ pub no_dirs: bool,
1616+ pub no_files: bool,
1717+}
1818+1919+impl Default for GlobOptions {
2020+ fn default() -> Self {
2121+ Self {
2222+ max_depth: None,
2323+ no_dirs: false,
2424+ no_files: false,
2525+ }
2626+ }
2727+}
2828+2929+/// Match files and directories using a glob pattern.
3030+/// Returns a vector of relative paths (relative to the base path) that match the pattern.
3131+pub fn glob_match(
3232+ pattern_str: &str,
3333+ base_path: Arc<vfs::VfsPath>,
3434+ options: GlobOptions,
3535+) -> Result<Vec<String>, ShellError> {
3636+ if pattern_str.is_empty() {
3737+ return Err(ShellError::GenericError {
3838+ error: "glob pattern must not be empty".into(),
3939+ msg: "glob pattern is empty".into(),
4040+ span: None,
4141+ help: Some("add characters to the glob pattern".into()),
4242+ inner: vec![],
4343+ });
4444+ }
4545+4646+ // Parse the pattern
4747+ let pattern = Pattern::new(pattern_str).map_err(|e| ShellError::GenericError {
4848+ error: "error with glob pattern".into(),
4949+ msg: format!("{}", e),
5050+ span: None,
5151+ help: None,
5252+ inner: vec![],
5353+ })?;
5454+5555+ // Determine max depth
5656+ let max_depth = if let Some(d) = options.max_depth {
5757+ d
5858+ } else if pattern_str.contains("**") {
5959+ usize::MAX
6060+ } else {
6161+ // Count number of / in pattern to determine depth
6262+ pattern_str.split('/').count()
6363+ };
6464+6565+ // Normalize pattern: remove leading / for relative matching
6666+ let normalized_pattern = pattern_str.trim_start_matches('/');
6767+ let is_recursive = normalized_pattern.contains("**");
6868+6969+ // Collect matching paths
7070+ let mut matches = Vec::new();
7171+7272+ fn walk_directory(
7373+ current_path: Arc<vfs::VfsPath>,
7474+ current_relative_path: String,
7575+ pattern: &Pattern,
7676+ normalized_pattern: &str,
7777+ current_depth: usize,
7878+ max_depth: usize,
7979+ matches: &mut Vec<String>,
8080+ no_dirs: bool,
8181+ no_files: bool,
8282+ is_recursive: bool,
8383+ ) -> Result<(), ShellError> {
8484+ if current_depth > max_depth {
8585+ return Ok(());
8686+ }
8787+8888+ // Walk through directory entries
8989+ if let Ok(entries) = current_path.read_dir() {
9090+ for entry in entries {
9191+ let filename = entry.filename();
9292+ let entry_path = current_path.join(&filename)
9393+ .map_err(|e| ShellError::GenericError {
9494+ error: "path error".into(),
9595+ msg: e.to_string(),
9696+ span: None,
9797+ help: None,
9898+ inner: vec![],
9999+ })?;
100100+101101+ // Build relative path from base
102102+ let new_relative = if current_relative_path.is_empty() {
103103+ filename.clone()
104104+ } else {
105105+ format!("{}/{}", current_relative_path, filename)
106106+ };
107107+108108+ let metadata = entry_path.metadata().map_err(|e| ShellError::GenericError {
109109+ error: "path error".into(),
110110+ msg: e.to_string(),
111111+ span: None,
112112+ help: None,
113113+ inner: vec![],
114114+ })?;
115115+116116+ // Check if this path matches the pattern
117117+ // For patterns without path separators, match just the filename
118118+ // For patterns with path separators, match the full relative path
119119+ let path_to_match = if normalized_pattern.contains('/') {
120120+ &new_relative
121121+ } else {
122122+ &filename
123123+ };
124124+125125+ if pattern.matches(path_to_match) {
126126+ let should_include = match metadata.file_type {
127127+ VfsFileType::Directory => !no_dirs,
128128+ VfsFileType::File => !no_files,
129129+ };
130130+ if should_include {
131131+ matches.push(new_relative.clone());
132132+ }
133133+ }
134134+135135+ // Recursively walk into subdirectories
136136+ if metadata.file_type == VfsFileType::Directory {
137137+ // Continue if: recursive pattern, or we haven't reached max depth, or pattern has more components
138138+ let should_recurse = is_recursive
139139+ || current_depth < max_depth
140140+ || (normalized_pattern.contains('/') && current_depth < normalized_pattern.split('/').count());
141141+142142+ if should_recurse {
143143+ walk_directory(
144144+ Arc::new(entry_path),
145145+ new_relative,
146146+ pattern,
147147+ normalized_pattern,
148148+ current_depth + 1,
149149+ max_depth,
150150+ matches,
151151+ no_dirs,
152152+ no_files,
153153+ is_recursive,
154154+ )?;
155155+ }
156156+ }
157157+ }
158158+ }
159159+160160+ Ok(())
161161+ }
162162+163163+ // Start walking from base path
164164+ walk_directory(
165165+ base_path,
166166+ String::new(),
167167+ &pattern,
168168+ normalized_pattern,
169169+ 0,
170170+ max_depth,
171171+ &mut matches,
172172+ options.no_dirs,
173173+ options.no_files,
174174+ is_recursive,
175175+ )?;
176176+177177+ Ok(matches)
178178+}
179179+180180+#[derive(Clone)]
181181+pub struct Glob;
182182+183183+impl Command for Glob {
184184+ fn name(&self) -> &str {
185185+ "glob"
186186+ }
187187+188188+ fn signature(&self) -> Signature {
189189+ Signature::build("glob")
190190+ .required(
191191+ "pattern",
192192+ SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]),
193193+ "The glob expression.",
194194+ )
195195+ .named(
196196+ "depth",
197197+ SyntaxShape::Int,
198198+ "directory depth to search",
199199+ Some('d'),
200200+ )
201201+ .switch(
202202+ "no-dir",
203203+ "Whether to filter out directories from the returned paths",
204204+ Some('D'),
205205+ )
206206+ .switch(
207207+ "no-file",
208208+ "Whether to filter out files from the returned paths",
209209+ Some('F'),
210210+ )
211211+ .input_output_type(Type::Nothing, Type::List(Box::new(Type::String)))
212212+ .category(Category::FileSystem)
213213+ }
214214+215215+ fn description(&self) -> &str {
216216+ "Creates a list of files and/or folders based on the glob pattern provided."
217217+ }
218218+219219+ fn run(
220220+ &self,
221221+ engine_state: &EngineState,
222222+ stack: &mut Stack,
223223+ call: &nu_protocol::engine::Call,
224224+ _input: PipelineData,
225225+ ) -> Result<PipelineData, ShellError> {
226226+ let span = call.head;
227227+ let pattern_value: Value = call.req(engine_state, stack, 0)?;
228228+ let pattern_span = pattern_value.span();
229229+ let depth: Option<i64> = call.get_flag(engine_state, stack, "depth")?;
230230+ let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
231231+ let no_files = call.has_flag(engine_state, stack, "no-file")?;
232232+233233+ let pattern_str = match pattern_value {
234234+ Value::String { val, .. } | Value::Glob { val, .. } => val,
235235+ _ => {
236236+ return Err(ShellError::IncorrectValue {
237237+ msg: "Incorrect glob pattern supplied to glob. Please use string or glob only."
238238+ .to_string(),
239239+ val_span: call.head,
240240+ call_span: pattern_span,
241241+ });
242242+ }
243243+ };
244244+245245+ if pattern_str.is_empty() {
246246+ return Err(ShellError::GenericError {
247247+ error: "glob pattern must not be empty".into(),
248248+ msg: "glob pattern is empty".into(),
249249+ span: Some(pattern_span),
250250+ help: Some("add characters to the glob pattern".into()),
251251+ inner: vec![],
252252+ });
253253+ }
254254+255255+ // Determine if pattern is absolute (starts with /)
256256+ let is_absolute = pattern_str.starts_with('/');
257257+ let base_path = if is_absolute {
258258+ get_vfs()
259259+ } else {
260260+ get_pwd()
261261+ };
262262+263263+ // Use the glob_match function
264264+ let options = GlobOptions {
265265+ max_depth: depth.map(|d| d as usize),
266266+ no_dirs,
267267+ no_files,
268268+ };
269269+270270+ let matches = glob_match(&pattern_str, base_path, options)?;
271271+272272+ // Convert matches to Value stream
273273+ let signals = engine_state.signals().clone();
274274+ let values = matches.into_iter().map(move |path| Value::string(path, span));
275275+276276+ Ok(PipelineData::list_stream(
277277+ ListStream::new(values, span, signals.clone()),
278278+ None,
279279+ ))
280280+ }
281281+}
282282+
+2
src/cmd/mod.rs
···11pub mod cd;
22pub mod eval;
33pub mod fetch;
44+pub mod glob;
45pub mod job;
56pub mod job_kill;
67pub mod job_list;
···1920pub use cd::Cd;
2021pub use eval::Eval;
2122pub use fetch::Fetch;
2323+pub use glob::Glob;
2224pub use job::Job;
2325pub use job_kill::JobKill;
2426pub use job_list::JobList;
+79-21
src/cmd/source_file.rs
···11use crate::{
22+ cmd::glob::glob_match,
23 error::{CommandError, to_shell_err},
33- globals::{get_pwd, print_to_console, set_pwd},
44+ globals::{get_pwd, get_vfs, print_to_console, set_pwd},
45};
66+use std::sync::Arc;
57use nu_engine::{CallExt, get_eval_block_with_early_return};
68use nu_parser::parse;
79use nu_protocol::{
88- Category, PipelineData, ShellError, Signature, SyntaxShape, Type,
1010+ Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
911 engine::{Command, EngineState, Stack, StateWorkingSet},
1012};
1113···19212022 fn signature(&self) -> Signature {
2123 Signature::build(self.name())
2222- .required("path", SyntaxShape::Filepath, "the file to source")
2424+ .required(
2525+ "path",
2626+ SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
2727+ "the file to source",
2828+ )
2329 .input_output_type(Type::Nothing, Type::Nothing)
2430 .category(Category::Core)
2531 }
···3642 _input: PipelineData,
3743 ) -> Result<PipelineData, ShellError> {
3844 let span = call.arguments_span();
3939- let path: String = call.req(engine_state, stack, 0)?;
4545+ let path: Value = call.req(engine_state, stack, 0)?;
4646+4747+ // Check if path is a glob pattern
4848+ let path_str = match &path {
4949+ Value::String { val, .. } | Value::Glob { val, .. } => val.clone(),
5050+ _ => {
5151+ return Err(ShellError::GenericError {
5252+ error: "not a path or glob pattern".into(),
5353+ msg: String::new(),
5454+ span: Some(span),
5555+ help: None,
5656+ inner: vec![],
5757+ });
5858+ }
5959+ };
40604161 let pwd = get_pwd();
6262+ let is_absolute = path_str.starts_with('/');
6363+ let base_path: Arc<vfs::VfsPath> = if is_absolute {
6464+ get_vfs()
6565+ } else {
6666+ pwd.clone()
6767+ };
42684343- let path = pwd.join(&path).map_err(to_shell_err(span))?;
4444- let contents = path.read_to_string().map_err(to_shell_err(span))?;
6969+ // Check if it's a glob pattern (contains *, ?, [, or **)
7070+ let is_glob = path_str.contains('*')
7171+ || path_str.contains('?')
7272+ || path_str.contains('[')
7373+ || path_str.contains("**");
45744646- set_pwd(path.parent().into());
4747- let res = eval(engine_state, stack, &contents, Some(&path.filename()));
4848- set_pwd(pwd);
7575+ let paths_to_source = if is_glob {
7676+ // Expand glob pattern
7777+ let options = crate::cmd::glob::GlobOptions {
7878+ max_depth: None,
7979+ no_dirs: true, // Only source files, not directories
8080+ no_files: false,
8181+ };
8282+ glob_match(&path_str, base_path.clone(), options)?
8383+ } else {
8484+ // Single file path
8585+ vec![path_str]
8686+ };
49875050- match res {
5151- Ok(d) => Ok(d),
5252- Err(err) => {
5353- let msg: String = err.into();
5454- print_to_console(&msg, true);
5555- Err(ShellError::GenericError {
5656- error: "source error".into(),
5757- msg: "can't source file".into(),
5858- span: Some(span),
5959- help: None,
6060- inner: vec![],
6161- })
8888+ // Source each matching file
8989+ for rel_path in paths_to_source {
9090+ let full_path = base_path.join(&rel_path).map_err(to_shell_err(span))?;
9191+9292+ let metadata = full_path.metadata().map_err(to_shell_err(span))?;
9393+ if metadata.file_type != vfs::VfsFileType::File {
9494+ continue;
9595+ }
9696+9797+ let contents = full_path.read_to_string().map_err(to_shell_err(span))?;
9898+9999+ set_pwd(full_path.parent().into());
100100+ let res = eval(engine_state, stack, &contents, Some(&full_path.filename()));
101101+ set_pwd(pwd.clone());
102102+103103+ match res {
104104+ Ok(p) => {
105105+ print_to_console(&p.collect_string("\n", &engine_state.config)?, true);
106106+ }
107107+ Err(err) => {
108108+ let msg: String = err.into();
109109+ print_to_console(&msg, true);
110110+ return Err(ShellError::GenericError {
111111+ error: "source error".into(),
112112+ msg: format!("can't source file: {}", rel_path),
113113+ span: Some(span),
114114+ help: None,
115115+ inner: vec![],
116116+ });
117117+ }
62118 }
63119 }
120120+121121+ Ok(PipelineData::Empty)
64122 }
65123}
66124