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