feat(core): Support environment variable parsing in config files (#1640)

This commit is contained in:
Mg Pig
2025-12-02 17:54:31 +08:00
committed by GitHub
parent ae6d929f4a
commit 53f279f5ff
10 changed files with 898 additions and 6 deletions

View File

@@ -0,0 +1,247 @@
//! 环境变量解析模块
//!
//! 提供配置文件中环境变量占位符的解析功能
//! 支持 Shell 风格的语法:${VAR}、$VAR、${VAR:-default} 等
use std::borrow::Cow;
/// 解析字符串中的环境变量占位符
///
/// 支持的语法:
/// - `${VAR_NAME}` - 标准格式(推荐)
/// - `$VAR_NAME` - 简写格式
/// - `${VAR_NAME:-default}` - 带默认值bash 标准语法)
///
/// # 参数
/// - `text`: 待解析的字符串
///
/// # 返回值
/// - `String`: 替换后的字符串
/// - `bool`: 是否检测到并替换了环境变量
pub fn expand_env_vars(text: &str) -> (String, bool) {
// 使用 shellexpand::env() 解析环境变量
// 该函数仅处理环境变量,不处理 tilde (~) 扩展,适合配置文件场景
match shellexpand::env(text) {
Ok(expanded) => {
// 通过比较原始字符串和扩展后的字符串判断是否发生了替换
let changed = match &expanded {
Cow::Borrowed(_) => false, // 未发生变化,仍是借用
Cow::Owned(_) => true, // 发生了变化,产生了新字符串
};
(expanded.into_owned(), changed)
}
Err(e) => {
// 如果解析失败(例如变量引用语法错误),记录警告并返回原字符串
tracing::warn!("Failed to expand environment variables in config: {}", e);
(text.to_string(), false)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_standard_syntax() {
std::env::set_var("TEST_VAR_STANDARD", "test_value");
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
assert_eq!(result, "secret=test_value");
assert!(changed);
}
#[test]
fn test_expand_short_syntax() {
std::env::set_var("TEST_VAR_SHORT", "short_value");
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
assert_eq!(result, "key=short_value");
assert!(changed);
}
#[test]
fn test_expand_with_default() {
// 确保变量未定义
std::env::remove_var("UNDEFINED_VAR_WITH_DEFAULT");
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
assert_eq!(result, "port=8080");
assert!(changed);
}
#[test]
fn test_no_env_vars() {
let (result, changed) = expand_env_vars("plain text without variables");
assert_eq!(result, "plain text without variables");
assert!(!changed);
}
#[test]
fn test_empty_string() {
let (result, changed) = expand_env_vars("");
assert_eq!(result, "");
assert!(!changed);
}
#[test]
fn test_multiple_vars() {
std::env::set_var("VAR1", "value1");
std::env::set_var("VAR2", "value2");
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
assert_eq!(result, "value1 and value2");
assert!(changed);
}
#[test]
fn test_undefined_var_without_default() {
// 确保变量未定义
std::env::remove_var("COMPLETELY_UNDEFINED_VAR");
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
// shellexpand::env 对未定义的变量会保持原样
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
assert!(!changed);
}
#[test]
fn test_complex_toml_config() {
std::env::set_var("ET_SECRET", "my-secret-key");
std::env::set_var("ET_PORT", "11010");
let config = r#"
[network_identity]
network_name = "test-network"
network_secret = "${ET_SECRET}"
[[peer]]
uri = "tcp://127.0.0.1:${ET_PORT}"
"#;
let (result, changed) = expand_env_vars(config);
assert!(changed);
assert!(result.contains(r#"network_secret = "my-secret-key""#));
assert!(result.contains(r#"uri = "tcp://127.0.0.1:11010""#));
}
#[test]
fn test_escape_syntax_double_dollar() {
std::env::set_var("ESCAPED_VAR", "should_not_expand");
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
// $$ 会被转义为单个 $,不会触发变量扩展
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
assert_eq!(result, "value=${ESCAPED_VAR}");
assert!(changed); // $$ -> $ 被视为一次变换
}
#[test]
fn test_escape_syntax_backslash() {
std::env::set_var("ESCAPED_VAR", "should_not_expand");
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
// 这不是推荐的转义方式,此测试仅为记录实际行为
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
assert_eq!(result, r"value=\should_not_expand");
assert!(changed);
}
#[test]
fn test_multiple_dollar_signs() {
std::env::set_var("TEST_VAR", "value");
// 测试多个连续的 $ 符号
let (result1, changed1) = expand_env_vars("$$");
assert_eq!(result1, "$");
assert!(changed1);
let (result2, changed2) = expand_env_vars("$$$$");
assert_eq!(result2, "$$");
assert!(changed2);
// $$ 后跟变量扩展
let (result3, changed3) = expand_env_vars("$$$TEST_VAR");
assert_eq!(result3, "$value");
assert!(changed3);
}
#[test]
fn test_empty_var_value() {
std::env::set_var("EMPTY_VAR", "");
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
// 变量存在但值为空
assert_eq!(result, "value=");
assert!(changed);
}
#[test]
fn test_default_with_special_chars() {
std::env::remove_var("UNDEFINED_SPECIAL");
// 测试默认值包含冒号、等号、空格等特殊字符
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
assert_eq!(result, "url=http://localhost:8080");
assert!(changed);
let (result2, changed2) = expand_env_vars("key=${UNDEFINED_SPECIAL:-name=value}");
assert_eq!(result2, "key=name=value");
assert!(changed2);
let (result3, changed3) = expand_env_vars("msg=${UNDEFINED_SPECIAL:-hello world}");
assert_eq!(result3, "msg=hello world");
assert!(changed3);
}
#[test]
fn test_var_name_with_numbers_underscores() {
std::env::set_var("VAR_123", "num_value");
std::env::set_var("_VAR", "underscore_prefix");
std::env::set_var("VAR_", "underscore_suffix");
let (result1, changed1) = expand_env_vars("${VAR_123}");
assert_eq!(result1, "num_value");
assert!(changed1);
let (result2, changed2) = expand_env_vars("${_VAR}");
assert_eq!(result2, "underscore_prefix");
assert!(changed2);
let (result3, changed3) = expand_env_vars("${VAR_}");
assert_eq!(result3, "underscore_suffix");
assert!(changed3);
}
#[test]
fn test_invalid_syntax() {
// 测试无效语法的处理
let (result1, changed1) = expand_env_vars("${}");
// shellexpand 会保留无效语法原样
assert_eq!(result1, "${}");
assert!(!changed1);
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
std::env::remove_var("VAR");
let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
// 如果 VAR 未定义shellexpand 会返回错误或保持原样
assert_eq!(result2, "incomplete ${VAR");
// 注意changed2 的值取决于 shellexpand 是否认为这是有效语法
// 因此不对 changed2 做断言
}
#[test]
fn test_mixed_defined_undefined_vars() {
std::env::set_var("DEFINED_VAR", "defined");
std::env::remove_var("UNDEFINED_VAR");
// 混合已定义和未定义的变量
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
// 因此整个字符串会保持不变
let (result, changed) = expand_env_vars("${DEFINED_VAR} and ${UNDEFINED_VAR}");
assert_eq!(result, "${DEFINED_VAR} and ${UNDEFINED_VAR}");
assert!(!changed);
}
#[test]
fn test_nested_braces() {
std::env::set_var("OUTER", "outer_value");
// 嵌套的大括号是无效语法shellexpand::env 会返回错误
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
// 由于语法错误,整个字符串保持不变
assert_eq!(result, "${OUTER} and ${{INNER}}");
assert!(!changed);
}
}