diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-01-27 06:02:59 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-01-27 06:02:59 +0800 |
| commit | 4eef9ce364bb660421a96052a3fb126a33b22c63 (patch) | |
| tree | a36947411d83205dc743881cd2a30d8c907d4b57 /utils/src/globber.rs | |
| parent | 243d521fd19af169910506529e737a797e9bc583 (diff) | |
Extract CLI utilities into a separate crate
Diffstat (limited to 'utils/src/globber.rs')
| -rw-r--r-- | utils/src/globber.rs | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/utils/src/globber.rs b/utils/src/globber.rs new file mode 100644 index 0000000..e20caf9 --- /dev/null +++ b/utils/src/globber.rs @@ -0,0 +1,276 @@ +use std::{io::Error, path::PathBuf, str::FromStr}; + +use just_enough_vcs::utils::string_proc::format_path::format_path_str; + +use crate::globber::constants::{SPLIT_STR, get_base_dir_current}; + +pub struct Globber { + pattern: String, + base: PathBuf, + names: Vec<String>, +} + +#[allow(dead_code)] +impl Globber { + pub fn new(pattern: String, base: PathBuf) -> Self { + Self { + pattern, + base, + names: Vec::new(), + } + } + + pub fn names(&self) -> Vec<&String> { + self.names.iter().collect() + } + + pub fn base(&self) -> &PathBuf { + &self.base + } + + pub fn into_names(self) -> Vec<String> { + self.names + } + + pub fn paths(&self) -> Vec<PathBuf> { + self.names.iter().map(|n| self.base.join(n)).collect() + } + + pub fn glob<F>(mut self, get_names: F) -> Result<Self, std::io::Error> + where + F: Fn(PathBuf) -> Vec<GlobItem>, + { + let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern); + + let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) { + let (path_part, pattern_part) = full_path.split_at(last_split); + let mut path = path_part.to_string(); + if !path.ends_with(SPLIT_STR) { + path.push_str(SPLIT_STR); + } + ( + format_path_str(path)?, + pattern_part[SPLIT_STR.len()..].to_string(), + ) + } else { + (String::default(), full_path) + }; + + self.base = match PathBuf::from_str(&path) { + Ok(r) => r, + Err(_) => { + return Err(Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid path: \"{}\"", &path), + )); + } + }; + + let pattern = if pattern.is_empty() { + "*".to_string() + } else if pattern == "." { + "*".to_string() + } else if pattern.ends_with(SPLIT_STR) { + format!("{}*", pattern) + } else { + pattern + }; + + if !pattern.contains('*') && !pattern.contains('?') { + self.names = vec![pattern]; + return Ok(self); + } + + let mut collected = Vec::new(); + + collect_files(&path.into(), "./".to_string(), &mut collected, &get_names); + fn collect_files<F>( + base: &PathBuf, + current: String, + file_names: &mut Vec<String>, + get_names: &F, + ) where + F: Fn(PathBuf) -> Vec<GlobItem>, + { + let current_path = if current.is_empty() { + base.clone() + } else { + base.join(¤t) + }; + + let items = get_names(current_path); + for item in items { + match item { + GlobItem::File(file_name) => { + let relative_path = { + format_path_str(format!("{}{}{}", current, SPLIT_STR, file_name)) + .unwrap_or_default() + }; + file_names.push(relative_path) + } + GlobItem::Directory(dir_name) => { + let new_current = { + format_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name)) + .unwrap_or_default() + }; + collect_files(base, new_current, file_names, get_names); + } + } + } + } + + self.names = collected + .iter() + .filter_map(|name| match_pattern(name, &pattern)) + .collect(); + + Ok(self) + } +} + +fn match_pattern(name: &str, pattern: &str) -> Option<String> { + if pattern.is_empty() { + return None; + } + + let name_chars: Vec<char> = name.chars().collect(); + let pattern_chars: Vec<char> = pattern.chars().collect(); + + let mut name_idx = 0; + let mut pattern_idx = 0; + let mut star_idx = -1; + let mut match_idx = -1; + + while name_idx < name_chars.len() { + if pattern_idx < pattern_chars.len() + && (pattern_chars[pattern_idx] == '?' + || pattern_chars[pattern_idx] == name_chars[name_idx]) + { + name_idx += 1; + pattern_idx += 1; + } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { + star_idx = pattern_idx as i32; + match_idx = name_idx as i32; + pattern_idx += 1; + } else if star_idx != -1 { + pattern_idx = (star_idx + 1) as usize; + match_idx += 1; + name_idx = match_idx as usize; + } else { + return None; + } + } + + while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { + pattern_idx += 1; + } + + if pattern_idx == pattern_chars.len() { + Some(name.to_string()) + } else { + None + } +} + +impl<T: AsRef<str>> From<T> for Globber { + fn from(pattern: T) -> Self { + let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string()); + Self::new(pattern, base_dir) + } +} + +#[derive(Debug, Clone, Hash)] +pub enum GlobItem { + File(String), + Directory(String), +} + +impl PartialEq for GlobItem { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (GlobItem::File(a), GlobItem::File(b)) => a == b, + (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b, + _ => false, + } + } +} + +impl std::fmt::Display for GlobItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GlobItem::File(name) => write!(f, "{}", name), + GlobItem::Directory(name) => write!(f, "{}", name), + } + } +} + +impl Eq for GlobItem {} + +pub mod constants { + use std::{env::current_dir, path::PathBuf}; + + #[cfg(unix)] + pub(crate) const CURRENT_DIR_PREFIX: &str = "./"; + #[cfg(windows)] + pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\"; + + #[cfg(unix)] + pub(crate) const USER_DIR_PREFIX: &str = "~"; + #[cfg(windows)] + pub(crate) const USER_DIR_PREFIX: &str = "~\\"; + + #[cfg(unix)] + pub(crate) const ROOT_DIR_PREFIX: &str = "/"; + #[cfg(windows)] + pub(crate) const ROOT_DIR_PREFIX: &str = "\\"; + + #[cfg(unix)] + pub(crate) const SPLIT_STR: &str = "/"; + #[cfg(windows)] + pub(crate) const SPLIT_STR: &str = "\\"; + + pub fn get_base_dir_current(input: String) -> (PathBuf, String) { + get_base_dir(input, current_dir().unwrap_or_default()) + } + + pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) { + if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) { + (current_dir, remaining.to_string()) + } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) { + (dirs::home_dir().unwrap_or_default(), remaining.to_string()) + } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) { + { + #[cfg(unix)] + { + (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string()) + } + #[cfg(windows)] + { + let current_drive = current_dir + .components() + .find_map(|comp| { + if let std::path::Component::Prefix(prefix_component) = comp { + Some(prefix_component) + } else { + None + } + }) + .and_then(|prefix_component| match prefix_component.kind() { + std::path::Prefix::Disk(drive_letter) + | std::path::Prefix::VerbatimDisk(drive_letter) => { + Some((drive_letter as char).to_string()) + } + _ => None, + }) + .unwrap_or_else(|| "C".to_string()); + ( + PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)), + remaining.to_string(), + ) + } + } + } else { + (current_dir, input) + } + } +} |
