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:
Wind 2024-04-13 23:03:17 +08:00 committed by GitHub
parent 56cdee1fd8
commit 0110345755
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 462 additions and 332 deletions

View file

@ -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 nu_engine::{command_prelude::*, current_dir};
use nu_glob::Pattern;
use nu_protocol::NuGlob;
use serde::Deserialize;
use std::path::Path;
use std::sync::{atomic::AtomicBool, Arc};
#[derive(Clone)]
pub struct Du;
@ -33,7 +35,7 @@ impl Command for Du {
Signature::build("du")
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
.allow_variants_without_examples(true)
.optional(
.rest(
"path",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
"Starting directory.",
@ -93,17 +95,76 @@ 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 paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let paths = if call.rest_iter(0).count() == 0 {
None
} else {
Some(paths)
};
match paths {
None => {
let args = DuArgs {
path: opt_for_glob_pattern(engine_state, stack, call, 0)?,
all: call.has_flag(engine_state, stack, "all")?,
deref: call.has_flag(engine_state, stack, "deref")?,
exclude: call.get_flag(engine_state, stack, "exclude")?,
path: None,
all,
deref,
exclude,
max_depth,
min_size,
};
Ok(
du_for_one_pattern(args, &current_dir, tag, engine_state.ctrlc.clone())?
.into_pipeline_data(engine_state.ctrlc.clone()),
)
}
Some(paths) => {
let mut result_iters = vec![];
for p in paths {
let args = DuArgs {
path: Some(p),
all,
deref,
exclude: exclude.clone(),
max_depth,
min_size,
};
result_iters.push(du_for_one_pattern(
args,
&current_dir,
tag,
engine_state.ctrlc.clone(),
)?)
}
// chain all iterators on result.
Ok(result_iters
.into_iter()
.flatten()
.into_pipeline_data(engine_state.ctrlc.clone()))
}
}
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Disk usage of the current directory",
example: "du",
result: None,
}]
}
}
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)
@ -115,15 +176,15 @@ impl Command for Du {
let include_files = args.all;
let mut paths = match args.path {
Some(p) => nu_engine::glob_from(&p, &current_dir, call.head, None),
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.head,
current_dir,
call_span,
None,
),
}
@ -142,7 +203,7 @@ impl Command for Du {
let min_size = args.min_size.map(|f| f.item as u64);
let params = DirBuilder {
tag,
tag: call_span,
min: min_size,
deref,
exclude,
@ -154,29 +215,17 @@ impl Command for Du {
match p {
Ok(a) => {
if a.is_dir() {
output.push(
DirInfo::new(a, &params, max_depth, engine_state.ctrlc.clone()).into(),
);
} else if let Ok(v) = FileInfo::new(a, deref, tag) {
output.push(DirInfo::new(a, &params, 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, tag));
output.push(Value::error(e, call_span));
}
}
}
Ok(output.into_pipeline_data(engine_state.ctrlc.clone()))
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Disk usage of the current directory",
example: "du",
result: None,
}]
}
Ok(output.into_iter())
}
#[cfg(test)]

View file

