1use crate::bones::{Bone, BonesCommand, BonesCore, BonesDirective};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::env;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct Config {
11 pub default_shell: DefaultShell,
12 pub scripts: Scripts,
13 pub env_files: Vec<String>,
15 pub version: String,
16}
17impl Config {
18 pub fn get_command_for_args(
22 &self,
23 args: &[String],
24 ) -> Result<(&Command, String, Vec<String>), String> {
25 fn get_command_for_scripts_and_args<'a>(
28 scripts: &'a Scripts,
29 args: &[String],
30 first_time: bool,
31 ) -> Result<(&'a Command, String, Vec<String>), String> {
32 let command_name = args.first();
34 let command_name = match command_name {
35 Some(command_name) => command_name,
36 None => {
37 return Err(match first_time {
38 true => String::from("Please provide a command to run. You can use `bonnie help` to see the available commands in this directory."),
39 false => String::from("Please provide a subcommand to run. You can use `bonnie help` to see the available commands in this directory."),
40 })
41 }
42 };
43 let command = scripts.get(command_name);
45 let command = match command {
46 Some(command) => command,
47 None => {
48 return Err(match first_time {
49 true => format!("Unknown command '{}'.", command_name),
50 false => format!("Unknown subcommand '{}'.", command_name),
51 })
52 }
53 };
54 let final_command_and_relevant_args = match &command.subcommands {
56 Some(_) if command.cmd.is_some() && args.len() == 1 => {
58 (command, command_name.to_string(), {
59 let mut args_for_interpolation = args.to_vec();
61 args_for_interpolation.remove(0);
62 args_for_interpolation
63 })
64 }
65 Some(subcommands) if command.order.is_none() => {
67 let mut args_without_this = args.to_vec();
69 args_without_this.remove(0);
70 get_command_for_scripts_and_args(subcommands, &args_without_this, false)?
71 }
73 Some(_) => (command, command_name.to_string(), {
75 let mut args_for_interpolation = args.to_vec();
77 args_for_interpolation.remove(0);
78 args_for_interpolation
79 }),
80 None => (command, command_name.to_string(), {
82 let mut args_for_interpolation = args.to_vec();
84 args_for_interpolation.remove(0);
85 args_for_interpolation
86 }),
87 };
88
89 Ok(final_command_and_relevant_args)
90 }
91
92 let data = get_command_for_scripts_and_args(&self.scripts, args, true)?;
94
95 Ok(data)
96 }
97 pub fn document(&self, cmd_to_doc: Option<String>) -> Result<String, String> {
100 let mut meta = format!(
102 "This is the help page for a configuration file. If you'd like help about Bonnie generally, run `bonnie -h` instead.
103Version: {}",
104 self.version,
105 );
106 let mut env_files = Vec::new();
108 for env_file in &self.env_files {
109 env_files.push(format!(" {}", env_file));
110 }
111 if !env_files.is_empty() {
112 meta += &format!("\nEnvironment variable files:\n{}", env_files.join("\n"));
113 }
114
115 let msg;
116 if let Some(cmd_name) = cmd_to_doc {
117 let cmd = self.scripts.get(&cmd_name);
118 let cmd = match cmd {
119 Some(cmd) => cmd,
120 None => return Err(format!("Command '{}' not found. You can see all supported commands by running `bonnie help`.", cmd_name))
121 };
122 msg = cmd.document(&cmd_name);
123 } else {
124 let mut msgs = Vec::new();
126 let mut cmds: Vec<(&String, &Command)> = self.scripts.iter().collect();
128 cmds.sort_by(|(name, _), (name2, _)| name.cmp(name2));
129 for (cmd_name, cmd) in cmds {
130 msgs.push(cmd.document(cmd_name));
131 }
132
133 msg = msgs.join("\n");
134 }
135 let mut longest_left: usize = 0;
139 for line in msg.lines() {
140 let left_len = line.split("{TABS}").collect::<Vec<&str>>()[0].len();
142 if left_len > longest_left {
143 longest_left = left_len;
144 }
145 }
146 let mut spaced_msg_lines = Vec::new();
148 for line in msg.lines() {
149 let left_len = line.split("{TABS}").collect::<Vec<&str>>()[0].len();
150 let spaces = " ".repeat(longest_left - left_len + 4);
152 spaced_msg_lines.push(line.replace("{TABS}", &spaces));
153 }
154 let spaced_msg = spaced_msg_lines.join("\n");
155
156 Ok(format!("{}\n\n{}", meta, spaced_msg))
157 }
158}
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
160pub struct DefaultShell {
161 pub generic: Shell,
162 pub targets: HashMap<String, Shell>, }
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168pub struct Shell {
169 pub parts: Vec<String>,
170 pub delimiter: String,
171}
172pub type TargetString = String; pub type Scripts = HashMap<String, Command>;
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176pub struct Command {
177 pub args: Vec<String>,
178 pub env_vars: Vec<String>,
179 pub subcommands: Option<Scripts>, pub order: Option<BonesDirective>, pub cmd: Option<CommandWrapper>, pub description: Option<String>, }
184impl Command {
185 pub fn prepare(
191 &self,
192 name: &str,
193 prog_args: &[String],
194 default_shell: &DefaultShell,
195 ) -> Result<Bone, String> {
196 let bone = self.prepare_internal(name, prog_args, default_shell, None)?;
197
198 Ok(bone)
199 }
200 fn prepare_internal(
203 &self,
204 name: &str,
205 prog_args: &[String],
206 default_shell: &DefaultShell,
207 top_level_args: Option<&[String]>,
208 ) -> Result<Bone, String> {
209 let args = match top_level_args {
210 Some(args) => args,
211 None => &self.args,
212 };
213 let at_top_level = top_level_args.is_none();
214 if self.subcommands.is_none() || (self.subcommands.is_some() && self.cmd.is_some()) {
215 let command_wrapper = self.cmd.as_ref().unwrap();
219 let mut cmd_strs: Vec<String> = Vec::new();
222 let (cmds, shell) = command_wrapper.get_commands_and_shell(default_shell);
223 for cmd_str in cmds {
224 let with_env_vars = Command::interpolate_env_vars(&cmd_str, &self.env_vars)?;
225 let (with_args, remaining_args) =
226 Command::interpolate_specific_args(&with_env_vars, name, args, prog_args)?;
227 let ready_cmd =
228 Command::interpolate_remaining_arguments(&with_args, &remaining_args);
229 cmd_strs.push(ready_cmd);
230 }
231
232 Ok(
233 Bone::Simple(BonesCore {
235 cmd: cmd_strs.join(&shell.delimiter),
237 shell: shell.parts.to_vec(),
239 }),
240 )
241 } else if self.subcommands.is_some() && self.order.is_some() {
242 let mut cmds: HashMap<String, Bone> = HashMap::new();
244 if at_top_level && args.len() > prog_args.len() {
248 return Err(
249 format!(
250 "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.",
251 command=name,
252 num_required_args=args.len(),
253 num_given_args=&prog_args.len()
254 )
255 );
256 }
257 for (subcommand_name, subcommand) in self.subcommands.as_ref().unwrap().iter() {
259 let cmd = subcommand.prepare_internal(
262 subcommand_name,
263 prog_args,
264 default_shell,
265 Some(args),
266 )?;
267 cmds.insert(subcommand_name.to_string(), cmd);
268 }
269
270 Ok(Bone::Complex(
272 BonesCommand::new(self.order.as_ref().unwrap(), cmds), ))
274 } else {
275 panic!("Critical logic failure in preparing command. You should report this as a bug.");
277 }
278 }
279 fn interpolate_specific_args(
284 cmd_str: &str,
285 name: &str,
286 args: &[String],
287 prog_args: &[String],
288 ) -> Result<(String, Vec<String>), String> {
289 if args.len() > prog_args.len() {
292 return Err(
293 format!(
294 "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.",
295 command=name,
296 num_required_args=args.len(),
297 num_given_args=&prog_args.len()
298 )
299 );
300 }
301 let mut with_args = cmd_str.to_string();
303 for (idx, arg) in args.iter().enumerate() {
305 let given_value = &prog_args[idx];
308 let arg_with_sign = "%".to_string() + arg;
309 let new_command = with_args.replace(&arg_with_sign, given_value);
310 with_args = new_command;
312 }
313 let (_, remaining_args) = prog_args.split_at(args.len());
316
317 Ok((with_args, remaining_args.to_vec())) }
319 fn interpolate_env_vars(cmd_str: &str, env_vars: &[String]) -> Result<String, String> {
324 let mut with_env_vars = cmd_str.to_string();
325 for env_var_name in env_vars.iter() {
326 let env_var = env::var(env_var_name);
328 let env_var = match env_var {
329 Ok(env_var) => env_var,
330 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))
331 };
332 let to_replace = "%".to_string() + env_var_name;
334 let new_command = with_env_vars.replace(&to_replace, &env_var);
335 with_env_vars = new_command;
337 }
338
339 Ok(with_env_vars)
340 }
341 fn interpolate_remaining_arguments(cmd_str: &str, prog_args: &[String]) -> String {
345 let mut interpolated = String::new();
348 let split_on_operator: Vec<&str> = cmd_str.split("%%").collect();
349 for (idx, part) in split_on_operator.iter().enumerate() {
350 if idx == split_on_operator.len() - 1 {
351 interpolated.push_str(part);
353 } else if part.ends_with('\\') {
354 interpolated.push_str(&part[0..part.len() - 1]);
358 interpolated.push_str("%%");
359 } else {
360 interpolated.push_str(part);
363 interpolated.push_str(&prog_args.join(" "));
364 }
365 }
366
367 interpolated
368 }
369 fn document(&self, name: &str) -> String {
371 let mut msgs = Vec::new();
372 let doc = match &self.description {
374 Some(desc) => desc.to_string(),
375 None => String::from("no 'desc' property set"),
376 };
377
378 let mut left = String::new();
380 for env_var in &self.env_vars {
382 left += &format!("<{}> ", env_var);
383 }
384 left += name;
386 for arg in &self.args {
388 left += &format!(" <{}>", arg);
389 }
390 if self.order.is_some() {
392 left += " (ordered)";
393 }
394 msgs.push(format!("{}{{TABS}}{}", left, doc));
397
398 if let Some(subcommands_map) = &self.subcommands {
400 let mut subcommands_iter: Vec<(&String, &Command)> = subcommands_map.iter().collect();
402 subcommands_iter.sort_by(|(name, _), (name2, _)| name.cmp(name2));
403 for (cmd_name, cmd) in subcommands_iter {
404 let subcmd_doc = cmd.document(cmd_name);
405 msgs.push(
406 format!(" {}", subcmd_doc.replace("\n", "\n ")),
408 );
409 }
410 }
411
412 msgs.join("\n")
413 }
414}
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
417pub struct CommandWrapper {
418 pub generic: CommandCore,
419 pub targets: HashMap<TargetString, CommandCore>, }
421impl CommandWrapper {
422 fn get_commands_and_shell(&self, default_shell: &DefaultShell) -> (Vec<String>, Shell) {
425 let running_on = match true {
428 _ if cfg!(target_os = "windows") => "windows",
429 _ if cfg!(target_os = "macos") => "macos",
430 _ if cfg!(target_os = "ios") => "ios",
431 _ if cfg!(target_os = "linux") => "linux",
432 _ if cfg!(target_os = "android") => "android",
433 _ if cfg!(target_os = "freebsd") => "freebsd",
434 _ if cfg!(target_os = "dragonfly") => "dragonfly",
435 _ if cfg!(target_os = "openbsd") => "openbsd",
436 _ if cfg!(target_os = "netbsd") => "netbsd",
437 _ => "unknown", };
439 let target_specific_command_core = self.targets.get(running_on);
441 let command_core = match target_specific_command_core {
442 Some(command_core) => command_core,
443 None => &self.generic,
444 };
445 let cmd = &command_core.exec;
447 let shell = match &command_core.shell {
449 Some(shell) => shell,
450 None => {
451 let target_specific_shell = default_shell.targets.get(running_on);
455 match target_specific_shell {
456 Some(default_shell) => default_shell,
457 None => &default_shell.generic,
458 }
459 }
460 };
461
462 (cmd.to_vec(), shell.clone())
463 }
464}
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
469pub struct CommandCore {
470 pub exec: Vec<String>, pub shell: Option<Shell>, }