Skip to main content

slash_lib/builtins/
find.rs

1use std::path::PathBuf;
2
3use slash_lang::parser::ast::Arg;
4
5use crate::command::{MethodDef, SlashCommand};
6use crate::executor::{CommandOutput, ExecutionError, PipeValue};
7
8/// `/find(pattern)` — search for files matching a glob pattern.
9///
10/// `/find(src/**/*.rs)` — list matching file paths, one per line.
11/// `/find(src).content(TODO)` — search file contents for a string.
12///
13/// Pure Rust directory walking + pattern matching. No subprocess.
14pub 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            // Search file contents for the needle.
45            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            // Just list matching paths.
58            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
83/// Walk directories matching a glob-like pattern.
84///
85/// Supports `*` (any segment), `**` (recursive), and `?` (single char).
86/// This is a minimal implementation — no external crate dependency.
87fn 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        // Match zero or more directories.
105        // Try skipping ** (depth + 1 at current dir).
106        walk_recursive(dir, parts, depth + 1, results);
107
108        // Try descending into each subdirectory with ** still active.
109        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        // Match against entries in this directory.
119        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
138/// Simple glob matching: `*` matches any sequence, `?` matches one char.
139fn 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        // Try matching * against 0..=remaining chars.
151        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}