pub(crate) fn is_valid_key(key: &str) -> bool {
let mut chars = key.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub(crate) fn parse_line(line: &str) -> Option<(&str, String)> {
let trimmed = line.trim();
if trimmed.starts_with('#') {
return None;
}
let body = trimmed
.strip_prefix("export ")
.map_or(trimmed, str::trim_start);
let (key, raw) = body.split_once('=')?;
let key = key.trim();
is_valid_key(key).then(|| (key, unquote(raw.trim())))
}
fn unquote(raw: &str) -> String {
if let Some(rest) = raw.strip_prefix('"') {
if let Some(value) = unescape_double_quoted(rest) {
return value;
}
} else if let Some(rest) = raw.strip_prefix('\'') {
if let Some(end) = rest.find('\'') {
return rest[..end].to_string();
}
}
strip_inline_comment(raw).to_string()
}
fn unescape_double_quoted(rest: &str) -> Option<String> {
let mut out = String::new();
let mut chars = rest.chars();
while let Some(c) = chars.next() {
match c {
'"' => return Some(out),
'\\' => match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some(escaped @ ('"' | '\\')) => out.push(escaped),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
},
other => out.push(other),
}
}
None
}
fn strip_inline_comment(raw: &str) -> &str {
match raw.find(" #").into_iter().chain(raw.find("\t#")).min() {
Some(index) => raw[..index].trim_end(),
None => raw,
}
}
pub(crate) fn format_value(value: &str) -> String {
let needs_quotes =
value.contains(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | '#' | '\\'));
if !needs_quotes {
return value.to_string();
}
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for c in value.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out.push('"');
out
}
#[cfg(test)]
#[path = "_line.test.rs"]
mod tests;