1use anyhow::Result;
4use regex::Regex;
5use std::path::Path;
6
7use crate::models::{AliasEntry, EntryType};
8
9pub fn parse_shell_file(path: &Path) -> Result<Vec<AliasEntry>> {
17 let content = std::fs::read_to_string(path)?;
18 let mut entries = Vec::new();
19
20 entries.extend(extract_aliases(&content, path));
22
23 entries.extend(extract_functions(&content, path));
25
26 Ok(entries)
27}
28
29fn extract_aliases(
31 content: &str,
32 source: &Path,
33) -> Vec<AliasEntry> {
34 let mut entries = Vec::new();
35 let lines: Vec<&str> = content.lines().collect();
36
37 let alias_pattern =
41 Regex::new(r#"^\s*alias\s+([a-zA-Z_][a-zA-Z0-9._-]*)=(?:'([^']*)'|"([^"]*)"|([^\s]+))"#).unwrap();
42
43 for (line_num, line) in lines.iter().enumerate() {
44 if let Some(captures) = alias_pattern.captures(line) {
45 let name = captures.get(1).unwrap().as_str().to_string();
46 let value = captures
48 .get(2)
49 .or_else(|| captures.get(3))
50 .or_else(|| captures.get(4))
51 .map(|m| m.as_str())
52 .unwrap_or("")
53 .to_string();
54
55 let comments = extract_comments(&lines, line_num);
56
57 entries.push(AliasEntry {
58 name,
59 entry_type: EntryType::Alias,
60 value,
61 comments,
62 source_file: source.to_path_buf(),
63 });
64 }
65 }
66
67 entries
68}
69
70fn extract_functions(
72 content: &str,
73 source: &Path,
74) -> Vec<AliasEntry> {
75 let mut entries = Vec::new();
76 let lines: Vec<&str> = content.lines().collect();
77
78 let func_pattern = Regex::new(r#"^\s*(?:function\s+)?([a-zA-Z_][a-zA-Z0-9._-]*)\s*\(\)\s*\{"#).unwrap();
82
83 for (line_num, line) in lines.iter().enumerate() {
84 if let Some(captures) = func_pattern.captures(line) {
85 let name = captures.get(1).unwrap().as_str().to_string();
86
87 let mut body_lines = vec![line.to_string()];
89 let mut brace_count = 1;
90
91 for next_line in lines.iter().skip(line_num + 1) {
92 body_lines.push(next_line.to_string());
93 brace_count += next_line.matches('{').count() as i32;
94 brace_count -= next_line.matches('}').count() as i32;
95
96 if brace_count == 0 {
97 break;
98 }
99 }
100
101 let value = body_lines.join("\n");
102 let comments = extract_comments(&lines, line_num);
103
104 entries.push(AliasEntry {
105 name,
106 entry_type: EntryType::Function,
107 value,
108 comments,
109 source_file: source.to_path_buf(),
110 });
111 }
112 }
113
114 entries
115}
116
117fn extract_comments(
133 lines: &[&str],
134 line_number: usize,
135) -> Option<Vec<String>> {
136 if line_number > 0 {
138 if let Some(description) = extract_concise_comment(lines[line_number - 1]) {
139 return Some(vec![description]);
140 }
141 }
142
143 extract_multiline_comment(lines, line_number)
145}
146
147fn extract_concise_comment(line: &str) -> Option<String> {
150 let pattern = Regex::new(r#"#@:\s*(.+?)\s*:f#"#).ok()?;
151 pattern.captures(line).and_then(|caps| caps.get(1).map(|m| m.as_str().trim().to_string()))
152}
153
154fn extract_multiline_comment(
161 lines: &[&str],
162 mut line_number: usize,
163) -> Option<Vec<String>> {
164 if line_number == 0 {
165 return None;
166 }
167
168 line_number = line_number.saturating_sub(1);
169
170 let mut closing_found = false;
172 let mut description_lines = Vec::new();
173 let mut temp_line_num = line_number;
174
175 loop {
177 let line = lines[temp_line_num].trim();
178
179 if line == "# fla" {
181 closing_found = true;
182 } else if line == "# alf" && closing_found {
183 return if description_lines.is_empty() {
185 None
186 } else {
187 description_lines.reverse();
188 Some(description_lines)
189 };
190 } else if closing_found && line.starts_with('#') && line != "# fla" && line != "# alf" {
191 let text = line.trim_start_matches('#').trim();
193 if !text.is_empty() {
194 description_lines.push(text.to_string());
195 }
196 } else if closing_found && !line.starts_with('#') && !line.is_empty() {
197 return None;
199 } else if closing_found && line.is_empty() {
200 return None;
202 } else if !closing_found && line.is_empty() {
203 return None;
205 } else if !closing_found && line.starts_with('#') && line != "# alf" {
206 return None;
209 }
210
211 if temp_line_num == 0 {
212 break;
213 }
214 temp_line_num -= 1;
215 }
216
217 None
218}
219
220#[cfg(test)]
221mod parser_tests;