use anyhow::Result;
use regex::Regex;
use std::path::Path;
use crate::models::{AliasEntry, EntryType};
pub fn parse_shell_file(path: &Path) -> Result<Vec<AliasEntry>> {
let content = std::fs::read_to_string(path)?;
let mut entries = Vec::new();
entries.extend(extract_aliases(&content, path));
entries.extend(extract_functions(&content, path));
Ok(entries)
}
fn extract_aliases(
content: &str,
source: &Path,
) -> Vec<AliasEntry> {
let mut entries = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let alias_pattern =
Regex::new(r#"^\s*alias\s+([a-zA-Z_][a-zA-Z0-9._-]*)=(?:'([^']*)'|"([^"]*)"|([^\s]+))"#).unwrap();
for (line_num, line) in lines.iter().enumerate() {
if let Some(captures) = alias_pattern.captures(line) {
let name = captures.get(1).unwrap().as_str().to_string();
let value = captures
.get(2)
.or_else(|| captures.get(3))
.or_else(|| captures.get(4))
.map(|m| m.as_str())
.unwrap_or("")
.to_string();
let comments = extract_comments(&lines, line_num);
entries.push(AliasEntry {
name,
entry_type: EntryType::Alias,
value,
comments,
source_file: source.to_path_buf(),
});
}
}
entries
}
fn extract_functions(
content: &str,
source: &Path,
) -> Vec<AliasEntry> {
let mut entries = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let func_pattern = Regex::new(r#"^\s*(?:function\s+)?([a-zA-Z_][a-zA-Z0-9._-]*)\s*\(\)\s*\{"#).unwrap();
for (line_num, line) in lines.iter().enumerate() {
if let Some(captures) = func_pattern.captures(line) {
let name = captures.get(1).unwrap().as_str().to_string();
let mut body_lines = vec![line.to_string()];
let mut brace_count = 1;
for next_line in lines.iter().skip(line_num + 1) {
body_lines.push(next_line.to_string());
brace_count += next_line.matches('{').count() as i32;
brace_count -= next_line.matches('}').count() as i32;
if brace_count == 0 {
break;
}
}
let value = body_lines.join("\n");
let comments = extract_comments(&lines, line_num);
entries.push(AliasEntry {
name,
entry_type: EntryType::Function,
value,
comments,
source_file: source.to_path_buf(),
});
}
}
entries
}
fn extract_comments(
lines: &[&str],
line_number: usize,
) -> Option<Vec<String>> {
if line_number > 0 {
if let Some(description) = extract_concise_comment(lines[line_number - 1]) {
return Some(vec![description]);
}
}
extract_multiline_comment(lines, line_number)
}
fn extract_concise_comment(line: &str) -> Option<String> {
let pattern = Regex::new(r#"#@:\s*(.+?)\s*:f#"#).ok()?;
pattern.captures(line).and_then(|caps| caps.get(1).map(|m| m.as_str().trim().to_string()))
}
fn extract_multiline_comment(
lines: &[&str],
mut line_number: usize,
) -> Option<Vec<String>> {
if line_number == 0 {
return None;
}
line_number = line_number.saturating_sub(1);
let mut closing_found = false;
let mut description_lines = Vec::new();
let mut temp_line_num = line_number;
loop {
let line = lines[temp_line_num].trim();
if line == "# fla" {
closing_found = true;
} else if line == "# alf" && closing_found {
return if description_lines.is_empty() {
None
} else {
description_lines.reverse();
Some(description_lines)
};
} else if closing_found && line.starts_with('#') && line != "# fla" && line != "# alf" {
let text = line.trim_start_matches('#').trim();
if !text.is_empty() {
description_lines.push(text.to_string());
}
} else if closing_found && !line.starts_with('#') && !line.is_empty() {
return None;
} else if closing_found && line.is_empty() {
return None;
} else if !closing_found && line.is_empty() {
return None;
} else if !closing_found && line.starts_with('#') && line != "# alf" {
return None;
}
if temp_line_num == 0 {
break;
}
temp_line_num -= 1;
}
None
}
#[cfg(test)]
mod parser_tests;