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}