+1
Cargo.lock
+1
Cargo.lock
+1
Cargo.toml
+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
+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
+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
+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
+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);