use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use super::error::ZxError;
const QUOTES: [&str; 3] = ["\"", "'", "`"];
const TRIPLE_QUOTES: [&str; 3] = ["\"\"\"", "'''", "```"];
fn find_closing_quote(text: &str, quote: char) -> Option<usize> {
let mut chars = text.char_indices();
while let Some((i, c)) = chars.next() {
if c == '\\' && quote != '\'' {
chars.next();
} else if c == quote {
return Some(i);
}
}
None
}
fn strip_inline_comment(value: &str) -> &str {
let cut = value
.match_indices('#')
.map(|(i, _)| i)
.find(|&i| i == 0 || value[..i].ends_with(char::is_whitespace));
match cut {
Some(i) => value[..i].trim(),
None => value.trim(),
}
}
fn read_quoted(
lines: &[&str],
start: usize,
first: &str,
closer: impl Fn(&str) -> Option<usize>,
) -> (String, usize) {
if let Some(end) = closer(first) {
return (first[..end].to_string(), start + 1);
}
let mut parts: Vec<&str> = if first.is_empty() {
vec![]
} else {
vec![first]
};
for (i, line) in lines.iter().enumerate().skip(start + 1) {
if let Some(end) = closer(line) {
let tail = &line[..end];
if !tail.trim().is_empty() || parts.is_empty() {
parts.push(tail);
}
return (parts.join("\n"), i + 1);
}
parts.push(line);
}
(first.to_string(), start + 1)
}
fn read_value(lines: &[&str], index: usize, raw: &str) -> (String, usize) {
if let Some(triple) = TRIPLE_QUOTES.iter().find(|q| raw.starts_with(*q)) {
return read_quoted(lines, index, &raw[3..], |line| line.find(triple));
}
if let Some(quote) = QUOTES.iter().find(|q| raw.starts_with(*q)) {
let q = quote.chars().next().expect("non-empty quote");
return read_quoted(lines, index, &raw[1..], |line| find_closing_quote(line, q));
}
(strip_inline_comment(raw).to_string(), index + 1)
}
pub fn parse(content: &str) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
let lines: Vec<&str> = content
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l))
.collect();
let mut index = 0;
while index < lines.len() {
let trimmed = lines[index].trim();
let line = strip_export(trimmed);
let key = match line.find('=') {
Some(eq) if eq > 0 => line[..eq].trim(),
_ => "",
};
if line.starts_with('#') || key.is_empty() || key.contains(char::is_whitespace) {
index += 1;
continue;
}
let eq = line.find('=').expect("key implies '='");
let (value, next) = read_value(&lines, index, line[eq + 1..].trim());
result.insert(key.to_string(), value);
index = next;
}
result
}
fn strip_export(line: &str) -> &str {
if let Some(rest) = line.strip_prefix("export") {
if rest.starts_with(char::is_whitespace) {
return rest.trim_start();
}
}
line
}
fn quote_value(value: &str) -> String {
let needs_quotes = value
.chars()
.any(|c| c.is_whitespace() || matches!(c, '#' | '\'' | '"' | '`'));
if !needs_quotes {
return value.to_string();
}
if !value.contains(['\r', '\n']) {
if !value.contains('\'') {
return format!("'{value}'");
}
if !value.contains('\\') {
if let Some(q) = QUOTES.iter().find(|q| !value.contains(*q)) {
return format!("{q}{value}{q}");
}
}
}
let triple = TRIPLE_QUOTES
.iter()
.find(|q| !value.contains(*q))
.unwrap_or(&TRIPLE_QUOTES[0]);
format!("{triple}\n{value}\n{triple}")
}
pub fn stringify<'a, I, K, V>(env: I) -> String
where
I: IntoIterator<Item = (&'a K, &'a V)>,
K: AsRef<str> + ?Sized + 'a,
V: AsRef<str> + ?Sized + 'a,
{
env.into_iter()
.map(|(k, v)| format!("{}={}", k.as_ref(), quote_value(v.as_ref())))
.collect::<Vec<_>>()
.join("\n")
}
fn read_file(file: &Path) -> Result<BTreeMap<String, String>, ZxError> {
let text = std::fs::read_to_string(file)?;
Ok(parse(&text))
}
pub fn load<P: AsRef<Path>>(files: &[P]) -> Result<BTreeMap<String, String>, ZxError> {
let mut env = BTreeMap::new();
for file in files {
for (k, v) in read_file(file.as_ref())? {
env.entry(k).or_insert(v);
}
}
Ok(env)
}
pub fn load_safe<P: AsRef<Path>>(files: &[P]) -> BTreeMap<String, String> {
let mut env = BTreeMap::new();
for file in files {
if let Ok(parsed) = read_file(file.as_ref()) {
for (k, v) in parsed {
env.entry(k).or_insert(v);
}
}
}
env
}
pub fn config<P: AsRef<Path>>(files: &[P]) -> HashMap<String, String> {
let mut env: HashMap<String, String> = std::env::vars().collect();
for (k, v) in load_safe(files) {
let taken = if cfg!(windows) {
env.keys().any(|e| e.eq_ignore_ascii_case(&k))
} else {
env.contains_key(&k)
};
if !taken {
env.insert(k, v);
}
}
env
}