Files
sausage/userland/fd/src/patterns.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
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.
2026-11-06 17:21:51 +00:00

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()));
}