use anyhow::Result;
use std::fs;
use std::path::Path;
#[allow(unused_assignments)]
pub fn amberify(input: &Path) -> Result<String> {
let content = fs::read_to_string(input)?;
let mut output = String::new();
output.push_str(&format!("// Amber conversion of {}\n", input.display()));
output.push_str("// Generated by portail amberify\n\n");
output.push_str("import {{ read, write, exists }} from \"std/fs\"\n");
output.push_str("import {{ trim, split, replace }} from \"std/text\"\n\n");
let lines: Vec<&str> = content.lines().collect();
let mut in_function = false;
#[allow(unused_variables)]
let mut in_loop = false;
#[allow(unused_variables)]
let mut in_if = false;
for line in lines.iter() {
let trimmed = line.trim();
if trimmed.starts_with("#!") {
continue;
}
if trimmed.starts_with("#") {
let comment = trimmed.trim_start_matches('#').trim();
output.push_str(&format!("// {}\n", comment));
continue;
}
if trimmed.contains("=") && !trimmed.starts_with("if") && !trimmed.starts_with("for") {
let parts: Vec<&str> = trimmed.splitn(2, '=').collect();
if parts.len() == 2 {
let var_name = parts[0]
.trim()
.trim_start_matches("let ")
.trim_start_matches("local ");
let value = parts[1].trim().trim_matches('"');
if value.starts_with("$(") && value.ends_with(')') {
let cmd = &value[2..value.len() - 1];
output.push_str(&format!("let {} = $ {} $\n", var_name, cmd));
} else {
output.push_str(&format!("let {} = \"{}\"\n", var_name, value));
}
continue;
}
}
if trimmed.starts_with("echo ") || trimmed.starts_with("echo(") {
let content = if trimmed.starts_with("echo(") {
trimmed
.strip_prefix("echo(")
.unwrap()
.strip_suffix(')')
.unwrap_or("")
} else {
trimmed.strip_prefix("echo ").unwrap_or("")
};
let converted = content.replace("${", "{").replace("$", "");
output.push_str(&format!("echo({})\n", converted));
continue;
}
if trimmed.starts_with("fun ") || trimmed.starts_with("function ") {
in_function = true;
let name = if trimmed.starts_with("fun ") {
trimmed.strip_prefix("fun ").unwrap()
} else {
trimmed.strip_prefix("function ").unwrap()
};
output.push_str(&format!(
"fun {} {{\n",
name.trim_end_matches("(){").trim_end_matches("()")
));
continue;
}
if trimmed.starts_with("if ") {
in_if = true;
let condition = trimmed
.strip_prefix("if ")
.unwrap()
.trim_end_matches("; then")
.trim_end_matches("then");
let converted = condition
.replace("[ ", "")
.replace(" ]", "")
.replace(" -eq ", " == ")
.replace(" -ne ", " != ")
.replace(" -gt ", " > ")
.replace(" -lt ", " < ")
.replace(" -ge ", " >= ")
.replace(" -le ", " <= ")
.replace(" -z ", " len() == 0 ")
.replace(" -n ", " len() > 0 ");
output.push_str(&format!("if {} {{\n", converted));
continue;
}
if trimmed == "else" || trimmed == "else;" {
output.push_str("} else {\n");
continue;
}
if trimmed == "fi" || trimmed == "fi;" {
in_if = false;
output.push_str("}\n");
continue;
}
if trimmed.starts_with("for ") {
in_loop = true;
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 4 && parts[2] == "in" {
let var_name = parts[1];
let iterable = parts[3..]
.join(" ")
.trim_end_matches("; do")
.trim_end_matches("do")
.to_string();
output.push_str(&format!("for {} in {} {{\n", var_name, iterable));
}
continue;
}
if trimmed == "done" || trimmed == "done;" {
in_loop = false;
output.push_str("}\n");
continue;
}
if trimmed == "}" && in_function {
in_function = false;
output.push_str("}\n\n");
continue;
}
if trimmed.starts_with("return ") {
let value = trimmed.strip_prefix("return ").unwrap();
output.push_str(&format!("return {}\n", value));
continue;
}
if trimmed.starts_with("exit ") {
let code = trimmed.strip_prefix("exit ").unwrap();
output.push_str(&format!("exit {}\n", code));
continue;
}
if !trimmed.is_empty() && !trimmed.starts_with("//") {
if trimmed.starts_with("$ ") || trimmed.starts_with("${") {
output.push_str(&format!("$ {} $\n", trimmed));
} else {
output.push_str(&format!("$ {} $\n", trimmed));
}
}
}
Ok(output)
}
pub fn process_path(input: &Path, output_dir: Option<&Path>) -> Result<()> {
if input.is_dir() {
for entry in fs::read_dir(input)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "sh") {
process_file(&path, output_dir)?;
}
}
} else {
process_file(input, output_dir)?;
}
Ok(())
}
fn process_file(input: &Path, output_dir: Option<&Path>) -> Result<()> {
let amber_content = amberify(input)?;
let output_path = if let Some(dir) = output_dir {
let filename = input.file_stem().unwrap_or_default();
dir.join(format!("{}.ab", filename.to_string_lossy()))
} else {
let mut output = input.to_path_buf();
output.set_extension("ab");
output
};
fs::write(&output_path, amber_content)?;
println!("✓ {} → {}", input.display(), output_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_basic_conversion() {
let dir = std::env::temp_dir();
let input_path = dir.join("test_input.sh");
let mut file = fs::File::create(&input_path).unwrap();
writeln!(file, "#!/bin/bash").unwrap();
writeln!(file, "# This is a comment").unwrap();
writeln!(file, "NAME=\"world\"").unwrap();
writeln!(file, "echo \"Hello, ${{NAME}}!\"").unwrap();
let result = amberify(&input_path).unwrap();
assert!(result.contains("// This is a comment"));
assert!(result.contains("let NAME"));
assert!(result.contains("echo"));
fs::remove_file(&input_path).unwrap();
}
}