///|
/// Expand `$VAR` and `${VAR}` references in all values of a parsed map.
///
/// Variables are resolved from the parsed map itself first, then from
/// the provided `env` map (e.g. system environment variables).
///
/// If a referenced variable is not found, it is left as-is.
pub fn expand(
parsed : Map[String, String],
env? : Map[String, String] = Map([], capacity=0),
) -> Map[String, String] {
let result = Map([], capacity=parsed.length())
for key, value in parsed {
result[key] = expand_value(value, parsed, env~, 0)
}
result
}
///|
/// Expand `$VAR` and `${VAR}` references in a single value string.
/// Recursively expands resolved values up to `max_depth` to handle chained references.
fn expand_value(
value : String,
parsed : Map[String, String],
env~ : Map[String, String],
depth : Int,
) -> String {
if depth > 100 {
return value
}
let buf = StringBuilder::new()
let len = value.length()
let mut i = 0
while i < len {
let ch = Int::unsafe_to_char(value.code_unit_at(i).to_int())
if ch == '$' && i + 1 < len {
let next = Int::unsafe_to_char(value.code_unit_at(i + 1).to_int())
if next == '$' {
// $$ -> literal $
buf.write_char('$')
i += 2
} else if next == '{' {
// ${VAR} or ${VAR:-default} syntax
i += 2
let var_start = i
while i < len &&
Int::unsafe_to_char(value.code_unit_at(i).to_int()) != '}' {
i += 1
}
let raw = value[var_start:i].to_owned()
if i < len {
i += 1
} // skip closing }
let (var_name, default_val, alt_val) = parse_var_spec(raw)
let resolved = resolve_var(var_name, parsed, env)
match resolved {
Some(v) =>
match alt_val {
Some(alt) => buf.write_string(alt)
None => buf.write_string(v)
}
None =>
if default_val is Some(def) {
buf.write_string(def)
} else if alt_val is Some(_) {
buf.write_string("")
} else {
buf.write_string("${")
buf.write_string(raw)
buf.write_char('}')
}
}
} else if next == '_' ||
(next >= 'a' && next <= 'z') ||
(next >= 'A' && next <= 'Z') {
// $VAR syntax
i += 1
let var_start = i
while i < len {
let c = Int::unsafe_to_char(value.code_unit_at(i).to_int())
if (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' {
i += 1
} else {
break
}
}
let var_name = value[var_start:i].to_owned()
match resolve_var(var_name, parsed, env) {
Some(v) => buf.write_string(v)
None => {
buf.write_char('$')
buf.write_string(var_name)
}
}
} else {
buf.write_char(ch)
i += 1
}
} else {
buf.write_char(ch)
i += 1
}
}
let result = buf.to_string()
if result != value {
expand_value(result, parsed, env~, depth + 1)
} else {
result
}
}
///|
/// Try to resolve a variable name from parsed map first, then env map.
/// Returns Some(value) if found, None otherwise.
fn resolve_var(
var_name : String,
parsed : Map[String, String],
env : Map[String, String],
) -> String? {
for k, v in parsed {
if k == var_name {
return Some(v)
}
}
for k, v in env {
if k == var_name {
return Some(v)
}
}
None
}
///|
/// Parse a variable spec like `VAR`, `VAR:-default`, or `VAR:+alt`.
/// Returns (var_name, optional_default, optional_alt).
fn parse_var_spec(raw : String) -> (String, String?, String?) {
let len = raw.length()
let mut i = 0
while i < len {
let ch = Int::unsafe_to_char(raw.code_unit_at(i).to_int())
if ch == ':' && i + 1 < len {
let next = Int::unsafe_to_char(raw.code_unit_at(i + 1).to_int())
if next == '-' {
let var_name = raw[0:i].to_owned()
let default_val = raw[i + 2:len].to_owned()
return (var_name, Some(default_val), None)
} else if next == '+' {
let var_name = raw[0:i].to_owned()
let alt_val = raw[i + 2:len].to_owned()
return (var_name, None, Some(alt_val))
}
}
i += 1
}
(raw, None, None)
}