vector/config/
vars.rs

1use std::{collections::HashMap, sync::LazyLock};
2
3use regex::{Captures, Regex};
4
5// Environment variable names can have any characters from the Portable Character Set other
6// than NUL.  However, for Vector's interpolation, we are closer to what a shell supports which
7// is solely of uppercase letters, digits, and the '_' (that is, the `[:word:]` regex class).
8// In addition to these characters, we allow `.` as this commonly appears in environment
9// variable names when they come from a Java properties file.
10//
11// https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
12pub static ENVIRONMENT_VARIABLE_INTERPOLATION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13    Regex::new(
14        r"(?x)
15        \$\$|
16        \$([[:word:].]+)|
17        \$\{([[:word:].]+)(?:(:?-|:?\?)([^}]*))?\}",
18    )
19    .unwrap()
20});
21
22/// Result<interpolated config, errors>
23pub fn interpolate(input: &str, vars: &HashMap<String, String>) -> Result<String, Vec<String>> {
24    let mut errors = Vec::new();
25
26    let interpolated = ENVIRONMENT_VARIABLE_INTERPOLATION_REGEX
27        .replace_all(input, |caps: &Captures<'_>| {
28            let flags = caps.get(3).map(|m| m.as_str()).unwrap_or_default();
29            let def_or_err = caps.get(4).map(|m| m.as_str()).unwrap_or_default();
30            caps.get(1)
31                .or_else(|| caps.get(2))
32                .map(|m| m.as_str())
33                .map(|name| {
34                    let val = vars.get(name).map(|v| v.as_str());
35                    match flags {
36                        ":-" => match val {
37                            Some(v) if !v.is_empty() => v,
38                            _ => def_or_err,
39                        },
40                        "-" => val.unwrap_or(def_or_err),
41                        ":?" => match val {
42                            Some(v) if !v.is_empty() => v,
43                            _ => {
44                                errors.push(format!(
45                                    "Non-empty environment variable required in config. name = {name:?}, error = {def_or_err:?}",
46                                ));
47                                ""
48                            },
49                        }
50                        "?" => val.unwrap_or_else(|| {
51                            errors.push(format!(
52                                "Missing environment variable required in config. name = {name:?}, error = {def_or_err:?}",
53                            ));
54                            ""
55                        }),
56                        _ => val.unwrap_or_else(|| {
57                            errors.push(format!(
58                                "Missing environment variable in config. name = {name:?}",
59                            ));
60                            ""
61                        }),
62                    }
63                })
64                .unwrap_or("$")
65                .to_string()
66        })
67        .into_owned();
68
69    if errors.is_empty() {
70        Ok(interpolated)
71    } else {
72        Err(errors)
73    }
74}
75
76#[cfg(test)]
77mod test {
78    use super::interpolate;
79    #[test]
80    fn interpolation() {
81        let vars = vec![
82            ("FOO".into(), "dogs".into()),
83            ("FOOBAR".into(), "cats".into()),
84            // Java commonly uses .s in env var names
85            ("FOO.BAR".into(), "turtles".into()),
86            ("EMPTY".into(), "".into()),
87        ]
88        .into_iter()
89        .collect();
90
91        assert_eq!("dogs", interpolate("$FOO", &vars).unwrap());
92        assert_eq!("dogs", interpolate("${FOO}", &vars).unwrap());
93        assert_eq!("cats", interpolate("${FOOBAR}", &vars).unwrap());
94        assert_eq!("xcatsy", interpolate("x${FOOBAR}y", &vars).unwrap());
95        assert!(interpolate("x$FOOBARy", &vars).is_err());
96        assert_eq!("$ x", interpolate("$ x", &vars).unwrap());
97        assert_eq!("$FOO", interpolate("$$FOO", &vars).unwrap());
98        assert_eq!("dogs=bar", interpolate("$FOO=bar", &vars).unwrap());
99        assert!(interpolate("$NOT_FOO", &vars).is_err());
100        assert!(interpolate("$NOT-FOO", &vars).is_err());
101        assert_eq!("turtles", interpolate("$FOO.BAR", &vars).unwrap());
102        assert_eq!("${FOO x", interpolate("${FOO x", &vars).unwrap());
103        assert_eq!("${}", interpolate("${}", &vars).unwrap());
104        assert_eq!("dogs", interpolate("${FOO:-cats}", &vars).unwrap());
105        assert_eq!("dogcats", interpolate("${NOT:-dogcats}", &vars).unwrap());
106        assert_eq!(
107            "dogs and cats",
108            interpolate("${NOT:-dogs and cats}", &vars).unwrap()
109        );
110        assert_eq!("${:-cats}", interpolate("${:-cats}", &vars).unwrap());
111        assert_eq!("", interpolate("${NOT:-}", &vars).unwrap());
112        assert_eq!("cats", interpolate("${NOT-cats}", &vars).unwrap());
113        assert_eq!("", interpolate("${EMPTY-cats}", &vars).unwrap());
114        assert_eq!("dogs", interpolate("${FOO:?error cats}", &vars).unwrap());
115        assert_eq!("dogs", interpolate("${FOO?error cats}", &vars).unwrap());
116        assert_eq!("", interpolate("${EMPTY?error cats}", &vars).unwrap());
117        assert!(interpolate("${NOT:?error cats}", &vars).is_err());
118        assert!(interpolate("${NOT?error cats}", &vars).is_err());
119        assert!(interpolate("${EMPTY:?error cats}", &vars).is_err());
120    }
121}