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