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 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 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}