rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs (ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r), fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like client of the process interface. search-test.nu: 42 cases match native tools.
178 lines
5.7 KiB
Rust
178 lines
5.7 KiB
Rust
use std::ffi::OsStr;
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
|
use memchr::memmem;
|
|
use regex::bytes::{RegexSet, RegexSetBuilder};
|
|
|
|
use crate::regex_helper::{self, pattern_matches_strings_with_leading_dot};
|
|
|
|
pub trait Matcher {
|
|
fn matches_path(&self, path: &OsStr) -> bool;
|
|
}
|
|
|
|
pub type Patterns = Box<dyn Matcher + Send + Sync>;
|
|
|
|
#[derive(Eq, PartialEq, Copy, Clone)]
|
|
pub enum PatternType {
|
|
Regex,
|
|
Fixed,
|
|
Glob,
|
|
Exact,
|
|
}
|
|
|
|
impl Matcher for RegexSet {
|
|
fn matches_path(&self, path: &OsStr) -> bool {
|
|
let haystack = path.as_encoded_bytes();
|
|
let matches = self.matches(haystack);
|
|
// Return true if the number of regexes that matched
|
|
// equals the total number of regexes.
|
|
matches.iter().count() == self.len()
|
|
}
|
|
}
|
|
|
|
impl Matcher for GlobSet {
|
|
fn matches_path(&self, path: &OsStr) -> bool {
|
|
self.matches_all(path)
|
|
}
|
|
}
|
|
|
|
/// In the common case a single glob, it is simpler, and
|
|
/// faster to just use a single Glob instead of a GlobSet
|
|
impl Matcher for GlobMatcher {
|
|
fn matches_path(&self, path: &OsStr) -> bool {
|
|
self.is_match(path)
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches fixed strings
|
|
pub struct FixedStrings(pub Vec<String>);
|
|
|
|
impl Matcher for FixedStrings {
|
|
fn matches_path(&self, path: &OsStr) -> bool {
|
|
let path = path.as_encoded_bytes();
|
|
self.0.iter().all(|f| bytes_contains(path, f.as_bytes()))
|
|
}
|
|
}
|
|
|
|
pub struct ExactStrings(pub Vec<String>);
|
|
|
|
impl Matcher for ExactStrings {
|
|
fn matches_path(&self, path: &OsStr) -> bool {
|
|
self.0.iter().all(|s| s.as_str() == path)
|
|
}
|
|
}
|
|
|
|
/// Matcher that matches everything
|
|
pub struct MatchAll;
|
|
impl Matcher for MatchAll {
|
|
fn matches_path(&self, _path: &OsStr) -> bool {
|
|
true
|
|
}
|
|
}
|
|
pub fn build_patterns(
|
|
mut patterns: Vec<String>,
|
|
pattern_type: PatternType,
|
|
ignore_case: bool,
|
|
) -> Result<Patterns> {
|
|
if patterns.is_empty() || is_empty_pattern(&patterns, pattern_type) {
|
|
return Ok(Box::new(MatchAll));
|
|
}
|
|
match pattern_type {
|
|
PatternType::Glob => build_glob_matcher(patterns, ignore_case),
|
|
PatternType::Fixed if !ignore_case => Ok(Box::new(FixedStrings(patterns))),
|
|
PatternType::Exact if !ignore_case => Ok(Box::new(ExactStrings(patterns))),
|
|
typ => {
|
|
// TODO: is there a better way we could handle case insensitive fixed strings?
|
|
if typ == PatternType::Fixed {
|
|
for pattern in patterns.iter_mut() {
|
|
*pattern = regex::escape(pattern);
|
|
}
|
|
} else if typ == PatternType::Exact {
|
|
// Would using eq_ignore_ascii case be good enough?
|
|
for pattern in patterns.iter_mut() {
|
|
// Anchor the escaped pattern so the full filename (or path) must match exactly.
|
|
// Literal. No substring matching.
|
|
*pattern = format!("^{}$", regex::escape(pattern));
|
|
}
|
|
}
|
|
Ok(Box::new(build_regex_matcher(patterns, ignore_case)?))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn patterns_match_strings_with_leading_dots(
|
|
patterns: &[String],
|
|
pattern_type: PatternType,
|
|
) -> bool {
|
|
let mut iter = patterns.iter();
|
|
match pattern_type {
|
|
PatternType::Regex => iter.any(|pat| pattern_matches_strings_with_leading_dot(pat)),
|
|
// fixed strings aren't anchored so always false
|
|
PatternType::Fixed => false,
|
|
// globs and exact just check if it starts with a .
|
|
PatternType::Glob | PatternType::Exact => patterns.iter().any(|pat| pat.starts_with(".")),
|
|
}
|
|
}
|
|
|
|
pub fn pattern_has_uppercase_char(pattern_type: PatternType, pattern: &str) -> bool {
|
|
match pattern_type {
|
|
PatternType::Regex => regex_helper::pattern_has_uppercase_char(pattern),
|
|
// For everything else it's sufficient to just check if there are any uppercase letters
|
|
_ => pattern.chars().any(char::is_uppercase),
|
|
}
|
|
}
|
|
|
|
fn build_glob_matcher(patterns: Vec<String>, ignore_case: bool) -> Result<Patterns> {
|
|
Ok(if patterns.len() == 1 {
|
|
Box::new(build_glob(&patterns[0], ignore_case)?.compile_matcher())
|
|
} else {
|
|
let mut builder = GlobSetBuilder::new();
|
|
for pat in patterns {
|
|
builder.add(build_glob(&pat, ignore_case)?);
|
|
}
|
|
Box::new(builder.build()?)
|
|
})
|
|
}
|
|
|
|
fn build_glob(pattern: &str, ignore_case: bool) -> Result<Glob> {
|
|
Ok(GlobBuilder::new(pattern)
|
|
.literal_separator(true)
|
|
.case_insensitive(ignore_case)
|
|
.build()?)
|
|
}
|
|
|
|
fn build_regex_matcher(patterns: Vec<String>, ignore_case: bool) -> Result<RegexSet> {
|
|
RegexSetBuilder::new(patterns)
|
|
.case_insensitive(ignore_case)
|
|
.dot_matches_new_line(true)
|
|
.build()
|
|
.map_err(|e| {
|
|
anyhow!(
|
|
"{}\n\nNote: You can search for literal substrings with '--fixed-strings' \
|
|
or literal strings with '--exact' options (instead of a regular expression). \
|
|
Alternatively, you can also use the '--glob' option to match on a glob pattern.",
|
|
e,
|
|
)
|
|
})
|
|
}
|
|
|
|
fn is_empty_pattern(patterns: &[String], pattern_type: PatternType) -> bool {
|
|
pattern_type != PatternType::Exact && patterns.len() == 1 && patterns[0].is_empty()
|
|
}
|
|
|
|
/// Test if the needle is a substring of the haystack
|
|
fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool {
|
|
memmem::find(haystack, needle).is_some()
|
|
}
|
|
|
|
#[test]
|
|
fn empty_matches_all() {
|
|
let patterns_none = build_patterns(Vec::new(), PatternType::Glob, false).unwrap();
|
|
let patterns_empty_str =
|
|
build_patterns(vec!["".to_string()], PatternType::Glob, false).unwrap();
|
|
|
|
assert!(patterns_none.matches_path("abcd".as_ref()));
|
|
assert!(patterns_empty_str.matches_path("abcd".as_ref()));
|
|
}
|