openrr_config/
evaluate.rs

1use std::{env, path::Path, process::Command};
2
3use anyhow::{bail, Context, Result};
4
5/// Evaluates the given string and returns a concatenated string of the results.
6///
7/// # Syntax
8///
9/// Command:
10///
11/// ```text
12/// $(...)
13/// ```
14///
15/// Environment variable:
16///
17/// ```text
18/// ${...}
19/// ```
20///
21/// # Note
22///
23/// Nesting and escaping are not supported yet.
24pub fn evaluate(mut s: &str, current_dir: Option<&Path>) -> Result<String> {
25    let mut out = String::new();
26
27    loop {
28        match s.find('$') {
29            Some(pos) => {
30                out.push_str(&s[..pos]);
31                s = &s[pos..];
32            }
33            None => {
34                out.push_str(s);
35                break;
36            }
37        }
38        match s.as_bytes().get(1) {
39            Some(b'(') => {
40                let end = match s.find(')') {
41                    Some(end) => end,
42                    None => bail!("unclosed command literal {s:?}"),
43                };
44                let script = &s[2..end];
45                s = s.get(end + 1..).unwrap_or_default();
46
47                let mut script = script.split(' ');
48                let mut cmd = Command::new(script.next().unwrap());
49                cmd.args(script);
50                if let Some(dir) = current_dir {
51                    cmd.current_dir(dir);
52                }
53                let output = cmd
54                    .output()
55                    .with_context(|| format!("could not run `{cmd:?}`"))?;
56                if !output.status.success() {
57                    bail!(
58                        "`{cmd:?}` didn't exit successfully\nstdout:\n{}\n\nstderr:\n{}\n",
59                        String::from_utf8_lossy(&output.stdout),
60                        String::from_utf8_lossy(&output.stderr)
61                    );
62                }
63                let mut output = String::from_utf8(output.stdout)?;
64                while output.ends_with('\n') || output.ends_with('\r') {
65                    output.pop();
66                }
67                out.push_str(&output);
68            }
69            Some(b'{') => {
70                let end = match s.find('}') {
71                    Some(end) => end,
72                    None => bail!("unclosed environment variable literal {s:?}"),
73                };
74                let key = &s[2..end];
75                s = s.get(end + 1..).unwrap_or_default();
76
77                let var = env::var(key)
78                    .with_context(|| format!("could not get environment variable {key:?}"))?;
79                out.push_str(&var);
80            }
81            Some(_) => {
82                out.push('$');
83                s = &s[1..];
84            }
85            None => {
86                out.push('$');
87                break;
88            }
89        }
90    }
91
92    Ok(out)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test() {
101        assert_eq!(evaluate("a", None).unwrap(), "a");
102        assert_eq!(evaluate("a$", None).unwrap(), "a$");
103        assert_eq!(evaluate("$a", None).unwrap(), "$a");
104        assert_eq!(evaluate("$(echo a)", None).unwrap(), "a");
105        assert_eq!(evaluate("$(echo a)b", None).unwrap(), "ab");
106        assert_eq!(evaluate("$(echo a))", None).unwrap(), "a)");
107        assert_eq!(evaluate("$$(echo a)", None).unwrap(), "$a");
108        assert_eq!(
109            evaluate("$(echo a)", Some(&env::current_dir().unwrap())).unwrap(),
110            "a"
111        );
112        evaluate("$(echo a", None).unwrap_err();
113        evaluate("${OPENRR_CONFIG_TEST_ENV}", None).unwrap_err();
114        env::set_var("OPENRR_CONFIG_TEST_ENV", "a");
115        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}", None).unwrap(), "a");
116        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}b", None).unwrap(), "ab");
117        assert_eq!(evaluate("${OPENRR_CONFIG_TEST_ENV}}", None).unwrap(), "a}");
118        assert_eq!(
119            evaluate("$${OPENRR_CONFIG_TEST_ENV}}", None).unwrap(),
120            "$a}"
121        );
122        evaluate("${OPENRR_CONFIG_TEST_ENV", None).unwrap_err();
123    }
124}