char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somdeslop: split the task lib into modules7e18c89

main
4.1 KiB120 linesraw
1use std::collections::HashMap;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::EnvMap;
7use crate::graph::Task;
8
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    #[error("Cannot load env file {}: {source}", .path.display())]
12    Load {
13        path: PathBuf,
14        #[source]
15        source: io::Error,
16    },
17}
18
19fn parse(source: &str) -> io::Result<Vec<(String, String)>> {
20    if source.contains('\0') {
21        return Err(io::Error::new(
22            io::ErrorKind::InvalidInput,
23            "Environment files cannot contain NUL",
24        ));
25    }
26    let source = source.replace('\r', "");
27    let mut rest = source.strip_prefix('\u{feff}').unwrap_or(&source);
28    let mut values = Vec::new();
29    while !rest.is_empty() {
30        rest = rest.trim_start_matches([' ', '\t', '\n']);
31        let (line, next) = rest.split_once('\n').unwrap_or((rest, ""));
32        let Some((raw_key, _)) = line.split_once('=') else {
33            rest = next;
34            continue;
35        };
36        if line.starts_with('#') {
37            rest = next;
38            continue;
39        }
40        let mut key = raw_key.trim_matches([' ', '\t']);
41        if let Some(exported) = key.strip_prefix("export ") {
42            key = exported.trim_start_matches([' ', '\t']);
43        }
44        if key.is_empty() {
45            rest = next;
46            continue;
47        }
48        let value = rest[raw_key.len() + 1..].trim_start_matches([' ', '\t']);
49        if let Some(quote @ (b'\'' | b'"' | b'`')) = value.as_bytes().first().copied() {
50            let quote = quote as char;
51            if let Some(end) = value[1..].find(quote).map(|index| index + 1) {
52                let quoted = &value[1..end];
53                let parsed = if quote == '"' {
54                    quoted.replace("\\n", "\n")
55                } else {
56                    quoted.to_owned()
57                };
58                values.push((key.to_owned(), parsed));
59                rest = value[end + 1..]
60                    .split_once('\n')
61                    .map_or("", |(_, next)| next);
62                continue;
63            }
64            let (value, next) = value.split_once('\n').unwrap_or((value, ""));
65            values.push((key.to_owned(), value.to_owned()));
66            rest = next;
67            continue;
68        }
69        let (value, next) = value.split_once('\n').unwrap_or((value, ""));
70        let value = value.split_once('#').map_or(value, |(value, _)| value);
71        values.push((key.to_owned(), value.trim_matches([' ', '\t']).to_owned()));
72        rest = next;
73    }
74    Ok(values)
75}
76
77pub fn load_values(paths: &[PathBuf]) -> Result<Vec<(String, String)>, Error> {
78    let mut values = Vec::new();
79    for path in paths {
80        let source = std::fs::read_to_string(path)
81            .map_err(|source| Error::Load { path: path.clone(), source })?;
82        values.extend(parse(&source).map_err(|source| Error::Load { path: path.clone(), source })?);
83    }
84    Ok(values)
85}
86
87pub(crate) struct EnvLoader {
88    root: PathBuf,
89    cache: HashMap<PathBuf, Arc<Vec<(String, String)>>>,
90}
91
92impl EnvLoader {
93    pub(crate) fn new(root: PathBuf) -> Self {
94        EnvLoader { root, cache: HashMap::new() }
95    }
96
97    fn load(&mut self, relative: &Path) -> crate::Result<Arc<Vec<(String, String)>>> {
98        let path = self.root.join(relative);
99        if let Some(values) = self.cache.get(&path) {
100            return Ok(values.clone());
101        }
102        let values = Arc::new(load_values(std::slice::from_ref(&path))?);
103        self.cache.insert(path, values.clone());
104        Ok(values)
105    }
106
107    /// The environment a task runs with: inherited values, overridden by
108    /// its own env file when it declares one.
109    pub(crate) fn effective(&mut self, task: &Task, inherited: &EnvMap) -> crate::Result<EnvMap> {
110        let mut env = inherited.clone();
111        let Some(file) = &task.env_file else {
112            return Ok(env);
113        };
114        for (key, value) in self.load(file)?.iter() {
115            let key = avril_caps::env_name(key).map_err(|_| crate::Error::EnvName(key.clone()))?;
116            env.insert(key.into_owned(), value.clone());
117        }
118        Ok(env)
119    }
120}