From cadaef3f076fb0bebeb4b72813361b7e6aa5f186 Mon Sep 17 00:00:00 2001 From: sharkdp Date: Sun, 6 Dec 2020 15:57:33 +0100 Subject: [PATCH] Show error if pattern matches leading dot but --hidden is not given, closes #615 --- src/main.rs | 13 +++++++++++- src/regex_helper.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++ tests/tests.rs | 15 ++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index cf0f946..5208765 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ use crate::filetypes::FileTypes; use crate::filter::OwnerFilter; use crate::filter::{SizeFilter, TimeFilter}; use crate::options::Options; -use crate::regex_helper::pattern_has_uppercase_char; +use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_with_leading_dot}; // We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481 // FIXME: re-enable jemalloc on macOS, see comment in Cargo.toml file for more infos @@ -431,6 +431,17 @@ fn run() -> Result { }), }; + if cfg!(unix) + && config.ignore_hidden + && pattern_matches_strings_with_leading_dot(&pattern_regex) + { + return Err(anyhow!( + "The pattern seems to only match files with a leading dot, but hidden files are \ + filtered by default. Consider adding -H/--hidden to search hidden files as well \ + or adjust your search pattern." + )); + } + let re = RegexBuilder::new(&pattern_regex) .case_insensitive(!config.case_sensitive) .dot_matches_new_line(true) diff --git a/src/regex_helper.rs b/src/regex_helper.rs index 4cadd96..bc7f77a 100644 --- a/src/regex_helper.rs +++ b/src/regex_helper.rs @@ -34,6 +34,45 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool { } } +/// Determine if a regex pattern only matches strings starting with a literal dot (hidden files) +pub fn pattern_matches_strings_with_leading_dot(pattern: &str) -> bool { + let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build(); + + parser + .parse(pattern) + .map(|hir| hir_matches_strings_with_leading_dot(&hir)) + .unwrap_or(false) +} + +/// See above. +fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool { + use regex_syntax::hir::*; + + // Note: this only really detects the simplest case where a regex starts with + // "^\\.", i.e. a start text anchor and a literal dot character. There are a lot + // of other patterns that ONLY match hidden files, e.g. ^(\\.foo|\\.bar) which are + // not (yet) detected by this algorithm. + match *hir.kind() { + HirKind::Concat(ref hirs) => { + let mut hirs = hirs.iter(); + if let Some(hir) = hirs.next() { + if *hir.kind() != HirKind::Anchor(Anchor::StartText) { + return false; + } + } else { + return false; + } + + if let Some(hir) = hirs.next() { + *hir.kind() == HirKind::Literal(Literal::Unicode('.')) + } else { + false + } + } + _ => false, + } +} + #[test] fn pattern_has_uppercase_char_simple() { assert!(pattern_has_uppercase_char("A")); @@ -50,3 +89,12 @@ fn pattern_has_uppercase_char_advanced() { assert!(!pattern_has_uppercase_char(r"\Acargo")); assert!(!pattern_has_uppercase_char(r"carg\x6F")); } + +#[test] +fn matches_strings_with_leading_dot_simple() { + assert!(pattern_matches_strings_with_leading_dot("^\\.gitignore")); + + assert!(!pattern_matches_strings_with_leading_dot("^.gitignore")); + assert!(!pattern_matches_strings_with_leading_dot("\\.gitignore")); + assert!(!pattern_matches_strings_with_leading_dot("^gitignore")); +} diff --git a/tests/tests.rs b/tests/tests.rs index e67eb5b..f57541a 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1699,3 +1699,18 @@ fn test_number_parsing_errors() { te.assert_failure(&["--max-results=a"]); } + +/// Print error if search pattern starts with a dot and --hidden is not set +/// (Unix only, hidden files on Windows work differently) +#[test] +#[cfg(unix)] +fn test_error_if_hidden_not_set_and_pattern_starts_with_dot() { + let te = TestEnv::new(&[], &[".gitignore", ".whatever", "non-hidden"]); + + te.assert_failure(&["^\\.gitignore"]); + te.assert_failure(&["--glob", ".gitignore"]); + + te.assert_output(&["--hidden", "^\\.gitignore"], ".gitignore"); + te.assert_output(&["--hidden", "--glob", ".gitignore"], ".gitignore"); + te.assert_output(&[".gitignore"], ""); +}