mirror of
https://github.com/nushell/nushell.git
synced 2024-11-07 17:14:23 +01:00
making ls
and du
supports rest parameters. (#12327)
# Description Close: #12147 Close: #11796 About the change: it make pattern handling into a function: `ls_for_one_pattern`(for ls), `du_for_one_pattern`(for du). Then iterates on user input pattern, call these core function, and chaining these iterator to one pipelinedata.
This commit is contained in:
parent
56cdee1fd8
commit
0110345755
@ -1,9 +1,11 @@
|
|||||||
use super::util::opt_for_glob_pattern;
|
use super::util::get_rest_for_glob_pattern;
|
||||||
use crate::{DirBuilder, DirInfo, FileInfo};
|
use crate::{DirBuilder, DirInfo, FileInfo};
|
||||||
use nu_engine::{command_prelude::*, current_dir};
|
use nu_engine::{command_prelude::*, current_dir};
|
||||||
use nu_glob::Pattern;
|
use nu_glob::Pattern;
|
||||||
use nu_protocol::NuGlob;
|
use nu_protocol::NuGlob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Du;
|
pub struct Du;
|
||||||
@ -33,7 +35,7 @@ impl Command for Du {
|
|||||||
Signature::build("du")
|
Signature::build("du")
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.optional(
|
.rest(
|
||||||
"path",
|
"path",
|
||||||
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
|
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
|
||||||
"Starting directory.",
|
"Starting directory.",
|
||||||
@ -93,81 +95,59 @@ impl Command for Du {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let all = call.has_flag(engine_state, stack, "all")?;
|
||||||
|
let deref = call.has_flag(engine_state, stack, "deref")?;
|
||||||
|
let exclude = call.get_flag(engine_state, stack, "exclude")?;
|
||||||
let current_dir = current_dir(engine_state, stack)?;
|
let current_dir = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
let args = DuArgs {
|
let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
||||||
path: opt_for_glob_pattern(engine_state, stack, call, 0)?,
|
let paths = if call.rest_iter(0).count() == 0 {
|
||||||
all: call.has_flag(engine_state, stack, "all")?,
|
None
|
||||||
deref: call.has_flag(engine_state, stack, "deref")?,
|
} else {
|
||||||
exclude: call.get_flag(engine_state, stack, "exclude")?,
|
Some(paths)
|
||||||
max_depth,
|
|
||||||
min_size,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let exclude = args.exclude.map_or(Ok(None), move |x| {
|
match paths {
|
||||||
Pattern::new(x.item.as_ref())
|
None => {
|
||||||
.map(Some)
|
let args = DuArgs {
|
||||||
.map_err(|e| ShellError::InvalidGlobPattern {
|
path: None,
|
||||||
msg: e.msg.into(),
|
all,
|
||||||
span: x.span,
|
deref,
|
||||||
})
|
exclude,
|
||||||
})?;
|
max_depth,
|
||||||
|
min_size,
|
||||||
let include_files = args.all;
|
};
|
||||||
let mut paths = match args.path {
|
Ok(
|
||||||
Some(p) => nu_engine::glob_from(&p, ¤t_dir, call.head, None),
|
du_for_one_pattern(args, ¤t_dir, tag, engine_state.ctrlc.clone())?
|
||||||
// The * pattern should never fail.
|
.into_pipeline_data(engine_state.ctrlc.clone()),
|
||||||
None => nu_engine::glob_from(
|
)
|
||||||
&Spanned {
|
|
||||||
item: NuGlob::Expand("*".into()),
|
|
||||||
span: Span::unknown(),
|
|
||||||
},
|
|
||||||
¤t_dir,
|
|
||||||
call.head,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
.map(|f| f.1)?
|
|
||||||
.filter(move |p| {
|
|
||||||
if include_files {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
matches!(p, Ok(f) if f.is_dir())
|
|
||||||
}
|
}
|
||||||
});
|
Some(paths) => {
|
||||||
|
let mut result_iters = vec![];
|
||||||
let all = args.all;
|
for p in paths {
|
||||||
let deref = args.deref;
|
let args = DuArgs {
|
||||||
let max_depth = args.max_depth.map(|f| f.item as u64);
|
path: Some(p),
|
||||||
let min_size = args.min_size.map(|f| f.item as u64);
|
all,
|
||||||
|
deref,
|
||||||
let params = DirBuilder {
|
exclude: exclude.clone(),
|
||||||
tag,
|
max_depth,
|
||||||
min: min_size,
|
min_size,
|
||||||
deref,
|
};
|
||||||
exclude,
|
result_iters.push(du_for_one_pattern(
|
||||||
all,
|
args,
|
||||||
};
|
¤t_dir,
|
||||||
|
tag,
|
||||||
let mut output: Vec<Value> = vec![];
|
engine_state.ctrlc.clone(),
|
||||||
for p in paths.by_ref() {
|
)?)
|
||||||
match p {
|
|
||||||
Ok(a) => {
|
|
||||||
if a.is_dir() {
|
|
||||||
output.push(
|
|
||||||
DirInfo::new(a, ¶ms, max_depth, engine_state.ctrlc.clone()).into(),
|
|
||||||
);
|
|
||||||
} else if let Ok(v) = FileInfo::new(a, deref, tag) {
|
|
||||||
output.push(v.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
output.push(Value::error(e, tag));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chain all iterators on result.
|
||||||
|
Ok(result_iters
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output.into_pipeline_data(engine_state.ctrlc.clone()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -179,6 +159,75 @@ impl Command for Du {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn du_for_one_pattern(
|
||||||
|
args: DuArgs,
|
||||||
|
current_dir: &Path,
|
||||||
|
call_span: Span,
|
||||||
|
ctrl_c: Option<Arc<AtomicBool>>,
|
||||||
|
) -> Result<impl Iterator<Item = Value> + Send, ShellError> {
|
||||||
|
let exclude = args.exclude.map_or(Ok(None), move |x| {
|
||||||
|
Pattern::new(x.item.as_ref())
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| ShellError::InvalidGlobPattern {
|
||||||
|
msg: e.msg.into(),
|
||||||
|
span: x.span,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let include_files = args.all;
|
||||||
|
let mut paths = match args.path {
|
||||||
|
Some(p) => nu_engine::glob_from(&p, current_dir, call_span, None),
|
||||||
|
// The * pattern should never fail.
|
||||||
|
None => nu_engine::glob_from(
|
||||||
|
&Spanned {
|
||||||
|
item: NuGlob::Expand("*".into()),
|
||||||
|
span: Span::unknown(),
|
||||||
|
},
|
||||||
|
current_dir,
|
||||||
|
call_span,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.map(|f| f.1)?
|
||||||
|
.filter(move |p| {
|
||||||
|
if include_files {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
matches!(p, Ok(f) if f.is_dir())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let all = args.all;
|
||||||
|
let deref = args.deref;
|
||||||
|
let max_depth = args.max_depth.map(|f| f.item as u64);
|
||||||
|
let min_size = args.min_size.map(|f| f.item as u64);
|
||||||
|
|
||||||
|
let params = DirBuilder {
|
||||||
|
tag: call_span,
|
||||||
|
min: min_size,
|
||||||
|
deref,
|
||||||
|
exclude,
|
||||||
|
all,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output: Vec<Value> = vec![];
|
||||||
|
for p in paths.by_ref() {
|
||||||
|
match p {
|
||||||
|
Ok(a) => {
|
||||||
|
if a.is_dir() {
|
||||||
|
output.push(DirInfo::new(a, ¶ms, max_depth, ctrl_c.clone()).into());
|
||||||
|
} else if let Ok(v) = FileInfo::new(a, deref, call_span) {
|
||||||
|
output.push(v.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output.push(Value::error(e, call_span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::Du;
|
use super::Du;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::util::opt_for_glob_pattern;
|
use super::util::get_rest_for_glob_pattern;
|
||||||
use crate::{DirBuilder, DirInfo};
|
use crate::{DirBuilder, DirInfo};
|
||||||
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
||||||
use nu_engine::{command_prelude::*, env::current_dir};
|
use nu_engine::{command_prelude::*, env::current_dir};
|
||||||
@ -18,6 +18,18 @@ use std::{
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ls;
|
pub struct Ls;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Args {
|
||||||
|
all: bool,
|
||||||
|
long: bool,
|
||||||
|
short_names: bool,
|
||||||
|
full_paths: bool,
|
||||||
|
du: bool,
|
||||||
|
directory: bool,
|
||||||
|
use_mime_type: bool,
|
||||||
|
call_span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
impl Command for Ls {
|
impl Command for Ls {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"ls"
|
"ls"
|
||||||
@ -36,7 +48,7 @@ impl Command for Ls {
|
|||||||
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
|
||||||
// LsGlobPattern is similar to string, it won't auto-expand
|
// LsGlobPattern is similar to string, it won't auto-expand
|
||||||
// and we use it to track if the user input is quoted.
|
// and we use it to track if the user input is quoted.
|
||||||
.optional("pattern", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The glob pattern to use.")
|
.rest("pattern", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The glob pattern to use.")
|
||||||
.switch("all", "Show hidden files", Some('a'))
|
.switch("all", "Show hidden files", Some('a'))
|
||||||
.switch(
|
.switch(
|
||||||
"long",
|
"long",
|
||||||
@ -81,241 +93,55 @@ impl Command for Ls {
|
|||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
let pattern_arg = opt_for_glob_pattern(engine_state, stack, call, 0)?;
|
let args = Args {
|
||||||
let pattern_arg = {
|
all,
|
||||||
if let Some(path) = pattern_arg {
|
long,
|
||||||
// it makes no sense to list an empty string.
|
short_names,
|
||||||
if path.item.as_ref().is_empty() {
|
full_paths,
|
||||||
return Err(ShellError::FileNotFoundCustom {
|
du,
|
||||||
msg: "empty string('') directory or file does not exist".to_string(),
|
directory,
|
||||||
span: path.span,
|
use_mime_type,
|
||||||
});
|
call_span,
|
||||||
}
|
|
||||||
match path.item {
|
|
||||||
NuGlob::DoNotExpand(p) => Some(Spanned {
|
|
||||||
item: NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(p)),
|
|
||||||
span: path.span,
|
|
||||||
}),
|
|
||||||
NuGlob::Expand(p) => Some(Spanned {
|
|
||||||
item: NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(p)),
|
|
||||||
span: path.span,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pattern_arg
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// it indicates we need to append an extra '*' after pattern for listing given directory
|
let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
||||||
// Example: 'ls directory' -> 'ls directory/*'
|
let input_pattern_arg = if call.rest_iter(0).count() == 0 {
|
||||||
let mut extra_star_under_given_directory = false;
|
|
||||||
let (path, p_tag, absolute_path, quoted) = match pattern_arg {
|
|
||||||
Some(pat) => {
|
|
||||||
let p_tag = pat.span;
|
|
||||||
let expanded = nu_path::expand_path_with(
|
|
||||||
pat.item.as_ref(),
|
|
||||||
&cwd,
|
|
||||||
matches!(pat.item, NuGlob::Expand(..)),
|
|
||||||
);
|
|
||||||
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
|
||||||
if !directory && expanded.is_dir() {
|
|
||||||
if permission_denied(&expanded) {
|
|
||||||
#[cfg(unix)]
|
|
||||||
let error_msg = format!(
|
|
||||||
"The permissions of {:o} do not allow access for this user",
|
|
||||||
expanded
|
|
||||||
.metadata()
|
|
||||||
.expect(
|
|
||||||
"this shouldn't be called since we already know there is a dir"
|
|
||||||
)
|
|
||||||
.permissions()
|
|
||||||
.mode()
|
|
||||||
& 0o0777
|
|
||||||
);
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
let error_msg = String::from("Permission denied");
|
|
||||||
return Err(ShellError::GenericError {
|
|
||||||
error: "Permission denied".into(),
|
|
||||||
msg: error_msg,
|
|
||||||
span: Some(p_tag),
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_empty_dir(&expanded) {
|
|
||||||
return Ok(Value::list(vec![], call_span).into_pipeline_data());
|
|
||||||
}
|
|
||||||
extra_star_under_given_directory = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's absolute path if:
|
|
||||||
// 1. pattern is absolute.
|
|
||||||
// 2. pattern can be expanded, and after expands to real_path, it's absolute.
|
|
||||||
// here `expand_to_real_path` call is required, because `~/aaa` should be absolute
|
|
||||||
// path.
|
|
||||||
let absolute_path = Path::new(pat.item.as_ref()).is_absolute()
|
|
||||||
|| (pat.item.is_expand()
|
|
||||||
&& expand_to_real_path(pat.item.as_ref()).is_absolute());
|
|
||||||
(
|
|
||||||
expanded,
|
|
||||||
p_tag,
|
|
||||||
absolute_path,
|
|
||||||
matches!(pat.item, NuGlob::DoNotExpand(_)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
|
||||||
if directory {
|
|
||||||
(PathBuf::from("."), call_span, false, false)
|
|
||||||
} else if is_empty_dir(current_dir(engine_state, stack)?) {
|
|
||||||
return Ok(Value::list(vec![], call_span).into_pipeline_data());
|
|
||||||
} else {
|
|
||||||
(PathBuf::from("*"), call_span, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let hidden_dir_specified = is_hidden_dir(&path);
|
|
||||||
// when it's quoted, we need to escape our glob pattern(but without the last extra
|
|
||||||
// start which may be added under given directory)
|
|
||||||
// so we can do ls for a file or directory like `a[123]b`
|
|
||||||
let path = if quoted {
|
|
||||||
let p = path.display().to_string();
|
|
||||||
let mut glob_escaped = Pattern::escape(&p);
|
|
||||||
if extra_star_under_given_directory {
|
|
||||||
glob_escaped.push(std::path::MAIN_SEPARATOR);
|
|
||||||
glob_escaped.push('*');
|
|
||||||
}
|
|
||||||
glob_escaped
|
|
||||||
} else {
|
|
||||||
let mut p = path.display().to_string();
|
|
||||||
if extra_star_under_given_directory {
|
|
||||||
p.push(std::path::MAIN_SEPARATOR);
|
|
||||||
p.push('*');
|
|
||||||
}
|
|
||||||
p
|
|
||||||
};
|
|
||||||
|
|
||||||
let glob_path = Spanned {
|
|
||||||
// use NeedExpand, the relative escaping logic is handled previously
|
|
||||||
item: NuGlob::Expand(path.clone()),
|
|
||||||
span: p_tag,
|
|
||||||
};
|
|
||||||
|
|
||||||
let glob_options = if all {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let glob_options = MatchOptions {
|
Some(pattern_arg)
|
||||||
recursive_match_hidden_dir: false,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
Some(glob_options)
|
|
||||||
};
|
};
|
||||||
let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span, glob_options)?;
|
match input_pattern_arg {
|
||||||
|
None => Ok(ls_for_one_pattern(None, args, ctrl_c.clone(), cwd)?
|
||||||
let mut paths_peek = paths.peekable();
|
.into_pipeline_data_with_metadata(
|
||||||
if paths_peek.peek().is_none() {
|
PipelineMetadata {
|
||||||
return Err(ShellError::GenericError {
|
data_source: DataSource::Ls,
|
||||||
error: format!("No matches found for {}", &path),
|
},
|
||||||
msg: "Pattern, file or folder not found".into(),
|
ctrl_c,
|
||||||
span: Some(p_tag),
|
)),
|
||||||
help: Some("no matches found".into()),
|
Some(pattern) => {
|
||||||
inner: vec![],
|
let mut result_iters = vec![];
|
||||||
});
|
for pat in pattern {
|
||||||
}
|
result_iters.push(ls_for_one_pattern(
|
||||||
|
Some(pat),
|
||||||
let mut hidden_dirs = vec![];
|
args,
|
||||||
|
ctrl_c.clone(),
|
||||||
Ok(paths_peek
|
cwd.clone(),
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
error: format!("Invalid file name: {:}", path.to_string_lossy()),
|
|
||||||
msg: "invalid file name".into(),
|
|
||||||
span: Some(call_span),
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
match display_name {
|
|
||||||
Ok(name) => {
|
|
||||||
let entry = dir_entry_dict(
|
|
||||||
&path,
|
|
||||||
&name,
|
|
||||||
metadata.as_ref(),
|
|
||||||
call_span,
|
|
||||||
long,
|
|
||||||
du,
|
|
||||||
ctrl_c.clone(),
|
|
||||||
use_mime_type,
|
|
||||||
);
|
|
||||||
match entry {
|
|
||||||
Ok(value) => Some(value),
|
|
||||||
Err(err) => Some(Value::error(err, call_span)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => Some(Value::error(err, call_span)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => Some(Value::nothing(call_span)),
|
|
||||||
})
|
// Here nushell needs to use
|
||||||
.into_pipeline_data_with_metadata(
|
// use `flatten` to chain all iterators into one.
|
||||||
PipelineMetadata {
|
Ok(result_iters
|
||||||
data_source: DataSource::Ls,
|
.into_iter()
|
||||||
},
|
.flatten()
|
||||||
engine_state.ctrlc.clone(),
|
.into_pipeline_data_with_metadata(
|
||||||
))
|
PipelineMetadata {
|
||||||
|
data_source: DataSource::Ls,
|
||||||
|
},
|
||||||
|
ctrl_c,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -365,6 +191,248 @@ impl Command for Ls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ls_for_one_pattern(
|
||||||
|
pattern_arg: Option<Spanned<NuGlob>>,
|
||||||
|
args: Args,
|
||||||
|
ctrl_c: Option<Arc<AtomicBool>>,
|
||||||
|
cwd: PathBuf,
|
||||||
|
) -> Result<Box<dyn Iterator<Item = Value> + Send>, ShellError> {
|
||||||
|
let Args {
|
||||||
|
all,
|
||||||
|
long,
|
||||||
|
short_names,
|
||||||
|
full_paths,
|
||||||
|
du,
|
||||||
|
directory,
|
||||||
|
use_mime_type,
|
||||||
|
call_span,
|
||||||
|
} = args;
|
||||||
|
let pattern_arg = {
|
||||||
|
if let Some(path) = pattern_arg {
|
||||||
|
// it makes no sense to list an empty string.
|
||||||
|
if path.item.as_ref().is_empty() {
|
||||||
|
return Err(ShellError::FileNotFoundCustom {
|
||||||
|
msg: "empty string('') directory or file does not exist".to_string(),
|
||||||
|
span: path.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match path.item {
|
||||||
|
NuGlob::DoNotExpand(p) => Some(Spanned {
|
||||||
|
item: NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(p)),
|
||||||
|
span: path.span,
|
||||||
|
}),
|
||||||
|
NuGlob::Expand(p) => Some(Spanned {
|
||||||
|
item: NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(p)),
|
||||||
|
span: path.span,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pattern_arg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// it indicates we need to append an extra '*' after pattern for listing given directory
|
||||||
|
// Example: 'ls directory' -> 'ls directory/*'
|
||||||
|
let mut extra_star_under_given_directory = false;
|
||||||
|
let (path, p_tag, absolute_path, quoted) = match pattern_arg {
|
||||||
|
Some(pat) => {
|
||||||
|
let p_tag = pat.span;
|
||||||
|
let expanded = nu_path::expand_path_with(
|
||||||
|
pat.item.as_ref(),
|
||||||
|
&cwd,
|
||||||
|
matches!(pat.item, NuGlob::Expand(..)),
|
||||||
|
);
|
||||||
|
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||||
|
if !directory && expanded.is_dir() {
|
||||||
|
if permission_denied(&expanded) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
let error_msg = format!(
|
||||||
|
"The permissions of {:o} do not allow access for this user",
|
||||||
|
expanded
|
||||||
|
.metadata()
|
||||||
|
.expect("this shouldn't be called since we already know there is a dir")
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o0777
|
||||||
|
);
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let error_msg = String::from("Permission denied");
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: "Permission denied".into(),
|
||||||
|
msg: error_msg,
|
||||||
|
span: Some(p_tag),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if is_empty_dir(&expanded) {
|
||||||
|
return Ok(Box::new(vec![].into_iter()));
|
||||||
|
}
|
||||||
|
extra_star_under_given_directory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's absolute path if:
|
||||||
|
// 1. pattern is absolute.
|
||||||
|
// 2. pattern can be expanded, and after expands to real_path, it's absolute.
|
||||||
|
// here `expand_to_real_path` call is required, because `~/aaa` should be absolute
|
||||||
|
// path.
|
||||||
|
let absolute_path = Path::new(pat.item.as_ref()).is_absolute()
|
||||||
|
|| (pat.item.is_expand() && expand_to_real_path(pat.item.as_ref()).is_absolute());
|
||||||
|
(
|
||||||
|
expanded,
|
||||||
|
p_tag,
|
||||||
|
absolute_path,
|
||||||
|
matches!(pat.item, NuGlob::DoNotExpand(_)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
||||||
|
if directory {
|
||||||
|
(PathBuf::from("."), call_span, false, false)
|
||||||
|
} else if is_empty_dir(&cwd) {
|
||||||
|
return Ok(Box::new(vec![].into_iter()));
|
||||||
|
} else {
|
||||||
|
(PathBuf::from("*"), call_span, false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hidden_dir_specified = is_hidden_dir(&path);
|
||||||
|
// when it's quoted, we need to escape our glob pattern(but without the last extra
|
||||||
|
// start which may be added under given directory)
|
||||||
|
// so we can do ls for a file or directory like `a[123]b`
|
||||||
|
let path = if quoted {
|
||||||
|
let p = path.display().to_string();
|
||||||
|
let mut glob_escaped = Pattern::escape(&p);
|
||||||
|
if extra_star_under_given_directory {
|
||||||
|
glob_escaped.push(std::path::MAIN_SEPARATOR);
|
||||||
|
glob_escaped.push('*');
|
||||||
|
}
|
||||||
|
glob_escaped
|
||||||
|
} else {
|
||||||
|
let mut p = path.display().to_string();
|
||||||
|
if extra_star_under_given_directory {
|
||||||
|
p.push(std::path::MAIN_SEPARATOR);
|
||||||
|
p.push('*');
|
||||||
|
}
|
||||||
|
p
|
||||||
|
};
|
||||||
|
|
||||||
|
let glob_path = Spanned {
|
||||||
|
// use NeedExpand, the relative escaping logic is handled previously
|
||||||
|
item: NuGlob::Expand(path.clone()),
|
||||||
|
span: p_tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
let glob_options = if all {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let glob_options = MatchOptions {
|
||||||
|
recursive_match_hidden_dir: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
Some(glob_options)
|
||||||
|
};
|
||||||
|
let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span, glob_options)?;
|
||||||
|
|
||||||
|
let mut paths_peek = paths.peekable();
|
||||||
|
if paths_peek.peek().is_none() {
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: format!("No matches found for {}", &path),
|
||||||
|
msg: "Pattern, file or folder not found".into(),
|
||||||
|
span: Some(p_tag),
|
||||||
|
help: Some("no matches found".into()),
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hidden_dirs = vec![];
|
||||||
|
|
||||||
|
let one_ctrl_c = ctrl_c.clone();
|
||||||
|
Ok(Box::new(paths_peek.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
error: format!("Invalid file name: {:}", path.to_string_lossy()),
|
||||||
|
msg: "invalid file name".into(),
|
||||||
|
span: Some(call_span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
match display_name {
|
||||||
|
Ok(name) => {
|
||||||
|
let entry = dir_entry_dict(
|
||||||
|
&path,
|
||||||
|
&name,
|
||||||
|
metadata.as_ref(),
|
||||||
|
call_span,
|
||||||
|
long,
|
||||||
|
du,
|
||||||
|
one_ctrl_c.clone(),
|
||||||
|
use_mime_type,
|
||||||
|
);
|
||||||
|
match entry {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(err) => Some(Value::error(err, call_span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Some(Value::error(err, call_span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Some(Value::nothing(call_span)),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
fn permission_denied(dir: impl AsRef<Path>) -> bool {
|
fn permission_denied(dir: impl AsRef<Path>) -> bool {
|
||||||
match dir.as_ref().read_dir() {
|
match dir.as_ref().read_dir() {
|
||||||
Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
|
Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
|
||||||
|
@ -131,34 +131,3 @@ pub fn get_rest_for_glob_pattern(
|
|||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get optional arguments from given `call` with position `pos`.
|
|
||||||
///
|
|
||||||
/// It's similar to `call.opt`, except that it always returns NuGlob.
|
|
||||||
pub fn opt_for_glob_pattern(
|
|
||||||
engine_state: &EngineState,
|
|
||||||
stack: &mut Stack,
|
|
||||||
call: &Call,
|
|
||||||
pos: usize,
|
|
||||||
) -> Result<Option<Spanned<NuGlob>>, ShellError> {
|
|
||||||
if let Some(expr) = call.positional_nth(pos) {
|
|
||||||
let eval_expression = get_eval_expression(engine_state);
|
|
||||||
let result = eval_expression(engine_state, stack, expr)?;
|
|
||||||
let result_span = result.span();
|
|
||||||
let result = match result {
|
|
||||||
Value::String { val, .. }
|
|
||||||
if matches!(
|
|
||||||
&expr.expr,
|
|
||||||
Expr::FullCellPath(_) | Expr::StringInterpolation(_)
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
// should quote if given input type is not glob.
|
|
||||||
Value::glob(val, expr.ty != Type::Glob, result_span)
|
|
||||||
}
|
|
||||||
other => other,
|
|
||||||
};
|
|
||||||
FromValue::from_value(result).map(Some)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -50,7 +50,7 @@ fn test_du_flag_max_depth() {
|
|||||||
#[case("a[bc]d")]
|
#[case("a[bc]d")]
|
||||||
#[case("a][c")]
|
#[case("a][c")]
|
||||||
fn du_files_with_glob_metachars(#[case] src_name: &str) {
|
fn du_files_with_glob_metachars(#[case] src_name: &str) {
|
||||||
Playground::setup("umv_test_16", |dirs, sandbox| {
|
Playground::setup("du_test_16", |dirs, sandbox| {
|
||||||
sandbox.with_files(vec![EmptyFile(src_name)]);
|
sandbox.with_files(vec![EmptyFile(src_name)]);
|
||||||
|
|
||||||
let src = dirs.test().join(src_name);
|
let src = dirs.test().join(src_name);
|
||||||
@ -82,3 +82,21 @@ fn du_files_with_glob_metachars(#[case] src_name: &str) {
|
|||||||
fn du_files_with_glob_metachars_nw(#[case] src_name: &str) {
|
fn du_files_with_glob_metachars_nw(#[case] src_name: &str) {
|
||||||
du_files_with_glob_metachars(src_name);
|
du_files_with_glob_metachars(src_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn du_with_multiple_path() {
|
||||||
|
let actual = nu!(cwd: "tests/fixtures", "du cp formats | get path | path basename");
|
||||||
|
assert!(actual.out.contains("cp"));
|
||||||
|
assert!(actual.out.contains("formats"));
|
||||||
|
assert!(!actual.out.contains("lsp"));
|
||||||
|
assert!(actual.status.success());
|
||||||
|
|
||||||
|
// report errors if one path not exists
|
||||||
|
let actual = nu!(cwd: "tests/fixtures", "du cp asdf | get path | path basename");
|
||||||
|
assert!(actual.err.contains("directory not found"));
|
||||||
|
assert!(!actual.status.success());
|
||||||
|
|
||||||
|
// du with spreading empty list should returns nothing.
|
||||||
|
let actual = nu!(cwd: "tests/fixtures", "du ...[] | length");
|
||||||
|
assert_eq!(actual.out, "0");
|
||||||
|
}
|
||||||
|
@ -733,3 +733,29 @@ fn list_with_tilde() {
|
|||||||
assert!(actual.out.contains("~tilde"));
|
assert!(actual.out.contains("~tilde"));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_with_multiple_path() {
|
||||||
|
Playground::setup("ls_multiple_path", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(vec![
|
||||||
|
EmptyFile("f1.txt"),
|
||||||
|
EmptyFile("f2.txt"),
|
||||||
|
EmptyFile("f3.txt"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let actual = nu!(cwd: dirs.test(), "ls f1.txt f2.txt");
|
||||||
|
assert!(actual.out.contains("f1.txt"));
|
||||||
|
assert!(actual.out.contains("f2.txt"));
|
||||||
|
assert!(!actual.out.contains("f3.txt"));
|
||||||
|
assert!(actual.status.success());
|
||||||
|
|
||||||
|
// report errors if one path not exists
|
||||||
|
let actual = nu!(cwd: dirs.test(), "ls asdf f1.txt");
|
||||||
|
assert!(actual.err.contains("directory not found"));
|
||||||
|
assert!(!actual.status.success());
|
||||||
|
|
||||||
|
// ls with spreading empty list should returns nothing.
|
||||||
|
let actual = nu!(cwd: dirs.test(), "ls ...[] | length");
|
||||||
|
assert_eq!(actual.out, "0");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user