forked from extern/nushell
Allow multiple patterns in ls command (#6098)
* Allow multiple patterns in ls command * Run formatter * Comply with style * Fix format error
This commit is contained in:
parent
b2c466bca6
commit
65f0edd14b
@ -1,15 +1,17 @@
|
||||
use crate::DirBuilder;
|
||||
use crate::DirInfo;
|
||||
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
||||
use itertools::Itertools;
|
||||
use nu_engine::env::current_dir;
|
||||
use nu_engine::CallExt;
|
||||
use nu_glob::MatchOptions;
|
||||
use nu_path::expand_to_real_path;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::IntoPipelineData;
|
||||
use nu_protocol::{
|
||||
Category, DataSource, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
|
||||
PipelineMetadata, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
|
||||
Category, DataSource, Example, IntoInterruptiblePipelineData, PipelineData, PipelineMetadata,
|
||||
ShellError, Signature, Span, Spanned, SyntaxShape, Value,
|
||||
};
|
||||
use pathdiff::diff_paths;
|
||||
|
||||
@ -38,7 +40,11 @@ impl Command for Ls {
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("ls")
|
||||
// Using a string instead of a glob pattern shape so it won't auto-expand
|
||||
.optional("pattern", SyntaxShape::String, "the glob pattern to use")
|
||||
.rest(
|
||||
"pattern(s)",
|
||||
SyntaxShape::String,
|
||||
"the glob pattern(s) to use",
|
||||
)
|
||||
.switch("all", "Show hidden files", Some('a'))
|
||||
.switch(
|
||||
"long",
|
||||
@ -81,19 +87,23 @@ impl Command for Ls {
|
||||
let call_span = call.head;
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
let pattern_arg: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
|
||||
let mut shell_errors: Vec<ShellError> = vec![];
|
||||
let pattern_args: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
let glob_results = if !pattern_args.is_empty() {
|
||||
pattern_args
|
||||
.into_iter()
|
||||
.flat_map(|pattern_arg| {
|
||||
let mut path = expand_to_real_path(pattern_arg.clone().item);
|
||||
let p_tag = pattern_arg.span;
|
||||
let cwd = cwd.clone();
|
||||
let ctrl_c = ctrl_c.clone();
|
||||
|
||||
let (path, p_tag, absolute_path) = match pattern_arg {
|
||||
Some(p) => {
|
||||
let p_tag = p.span;
|
||||
let mut p = expand_to_real_path(p.item);
|
||||
|
||||
let expanded = nu_path::expand_path_with(&p, &cwd);
|
||||
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||
if !directory && expanded.is_dir() {
|
||||
if permission_denied(&p) {
|
||||
#[cfg(unix)]
|
||||
let error_msg = format!(
|
||||
let expanded = nu_path::expand_path_with(&path, &cwd);
|
||||
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||
if !directory && expanded.is_dir() {
|
||||
if permission_denied(&path) {
|
||||
#[cfg(unix)]
|
||||
let error_msg = format!(
|
||||
"The permissions of {:o} do not allow access for this user",
|
||||
expanded
|
||||
.metadata()
|
||||
@ -104,94 +114,221 @@ impl Command for Ls {
|
||||
.mode()
|
||||
& 0o0777
|
||||
);
|
||||
#[cfg(not(unix))]
|
||||
let error_msg = String::from("Permission denied");
|
||||
return Err(ShellError::GenericError(
|
||||
"Permission denied".to_string(),
|
||||
error_msg,
|
||||
Some(p_tag),
|
||||
#[cfg(not(unix))]
|
||||
let error_msg = String::from("Permission denied");
|
||||
shell_errors.push(ShellError::GenericError(
|
||||
"Permission denied".to_string(),
|
||||
error_msg,
|
||||
Some(p_tag),
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
if is_empty_dir(&expanded) {
|
||||
return Vec::from([Value::nothing(call_span)]).into_iter();
|
||||
}
|
||||
path.push("*");
|
||||
}
|
||||
|
||||
let absolute_path = path.is_absolute();
|
||||
|
||||
let hidden_dir_specified = is_hidden_dir(&path);
|
||||
|
||||
let glob_path = Spanned {
|
||||
item: path.display().to_string(),
|
||||
span: p_tag,
|
||||
};
|
||||
|
||||
let glob_options = if all {
|
||||
None
|
||||
} else {
|
||||
let mut glob_options = MatchOptions::new();
|
||||
glob_options.recursive_match_hidden_dir = false;
|
||||
Some(glob_options)
|
||||
};
|
||||
let (prefix, paths) =
|
||||
nu_engine::glob_from(&glob_path, &cwd, call_span, glob_options)
|
||||
.expect("glob failure");
|
||||
|
||||
let mut paths_peek = paths.peekable();
|
||||
if paths_peek.peek().is_none() {
|
||||
shell_errors.push(ShellError::GenericError(
|
||||
format!("No matches found for {}", &path.display().to_string()),
|
||||
"".to_string(),
|
||||
None,
|
||||
Some("no matches found".to_string()),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
if is_empty_dir(&expanded) {
|
||||
return Ok(Value::nothing(call_span).into_pipeline_data());
|
||||
}
|
||||
p.push("*");
|
||||
}
|
||||
let absolute_path = p.is_absolute();
|
||||
(p, p_tag, absolute_path)
|
||||
}
|
||||
None => {
|
||||
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
||||
if directory {
|
||||
(PathBuf::from("."), call_span, false)
|
||||
} else if is_empty_dir(current_dir(engine_state, stack)?) {
|
||||
return Ok(Value::nothing(call_span).into_pipeline_data());
|
||||
} else {
|
||||
(PathBuf::from("./*"), call_span, false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let hidden_dir_specified = is_hidden_dir(&path);
|
||||
let mut hidden_dirs = vec![];
|
||||
|
||||
let glob_path = Spanned {
|
||||
item: path.display().to_string(),
|
||||
span: p_tag,
|
||||
};
|
||||
paths_peek
|
||||
.into_iter()
|
||||
.filter_map(move |x| match x {
|
||||
Ok(path) => {
|
||||
let metadata = match std::fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(_) => None,
|
||||
};
|
||||
if path_contains_hidden_folder(&path, &hidden_dirs) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let glob_options = if all {
|
||||
None
|
||||
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
||||
if path.is_dir() {
|
||||
hidden_dirs.push(path);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let display_name = if short_names {
|
||||
path.file_name().map(|os| os.to_string_lossy().to_string())
|
||||
} else if full_paths || absolute_path {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
} else if let Some(prefix) = &prefix {
|
||||
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
||||
if directory {
|
||||
// When the path is the same as the cwd, path_diff should be "."
|
||||
let path_diff = if let Some(path_diff_not_dot) =
|
||||
diff_paths(&path, &cwd)
|
||||
{
|
||||
let path_diff_not_dot =
|
||||
path_diff_not_dot.to_string_lossy();
|
||||
if path_diff_not_dot.is_empty() {
|
||||
".".to_string()
|
||||
} else {
|
||||
path_diff_not_dot.to_string()
|
||||
}
|
||||
} else {
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
Some(path_diff)
|
||||
} else {
|
||||
let new_prefix =
|
||||
if let Some(pfx) = diff_paths(&prefix, &cwd) {
|
||||
pfx
|
||||
} else {
|
||||
prefix.to_path_buf()
|
||||
};
|
||||
|
||||
Some(
|
||||
new_prefix
|
||||
.join(remainder)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
}
|
||||
} else {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
ShellError::GenericError(
|
||||
format!("Invalid file name: {:}", path.to_string_lossy()),
|
||||
"invalid file name".into(),
|
||||
Some(call_span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
});
|
||||
|
||||
match display_name {
|
||||
Ok(name) => {
|
||||
let entry = dir_entry_dict(
|
||||
&path,
|
||||
&name,
|
||||
metadata.as_ref(),
|
||||
call_span,
|
||||
long,
|
||||
du,
|
||||
ctrl_c.clone(),
|
||||
);
|
||||
match entry {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
}
|
||||
}
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
}
|
||||
}
|
||||
_ => Some(Value::Nothing { span: call_span }),
|
||||
})
|
||||
.collect_vec()
|
||||
.into_iter()
|
||||
})
|
||||
.collect_vec()
|
||||
} else {
|
||||
let mut glob_options = MatchOptions::new();
|
||||
glob_options.recursive_match_hidden_dir = false;
|
||||
Some(glob_options)
|
||||
};
|
||||
let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span, glob_options)?;
|
||||
let (path, p_tag, absolute_path) = if directory {
|
||||
(PathBuf::from("."), call_span, false)
|
||||
} else if is_empty_dir(current_dir(engine_state, stack)?) {
|
||||
return Ok(Value::nothing(call_span).into_pipeline_data());
|
||||
} else {
|
||||
(PathBuf::from("./*"), call_span, false)
|
||||
};
|
||||
|
||||
let mut paths_peek = paths.peekable();
|
||||
if paths_peek.peek().is_none() {
|
||||
return Err(ShellError::GenericError(
|
||||
format!("No matches found for {}", &path.display().to_string()),
|
||||
"".to_string(),
|
||||
None,
|
||||
Some("no matches found".to_string()),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
let hidden_dir_specified = is_hidden_dir(&path);
|
||||
|
||||
let mut hidden_dirs = vec![];
|
||||
let glob_path = Spanned {
|
||||
item: path.display().to_string(),
|
||||
span: p_tag,
|
||||
};
|
||||
|
||||
Ok(paths_peek
|
||||
.into_iter()
|
||||
.filter_map(move |x| match x {
|
||||
Ok(path) => {
|
||||
let metadata = match std::fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(_) => None,
|
||||
};
|
||||
if path_contains_hidden_folder(&path, &hidden_dirs) {
|
||||
return None;
|
||||
}
|
||||
let glob_options = if all {
|
||||
None
|
||||
} else {
|
||||
let mut glob_options = MatchOptions::new();
|
||||
glob_options.recursive_match_hidden_dir = false;
|
||||
Some(glob_options)
|
||||
};
|
||||
let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span, glob_options)?;
|
||||
|
||||
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
||||
if path.is_dir() {
|
||||
hidden_dirs.push(path);
|
||||
let mut paths_peek = paths.peekable();
|
||||
if paths_peek.peek().is_none() {
|
||||
return Err(ShellError::GenericError(
|
||||
format!("No matches found for {}", &path.display().to_string()),
|
||||
"".to_string(),
|
||||
None,
|
||||
Some("no matches found".to_string()),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut hidden_dirs = vec![];
|
||||
|
||||
paths_peek
|
||||
.into_iter()
|
||||
.filter_map(move |x| match x {
|
||||
Ok(path) => {
|
||||
let metadata = match std::fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(_) => None,
|
||||
};
|
||||
if path_contains_hidden_folder(&path, &hidden_dirs) {
|
||||
return None;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let display_name = if short_names {
|
||||
path.file_name().map(|os| os.to_string_lossy().to_string())
|
||||
} else if full_paths || absolute_path {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
} else if let Some(prefix) = &prefix {
|
||||
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
||||
if directory {
|
||||
// When the path is the same as the cwd, path_diff should be "."
|
||||
let path_diff =
|
||||
if let Some(path_diff_not_dot) = diff_paths(&path, &cwd) {
|
||||
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
||||
if path.is_dir() {
|
||||
hidden_dirs.push(path);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let display_name = if short_names {
|
||||
path.file_name().map(|os| os.to_string_lossy().to_string())
|
||||
} else if full_paths || absolute_path {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
} else if let Some(prefix) = &prefix {
|
||||
if let Ok(remainder) = path.strip_prefix(&prefix) {
|
||||
if directory {
|
||||
// When the path is the same as the cwd, path_diff should be "."
|
||||
let path_diff = if let Some(path_diff_not_dot) =
|
||||
diff_paths(&path, &cwd)
|
||||
{
|
||||
let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
|
||||
if path_diff_not_dot.is_empty() {
|
||||
".".to_string()
|
||||
@ -202,53 +339,63 @@ impl Command for Ls {
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
Some(path_diff)
|
||||
} else {
|
||||
let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) {
|
||||
pfx
|
||||
Some(path_diff)
|
||||
} else {
|
||||
prefix.to_path_buf()
|
||||
};
|
||||
let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) {
|
||||
pfx
|
||||
} else {
|
||||
prefix.to_path_buf()
|
||||
};
|
||||
|
||||
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
||||
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
||||
}
|
||||
} else {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
}
|
||||
} else {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
}
|
||||
} else {
|
||||
Some(path.to_string_lossy().to_string())
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
ShellError::GenericError(
|
||||
format!("Invalid file name: {:}", path.to_string_lossy()),
|
||||
"invalid file name".into(),
|
||||
Some(call_span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
});
|
||||
.ok_or_else(|| {
|
||||
ShellError::GenericError(
|
||||
format!("Invalid file name: {:}", path.to_string_lossy()),
|
||||
"invalid file name".into(),
|
||||
Some(call_span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
});
|
||||
|
||||
match display_name {
|
||||
Ok(name) => {
|
||||
let entry = dir_entry_dict(
|
||||
&path,
|
||||
&name,
|
||||
metadata.as_ref(),
|
||||
call_span,
|
||||
long,
|
||||
du,
|
||||
ctrl_c.clone(),
|
||||
);
|
||||
match entry {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
match display_name {
|
||||
Ok(name) => {
|
||||
let entry = dir_entry_dict(
|
||||
&path,
|
||||
&name,
|
||||
metadata.as_ref(),
|
||||
call_span,
|
||||
long,
|
||||
du,
|
||||
ctrl_c.clone(),
|
||||
);
|
||||
match entry {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
}
|
||||
}
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
}
|
||||
Err(err) => Some(Value::Error { error: err }),
|
||||
}
|
||||
}
|
||||
_ => Some(Value::Nothing { span: call_span }),
|
||||
})
|
||||
_ => Some(Value::Nothing { span: call_span }),
|
||||
})
|
||||
.collect_vec()
|
||||
};
|
||||
|
||||
if !shell_errors.is_empty() {
|
||||
return Err(shell_errors.pop().expect("Vec pop error"));
|
||||
}
|
||||
|
||||
Ok(glob_results
|
||||
.into_iter()
|
||||
.filter(|result| !matches!(result, Value::Nothing { .. }))
|
||||
.into_pipeline_data_with_metadata(
|
||||
PipelineMetadata {
|
||||
data_source: DataSource::Ls,
|
||||
@ -279,6 +426,11 @@ impl Command for Ls {
|
||||
example: "ls *.rs",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "List all rust files and all toml files",
|
||||
example: "ls *.rs *.toml",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "List all files and directories whose name do not contain 'bar'",
|
||||
example: "ls -s | where name !~ bar",
|
||||
|
@ -335,6 +335,28 @@ fn lists_files_including_starting_with_dot() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_regular_files_using_multiple_asterisk_wildcards() {
|
||||
Playground::setup("ls_test_10", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![
|
||||
EmptyFile("los.txt"),
|
||||
EmptyFile("tres.txt"),
|
||||
EmptyFile("amigos.txt"),
|
||||
EmptyFile("arepas.clu"),
|
||||
]);
|
||||
|
||||
let actual = nu!(
|
||||
cwd: dirs.test(), pipeline(
|
||||
r#"
|
||||
ls *.txt *.clu
|
||||
| length
|
||||
"#
|
||||
));
|
||||
|
||||
assert_eq!(actual.out, "4");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_all_columns() {
|
||||
Playground::setup("ls_test_all_columns", |dirs, sandbox| {
|
||||
|
Loading…
Reference in New Issue
Block a user