From 01103457558c57b04d29db98bf3000f88117862b Mon Sep 17 00:00:00 2001 From: Wind Date: Sat, 13 Apr 2024 23:03:17 +0800 Subject: [PATCH] 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. --- crates/nu-command/src/filesystem/du.rs | 187 +++++--- crates/nu-command/src/filesystem/ls.rs | 530 +++++++++++++---------- crates/nu-command/src/filesystem/util.rs | 31 -- crates/nu-command/tests/commands/du.rs | 20 +- crates/nu-command/tests/commands/ls.rs | 26 ++ 5 files changed, 462 insertions(+), 332 deletions(-) diff --git a/crates/nu-command/src/filesystem/du.rs b/crates/nu-command/src/filesystem/du.rs index f6a27031f7..5035c75b7b 100644 --- a/crates/nu-command/src/filesystem/du.rs +++ b/crates/nu-command/src/filesystem/du.rs @@ -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,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 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")?, - max_depth, - min_size, + 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) }; - 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, ¤t_dir, call.head, None), - // The * pattern should never fail. - 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()) + match paths { + None => { + let args = DuArgs { + 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()), + ) } - }); - - 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, - min: min_size, - deref, - exclude, - all, - }; - - let mut output: Vec = vec![]; - 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)); + 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())) } } - - Ok(output.into_pipeline_data(engine_state.ctrlc.clone())) } fn examples(&self) -> Vec { @@ -179,6 +159,75 @@ impl Command for Du { } } +fn du_for_one_pattern( + args: DuArgs, + current_dir: &Path, + call_span: Span, + ctrl_c: Option>, +) -> Result + 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 = 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)] mod tests { use super::Du; diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index c9facf624a..a8cddee37a 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -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,241 +93,55 @@ 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 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 - } + let args = Args { + all, + long, + short_names, + full_paths, + du, + directory, + use_mime_type, + call_span, }; - // 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(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 { + 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 { - let glob_options = MatchOptions { - recursive_match_hidden_dir: false, - ..Default::default() - }; - Some(glob_options) + Some(pattern_arg) }; - 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![]; - - Ok(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, - 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)), - } + 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(), + )?) } - _ => Some(Value::nothing(call_span)), - }) - .into_pipeline_data_with_metadata( - PipelineMetadata { - data_source: DataSource::Ls, - }, - engine_state.ctrlc.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 { @@ -365,6 +191,248 @@ impl Command for Ls { } } +fn ls_for_one_pattern( + pattern_arg: Option>, + args: Args, + ctrl_c: Option>, + cwd: PathBuf, +) -> Result + 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) -> bool { match dir.as_ref().read_dir() { Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied), diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index d2b68ac58b..1b755875bd 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -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>, 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) - } -} diff --git a/crates/nu-command/tests/commands/du.rs b/crates/nu-command/tests/commands/du.rs index 2b2e39b36c..b5ec265153 100644 --- a/crates/nu-command/tests/commands/du.rs +++ b/crates/nu-command/tests/commands/du.rs @@ -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"); +} diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index 4c0ba59d19..61afe355e5 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -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"); + }) +}