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:
Matthew Ma 2022-07-26 11:08:19 -07:00 committed by GitHub
parent b2c466bca6
commit 65f0edd14b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 300 additions and 126 deletions

View File

@ -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",

View File

@ -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| {