@ -1,4 +1,4 @@
use super::util::opt_for_glob_pattern;
use super::util::get_rest_for_glob_pattern;
use crate::{DirBuilder, DirInfo};
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
use nu_engine::{command_prelude::*, env::current_dir};
@ -18,6 +18,18 @@ use std::{
#[derive(Clone)]
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 {
fn name(&self) -> &str {
"ls"
@ -36,7 +48,7 @@ impl Command for Ls {
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
// LsGlobPattern is similar to string, it won't auto-expand
// 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(
"long",
@ -81,7 +93,120 @@ impl Command for Ls {
let call_span = call.head;
let cwd = current_dir(engine_state, stack)?;
let pattern_arg = opt_for_glob_pattern(engine_state, stack, call, 0)?;
let args = Args {
all,
long,
short_names,
full_paths,
du,
directory,
use_mime_type,
call_span,
};
let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let input_pattern_arg = if call.rest_iter(0).count() == 0 {
None
} else {
Some(pattern_arg)
};
match input_pattern_arg {
None => Ok(ls_for_one_pattern(None, args, ctrl_c.clone(), cwd)?
.into_pipeline_data_with_metadata(
PipelineMetadata {
data_source: DataSource::Ls,
},
ctrl_c,
)),
Some(pattern) => {
let mut result_iters = vec![];
for pat in pattern {
result_iters.push(ls_for_one_pattern(
Some(pat),
args,
ctrl_c.clone(),
cwd.clone(),
)?)
}
// Here nushell needs to use
// use `flatten` to chain all iterators into one.
Ok(result_iters
.into_iter()
.flatten()
.into_pipeline_data_with_metadata(
PipelineMetadata {
data_source: DataSource::Ls,
},
ctrl_c,
))
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "List visible files in the current directory",
example: "ls",
result: None,
},
Example {
description: "List visible files in a subdirectory",
example: "ls subdir",
result: None,
},
Example {
description: "List visible files with full path in the parent directory",
example: "ls -f ..",
result: None,
},
Example {
description: "List Rust files",
example: "ls *.rs",
result: None,
},
Example {
description: "List files and directories whose name do not contain 'bar'",
example: "ls -s | where name !~ bar",
result: None,
},
Example {
description: "List all dirs in your home directory",
example: "ls -a ~ | where type == dir",
result: None,
},
Example {
description:
"List all dirs in your home directory which have not been modified in 7 days",
example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
result: None,
},
Example {
description: "List given paths and show directories themselves",
example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",
result: None,
},
]
}
}
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.
@ -125,9 +250,7 @@ impl Command for Ls {
"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"
)
.expect("this shouldn't be called since we already know there is a dir")
.permissions()
.mode()
& 0o0777
@ -143,7 +266,7 @@ impl Command for Ls {
});
}
if is_empty_dir(&expanded) {
return Ok(Value::list(vec![], call_span).into_pipeline_data());
return Ok(Box::new(vec![].into_iter()));
}
extra_star_under_given_directory = true;
}
@ -154,8 +277,7 @@ impl Command for Ls {
// 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());
|| (pat.item.is_expand() && expand_to_real_path(pat.item.as_ref()).is_absolute());
(
expanded,
p_tag,
@ -167,8 +289,8 @@ impl Command for Ls {
// 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 if is_empty_dir(&cwd) {
return Ok(Box::new(vec![].into_iter()));
} else {
(PathBuf::from("*"), call_span, false, false)
}
@ -226,8 +348,8 @@ impl Command for Ls {
let mut hidden_dirs = vec![];
Ok(paths_peek
.filter_map(move |x| match x {
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),
@ -252,8 +374,7 @@ impl Command for Ls {
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 = 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()
@ -297,7 +418,7 @@ impl Command for Ls {
call_span,
long,
du,
ctrl_c.clone(),
one_ctrl_c.clone(),
use_mime_type,
);
match entry {
@ -309,60 +430,7 @@ impl Command for Ls {
}
}
_ => Some(Value::nothing(call_span)),
})
.into_pipeline_data_with_metadata(
PipelineMetadata {
data_source: DataSource::Ls,
},
engine_state.ctrlc.clone(),
))
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "List visible files in the current directory",
example: "ls",
result: None,
},
Example {
description: "List visible files in a subdirectory",
example: "ls subdir",
result: None,
},
Example {
description: "List visible files with full path in the parent directory",
example: "ls -f ..",
result: None,
},
Example {
description: "List Rust files",
example: "ls *.rs",
result: None,
},
Example {
description: "List files and directories whose name do not contain 'bar'",
example: "ls -s | where name !~ bar",
result: None,
},
Example {
description: "List all dirs in your home directory",
example: "ls -a ~ | where type == dir",
result: None,
},
Example {
description:
"List all dirs in your home directory which have not been modified in 7 days",
example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
result: None,
},
Example {
description: "List given paths and show directories themselves",
example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",
result: None,
},
]
}
})))
}
fn permission_denied(dir: impl AsRef<Path>) -> bool {

View file

@ -131,34 +131,3 @@ pub fn get_rest_for_glob_pattern(
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)
}
}

View file

@ -50,7 +50,7 @@ fn test_du_flag_max_depth() {
#[case("a[bc]d")]
#[case("a][c")]
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)]);
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) {
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");
}

View file

@ -733,3 +733,29 @@ fn list_with_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");
})
}