1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use std::{env, path::Path, process::Command};

use anyhow::{bail, Context, Result};

/// Evaluates the given string and returns a concatenated string of the results.
///
/// # Syntax
///
/// Command:
///
/// ```text
/// $(...)
/// ```
///
/// Environment variable:
///
/// ```text
/// ${...}
/// ```
///
/// # Note
///
/// Nesting and escaping are not supported yet.
pub fn evaluate(mut s: &str, current_dir: Option<&Path>) -> Result<String> {
    let mut out = String::new();

    loop {
        match s.find('$') {
            Some(pos) => {
                out.push_str(&s[..pos]);
                s = &s[pos..];
            }
            None => {
                out.push_str(s);
                break;
            }
        }
        match s.as_bytes().get(1) {
            Some(b'(') => {
                let end = match s.find(')') {
                    Some(end) => end,
                    None => bail!("unclosed command literal {s:?}"),
                };
                let script = &s[2..end];
                s = s.get(end + 1..).unwrap_or_default();

                let mut script = script.split(' ');
                let mut cmd = Command::new(script.next().unwrap());
                cmd.args(script);
                if let Some(dir) = current_dir {
                    cmd.current_dir(dir);
                }
                let output = cmd
                    .output()
                    .with_context(|| format!("could not run `{cmd:?}`"))?;
                if !output.status.success() {
                    bail!(
                        "`{cmd:?}` didn't exit successfully\nstdout:\n{}\n\nstderr:\n{}\n",
                        String::from_utf8_lossy(&output.stdout),
                        String::from_utf8_lossy(&output.stderr)
                    );
                }
                let mut output = String::from_utf8(output.stdout)?;
                while output.ends_with('\n') || output.ends_with('\r') {
                    output.pop();
                }
                out.push_str(&output);
            }
            Some(b'{') => {
                let end = match s.find('}') {
                    Some(end) => end,
                    None => bail!("unclosed environment variable literal {s:?}"),
                };
                let key = &s[2..end];
                s = s.get(end + 1..).unwrap_or_default();

                let var = env::var(key)
                    .with_context(|| format!("could not get environment variable {key:?}"))?;
                out.push_str(&var);
            }
            Some(_) => {
                out.push('$');
                s = &s[1..];
            }
            None => {
                out.push('$');
                break;
            }
        }
    }

    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test() {
        assert_eq!(evaluate("a", None).unwrap(), "a");
        assert_eq!(evaluate("a$", None).unwrap(), "a$");
        assert_eq!(evaluate("$a", None).unwrap(), "$a");
        assert_eq!(evaluate("$(echo a)", None).unwrap(), "a");
        assert_eq!(evaluate("$(echo a)b", None).unwrap(), "ab");
        assert_eq!(evaluate("$(echo a))", None).unwrap(), "a)");
        assert_eq!(evaluate("$$(echo a)", None).unwrap(), "$a");
        assert_eq!(
            evaluate("$(echo a)", Some(&env::current_dir().unwrap())).unwrap(),
            "a"
        );
        evaluate("$(echo a", None).unwrap_err();
        evaluate("${OPENRR_CONFIG_TEST_ENV}", None).unwrap_err();
        env::set_var("OPENRR_CONFIG_TEST_ENV", "a");
        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}", None).unwrap(), "a");
        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}b", None).unwrap(), "ab");
        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}}", None).unwrap(), "a}");
        assert_eq!(
            evaluate("$${OPENRR_CONFIG_TEST_ENV}}", None).unwrap(),
            "$a}"
        );
        evaluate("${OPENRR_CONFIG_TEST_ENV", None).unwrap_err();
    }
}