opendev_runtime/custom_commands/
expansion.rs1use std::collections::HashMap;
4
5use regex::Regex;
6
7use super::CustomCommand;
8
9pub(super) fn parse_frontmatter(content: &str) -> (HashMap<String, String>, String) {
15 let trimmed = content.trim_start();
16 if !trimmed.starts_with("---") {
17 return (HashMap::new(), content.to_string());
18 }
19
20 let after_first = &trimmed[3..];
22 let after_first = after_first.trim_start_matches(['\r', '\n']);
23
24 if let Some(end_pos) = after_first.find("\n---") {
25 let fm_text = &after_first[..end_pos];
26 let body_start = end_pos + 4; let body = after_first[body_start..].trim_start_matches(['\r', '\n']);
28
29 let mut map = HashMap::new();
30 for line in fm_text.lines() {
31 let line = line.trim();
32 if line.is_empty() || line.starts_with('#') {
33 continue;
34 }
35 if let Some((key, value)) = line.split_once(':') {
36 let key = key.trim().to_string();
37 let value = value
38 .trim()
39 .trim_matches('"')
40 .trim_matches('\'')
41 .to_string();
42 map.insert(key, value);
43 }
44 }
45
46 (map, body.to_string())
47 } else {
48 (HashMap::new(), content.to_string())
50 }
51}
52
53pub(super) fn expand_shell_commands(content: &str) -> String {
58 let re = Regex::new(r"!`([^`]+)`").expect("valid regex");
59 re.replace_all(content, |caps: ®ex::Captures| {
60 let cmd = &caps[1];
61 match std::process::Command::new("sh").arg("-c").arg(cmd).output() {
62 Ok(output) if output.status.success() => {
63 String::from_utf8_lossy(&output.stdout).trim().to_string()
64 }
65 Ok(output) => {
66 let stderr = String::from_utf8_lossy(&output.stderr);
67 format!("[error: {cmd}: {}]", stderr.trim())
68 }
69 Err(e) => format!("[error: {cmd}: {e}]"),
70 }
71 })
72 .to_string()
73}
74
75impl CustomCommand {
76 pub fn expand(&self, arguments: &str, context: Option<&HashMap<String, String>>) -> String {
84 let mut result = self.template.replace("$ARGUMENTS", arguments);
85
86 let parts: Vec<&str> = if arguments.is_empty() {
88 Vec::new()
89 } else {
90 arguments.split_whitespace().collect()
91 };
92 for (i, part) in parts.iter().enumerate() {
93 let placeholder = format!("${}", i + 1);
94 result = result.replace(&placeholder, part);
95 }
96
97 let re = Regex::new(r"\$\d+").expect("valid regex");
99 result = re.replace_all(&result, "").to_string();
100
101 if let Some(ctx) = context {
103 for (key, value) in ctx {
104 let placeholder = format!("${}", key.to_uppercase());
105 result = result.replace(&placeholder, value);
106 }
107 }
108
109 result = expand_shell_commands(&result);
111
112 result.trim().to_string()
113 }
114}
115
116#[cfg(test)]
117#[path = "expansion_tests.rs"]
118mod tests;