use glob_match::glob_match;
use std::iter;
use std::path::{Path, PathBuf};
use crate::GlobEntry;
pub fn fast_glob(
patterns: &Vec<GlobEntry>,
) -> Result<impl iter::Iterator<Item = PathBuf>, std::io::Error> {
Ok(get_fast_patterns(patterns)
.into_iter()
.flat_map(|(base_path, patterns)| {
globwalk::GlobWalkerBuilder::from_patterns(base_path, &patterns)
.follow_links(true)
.build()
.unwrap()
.filter_map(Result::ok)
.map(|file| file.path().to_path_buf())
}))
}
pub fn get_fast_patterns(patterns: &Vec<GlobEntry>) -> Vec<(PathBuf, Vec<String>)> {
let mut optimized_patterns: Vec<(PathBuf, Vec<String>)> = vec![];
for pattern in patterns {
let base_path = PathBuf::from(&pattern.base);
let pattern = &pattern.pattern;
let is_negated = pattern.starts_with('!');
let mut pattern = pattern.clone();
if is_negated {
pattern.remove(0);
}
let mut folders = pattern.split('/').collect::<Vec<_>>();
if folders.len() <= 1 {
optimized_patterns.push((base_path, vec![pattern]));
} else {
let file_pattern = folders.pop().unwrap();
let all_folders = folders.clone();
let mut temp_paths = vec![base_path];
let mut bail = false;
for (i, folder) in folders.into_iter().enumerate() {
if folder.contains('*') {
let mut remaining_folders = all_folders[i..].to_vec();
remaining_folders.push(file_pattern);
let pattern = remaining_folders.join("/");
for path in &temp_paths {
optimized_patterns.push((path.to_path_buf(), vec![pattern.to_string()]));
}
bail = true;
break;
}
if folder.contains('{') && folder.contains('}') {
let branches = expand_braces(folder);
let existing_paths = temp_paths;
temp_paths = branches
.iter()
.flat_map(|branch| {
existing_paths
.clone()
.into_iter()
.map(|path| path.join(branch))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
}
else {
temp_paths = temp_paths
.into_iter()
.map(|path| path.join(folder))
.collect();
}
}
if !bail {
for path in &temp_paths {
optimized_patterns.push((path.to_path_buf(), vec![file_pattern.to_string()]));
}
}
}
if is_negated {
for (_, patterns) in &mut optimized_patterns {
for pattern in patterns {
pattern.insert(0, '!');
}
}
}
}
optimized_patterns
}
pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool {
let path = path.to_string_lossy();
globs
.iter()
.any(|g| glob_match(&format!("{}/{}", g.base, g.pattern), &path))
}
fn expand_braces(input: &str) -> Vec<String> {
let mut result: Vec<String> = vec![];
let mut in_braces = false;
let mut last_char: char = '\0';
let mut current = String::new();
let mut template: Vec<String> = vec![];
let mut branches: Vec<Vec<String>> = vec![];
for (i, c) in input.char_indices() {
let is_escaped = i > 0 && last_char == '\\';
last_char = c;
match c {
'{' if !is_escaped => {
if template.is_empty() {
template.push(String::new());
}
in_braces = true;
branches.push(vec![]);
template.push(String::new());
}
'}' if !is_escaped => {
in_braces = false;
if let Some(last) = branches.last_mut() {
last.push(current.clone());
}
current.clear();
}
',' if !is_escaped && in_braces => {
if let Some(last) = branches.last_mut() {
last.push(current.clone());
}
current.clear();
}
_ if in_braces => current.push(c),
_ => {
if template.is_empty() {
template.push(String::new());
}
if let Some(last) = template.last_mut() {
last.push(c);
}
}
};
}
if !template.is_empty() && !branches.is_empty() {
result.push("".to_string());
}
for (i, template) in template.into_iter().enumerate() {
result = result.into_iter().map(|x| x + &template).collect();
if let Some(branches) = branches.get(i) {
result = branches
.iter()
.flat_map(|branch| {
result
.clone()
.into_iter()
.map(|x| x + branch)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
}
}
result
}
#[cfg(test)]
mod tests {
use super::get_fast_patterns;
use crate::GlobEntry;
use std::path::PathBuf;
#[test]
fn it_should_keep_globs_that_start_with_file_wildcards_as_is() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "*.html".to_string(),
}]);
let expected = vec![(PathBuf::from("/projects"), vec!["*.html".to_string()])];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_keep_globs_that_start_with_folder_wildcards_as_is() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "**/*.html".to_string(),
}]);
let expected = vec![(PathBuf::from("/projects"), vec!["**/*.html".to_string()])];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_move_the_starting_folder_to_the_path() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "example/*.html".to_string(),
}]);
let expected = vec![(
PathBuf::from("/projects/example"),
vec!["*.html".to_string()],
)];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_move_the_starting_folders_to_the_path() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "example/other/*.html".to_string(),
}]);
let expected = vec![(
PathBuf::from("/projects/example/other"),
vec!["*.html".to_string()],
)];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_branch_expandable_folders() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "{foo,bar}/*.html".to_string(),
}]);
let expected = vec![
(PathBuf::from("/projects/foo"), vec!["*.html".to_string()]),
(PathBuf::from("/projects/bar"), vec!["*.html".to_string()]),
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_expand_multiple_expansions_in_the_same_folder() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "a-{b,c}-d-{e,f}-g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a-b-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-b-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-d-f-g"),
vec!["*.html".to_string()],
),
];
assert_eq!(actual, expected,);
}
#[test]
fn multiple_expansions_per_folder_starting_at_the_root() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a-c-d-f/b-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-d-f/b-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-e-f/b-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-e-f/b-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-d-f/c-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-d-f/c-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-e-f/c-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-e-f/c-d-e-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-d-f/b-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-d-f/b-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-e-f/b-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-e-f/b-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-d-f/c-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-d-f/c-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a-c-e-f/c-d-f-g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/b-c-e-f/c-d-f-g"),
vec!["*.html".to_string()],
),
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_stop_expanding_once_we_hit_a_wildcard() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "{foo,bar}/example/**/{baz,qux}/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/foo/example"),
vec!["**/{baz,qux}/*.html".to_string()],
),
(
PathBuf::from("/projects/bar/example"),
vec!["**/{baz,qux}/*.html".to_string()],
),
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_keep_the_negation_symbol_for_all_new_patterns() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "!{foo,bar}/*.html".to_string(),
}]);
let expected = vec![
(PathBuf::from("/projects/foo"), vec!["!*.html".to_string()]),
(PathBuf::from("/projects/bar"), vec!["!*.html".to_string()]),
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_expand_a_complex_example() {
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
pattern: "a/{b,c}/d/{e,f}/g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a/b/d/e/g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a/c/d/e/g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a/b/d/f/g"),
vec!["*.html".to_string()],
),
(
PathBuf::from("/projects/a/c/d/f/g"),
vec!["*.html".to_string()],
),
];
assert_eq!(actual, expected,);
}
}