Skip to main content

crok_lib/parser/
mod.rs

1use crate::script::{Script, ScriptError, ScriptErrorType, ScriptFile, ScriptLocation};
2use std::{
3    collections::{BTreeMap, HashMap},
4    sync::Arc,
5};
6
7pub mod v0;
8
9#[derive(thiserror::Error, Debug)]
10pub enum ScriptReadError {
11    #[error("Error reading script file {file}: {error}")]
12    ParseError {
13        file: ScriptFile,
14        error: ScriptError,
15    },
16    #[error("Error reading script file {file}: {error}")]
17    IoError {
18        file: ScriptFile,
19        error: std::io::Error,
20    },
21}
22
23#[derive(Default)]
24pub struct Scripts {
25    pub scripts: BTreeMap<ScriptFile, Script>,
26}
27
28const V0_HEADERS: &[&str] = &[
29    "#!/usr/bin/env crok --v0",
30    "#!/usr/bin/env clitest --v0",
31    "#!/usr/bin/env clitest",
32];
33
34fn is_v0_header(line: &str) -> bool {
35    V0_HEADERS.contains(&line)
36}
37
38pub fn parse_script(file: ScriptFile, script: &str) -> Result<Script, ScriptError> {
39    let version = script.lines().next().unwrap_or_default();
40    if is_v0_header(version) {
41        v0::parse_script(file, script)
42    } else {
43        Err(ScriptError::new(
44            ScriptErrorType::InvalidVersion,
45            ScriptLocation::new(file, 1),
46        ))
47    }
48}
49
50pub fn parse_script_file(
51    parent: Option<ScriptFile>,
52    file: ScriptFile,
53) -> Result<Script, Vec<ScriptReadError>> {
54    let mut errors = Vec::new();
55    let path = parent
56        .map(|p| Arc::new(p.file.parent().unwrap().join(&*file.file)))
57        .unwrap_or(file.file.clone());
58    let script_contents = match std::fs::read_to_string(path.as_ref()) {
59        Ok(contents) => contents,
60        Err(e) => {
61            errors.push(ScriptReadError::IoError {
62                file: file.clone(),
63                error: e,
64            });
65            return Err(errors);
66        }
67    };
68    let script = parse_script(file.clone(), &script_contents);
69    let mut script = match script {
70        Ok(script) => script,
71        Err(e) => {
72            errors.push(ScriptReadError::ParseError {
73                file: file.clone(),
74                error: e,
75            });
76            return Err(errors);
77        }
78    };
79
80    let mut includes = HashMap::new();
81    for (location, include_path) in script.includes() {
82        // Create a new ScriptFile for the included path
83        // Note: This assumes relative paths from the current script's directory
84        match parse_script_file(Some(location.file.clone()), ScriptFile::new(&include_path)) {
85            Ok(script) => {
86                includes.insert(include_path.clone(), script);
87            }
88            Err(e) => {
89                errors.extend(e);
90            }
91        }
92    }
93
94    script.includes = Arc::new(includes);
95
96    if errors.is_empty() {
97        Ok(script)
98    } else {
99        Err(errors)
100    }
101}
102
103pub fn parse_script_files<T>(files: &[T]) -> Result<Scripts, Vec<ScriptReadError>>
104where
105    for<'a> &'a T: Into<ScriptFile>,
106{
107    let mut errors = Vec::new();
108    let mut scripts = Scripts::default();
109
110    // Add initial files to the queue
111    for file in files {
112        let file = file.into();
113        let script = match parse_script_file(None, file.clone()) {
114            Ok(script) => script,
115            Err(e) => {
116                errors.extend(e);
117                continue;
118            }
119        };
120        scripts.scripts.insert(file, script);
121    }
122
123    if errors.is_empty() {
124        Ok(scripts)
125    } else {
126        Err(errors)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn accepts_clitest_shebang() {
136        let script = "#!/usr/bin/env clitest --v0\n\n$ echo hi\n! hi\n";
137        parse_script(ScriptFile::new("test.cli"), script).unwrap();
138    }
139
140    #[test]
141    fn accepts_clitest_shebang_without_v0_flag() {
142        let script = "#!/usr/bin/env clitest\n\n$ echo hi\n! hi\n";
143        parse_script(ScriptFile::new("test.cli"), script).unwrap();
144    }
145}