codemonument_bx 0.3.5

[DEPRECATED: Use `bx-cli` instead] Simple, cross-platform, and fast command aliases with superpowers.
Documentation
// 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)
}