lib/bones.rs
1// Bones is Bonnie's command execution runtime, which mainly handles ordered subcommands
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::process::Command as OsCommand;
7
8// This enables recursion of ordered subcommands (which would be the most complex use-case of Bonnie thus far)
9// This really represents (from Bonnie's perspective) a future for an exit code
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub enum Bone {
12 Simple(BonesCore),
13 Complex(BonesCommand),
14}
15impl Bone {
16 // Executes this command, returning its exit code
17 // This takes an optional buffer to write data about the command being executed in testing
18 pub fn run(
19 &self,
20 name: &str,
21 verbose: bool,
22 output: &mut impl std::io::Write,
23 ) -> Result<i32, String> {
24 match self {
25 Bone::Simple(core) => {
26 // Execute the command core
27 let exit_code = core.execute(name, verbose, output)?;
28 // Return the exit code of the command sequence
29 Ok(exit_code)
30 }
31 Bone::Complex(command) => {
32 // If it's complex and thus recursive, we depend on the Bones language parser
33 command.run(verbose, output)
34 }
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct BonesCommand {
41 // A HashMap of command names to vectors of raw commands to be executed
42 // The commands to run are expected to have interpolation and target/shell resolution already done
43 cmds: HashMap<String, Bone>,
44 // The directive from of how to run the commands (written as per Bones' specification)
45 directive: BonesDirective,
46}
47impl BonesCommand {
48 // This creates a full Bones command
49 // This is used when actual logic is given by the user (ordered subcommands)
50 pub fn new(directive: &BonesDirective, cmds: HashMap<String, Bone>) -> Self {
51 Self {
52 directive: directive.clone(),
53 cmds,
54 }
55 }
56 // Runs a Bones command by evaluating the directive itself and calling commands in sequence recursively
57 // Currently, the logic of the Bones language lives here
58 fn run(&self, verbose: bool, output: &mut impl std::io::Write) -> Result<i32, String> {
59 // This system is highly recursive, so everything is done in this function for progressively less complex directives
60 fn run_for_directive(
61 directive: &BonesDirective,
62 cmds: &HashMap<String, Bone>,
63 verbose: bool,
64 output: &mut impl std::io::Write,
65 ) -> Result<i32, String> {
66 // Get the token, which names the command we'll be running
67 let command_name = &directive.0;
68 // Now get the corresponding Bone if it exists
69 let bone = cmds.get(command_name);
70 let bone = match bone {
71 Some(bone) => bone,
72 None => return Err(format!("Error in executing Bones directive: subcommand '{}' not found. This is probably a typo in your Bonnie configuration.", command_name)),
73 };
74 // Now execute it and get the exit code (this may recursively call this function if ordered subcommands are nested, but that dcoesn't matter)
75 // Bonnie treats all command cores as futures for an exit code, we don't care about any side effects (printing, server execution, etc.)
76 let exit_code = bone.run(command_name, verbose, output)?;
77 // Iterate over the conditions given and check if any of them match that exit code
78 // We'll run the first one that does (even if more do after that)
79 // TODO document the above behaviour
80 let mut final_exit_code = exit_code;
81 for (operator, directive) in directive.1.iter() {
82 if operator.matches(&exit_code) {
83 // An operator has matched, check if it has an associated directive
84 final_exit_code = match directive {
85 // If it does, run that and get its exit code
86 Some(directive) => run_for_directive(directive, cmds, verbose, output)?,
87 // If not, return the exit code we just got above
88 None => exit_code,
89 };
90 }
91 }
92
93 // All nestings have resolved to one exit code, we return it
94 Ok(final_exit_code)
95 }
96
97 // Begin the recursion on this top-level directive
98 // This will eventually return the exit code from the lowest level of recursion, which we return
99 let exit_code = run_for_directive(&self.directive, &self.cmds, verbose, output)?;
100 Ok(exit_code)
101 }
102}
103
104// A directive telling the Bones engine how to progress between ordered subcommands
105// This maps the command to run to a set of conditions as to how to proceed based on its exit code
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub struct BonesDirective(String, HashMap<BonesOperator, Option<BonesDirective>>);
108// This is used for direct parsing, before we've had a chance to handle the operators
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110struct RawBonesDirective(String, HashMap<String, Option<RawBonesDirective>>);
111impl RawBonesDirective {
112 // This converts to a `BonesDirective` by parsing the operator strings into full operators
113 fn convert_to_proper(&self) -> Result<BonesDirective, String> {
114 // Parse the conditions `HashMap`
115 let mut parsed_conditions: HashMap<BonesOperator, Option<BonesDirective>> = HashMap::new();
116 for (raw_operator, raw_directive) in &self.1 {
117 let operator = BonesOperator::parse_str(raw_operator)?;
118 // Parse the directive recursively
119 // We need to use a full `match` statement for `?`
120 let directive = match raw_directive {
121 Some(raw_directive) => Some(raw_directive.convert_to_proper()?),
122 None => None,
123 };
124 parsed_conditions.insert(operator, directive);
125 }
126
127 Ok(
128 // We don't need to do any parsing on the command name, just the conditions
129 BonesDirective(self.0.to_string(), parsed_conditions),
130 )
131 }
132}
133// Bones operators can be more than just exit codes, this defines their possibilities
134// For deserialization, this is left tagged (we pre-parse)
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, std::hash::Hash)]
136pub enum BonesOperator {
137 // A simple exit code comparison
138 ExitCode(i32),
139 // A negative exit code comparison ('anything except ...')
140 NotExitCode(i32),
141 // An operator that will match no matter what its command returned
142 Any,
143 // An operator that will never match no matter what its command returned
144 None,
145 // The requirement for command success (an alias for `ExitCode(0)`)
146 Success,
147 // The requirement for command failure (an alias for `NotExitCode(0)`)
148 Failure,
149 // Matches if any contained operators match (or statement)
150 Union(Vec<BonesOperator>),
151 // Matches if all contained operators match (and statement)
152 // No it shouldn't be possible to have multiple exit codes match simultaneously but this is here anyway for potential future additions
153 Intersection(Vec<BonesOperator>),
154}
155impl BonesOperator {
156 // Checks if the given exit code matches this operator
157 fn matches(&self, exit_code: &i32) -> bool {
158 // This can be recursive due to the `Union` an d`Intersection` variants
159 fn matches(exit_code: &i32, variant: &BonesOperator) -> bool {
160 // Go through each different type of operator possible
161 match variant {
162 BonesOperator::Success => *exit_code == 0,
163 BonesOperator::Failure => *exit_code != 0,
164 BonesOperator::ExitCode(comparison) => exit_code == comparison,
165 BonesOperator::NotExitCode(comparison) => exit_code != comparison,
166 BonesOperator::Any => true,
167 BonesOperator::None => false,
168 BonesOperator::Union(operators) => {
169 let mut is_match = false;
170 for operator in operators {
171 let op_matches = operator.matches(exit_code);
172 // We only need one of them to be true
173 if op_matches {
174 is_match = true;
175 break;
176 }
177 }
178 is_match
179 }
180 BonesOperator::Intersection(operators) => {
181 let mut is_match = false;
182 for operator in operators {
183 let op_matches = operator.matches(exit_code);
184 // We only need one of them to be false (aka. all of them have to be true)
185 is_match = op_matches;
186 if !op_matches {
187 break;
188 }
189 }
190 is_match
191 }
192 }
193 }
194
195 matches(exit_code, self)
196 }
197 // Parses a string operator given in a directive string into a fully-fledged variant
198 fn parse_str(raw_operator: &str) -> Result<Self, String> {
199 // Attempt to parse it as an exit code integer (we'll use that twice)
200 let exit_code = raw_operator.parse::<i32>();
201 let operator = match raw_operator {
202 _ if exit_code.is_ok() => BonesOperator::ExitCode(exit_code.unwrap()),
203 _ if raw_operator.starts_with('!') => {
204 let exit_code_str = raw_operator.get(1..);
205 let exit_code = match exit_code_str {
206 Some(exit_code) => match exit_code.parse::<i32>() {
207 Ok(exit_code) => exit_code,
208 Err(_) => return Err(format!("Couldn't parse exit code as 32-bit integer from `NotExitCode` operator invocation '{}'.", raw_operator))
209 },
210 None => return Err(format!("Couldn't extract exit code from `NotExitCode` operator invocation '{}'.", raw_operator))
211 };
212 BonesOperator::NotExitCode(exit_code)
213 }
214 // The next four are simple because they have no attached data
215 "Any" => BonesOperator::Any,
216 "None" => BonesOperator::None,
217 "Success" => BonesOperator::Success,
218 "Failure" => BonesOperator::Failure,
219 // These require recursion
220 _ if raw_operator.contains('|') => {
221 let parts: Vec<&str> = raw_operator.split('|').collect();
222 let mut operators: Vec<BonesOperator> = Vec::new();
223 // Recursively parse each operator
224 for part in parts {
225 operators.push(BonesOperator::parse_str(part)?)
226 }
227 BonesOperator::Union(operators)
228 }
229 _ if raw_operator.contains('+') => {
230 let parts: Vec<&str> = raw_operator.split('+').collect();
231 let mut operators: Vec<BonesOperator> = Vec::new();
232 // Recursively parse each operator
233 for part in parts {
234 operators.push(BonesOperator::parse_str(part)?)
235 }
236 BonesOperator::Intersection(operators)
237 }
238 _ => {
239 return Err(format!(
240 "Unrecognized operator '{}' in Bones directive.",
241 raw_operator
242 ))
243 }
244 };
245
246 Ok(operator)
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
251pub struct BonesCore {
252 pub cmd: String, // All the stages are joined by the delimiter
253 pub shell: Vec<String>, // Vector of executable and arguments thereto
254}
255impl BonesCore {
256 fn execute(
257 &self,
258 name: &str,
259 verbose: bool,
260 output: &mut impl std::io::Write,
261 ) -> Result<i32, String> {
262 // Get the executable from the shell (the first element)
263 let executable = self.shell.get(0);
264 let executable = match executable {
265 // If the shell is not universal to all stages, we return an error
266 // We should not have to interpolate anything into the executable
267 Some(executable) if executable.contains("{COMMAND}") => return Err(format!("The shell for the command '{}' attempts to interpolate the command in its first element, which was expected to be a string literal with no interpolation, as it is an executable.", name)),
268 Some(executable) => executable,
269 None => return Err(format!("The shell for the command '{}' is empty. Shells must contain at least one element as an executable to invoke.", name))
270 };
271 // Get the arguments to that executable
272 // We interpolate the command in where necessary
273 let args = self.shell.get(1..);
274 let args: Vec<String> = match args {
275 Some(args) => args
276 .iter()
277 .map(|part| part.replace("{COMMAND}", &self.cmd))
278 .collect(),
279 // If there are no arguments, we really don't care, shells can be as weird as they want
280 None => Vec::new(),
281 };
282 // If we're in debug, write details about the command to the given output (technical)
283 if cfg!(debug_assertions) {
284 writeln!(output, "{}, {:?}", executable, args)
285 .expect("Failed to write technical information.");
286 }
287 // If the user wants it, write the actual command we'll run to the given output
288 if verbose {
289 writeln!(
290 output,
291 "Running command '{}' with arguments '{:?}'.",
292 executable, args
293 )
294 .expect("Failed to write verbose information.");
295 }
296 // Prepare the child process
297 let child = OsCommand::new(&executable).args(args).spawn();
298
299 // The child must be mutable so we can wait for it to finish later
300 let mut child = match child {
301 Ok(child) => child,
302 Err(_) => return Err(
303 format!(
304 "Command '{}' failed to run. This doesn't mean the command produced an error, but that the process couldn't even be initialised.",
305 &name
306 )
307 )
308 };
309 // If we don't wait on the child, any long-running commands will print into the prompt because the parent terminates first (try it yourself with the `long` command)
310 let child = child.wait();
311 let exit_status = match child {
312 Ok(exit_status) => exit_status,
313 Err(_) => return Err(
314 format!(
315 "Command '{}' didn't run (parent unable to wait on child process). See the Bonnie documentation for more details on this problem.",
316 &name
317 )
318 )
319 };
320
321 // We now need to pass that exit code through so Bonnie can terminate with it (otherwise `&&` chaining doesn't work as expected, etc.)
322 // This will work on both Unix and Windows (and so theoretically any other weird OSes that make any sense at all)
323 Ok(match exit_status.code() {
324 Some(exit_code) => exit_code, // If we have an exit code, use it
325 None if exit_status.success() => 0, // If we don't, but we know the command succeeded, return 0 (success code)
326 None => 1, // If we don't know an exit code but we know that the command failed, return 1 (general error code)
327 })
328 }
329}
330
331// This parses a directive string into a `BonesDirective` that can be executed
332// The logic of parsing and executing is made separate so we can cache the parsed form for large configuration files
333// This function basically interprets a miniature programming language
334// Right now, this is quite slow due to its extensive use of RegEx, any ideas to speed it up would be greatly appreciated!
335pub fn parse_directive_str(directive_str: &str) -> Result<BonesDirective, String> {
336 let directive_json: String;
337 // Check if we have the alternative super-simple form (just one command, rare but easy to parse)
338 if !directive_str.contains('{') {
339 directive_json = "[\"".to_string() + directive_str + "\", {}]"
340 } else {
341 // We transform the directive string into compliant JSON with a series of substitutions
342 // Execute non-regex substitutions
343 let stage1 = directive_str.replace("}", "}]");
344 // We can unwrap all the RegExps because we know they're valid
345 // Please refer to the Bones specification to understand how these work
346 let re1 = Regex::new(r"(?m)^(\s*)(.+) => (.+)\b \{").unwrap();
347 let sub1 = "$1\"$2\": [\"$3\", {";
348 let re2 = Regex::new(r"(?m)^(\s*)(.+) => (.+)\b").unwrap();
349 let sub2 = "$1\"$2\": [\"$3\", {}]";
350 let re3 = Regex::new(r"^\s*\b(.+) \{").unwrap();
351 let sub3 = "[\"$1\", {";
352 // Execute each of those substitutions
353 let stage2 = re1.replace_all(&stage1, sub1);
354 let stage3 = re2.replace_all(&stage2, sub2);
355 directive_json = re3.replace_all(&stage3, sub3).to_string();
356 }
357 // Now we can deserialize that directly using Serde
358 let raw_directive = serde_json::from_str::<RawBonesDirective>(&directive_json);
359 let raw_directive = match raw_directive {
360 Ok(raw_directive) => raw_directive,
361 Err(err) => return Err(format!("The following error occurred while parsing a Bones directive: '{}'. Please note that your code is transformed in several ways before this step, so you may need to refer to the documentation on Bones directives.", err))
362 };
363 // Now we handle the operators
364 let directive = raw_directive.convert_to_proper()?;
365
366 Ok(directive)
367}