slash_lib/builtins/
find.rs1use std::path::PathBuf;
2
3use slash_lang::parser::ast::Arg;
4
5use crate::command::{MethodDef, SlashCommand};
6use crate::executor::{CommandOutput, ExecutionError, PipeValue};
7
8pub struct Find;
15
16impl SlashCommand for Find {
17 fn name(&self) -> &str {
18 "find"
19 }
20
21 fn methods(&self) -> &[MethodDef] {
22 static METHODS: [MethodDef; 1] = [MethodDef::with_value("content")];
23 &METHODS
24 }
25
26 fn execute(
27 &self,
28 primary: Option<&str>,
29 args: &[Arg],
30 _input: Option<&PipeValue>,
31 ) -> Result<CommandOutput, ExecutionError> {
32 let pattern = primary.ok_or_else(|| {
33 ExecutionError::Runner("/find requires a pattern: /find(src/**/*.rs)".into())
34 })?;
35
36 let content_filter = args
37 .iter()
38 .find(|a| a.name == "content")
39 .and_then(|a| a.value.as_deref());
40
41 let paths = glob_walk(pattern)?;
42
43 let result = if let Some(needle) = content_filter {
44 let mut matches = Vec::new();
46 for path in &paths {
47 if let Ok(content) = std::fs::read_to_string(path) {
48 for (line_num, line) in content.lines().enumerate() {
49 if line.contains(needle) {
50 matches.push(format!("{}:{}:{}", path.display(), line_num + 1, line));
51 }
52 }
53 }
54 }
55 matches.join("\n")
56 } else {
57 paths
59 .iter()
60 .map(|p| p.display().to_string())
61 .collect::<Vec<_>>()
62 .join("\n")
63 };
64
65 if result.is_empty() {
66 return Ok(CommandOutput {
67 stdout: None,
68 stderr: None,
69 success: true,
70 });
71 }
72
73 let mut out = result;
74 out.push('\n');
75 Ok(CommandOutput {
76 stdout: Some(out.into_bytes()),
77 stderr: None,
78 success: true,
79 })
80 }
81}
82
83fn glob_walk(pattern: &str) -> Result<Vec<PathBuf>, ExecutionError> {
88 let mut results = Vec::new();
89 let parts: Vec<&str> = pattern.split('/').collect();
90 walk_recursive(&PathBuf::from("."), &parts, 0, &mut results);
91 results.sort();
92 Ok(results)
93}
94
95fn walk_recursive(dir: &PathBuf, parts: &[&str], depth: usize, results: &mut Vec<PathBuf>) {
96 if depth >= parts.len() {
97 return;
98 }
99
100 let part = parts[depth];
101 let is_last = depth == parts.len() - 1;
102
103 if part == "**" {
104 walk_recursive(dir, parts, depth + 1, results);
107
108 if let Ok(entries) = std::fs::read_dir(dir) {
110 for entry in entries.flatten() {
111 let path = entry.path();
112 if path.is_dir() {
113 walk_recursive(&path, parts, depth, results);
114 }
115 }
116 }
117 } else {
118 if let Ok(entries) = std::fs::read_dir(dir) {
120 for entry in entries.flatten() {
121 let name = entry.file_name();
122 let name_str = name.to_string_lossy();
123 if glob_matches(part, &name_str) {
124 let path = entry.path();
125 if is_last {
126 if path.is_file() {
127 results.push(path);
128 }
129 } else if path.is_dir() {
130 walk_recursive(&path, parts, depth + 1, results);
131 }
132 }
133 }
134 }
135 }
136}
137
138fn glob_matches(pattern: &str, name: &str) -> bool {
140 let p: Vec<char> = pattern.chars().collect();
141 let n: Vec<char> = name.chars().collect();
142 glob_match_inner(&p, 0, &n, 0)
143}
144
145fn glob_match_inner(p: &[char], pi: usize, n: &[char], ni: usize) -> bool {
146 if pi == p.len() {
147 return ni == n.len();
148 }
149 if p[pi] == '*' {
150 for skip in 0..=(n.len() - ni) {
152 if glob_match_inner(p, pi + 1, n, ni + skip) {
153 return true;
154 }
155 }
156 false
157 } else if p[pi] == '?' {
158 ni < n.len() && glob_match_inner(p, pi + 1, n, ni + 1)
159 } else {
160 ni < n.len() && p[pi] == n[ni] && glob_match_inner(p, pi + 1, n, ni + 1)
161 }
162}