diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs index fbf7b7dd16..5ea92f7cb2 100644 --- a/crates/nu-command/src/filesystem/cp.rs +++ b/crates/nu-command/src/filesystem/cp.rs @@ -19,6 +19,7 @@ const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; #[derive(Clone)] diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 0210cb691a..a20d50ba1b 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -3,6 +3,7 @@ use crate::DirInfo; use chrono::{DateTime, Local, LocalResult, TimeZone, Utc}; 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}; @@ -128,7 +129,15 @@ impl Command for Ls { item: path.display().to_string(), span: p_tag, }; - let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span)?; + + 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)?; let mut paths_peek = paths.peekable(); if paths_peek.peek().is_none() { diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs index 3672a71c78..1ddd3dc828 100644 --- a/crates/nu-command/src/filesystem/mv.rs +++ b/crates/nu-command/src/filesystem/mv.rs @@ -14,6 +14,7 @@ const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; #[derive(Clone)] diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index 4a46046bb5..d7dc6db84d 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -24,6 +24,7 @@ const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; #[derive(Clone)] diff --git a/crates/nu-command/src/platform/du.rs b/crates/nu-command/src/platform/du.rs index 926b3151a5..a5a1640d39 100644 --- a/crates/nu-command/src/platform/du.rs +++ b/crates/nu-command/src/platform/du.rs @@ -14,6 +14,7 @@ const GLOB_PARAMS: MatchOptions = MatchOptions { case_sensitive: true, require_literal_separator: true, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; #[derive(Clone)] diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 1decd69243..14c02806a3 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -417,7 +417,9 @@ impl ExternalCommand { let cwd = PathBuf::from(cwd); if arg.item.contains('*') { - if let Ok((prefix, matches)) = nu_engine::glob_from(&arg, &cwd, self.name.span) { + if let Ok((prefix, matches)) = + nu_engine::glob_from(&arg, &cwd, self.name.span, None) + { let matches: Vec<_> = matches.collect(); // FIXME: do we want to special-case this further? We might accidentally expand when they don't diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index e9f13cccb7..97c1d183eb 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -244,6 +244,42 @@ fn lists_all_hidden_files_when_glob_does_not_contain_dot() { }) } +#[test] +// TODO Remove this cfg value when we have an OS-agnostic way +// of creating hidden files using the playground. +#[cfg(unix)] +fn glob_with_hidden_directory() { + Playground::setup("ls_test_8", |dirs, sandbox| { + sandbox.within(".dir_b").with_files(vec![ + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + EmptyFile(".dotfile3"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls **/* + | length + "# + )); + + assert_eq!(actual.out, ""); + assert!(actual.err.contains("No matches found")); + + // will list files if provide `-a` flag. + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls -a **/* + | length + "# + )); + + assert_eq!(actual.out, "4"); + }) +} + #[test] #[cfg(unix)] fn fails_with_ls_to_dir_without_permission() { diff --git a/crates/nu-engine/src/glob_from.rs b/crates/nu-engine/src/glob_from.rs index cce050a49a..2025c1d8ae 100644 --- a/crates/nu-engine/src/glob_from.rs +++ b/crates/nu-engine/src/glob_from.rs @@ -2,6 +2,7 @@ use std::os::unix::fs::PermissionsExt; use std::path::{Component, Path, PathBuf}; +use nu_glob::MatchOptions; use nu_path::{canonicalize_with, expand_path_with}; use nu_protocol::{ShellError, Span, Spanned}; @@ -17,6 +18,7 @@ pub fn glob_from( pattern: &Spanned, cwd: &Path, span: Span, + options: Option, ) -> Result< ( Option, @@ -82,8 +84,9 @@ pub fn glob_from( }; let pattern = pattern.to_string_lossy().to_string(); + let glob_options = options.unwrap_or_else(MatchOptions::new); - let glob = nu_glob::glob(&pattern).map_err(|err| { + let glob = nu_glob::glob_with(&pattern, glob_options).map_err(|err| { nu_protocol::ShellError::GenericError( "Error extracting glob pattern".into(), err.to_string(), diff --git a/crates/nu-glob/src/lib.rs b/crates/nu-glob/src/lib.rs index aa9cf351a7..5069cd4b77 100644 --- a/crates/nu-glob/src/lib.rs +++ b/crates/nu-glob/src/lib.rs @@ -49,6 +49,7 @@ //! case_sensitive: false, //! require_literal_separator: false, //! require_literal_leading_dot: false, +//! recursive_match_hidden_dir: true, //! }; //! for entry in glob_with("local/*a*", options).unwrap() { //! if let Ok(path) = entry { @@ -376,7 +377,16 @@ impl Iterator for Paths { } if is_dir(&path) { - // the path is a directory, so it's a match + // the path is a directory, check if matched according + // to `hidden_dir_recursive` option. + if !self.options.recursive_match_hidden_dir + && path + .file_name() + .map(|name| name.to_string_lossy().starts_with('.')) + .unwrap_or(false) + { + continue; + } // push this directory's contents fill_todo( @@ -872,6 +882,10 @@ pub struct MatchOptions { /// conventionally considered hidden on Unix systems and it might be /// desirable to skip them when listing files. pub require_literal_leading_dot: bool, + + /// if given pattern contains `**`, this flag check if `**` matches hidden directory. + /// For example: if true, `**` will match `.abcdef/ghi`. + pub recursive_match_hidden_dir: bool, } impl MatchOptions { @@ -886,6 +900,7 @@ impl MatchOptions { /// case_sensitive: true, /// require_literal_separator: false, /// require_literal_leading_dot: false + /// recursive_match_hidden_dir: true, /// } /// ``` pub fn new() -> Self { @@ -893,6 +908,7 @@ impl MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, } } } @@ -1084,6 +1100,7 @@ mod test { case_sensitive: false, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; assert!(pat.matches_with("aBcDeFg", options)); @@ -1098,11 +1115,13 @@ mod test { case_sensitive: true, require_literal_separator: true, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; let options_not_require_literal = MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; assert!(Pattern::new("abc/def") @@ -1132,11 +1151,13 @@ mod test { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: true, + recursive_match_hidden_dir: true, }; let options_not_require_literal_leading_dot = MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, + recursive_match_hidden_dir: true, }; let f = |options| {