robin/utils/
command_utils.rs1use anyhow::{Result, anyhow};
2use regex::Regex;
3use serde_json;
4
5pub fn split_command_and_args(args: &[String]) -> (String, Vec<String>) {
6 if args.is_empty() {
7 return (String::new(), vec![]);
8 }
9
10 let mut command_parts = Vec::new();
11 let mut var_args = Vec::new();
12 let mut found_args = false;
13
14 for arg in args {
15 if arg.starts_with("--") {
16 found_args = true;
17 var_args.push(arg.clone());
18 } else if !found_args {
19 command_parts.push(arg.clone());
20 } else {
21 var_args.push(arg.clone());
22 }
23 }
24
25 (command_parts.join(" "), var_args)
26}
27
28pub fn replace_variables(script: &serde_json::Value, args: &[String]) -> Result<serde_json::Value> {
29 match script {
30 serde_json::Value::String(cmd) => {
31 let replaced = replace_variables_in_string(cmd, args)?;
32 Ok(serde_json::Value::String(replaced))
33 }
34 serde_json::Value::Array(commands) => {
35 let mut replaced_commands = Vec::new();
36 for cmd in commands {
37 if let Some(cmd_str) = cmd.as_str() {
38 let replaced = replace_variables_in_string(cmd_str, args)?;
39 replaced_commands.push(serde_json::Value::String(replaced));
40 } else {
41 replaced_commands.push(cmd.clone());
42 }
43 }
44 Ok(serde_json::Value::Array(replaced_commands))
45 }
46 _ => Ok(script.clone()),
47 }
48}
49
50fn replace_env_variables_in_string(script: &str) -> String {
59 let env_regex = Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(:?-)([^}]*)\}").unwrap();
60 let mut result = script.to_string();
61
62 for capture in env_regex.captures_iter(script) {
63 let full_match = &capture[0];
64 let var_name = &capture[1];
65 let operator = &capture[2];
66 let default = &capture[3];
67
68 let env_value = std::env::var(var_name).ok();
69 let value = match operator {
70 ":-" => match env_value {
71 Some(v) if !v.is_empty() => v,
72 _ => default.to_string(),
73 },
74 _ => env_value.unwrap_or_else(|| default.to_string()),
76 };
77
78 result = result.replace(full_match, &value);
79 }
80
81 result
82}
83
84fn replace_variables_in_string(script: &str, args: &[String]) -> Result<String> {
85 let script = replace_env_variables_in_string(script);
86 let var_regex = Regex::new(r"\{\{(\w+)(?:=([^}]+|\[[^\]]+\]))?\}\}").unwrap();
87 let mut result = script.clone();
88
89 for capture in var_regex.captures_iter(&script) {
90 let full_match = &capture[0];
91 let var_name = &capture[1];
92 let default_or_enum = capture.get(2).map(|m| m.as_str()).unwrap_or("");
93 let var_pattern = format!("--{}=", var_name);
94
95 if default_or_enum.starts_with('[') && default_or_enum.ends_with(']') {
96 let allowed_values: Vec<&str> = default_or_enum[1..default_or_enum.len() - 1]
97 .split(',')
98 .map(|s| s.trim())
99 .collect();
100
101 let value = args
102 .iter()
103 .find(|arg| arg.starts_with(&var_pattern))
104 .map(|arg| arg.trim_start_matches(&var_pattern))
105 .ok_or_else(|| anyhow!("Missing required variable: {}", var_name))?;
106
107 if !allowed_values.contains(&value) {
108 return Err(anyhow!(
109 "Value '{}' for {} must be one of: {}",
110 value,
111 var_name,
112 allowed_values.join(", ")
113 ));
114 }
115
116 result = result.replace(full_match, value);
117 } else {
118 let value = args
119 .iter()
120 .find(|arg| arg.starts_with(&var_pattern))
121 .map(|arg| arg.trim_start_matches(&var_pattern))
122 .or(if default_or_enum.is_empty() {
123 None
124 } else {
125 Some(default_or_enum)
126 })
127 .ok_or_else(|| anyhow!("Missing required variable: {}", var_name))?;
128
129 result = result.replace(full_match, value);
130 }
131 }
132
133 Ok(result)
134}