1use anyhow::{bail, Context, Result};
2use std::env;
3pub fn var(key: &str) -> Result<String> {
20 let value = env::var(key).with_context(|| {
21 format!(
22 "'{}' not found in env. Require to evaulate another var",
23 key,
24 )
25 })?;
26
27 let mut previous_frame: Option<String> = None;
31 let mut current_frame = String::new();
32 let mut stop_on_whitespace = false;
33 let mut in_double_quotes = false;
34 let mut capturing = false;
35 let mut add_next_quote = false;
36 let mut single_quote_no_special_handeling = false;
37
38 let mut chars = value.chars().peekable();
39 loop {
40 let Some(current_char) = chars.next() else {
41 break;
42 };
43
44 if single_quote_no_special_handeling {
45 if current_char == '\\' {
46 let Some(next_char) = chars.next() else {
48 bail!("Unable to parse: Lone '\\' at end of input");
49 };
50 match next_char {
51 '"' => {
52 current_frame.push(next_char);
53 }
54 _ => {
55 current_frame.push(current_char);
57 current_frame.push(next_char);
58 }
59 }
60 continue;
61 }
62
63 current_frame.push(current_char);
65 } else {
66 match current_char {
67 '\\' => {
68 let Some(next_char) = chars.next() else {
69 bail!("Unable to parse: Lone '\\' at end of input");
70 };
71
72 match next_char {
73 '"' => {
74 current_frame.push(next_char);
75 }
76 _ => {
77 current_frame.push(current_char);
79 current_frame.push(next_char);
80 }
81 }
82
83 continue;
84 }
85 '$' => {
86 let Some(next_char) = chars.peek() else {
87 bail!("Unable to parse: Lone '$' at end of input");
88 };
89
90 previous_frame = Some(current_frame);
92 current_frame = String::new();
93
94 match next_char {
95 &'{' => {
96 chars.next().unwrap();
98 }
99 &'(' => {
100 current_frame.push(current_char);
104 }
105 _ => {
106 stop_on_whitespace = true;
107 capturing = true;
108 }
109 }
110 continue;
111 }
112 '}' => {
113 let value = var(¤t_frame).with_context(|| {
115 format!(
116 "'{}' not found in env. Require to evaulate another var",
117 value,
118 )
119 })?;
120 let Some(ref prev) = previous_frame else {
123 bail!("Found an unmatched '}}'");
124 };
125 current_frame = prev.to_owned();
126 current_frame.push_str(&value);
127 continue;
128 }
129 ' ' | '\t' => {
130 if stop_on_whitespace {
131 let value = var(¤t_frame).with_context(|| {
134 format!(
135 "'{}' not found in env. Require to evaulate another var",
136 value,
137 )
138 })?;
139 let Some(ref prev) = previous_frame else {
142 bail!("Error. TODO: better error message");
143 };
144 current_frame = prev.to_owned();
145 current_frame.push_str(&value);
146 stop_on_whitespace = false;
148 capturing = false;
149 }
151 }
152 '\'' => {
153 single_quote_no_special_handeling = !single_quote_no_special_handeling;
155 }
156 '"' => {
157 if in_double_quotes {
158 if add_next_quote {
159 current_frame.push(current_char);
160 add_next_quote = false;
161 }
162
163 if capturing {
165 let value = var(¤t_frame).with_context(|| {
168 format!(
169 "'{}' not found in env. Require to evaulate another var",
170 value,
171 )
172 })?;
173 let Some(ref prev) = previous_frame else {
176 bail!("Error. TODO: better error message");
177 };
178 current_frame = prev.to_owned();
179 current_frame.push_str(&value);
180 capturing = false;
181 }
182 } else {
183 in_double_quotes = true;
184
185 let Some(next_char) = chars.peek() else {
186 bail!("Unable to parse: Lone '\"' at end of input");
187 };
188
189 if next_char != &'$' {
190 add_next_quote = true; current_frame.push(current_char);
193 }
194 }
195
196 continue;
197 }
198 _ => {
199 }
201 }
202 current_frame.push(current_char);
203 }
204 }
205
206 if capturing {
207 let value = var(¤t_frame).with_context(|| {
212 format!(
213 "'{}' not found in env. Require to evaulate another var",
214 value,
215 )
216 })?;
217 let Some(ref prev) = previous_frame else {
220 bail!("Error. TODO: better error message");
221 };
222 current_frame = prev.to_owned();
223 current_frame.push_str(&value);
224 }
225
226 Ok(current_frame)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use pretty_assertions::{assert_eq, assert_ne};
233 use serial_test::serial;
234
235 #[test]
239 #[serial]
240 fn test_nonrecursive() {
241 let key = "KEY";
242 let value = "VALUE".to_string();
243 env::set_var(key, &value);
244 assert_eq!(var(key).unwrap(), value);
245 }
246
247 #[test]
248 #[serial]
249 fn test_recursive() {
250 let key1 = "KEY1";
251 let key2 = "KEY2";
252 env::set_var(key2, "number2");
253 env::set_var(key1, "number1 ${KEY2} number3");
254 assert_eq!(var(key2).unwrap(), "number2".to_string());
255 assert_eq!(var(key1).unwrap(), "number1 number2 number3".to_string());
256 }
257
258 #[test]
259 #[serial]
260 fn test_more_recursive() {
261 let key1 = "KEY1";
262 let key2 = "KEY2";
263 let key3 = "KEY3";
264 env::set_var(key3, "number3");
265 env::set_var(key2, "number2 ${KEY3} number4");
266 env::set_var(key1, "number1 ${KEY2} number5");
267 assert_eq!(var(key3).unwrap(), "number3".to_string());
268 assert_eq!(var(key2).unwrap(), "number2 number3 number4".to_string());
269 assert_eq!(
270 var(key1).unwrap(),
271 "number1 number2 number3 number4 number5".to_string()
272 );
273 }
274
275 #[test]
276 #[serial]
277 fn test_key_not_found() {
278 let key = "KEY";
279 let _ = env::remove_var(key); assert!(var(key).is_err());
282 }
283
284 #[test]
285 #[serial]
286 fn test_real_world_example() {
287 env::set_var("HOME", "/home/agaia");
288 env::set_var("XDG_DATA_HOME", "${HOME}/.local/share");
289 env::set_var("DATABASE_URL", "sqlite:${XDG_DATA_HOME}/taskrs/data.db");
290 let db_path = var("DATABASE_URL").unwrap();
291 assert_eq!(db_path, "sqlite:/home/agaia/.local/share/taskrs/data.db");
292 }
293
294 #[test]
295 #[serial]
296 fn test_recursive_without_braces() {
297 let key1 = "KEY1";
298 let key2 = "KEY2";
299 env::set_var(key2, "number2");
300 env::set_var(key1, "number1 $KEY2 number3");
301 assert_eq!(var(key2).unwrap(), "number2".to_string());
302 assert_eq!(var(key1).unwrap(), "number1 number2 number3".to_string());
303 }
304
305 #[test]
306 #[serial]
307 fn test_more_recursive_without_braces() {
308 let key1 = "KEY1";
309 let key2 = "KEY2";
310 let key3 = "KEY3";
311 env::set_var(key3, "number3");
312 env::set_var(key2, "number2 $KEY3 number4");
313 env::set_var(key1, "number1 $KEY2 number5");
314 assert_eq!(var(key3).unwrap(), "number3".to_string());
315 assert_eq!(var(key2).unwrap(), "number2 number3 number4".to_string());
316 assert_eq!(
317 var(key1).unwrap(),
318 "number1 number2 number3 number4 number5".to_string()
319 );
320 }
321
322 #[test]
323 #[serial]
324 fn test_no_braces_stop_on_whitespace() {
325 let key1 = "KEY1";
326 let key1_longer = "KEY1BUTLONGER";
327 let key2 = "KEY2";
328 env::set_var(key1, "1");
329 env::set_var(key1_longer, "2");
330 env::set_var(key2, "prefix$KEY1BUTLONGER ");
331 assert_eq!(var(key2).unwrap(), "prefix2 ".to_string());
332 }
333
334 #[test]
335 #[serial]
336 fn test_no_braces_no_ending_whitespace() {
337 let key = "KEY";
338 let key2 = "KEY2";
339 env::set_var(key, "test");
340 env::set_var(key2, "$KEY");
341 assert_eq!(var(key2).unwrap(), "test".to_string());
342 }
343
344 #[test]
345 #[serial]
346 fn test_do_not_eval_subexpression() {
347 let key = "KEY";
348 let value = String::from("$(subexpression)");
349 env::set_var(&key, &value);
350 assert_eq!(var(key).unwrap(), value);
351 }
352
353 #[test]
354 #[serial]
355 fn test_simple_single_quote() {
356 env::set_var("KEY", "''");
357 assert_eq!(&var("KEY").unwrap(), "''");
358 }
359
360 #[test]
361 #[serial]
362 fn test_single_quote_with_dollar() {
363 env::set_var("KEY", "'$'");
364 assert_eq!(&var("KEY").unwrap(), "'$'");
365 }
366
367 #[test]
368 #[serial]
369 fn test_single_quote_with_not_var() {
370 env::set_var("KEY", "'${KEY2}'");
371 env::set_var("KEY2", "bad");
372 assert_eq!(&var("KEY").unwrap(), "'${KEY2}'");
373 }
374
375 #[test]
376 #[serial]
377 fn test_single_quote_with_not_var_no_braces() {
378 env::set_var("KEY", "'$KEY2'");
379 env::set_var("KEY2", "bad");
380 assert_eq!(&var("KEY").unwrap(), "'$KEY2'");
381 }
382
383 #[test]
384 #[serial]
385 fn test_single_quote_with_non_matching_brace() {
386 env::set_var("KEY", "'}'");
387 assert_eq!(&var("KEY").unwrap(), "'}'");
388 }
389
390 #[test]
391 #[serial]
392 fn test_single_quotes_encapsulating_quote() {
393 env::set_var("KEY", "'\"'");
394 assert_eq!(&var("KEY").unwrap(), "'\"'");
395 }
396
397 #[test]
398 #[serial]
399 fn test_some_nonrecursive_wierd_ones_from_my_env() {
400 let vars = vec![
401 ("is_vim", "ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'"),
402 ("tmux_version", "$(tmux -V | sed -En \"s/^tmux ([0-9]+(.[0-9]+)?).*/\\1/p\")"),
403 ];
404 for (k, v) in vars {
405 env::set_var(k, v);
406 assert_eq!(&var(k).unwrap(), v);
407 }
408 }
409
410 #[test]
411 #[serial]
412 fn test_trim_quotes() {
413 let key1 = "KEY1";
414 let key2 = "KEY2";
415 env::set_var(key2, "test");
416 env::set_var(key1, "\"${KEY2}\"");
417 assert_eq!(var(key1).unwrap(), "test".to_string());
418 }
419
420 #[test]
421 #[serial]
422 fn test_trim_quotes_with_random_padding_chars() {
423 let key1 = "KEY1";
424 let key2 = "KEY2";
425 env::set_var(key2, "test");
426 env::set_var(key1, "--\"${KEY2}\"--");
427 assert_eq!(var(key1).unwrap(), "--test--".to_string());
428 }
429
430 #[test]
431 #[serial]
432 fn test_recursive_and_trim_quotes() {
433 let key1 = "KEY1";
434 let key2 = "KEY2";
435 env::set_var(key2, "number2");
436 env::set_var(key1, "number1 \"${KEY2}\" number3");
437 assert_eq!(var(key2).unwrap(), "number2".to_string());
438 assert_eq!(var(key1).unwrap(), "number1 number2 number3".to_string());
439 }
440
441 #[test]
442 #[serial]
443 fn test_more_recursive_and_trim_quotes() {
444 let key1 = "KEY1";
445 let key2 = "KEY2";
446 let key3 = "KEY3";
447 env::set_var(key3, "number3");
448 env::set_var(key2, "number2 \"${KEY3}\" number4");
449 env::set_var(key1, "number1 \"${KEY2}\" number5");
450 assert_eq!(var(key3).unwrap(), "number3".to_string());
451 assert_eq!(var(key2).unwrap(), "number2 number3 number4".to_string());
452 assert_eq!(
453 var(key1).unwrap(),
454 "number1 number2 number3 number4 number5".to_string()
455 );
456 }
457
458 #[test]
459 #[serial]
460 fn test_stop_no_brace_var_on_quotes() {
461 let key1 = "KEY1";
462 let key2 = "KEY2";
463 env::set_var(key2, "number2");
464 env::set_var(key1, "number1\"$KEY2\"number3");
465 assert_eq!(var(key2).unwrap(), "number2".to_string());
466 assert_eq!(var(key1).unwrap(), "number1number2number3".to_string());
467 }
468
469 #[test]
470 #[serial]
471 fn test_stop_no_brace_var_on_quotes_more_recursive() {
472 let key1 = "KEY1";
473 let key2 = "KEY2";
474 let key3 = "KEY3";
475 env::set_var(key3, "number3");
476 env::set_var(key2, "number2\"$KEY3\"number4");
477 env::set_var(key1, "number1\"$KEY2\"number5");
478 assert_eq!(var(key3).unwrap(), "number3".to_string());
479 assert_eq!(var(key2).unwrap(), "number2number3number4".to_string());
480 assert_eq!(
481 var(key1).unwrap(),
482 "number1number2number3number4number5".to_string()
483 );
484 }
485}