mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00: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
5 changed files with 462 additions and 332 deletions
|
@ -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, ¤t_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,
|
||||
¤t_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, ¤t_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(),
|
||||
},
|
||||
¤t_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, ¶ms, max_depth, engine_state.ctrlc.clone()).into(),
|
||||
);
|
||||
} else if let Ok(v) = FileInfo::new(a, deref, tag) {
|
||||
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, 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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue