codemonument_bx 0.3.5

[DEPRECATED: Use `bx-cli` instead] Simple, cross-platform, and fast command aliases with superpowers.
Documentation
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
// This file contains the final schema into which all Bonnie configurations are parsed
// This does not reflect the actual syntax used in the configuration files themselves (see `raw_schema.rs`)

use crate::bones::{Bone, BonesCommand, BonesCore, BonesDirective};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Config {
	pub default_shell: DefaultShell,
	pub scripts: Scripts,
	// These last two properties are required for loading the config if it's cached
	pub env_files: Vec<String>,
	pub version: String,
}
impl Config {
	// Gets the command requested by the given vector of arguments
	// The given arguments are expected not to include the first program argument (`bonnie` or the like)
	// Returns the command itself, its name, and the arguments relevant thereto
	pub fn get_command_for_args(
		&self,
		args: &[String],
	) -> Result<(&Command, String, Vec<String>), String> {
		// We do everything in here for recursion
		// We need to know if this is the first time so we know to say 'command' or 'subcommand' in error messages
		fn get_command_for_scripts_and_args<'a>(
			scripts: &'a Scripts,
			args: &[String],
			first_time: bool,
		) -> Result<(&'a Command, String, Vec<String>), String> {
			// Get the name of the command
			let command_name = args.first();
			let command_name = match command_name {
                Some(command_name) => command_name,
                None => {
                    return Err(match first_time {
                        true => String::from("Please provide a command to run. You can use `bonnie help` to see the available commands in this directory."),
                        false => String::from("Please provide a subcommand to run. You can use `bonnie help` to see the available commands in this directory."),
                    })
                }
            };
			// Try to find it among those we know
			let command = scripts.get(command_name);
			let command = match command {
				Some(command) => command,
				None => {
					return Err(match first_time {
						true => format!("Unknown command '{}'.", command_name),
						false => format!("Unknown subcommand '{}'.", command_name),
					})
				}
			};
			// We found it, check if it has any unordered subcommands or a root-level command
			let final_command_and_relevant_args = match &command.subcommands {
				// It has a root-level command (which can't take arguments) and no more arguments are present, this is the command we want
				Some(_) if command.cmd.is_some() && args.len() == 1 => {
					(command, command_name.to_string(), {
						// We get the arguments to the program, excluding the name of this command, these are the arguments to be inteprolated
						let mut args_for_interpolation = args.to_vec();
						args_for_interpolation.remove(0);
						args_for_interpolation
					})
				}
				// It does, recurse on them
				Some(subcommands) if command.order.is_none() => {
					// We remove the first argument, which is the name of this, the parent command
					let mut args_without_this = args.to_vec();
					args_without_this.remove(0);
					get_command_for_scripts_and_args(subcommands, &args_without_this, false)?
					// It's no longer the first time obviously
				}
				// They're ordered and so individually uninvocable, this is the command we want
				Some(_) => (command, command_name.to_string(), {
					// We get the arguments to the program, excluding the name of this command, these are the arguments to be inteprolated
					let mut args_for_interpolation = args.to_vec();
					args_for_interpolation.remove(0);
					args_for_interpolation
				}),
				// It doesn't, this is the command we want
				None => (command, command_name.to_string(), {
					// We get the arguments to the program, excluding the name of this command, these are the arguments to be inteprolated
					let mut args_for_interpolation = args.to_vec();
					args_for_interpolation.remove(0);
					args_for_interpolation
				}),
			};

			Ok(final_command_and_relevant_args)
		}

		// Begin the recursion on the global scripts with the given arguments
		let data = get_command_for_scripts_and_args(&self.scripts, args, true)?;

