systemd_unit_edit/
specifier.rs1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
13pub struct SpecifierContext {
14 values: HashMap<String, String>,
15}
16
17impl SpecifierContext {
18 pub fn new() -> Self {
20 Self {
21 values: HashMap::new(),
22 }
23 }
24
25 pub fn set(&mut self, specifier: &str, value: &str) {
36 self.values.insert(specifier.to_string(), value.to_string());
37 }
38
39 pub fn get(&self, specifier: &str) -> Option<&str> {
41 self.values.get(specifier).map(|s| s.as_str())
42 }
43
44 pub fn with_unit_name(unit_name: &str) -> Self {
63 let mut ctx = Self::new();
64
65 ctx.set("N", unit_name);
67
68 let name_without_suffix = unit_name
70 .rsplit_once('.')
71 .map(|(name, _)| name)
72 .unwrap_or(unit_name);
73 ctx.set("n", name_without_suffix);
74
75 if let Some((prefix, instance_with_suffix)) = name_without_suffix.split_once('@') {
77 ctx.set("p", prefix);
78 ctx.set("i", instance_with_suffix);
79 }
80
81 ctx
82 }
83
84 pub fn expand(&self, input: &str) -> String {
101 let mut result = String::new();
102 let mut chars = input.chars().peekable();
103
104 while let Some(ch) = chars.next() {
105 if ch == '%' {
106 if let Some(&next) = chars.peek() {
107 chars.next(); if next == '%' {
109 result.push('%');
111 } else {
112 let specifier = next.to_string();
114 if let Some(value) = self.get(&specifier) {
115 result.push_str(value);
116 } else {
117 result.push('%');
119 result.push(next);
120 }
121 }
122 } else {
123 result.push('%');
125 }
126 } else {
127 result.push(ch);
128 }
129 }
130
131 result
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_basic_expansion() {
141 let mut ctx = SpecifierContext::new();
142 ctx.set("i", "instance");
143 ctx.set("u", "user");
144
145 assert_eq!(ctx.expand("Hello %i"), "Hello instance");
146 assert_eq!(ctx.expand("%u@%i"), "user@instance");
147 assert_eq!(ctx.expand("/home/%u/%i"), "/home/user/instance");
148 }
149
150 #[test]
151 fn test_percent_escape() {
152 let ctx = SpecifierContext::new();
153 assert_eq!(ctx.expand("100%% complete"), "100% complete");
154 assert_eq!(ctx.expand("%%u"), "%u");
155 }
156
157 #[test]
158 fn test_unknown_specifier() {
159 let ctx = SpecifierContext::new();
160 assert_eq!(ctx.expand("%x"), "%x");
162 assert_eq!(ctx.expand("test %z end"), "test %z end");
163 }
164
165 #[test]
166 fn test_percent_at_end() {
167 let ctx = SpecifierContext::new();
168 assert_eq!(ctx.expand("test%"), "test%");
169 }
170
171 #[test]
172 fn test_with_unit_name_simple() {
173 let ctx = SpecifierContext::with_unit_name("foo.service");
174 assert_eq!(ctx.get("N"), Some("foo.service"));
175 assert_eq!(ctx.get("n"), Some("foo"));
176 assert_eq!(ctx.get("p"), None);
177 assert_eq!(ctx.get("i"), None);
178 }
179
180 #[test]
181 fn test_with_unit_name_template() {
182 let ctx = SpecifierContext::with_unit_name("foo@bar.service");
183 assert_eq!(ctx.get("N"), Some("foo@bar.service"));
184 assert_eq!(ctx.get("n"), Some("foo@bar"));
185 assert_eq!(ctx.get("p"), Some("foo"));
186 assert_eq!(ctx.get("i"), Some("bar"));
187
188 assert_eq!(ctx.expand("Unit %N"), "Unit foo@bar.service");
189 assert_eq!(ctx.expand("Prefix %p"), "Prefix foo");
190 assert_eq!(ctx.expand("Instance %i"), "Instance bar");
191 }
192
193 #[test]
194 fn test_with_unit_name_complex_instance() {
195 let ctx = SpecifierContext::with_unit_name("getty@tty1.service");
196 assert_eq!(ctx.get("p"), Some("getty"));
197 assert_eq!(ctx.get("i"), Some("tty1"));
198 assert_eq!(ctx.expand("/dev/%i"), "/dev/tty1");
199 }
200
201 #[test]
202 fn test_multiple_specifiers() {
203 let mut ctx = SpecifierContext::new();
204 ctx.set("i", "inst");
205 ctx.set("u", "usr");
206 ctx.set("h", "/home/usr");
207
208 assert_eq!(
209 ctx.expand("%h/.config/%i/data"),
210 "/home/usr/.config/inst/data"
211 );
212 }
213
214 #[test]
215 fn test_no_specifiers() {
216 let ctx = SpecifierContext::new();
217 assert_eq!(ctx.expand("plain text"), "plain text");
218 assert_eq!(ctx.expand("/etc/config"), "/etc/config");
219 }
220}