1use std::collections::HashMap;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum CommandError {
6 #[error("io error: {0}")]
7 Io(#[from] std::io::Error),
8 #[error("unresolved placeholder: '{0}'")]
9 UnresolvedPlaceholder(String),
10}
11
12pub fn extract_placeholders(command: &str) -> Vec<String> {
15 let mut seen = std::collections::HashSet::new();
16 let mut result = Vec::new();
17
18 let mut chars = command.chars().peekable();
19 while let Some(c) = chars.next() {
20 if c == '{' {
21 if chars.peek() == Some(&'{') {
22 chars.next(); let mut name = String::new();
24 let mut closed = false;
25 while let Some(nc) = chars.next() {
26 if nc == '}' {
27 if chars.peek() == Some(&'}') {
28 chars.next(); closed = true;
30 break;
31 } else {
32 name.push(nc);
33 }
34 } else {
35 name.push(nc);
36 }
37 }
38 if closed && !name.is_empty() && seen.insert(name.clone()) {
39 result.push(name);
40 }
41 }
42 }
43 }
44 result
45}
46
47pub fn expand_placeholders(
50 command: &str,
51 values: &HashMap<String, String>,
52) -> Result<String, CommandError> {
53 let placeholders = extract_placeholders(command);
54 let mut result = command.to_string();
55 for name in &placeholders {
56 match values.get(name) {
57 Some(value) => {
58 let pattern = format!("{{{{{}}}}}", name);
59 result = result.replace(&pattern, value);
60 }
61 None => return Err(CommandError::UnresolvedPlaceholder(name.clone())),
62 }
63 }
64 Ok(result)
65}
66
67#[derive(Debug, Clone)]
68pub struct CapturedOutput {
69 pub stdout: String,
70 pub stderr: String,
71 pub exit_code: i32,
72}
73
74pub fn run_and_capture(host_alias: &str, command: &str) -> Result<CapturedOutput, CommandError> {
76 let output = std::process::Command::new("ssh")
77 .args([host_alias, command])
78 .output()?;
79
80 Ok(CapturedOutput {
81 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
82 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
83 exit_code: output.status.code().unwrap_or(-1),
84 })
85}
86
87pub fn build_session_args(host_alias: &str, command: &str) -> Vec<String> {
90 vec![
91 "ssh".to_string(),
92 host_alias.to_string(),
93 "-t".to_string(),
94 format!("{}; $SHELL", command),
95 ]
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_no_placeholders() {
104 let result = extract_placeholders("ls -la /var/log");
105 assert!(result.is_empty());
106 }
107
108 #[test]
109 fn test_single_placeholder() {
110 let result = extract_placeholders("tail -f {{logfile}}");
111 assert_eq!(result, vec!["logfile"]);
112 }
113
114 #[test]
115 fn test_multiple_placeholders() {
116 let result = extract_placeholders("grep {{pattern}} {{file}}");
117 assert_eq!(result, vec!["pattern", "file"]);
118 }
119
120 #[test]
121 fn test_dedup_placeholders() {
122 let result = extract_placeholders("echo {{name}} {{name}} {{other}}");
123 assert_eq!(result, vec!["name", "other"]);
124 }
125
126 #[test]
127 fn test_empty_braces_ignored() {
128 let result = extract_placeholders("echo {{}}");
130 assert!(result.is_empty());
131 }
132
133 #[test]
134 fn test_expand_placeholders() {
135 let mut values = HashMap::new();
136 values.insert("logfile".to_string(), "/var/log/app.log".to_string());
137 let result = expand_placeholders("tail -f {{logfile}}", &values).unwrap();
138 assert_eq!(result, "tail -f /var/log/app.log");
139 }
140
141 #[test]
142 fn test_expand_missing_fails() {
143 let values = HashMap::new();
144 let err = expand_placeholders("tail -f {{logfile}}", &values).unwrap_err();
145 assert!(matches!(err, CommandError::UnresolvedPlaceholder(name) if name == "logfile"));
146 }
147
148 #[test]
149 fn test_expand_multiple_placeholders() {
150 let mut values = HashMap::new();
151 values.insert("pattern".to_string(), "ERROR".to_string());
152 values.insert("file".to_string(), "/var/log/syslog".to_string());
153 let result = expand_placeholders("grep {{pattern}} {{file}}", &values).unwrap();
154 assert_eq!(result, "grep ERROR /var/log/syslog");
155 }
156
157 #[test]
158 fn test_build_session_args() {
159 let args = build_session_args("myserver", "htop");
160 assert_eq!(
161 args,
162 vec!["ssh", "myserver", "-t", "htop; $SHELL"]
163 );
164 }
165}