browser_automation_cli/
find_paths.rs1use std::path::{Path, PathBuf};
13
14use ignore::WalkBuilder;
15use rayon::prelude::*;
16use regex::RegexBuilder;
17use serde_json::{json, Value};
18
19use crate::error::{CliError, ErrorKind};
20
21#[derive(Debug, Clone)]
23pub struct FindPathsOpts {
24 pub pattern: String,
26 pub roots: Vec<PathBuf>,
28 pub extension: Option<String>,
30 pub hidden: bool,
32 pub no_ignore: bool,
34 pub max_depth: Option<usize>,
36 pub entry_type: Option<String>,
38 pub limit: usize,
40 pub glob: Option<String>,
42}
43
44impl Default for FindPathsOpts {
45 fn default() -> Self {
46 Self {
47 pattern: String::new(),
48 roots: vec![PathBuf::from(".")],
49 extension: None,
50 hidden: false,
51 no_ignore: false,
52 max_depth: None,
53 entry_type: None,
54 limit: 10_000,
55 glob: None,
56 }
57 }
58}
59
60pub fn find_paths(opts: &FindPathsOpts) -> Result<Value, CliError> {
62 crate::concurrency::install_rayon_pool_once();
63 let roots = if opts.roots.is_empty() {
64 vec![PathBuf::from(".")]
65 } else {
66 opts.roots.clone()
67 };
68 let re = if opts.pattern.is_empty() {
69 None
70 } else {
71 Some(
72 RegexBuilder::new(&opts.pattern)
73 .case_insensitive(true)
74 .build()
75 .map_err(|e| {
76 CliError::new(ErrorKind::Usage, format!("invalid find-paths pattern: {e}"))
77 })?,
78 )
79 };
80 let walk_threads = crate::concurrency::walk_threads();
81 let walk_one = |root: &PathBuf| -> Vec<String> {
84 let mut builder = WalkBuilder::new(root);
85 builder.hidden(!opts.hidden);
86 builder.git_ignore(!opts.no_ignore);
87 builder.git_global(!opts.no_ignore);
88 builder.git_exclude(!opts.no_ignore);
89 builder.ignore(!opts.no_ignore);
90 if let Some(d) = opts.max_depth {
91 builder.max_depth(Some(d));
92 }
93 builder.threads(walk_threads);
94 let mut local = Vec::new();
95 for entry in builder.build() {
96 if opts.limit > 0 && local.len() >= opts.limit {
97 break;
98 }
99 let entry = match entry {
100 Ok(e) => e,
101 Err(_) => continue,
102 };
103 let ft = entry.file_type();
104 let is_file = ft.map(|t| t.is_file()).unwrap_or(false);
105 let is_dir = ft.map(|t| t.is_dir()).unwrap_or(false);
106 if let Some(ref t) = opts.entry_type {
107 match t.as_str() {
108 "f" | "file" if !is_file => continue,
109 "d" | "dir" | "directory" if !is_dir => continue,
110 _ => {}
111 }
112 }
113 let path = entry.path();
114 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
115 if let Some(ref ext) = opts.extension {
116 let e = path.extension().and_then(|x| x.to_str()).unwrap_or("");
117 if !e.eq_ignore_ascii_case(ext.trim_start_matches('.')) {
118 continue;
119 }
120 }
121 if let Some(ref re) = re {
122 if !re.is_match(name) && !re.is_match(&path.to_string_lossy()) {
123 continue;
124 }
125 }
126 if let Some(ref g) = opts.glob {
127 if !glob_match(g, path) {
128 continue;
129 }
130 }
131 local.push(path.display().to_string());
132 }
133 local
134 };
135 let mut paths: Vec<String> = if roots.len() <= 1 {
136 roots.iter().flat_map(walk_one).collect()
137 } else {
138 roots.par_iter().flat_map(walk_one).collect()
139 };
140 if opts.limit > 0 && paths.len() > opts.limit {
141 paths.truncate(opts.limit);
142 }
143 Ok(json!({
144 "count": paths.len(),
145 "paths": paths,
146 "pattern": opts.pattern,
147 "glob": opts.glob,
148 "engine": "ignore",
149 "chrome": false,
150 "walk_threads": walk_threads,
151 "roots_parallel": roots.len() > 1,
152 "concurrency": crate::concurrency::effective_limit(),
153 }))
154}
155
156fn glob_match(pattern: &str, path: &Path) -> bool {
159 let path_s = path.to_string_lossy();
160 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
161 if glob_match_str(pattern, &path_s) {
163 return true;
164 }
165 if !pattern.contains('/') {
166 return glob_match_str(pattern, name);
167 }
168 false
169}
170
171fn glob_match_str(pattern: &str, text: &str) -> bool {
172 let mut re = String::from("(?i)^");
174 let mut chars = pattern.chars().peekable();
175 while let Some(c) = chars.next() {
176 match c {
177 '*' => {
178 if chars.peek() == Some(&'*') {
179 chars.next();
180 re.push_str(".*");
182 } else {
183 re.push_str("[^/]*");
184 }
185 }
186 '?' => re.push_str("[^/]"),
187 '.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' => {
188 re.push('\\');
189 re.push(c);
190 }
191 other => re.push(other),
192 }
193 }
194 re.push('$');
195 RegexBuilder::new(&re)
196 .case_insensitive(true)
197 .build()
198 .map(|r| r.is_match(text))
199 .unwrap_or(false)
200}
201
202pub fn roots_from(paths: &[String]) -> Vec<PathBuf> {
204 if paths.is_empty() {
205 vec![PathBuf::from(".")]
206 } else {
207 paths.iter().map(|p| Path::new(p).to_path_buf()).collect()
208 }
209}