		Ok(data)
	}
	// Provides a documentation message for this configuration
	// If a single command name is given, only it will be documented
	pub fn document(&self, cmd_to_doc: Option<String>) -> Result<String, String> {
		// Handle metadata about the whole file first with a preamble
		let mut meta = format!(
            "This is the help page for a configuration file. If you'd like help about Bonnie generally, run `bonnie -h` instead.
Version: {}",
            self.version,
        );
		// Environment variable files
		let mut env_files = Vec::new();
		for env_file in &self.env_files {
			env_files.push(format!("    {}", env_file));
		}
		if !env_files.is_empty() {
			meta += &format!("\nEnvironment variable files:\n{}", env_files.join("\n"));
		}

		let msg;
		if let Some(cmd_name) = cmd_to_doc {
			let cmd = self.scripts.get(&cmd_name);
			let cmd = match cmd {
                Some(cmd) => cmd,
                None => return Err(format!("Command '{}' not found. You can see all supported commands by running `bonnie help`.", cmd_name))
            };
			msg = cmd.document(&cmd_name);
		} else {
			// Loop through every command and document it
			let mut msgs = Vec::new();
			// Sort the subcommands alphabetically
			let mut cmds: Vec<(&String, &Command)> = self.scripts.iter().collect();
			cmds.sort_by(|(name, _), (name2, _)| name.cmp(name2));
			for (cmd_name, cmd) in cmds {
				msgs.push(cmd.document(cmd_name));
			}

			msg = msgs.join("\n");
		}
		// Space everything out evenly based on the longest command name (thing on the left)
		// First, we get the longest command name (thing on the left of where tabs will end up)
		// We loop through each line because otherwise subcommands stuff things up
		let mut longest_left: usize = 0;
		for line in msg.lines() {
			// Get the length of the stuff to the left of the tabs placeholder
			let left_len = line.split("{TABS}").collect::<Vec<&str>>()[0].len();
			if left_len > longest_left {
				longest_left = left_len;
			}
		}
		// Now we loop back through each line and add the appropriate amount of space
		let mut spaced_msg_lines = Vec::new();
		for line in msg.lines() {
			let left_len = line.split("{TABS}").collect::<Vec<&str>>()[0].len();
			// We want the longest line to have 4 spaces, then the rest should have (longest - length + 4) spaces
			let spaces = " ".repeat(longest_left - left_len + 4);
			spaced_msg_lines.push(line.replace("{TABS}", &spaces));
		}
		let spaced_msg = spaced_msg_lines.join("\n");

		Ok(format!("{}\n\n{}", meta, spaced_msg))
	}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DefaultShell {
	pub generic: Shell,
	pub targets: HashMap<String, Shell>, // If the required target is not found, `generic` will be tried
}
// Shells are a series of values, the first being the executable and the rest being raw arguments
// One of those arguments must contain '{COMMAND}', where the command will be interpolated
// They also specify a delimiter to use to separate multistage commands
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Shell {
	pub parts: Vec<String>,
	pub delimiter: String,
}
pub type TargetString = String; // A target like `linux` or `x86_64-unknown-linux-musl` (see `rustup` targets)
pub type Scripts = HashMap<String, Command>;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Command {
	pub args: Vec<String>,
	pub env_vars: Vec<String>,
	pub subcommands: Option<Scripts>, // Subcommands are fully-fledged commands (mostly)
	pub order: Option<BonesDirective>, // If this is specified, subcomands must not specify the `args` property, it may be specified at the top-level of this script as a sibling of `order`
	pub cmd: Option<CommandWrapper>,   // If subcommands are provided, a root command is optional
	pub description: Option<String>,   // This will be rendered in the config's help page
}
impl Command {
	// Prepares a command by interpolating everything and resolving shell/tagret logic
	// This requires the name of the command and the file's `DefaultShell` configuration
	// This interpolates arguments and environment variables
	// This returns a `BonesCommand` to be executed
	// This accepts an output for warnings (extracted for testing)
	pub fn prepare(
		&self,
		name: &str,
		prog_args: &[String],
		default_shell: &DefaultShell,
	) -> Result<Bone, String> {
		let bone = self.prepare_internal(name, prog_args, default_shell, None)?;

		Ok(bone)
	}
	// This is the internal command preparation logic, which is called recursively.
	// This also takes top-level arguments for recursing on ordered subcommands
	fn prepare_internal(
		&self,
		name: &str,
		prog_args: &[String],
		default_shell: &DefaultShell,
		top_level_args: Option<&[String]>,
	) -> Result<Bone, String> {
		let args = match top_level_args {
			Some(args) => args,
			None => &self.args,
		};
		let at_top_level = top_level_args.is_none();
		if self.subcommands.is_none() || (self.subcommands.is_some() && self.cmd.is_some()) {
			// We have either a direct command or a parent command that has irrelevant subcommands, either way we're interpolating into `cmd`
			// Get the vector of command wrappers
			// Assuming the transformation logic works, an error can't occur here
			let command_wrapper = self.cmd.as_ref().unwrap();
			// Interpolate for each individual command
			// We have to do this in a for loop for `?`
			let mut cmd_strs: Vec<String> = Vec::new();
			let (cmds, shell) = command_wrapper.get_commands_and_shell(default_shell);
			for cmd_str in cmds {
				let with_env_vars = Command::interpolate_env_vars(&cmd_str, &self.env_vars)?;
				let (with_args, remaining_args) =
					Command::interpolate_specific_args(&with_env_vars, name, args, prog_args)?;
				let ready_cmd =
					Command::interpolate_remaining_arguments(&with_args, &remaining_args);
				cmd_strs.push(ready_cmd);
			}

			Ok(
				// This does not contain recursive `BonesCommands`, so it's `Bone::Simple`
				Bone::Simple(BonesCore {
					// We join every stage of the command into one, separated by the given delimiters
					cmd: cmd_strs.join(&shell.delimiter),
					// The shell is then just the vector of executable and arguments
					shell: shell.parts.to_vec(),
				}),
			)
		} else if self.subcommands.is_some() && self.order.is_some() {
			// First, we resolve all the subcommands to vectors of strings to actually run
			let mut cmds: HashMap<String, Bone> = HashMap::new();
			// Now we run checks on whether the correct number of arguments have been provided if we're at the very top level
			// Otherwise error messages will relate to irrelevant subcommands
			// We don't check the case where too few arguments were provided because that's irrelevant (think about it)
			if at_top_level && args.len() > prog_args.len() {
				return Err(
                    format!(
                        "The command '{command}' requires {num_required_args} argument(s), but {num_given_args} argument(s) were provided (too few). Please provide all the required arguments.",
                        command=name,
                        num_required_args=args.len(),
                        num_given_args=&prog_args.len()
                    )
                );
			}
			// We `.unwrap()` here because we know more than the compiler
			for (subcommand_name, subcommand) in self.subcommands.as_ref().unwrap().iter() {
				// Parse the subcommand
				// We parse in the top-level arguments because ordered subcommands can't take their own, they inherit from this level (or the level this level inherits from, etc.)
				let cmd = subcommand.prepare_internal(
					subcommand_name,
					prog_args,
					default_shell,
					Some(args),
				)?;
				cmds.insert(subcommand_name.to_string(), cmd);
			}

			// Now we return a complex `Bone` (because it contains a `BonesCommand` with a directive)
			Ok(Bone::Complex(
				BonesCommand::new(self.order.as_ref().unwrap(), cmds), // We know more than the compiler by the check above
			))
		} else {
			// This should not be possible!
			panic!("Critical logic failure in preparing command. You should report this as a bug.");
		}
	}
	// Interpolates specific arguments (doesn't handle `%%`)
	// This takes a string to interpolate into and doesn't take `self` so the order is open
	// This returns the readied command string and the remaining arguments or an error if an argument couldn't be substituted in
	// Errors for when the argument can't be interpolated can be silenced for ordered subcommands (which have a universal argument list for many subcommands)
	fn interpolate_specific_args(
		cmd_str: &str,
		name: &str,
		args: &[String],
		prog_args: &[String],
	) -> Result<(String, Vec<String>), String> {
		// Check if the correct number of arguments was provided
		// Even if we're inserting the rest later, we still need the mandatory ones
		if args.len() > prog_args.len() {
			return Err(
                format!(
                    "The command '{command}' requires {num_required_args} argument(s), but {num_given_args} argument(s) were provided (too few). Please provide all the required arguments.",
                    command=name,
                    num_required_args=args.len(),
                    num_given_args=&prog_args.len()
                )
            );
		}
		// We don't warn if there are too many and we're not inserting the rest with `%%` later because that would mean checking every potential subcommand for `%%` as well if they exist
		let mut with_args = cmd_str.to_string();
		// We need to know the index so we can correlate to the index of the argument in `args`
		for (idx, arg) in args.iter().enumerate() {
			// The arrays are the same length, see above check
			// All arguments are shown in the command string as `%name` or the like, so we get that whole string
			let given_value = &prog_args[idx];
			let arg_with_sign = "%".to_string() + arg;
			let new_command = with_args.replace(&arg_with_sign, given_value);
			// We don't check if we changed something because that doesn't work for multistage or ordered subcommands
			with_args = new_command;
		}
		// Get the program args after a certain point so they can be inserted with `%%` if necessary
		// We do this by getting the part of slice after the specific arguments
		let (_, remaining_args) = prog_args.split_at(args.len());

		Ok((with_args, remaining_args.to_vec())) // FIXME
	}
	// Interpolates environment variables
	// This takes a string to interpolate into, the environment variables to interpolate, and the name of the command
	// This doesn't take `self` so the order is open
	// This returns the readied command string only, or an error relating to environment variable loading
	fn interpolate_env_vars(cmd_str: &str, env_vars: &[String]) -> Result<String, String> {
		let mut with_env_vars = cmd_str.to_string();
		for env_var_name in env_vars.iter() {
			// Load the environment variable
			let env_var = env::var(env_var_name);
			let env_var = match env_var {
                Ok(env_var) => env_var,
                Err(_) => return Err(format!("The environment variable '{}' couldn't be loaded. This means it either hasn't been defined (you may need to load another environment variable file) or contains invalid characters.", env_var_name))
            };
			// Interpolate it into the command itself
			let to_replace = "%".to_string() + env_var_name;
			let new_command = with_env_vars.replace(&to_replace, &env_var);
			// We don't check if we changed something because that doesn't work for multistage or ordered subcommands
			with_env_vars = new_command;
		}

		Ok(with_env_vars)
	}
	// Interpolates all the given arguments at `%%` if it exists
	// This takes a string to interpolate into and doesn't take `self` so the order is open
	// This returns the readied command string only
	fn interpolate_remaining_arguments(cmd_str: &str, prog_args: &[String]) -> String {
		// This is just a simple `replace` operation for the operator `%%`
		// Split the command by the block insertion operator `%%`
		let mut interpolated = String::new();
		let split_on_operator: Vec<&str> = cmd_str.split("%%").collect();
		for (idx, part) in split_on_operator.iter().enumerate() {
			if idx == split_on_operator.len() - 1 {
				// This is the last element, there's no operator after this
				interpolated.push_str(part);
			} else if part.ends_with('\\') {
				// This part ends with `\`, meaning the operator was escaped
				// We just give the `%%` back
				// We only give back the part up until the escape character
				interpolated.push_str(&part[0..part.len() - 1]);
				interpolated.push_str("%%");
			} else {
				// There's a legitimate operator that should be at the end of this part
				// We push the program's arguments
				interpolated.push_str(part);
				interpolated.push_str(&prog_args.join(" "));
			}
		}

		interpolated
	}
	// Gets a documentation message for this command based on its metadata and the `desc` properties
	fn document(&self, name: &str) -> String {
		let mut msgs = Vec::new();
		// Get the user-given docs (if they exist)
		let doc = match &self.description {
			Some(desc) => desc.to_string(),
			None => String::from("no 'desc' property set"),
		};

		// Set up the left side (command name and some arguments info)
		let mut left = String::new();
		// Environment variables (before the command name)
		for env_var in &self.env_vars {
			left += &format!("<{}> ", env_var);
		}
		// Command name
		left += name;
		// Arguments (after the command name)
		for arg in &self.args {
			left += &format!(" <{}>", arg);
		}
		// Ordered or not
		if self.order.is_some() {
			left += " (ordered)";
		}
		// TODO handle '%%' as `[...]`
		// That's a placeholder for a number of tabs that spaces everything evenly
		msgs.push(format!("{}{{TABS}}{}", left, doc));

		// Loop through every subcommand and document it
		if let Some(subcommands_map) = &self.subcommands {
			// Sort the subcommands alphabetically
			let mut subcommands_iter: Vec<(&String, &Command)> = subcommands_map.iter().collect();
			subcommands_iter.sort_by(|(name, _), (name2, _)| name.cmp(name2));
			for (cmd_name, cmd) in subcommands_iter {
				let subcmd_doc = cmd.document(cmd_name);
				msgs.push(
					// We add four spaces in front of every line (that way it works recursively for nested subcommands)
					format!("    {}", subcmd_doc.replace("\n", "\n    ")),
				);
			}
		}

		msgs.join("\n")
	}
}
// This defines how the command runs on different targets
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandWrapper {
	pub generic: CommandCore,
	pub targets: HashMap<TargetString, CommandCore>, // If empty or target not found, `generic` will be used
}
impl CommandWrapper {
	// Gets the command to run, interpolated into a shell from the ambient OS information
	// This critically resolves which target we're running on
	fn get_commands_and_shell(&self, default_shell: &DefaultShell) -> (Vec<String>, Shell) {
		// Get the current target (unfortuantely we can't actually get the value out of `cfg!` yet...)
		// If the user needs to set custom commands based on target arch etc., they can write a script for it, this is exhaustive enough!
		let running_on = match true {
			_ if cfg!(target_os = "windows") => "windows",
			_ if cfg!(target_os = "macos") => "macos",
			_ if cfg!(target_os = "ios") => "ios",
			_ if cfg!(target_os = "linux") => "linux",
			_ if cfg!(target_os = "android") => "android",
			_ if cfg!(target_os = "freebsd") => "freebsd",
			_ if cfg!(target_os = "dragonfly") => "dragonfly",
			_ if cfg!(target_os = "openbsd") => "openbsd",
			_ if cfg!(target_os = "netbsd") => "netbsd",
			_ => "unknown", // If they want to, the user could actually specify something for this (like begging to be run somewhere that makes sense)
		};
		// See if that target is specified explicitly
		let target_specific_command_core = self.targets.get(running_on);
		let command_core = match target_specific_command_core {
			Some(command_core) => command_core,
			None => &self.generic,
		};
		// Get the commands as a vector ready for interpolation
		let cmd = &command_core.exec;
		// Get the shell, using the configured per-file default if it was undefined
		let shell = match &command_core.shell {
			Some(shell) => shell,
			None => {
				// If a particular shell has been configured for the current target, use that
				// Otherwise, use the generic
				// Remember that the schema transformation inserts program-level defaults if they aren't configured for the file by the user
				let target_specific_shell = default_shell.targets.get(running_on);
				match target_specific_shell {
					Some(default_shell) => default_shell,
					None => &default_shell.generic,
				}
			}
		};

		(cmd.to_vec(), shell.clone())
	}
}
// This is the lowest level of command specification, there is no more recursion allowed here (thus avoiding circularity)
// Actual command must be specified here are strings (with potential interpolation of arguments and environment variables)
// This can also define which shell the command will use
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandCore {
	pub exec: Vec<String>, // These are the actual commands that will be run (named differently to avoid collisions)
	pub shell: Option<Shell>, // If given, this is the shell it will be run in, or the `default_shell` config for this target will be used
}