haku/
var.rs

1use std::collections::HashMap;
2use std::convert::From;
3use std::env;
4use std::usize;
5
6use crate::output;
7
8struct Escape {
9    what: &'static str,
10    with: &'static str,
11}
12
13/// Result of execution of an external command with shell
14#[derive(Clone, Debug, PartialEq)]
15pub struct ExecResult {
16    /// process exit code
17    pub(crate) code: i32,
18    /// process standard output
19    pub(crate) stdout: String,
20}
21
22/// Variable value
23#[derive(Clone, Debug, PartialEq)]
24pub enum VarValue {
25    /// undefined
26    Undefined,
27    /// contains a string value
28    Str(String),
29    /// contains an integer value
30    Int(i64),
31    /// contains a list of strings
32    List(Vec<String>),
33    /// contains a result of external shell execution command
34    Exec(ExecResult),
35}
36
37impl From<String> for VarValue {
38    fn from(s: String) -> Self {
39        VarValue::Str(s)
40    }
41}
42impl From<&str> for VarValue {
43    fn from(s: &str) -> Self {
44        VarValue::Str(s.to_string())
45    }
46}
47impl From<i64> for VarValue {
48    fn from(i: i64) -> Self {
49        VarValue::Int(i)
50    }
51}
52impl From<i32> for VarValue {
53    fn from(i: i32) -> Self {
54        VarValue::Int(i as i64)
55    }
56}
57impl From<u32> for VarValue {
58    fn from(i: u32) -> Self {
59        VarValue::Int(i as i64)
60    }
61}
62impl From<bool> for VarValue {
63    fn from(b: bool) -> Self {
64        if b {
65            VarValue::Int(1)
66        } else {
67            VarValue::Int(0)
68        }
69    }
70}
71
72impl ToString for VarValue {
73    fn to_string(&self) -> String {
74        match self {
75            VarValue::Undefined => String::new(),
76            VarValue::Str(s) => s.clone(),
77            VarValue::Int(i) => format!("{}", i),
78            VarValue::List(v) => {
79                let mut s = String::new();
80                for it in v.iter() {
81                    if !s.is_empty() {
82                        s += "\n";
83                    }
84                    s += it;
85                }
86                s
87            }
88            VarValue::Exec(ex) => {
89                if ex.code == 0 {
90                    ex.stdout.to_string()
91                } else {
92                    String::new()
93                }
94            }
95        }
96    }
97}
98
99impl VarValue {
100    /// Converts variable value to a one-line string. The difference between this and to_string
101    /// functions is that this one joins lists and standard output with space instead of
102    /// new line character. It is useful when passing a list or the result of previous shell
103    /// execution to the following shell call.
104    ///
105    /// Example:
106    ///
107    /// a = `ls *.txt`
108    /// rm "${a}"
109    ///
110    /// First, it gets a list of files with txt extension and then removes them in one call
111    pub(crate) fn to_flat_string(&self) -> String {
112        match self {
113            VarValue::Undefined => String::new(),
114            VarValue::Str(s) => s.clone(),
115            VarValue::Int(i) => format!("{}", i),
116            VarValue::List(v) => {
117                let mut s = String::new();
118                for it in v.iter() {
119                    if !s.is_empty() {
120                        s += " ";
121                    }
122                    s += it;
123                }
124                s
125            }
126            VarValue::Exec(ex) => {
127                if ex.code == 0 {
128                    let mut s = String::new();
129                    for l in ex.stdout.lines() {
130                        if !s.is_empty() {
131                            s += " ";
132                        }
133                        s += l.trim_end();
134                    }
135                    s
136                } else {
137                    String::new()
138                }
139            }
140        }
141    }
142
143    /// Return `true` if a variable is truthy:
144    ///
145    /// * non-zero integer value
146    /// * non-empty string
147    /// * non-empty list (it must either have more than one item or the first item must be
148    /// non-empty string
149    /// * result of shell execution with 0 exit code
150    pub(crate) fn is_true(&self) -> bool {
151        match self {
152            VarValue::Undefined => false,
153            VarValue::Int(i) => *i != 0,
154            VarValue::Str(s) => !s.is_empty(),
155            VarValue::List(v) => !v.is_empty() && !v[0].is_empty(),
156            VarValue::Exec(er) => er.code == 0,
157        }
158    }
159
160    /// Converts a value to integer:
161    ///
162    /// * string is parsed as i64
163    /// * shell execution is process exit code
164    /// * list - the first list item is parsed as i64
165    pub(crate) fn to_int(&self) -> i64 {
166        match self {
167            VarValue::Undefined => 0,
168            VarValue::Int(i) => *i,
169            VarValue::Str(s) => {
170                if s.is_empty() {
171                    0
172                } else {
173                    s.parse::<i64>().unwrap_or(0)
174                }
175            }
176            VarValue::List(v) => {
177                if v.is_empty() || v[0].is_empty() {
178                    0
179                } else {
180                    v[0].parse::<i64>().unwrap_or(0)
181                }
182            }
183            VarValue::Exec(ex) => {
184                if ex.stdout.is_empty() {
185                    0
186                } else if let Some(s) = ex.stdout.lines().next() {
187                    let strim = s.trim();
188                    strim.parse::<i64>().unwrap_or(0)
189                } else {
190                    0
191                }
192            }
193        }
194    }
195
196    /// Returns `true` if both values are equivalent
197    fn cmp_eq(&self, val: &VarValue) -> bool {
198        match self {
199            VarValue::Undefined => match val {
200                VarValue::Undefined => true,
201                _ => false,
202            },
203            VarValue::List(lst1) => match val {
204                VarValue::List(lst2) => {
205                    if lst1.len() != lst2.len() {
206                        false
207                    } else {
208                        for (idx, val) in lst1.iter().enumerate() {
209                            if val != &lst2[idx] {
210                                return false;
211                            }
212                        }
213                        true
214                    }
215                }
216                VarValue::Exec(ex_val) => ex_val.code == 0 && ex_val.stdout.trim() == self.to_string(),
217                VarValue::Str(s) => &self.to_flat_string() == s,
218                VarValue::Int(i) => {
219                    if lst1.len() != 1 {
220                        false
221                    } else {
222                        lst1[0] == format!("{}", *i)
223                    }
224                }
225                _ => false,
226            },
227            VarValue::Exec(ex) => match val {
228                VarValue::Exec(ex_val) => ex.code == ex_val.code,
229                VarValue::Str(s) => &ex.stdout == s,
230                VarValue::Int(i) => i64::from(ex.code) == *i,
231                VarValue::List(_) => ex.code == 0 && ex.stdout == val.to_string(),
232                _ => false,
233            },
234            VarValue::Str(s) => match val {
235                VarValue::Exec(ex_val) => s == &ex_val.stdout,
236                VarValue::Str(s_val) => s == s_val,
237                VarValue::Int(i) => s == &format!("{}", *i),
238                VarValue::List(_) => s == &val.to_flat_string(),
239                _ => false,
240            },
241            VarValue::Int(i) => match val {
242                VarValue::Exec(ex_val) => *i == i64::from(ex_val.code),
243                VarValue::Str(s_val) => &format!("{}", *i) == s_val,
244                VarValue::Int(i_val) => *i == *i_val,
245                VarValue::List(lst) => {
246                    if lst.len() != 1 {
247                        false
248                    } else {
249                        lst[0] == format!("{}", *i)
250                    }
251                }
252                _ => false,
253            },
254        }
255    }
256    /// Returns `true` if this value is greater than `val`.
257    ///
258    /// NOTE: for execution result the successful execution (exit code 0) is always greater
259    /// than failed one (exit code is not 0)
260    fn cmp_greater(&self, val: &VarValue) -> bool {
261        match self {
262            VarValue::Undefined => false,
263            VarValue::Exec(ex) => match val {
264                VarValue::Exec(ex_val) => {
265                    if ex.code == 0 && ex_val.code != 0 {
266                        true
267                    } else if ex.code != 0 && ex_val.code == 0 {
268                        false
269                    } else {
270                        ex.code > ex_val.code
271                    }
272                }
273                VarValue::Str(s) => ex.stdout > *s,
274                VarValue::Int(i) => i64::from(ex.code) > *i,
275                VarValue::List(_) => ex.code == 0 && ex.stdout > val.to_string(),
276                _ => true,
277            },
278            VarValue::Str(s) => match val {
279                VarValue::Exec(ex_val) => *s > ex_val.stdout,
280                VarValue::Str(s_val) => s > s_val,
281                VarValue::Int(i) => *s > format!("{}", *i),
282                VarValue::List(_) => *s > val.to_flat_string(),
283                _ => true,
284            },
285            VarValue::Int(i) => match val {
286                VarValue::Exec(ex_val) => *i > i64::from(ex_val.code),
287                VarValue::Str(s_val) => format!("{}", *i) > *s_val,
288                VarValue::Int(i_val) => *i > *i_val,
289                VarValue::List(lst) => {
290                    if lst.is_empty() {
291                        true
292                    } else {
293                        let vv = lst[0].parse::<i64>().unwrap_or(0i64);
294                        *i > vv
295                    }
296                }
297                _ => true,
298            },
299            VarValue::List(lst) => match val {
300                VarValue::Exec(ex) => ex.code != 0 || self.to_string() > ex.stdout,
301                VarValue::Str(s) => self.to_flat_string() > *s,
302                VarValue::Int(i) => {
303                    if lst.is_empty() {
304                        false
305                    } else {
306                        let vv = lst[0].parse::<i64>().unwrap_or(0i64);
307                        vv > *i
308                    }
309                }
310                VarValue::List(lst2) => {
311                    if lst.len() > lst2.len() {
312                        true
313                    } else {
314                        for (idx, v) in lst.iter().enumerate() {
315                            if *v <= lst2[idx] {
316                                return false;
317                            }
318                        }
319                        true
320                    }
321                }
322                _ => true,
323            },
324        }
325    }
326    /// Returns `true` if this value is less than `val`.
327    ///
328    /// NOTE: for execution result the failed execution (exit code is not 0) is always less
329    /// than the successful one (exit code is 0)
330    fn cmp_less(&self, val: &VarValue) -> bool {
331        match self {
332            VarValue::Undefined => match val {
333                VarValue::Undefined => false,
334                _ => true,
335            },
336            VarValue::Exec(ex) => match val {
337                VarValue::Exec(ex_val) => {
338                    if ex.code == 0 && ex_val.code != 0 {
339                        true
340                    } else if ex.code != 0 && ex_val.code == 0 {
341                        false
342                    } else {
343                        ex.code > ex_val.code
344                    }
345                }
346                VarValue::Str(s) => ex.stdout < *s,
347                VarValue::Int(i) => i64::from(ex.code) < *i,
348                VarValue::List(_) => ex.code != 0 || ex.stdout < val.to_string(),
349                _ => false,
350            },
351            VarValue::Str(s) => match val {
352                VarValue::Exec(ex_val) => *s < ex_val.stdout,
353                VarValue::Str(s_val) => s < s_val,
354                VarValue::Int(i) => *s < format!("{}", i),
355                VarValue::List(_) => *s < val.to_flat_string(),
356                _ => false,
357            },
358            VarValue::Int(i) => match val {
359                VarValue::Exec(ex_val) => *i < i64::from(ex_val.code),
360                VarValue::Str(s_val) => format!("{}", i) < *s_val,
361                VarValue::Int(i_val) => i < i_val,
362                VarValue::List(lst) => {
363                    if lst.is_empty() {
364                        false
365                    } else {
366                        let vv = lst[0].parse::<i64>().unwrap_or(0i64);
367                        *i < vv
368                    }
369                }
370                _ => false,
371            },
372            VarValue::List(lst) => match val {
373                VarValue::Exec(ex) => ex.code == 0 && self.to_string() < ex.stdout,
374                VarValue::Str(s) => self.to_flat_string() < *s,
375                VarValue::Int(i) => {
376                    if lst.is_empty() {
377                        false
378                    } else {
379                        let vv = lst[0].parse::<i64>().unwrap_or(0i64);
380                        vv < *i
381                    }
382                }
383                VarValue::List(lst2) => {
384                    if lst.len() > lst2.len() {
385                        false
386                    } else {
387                        for (idx, v) in lst.iter().enumerate() {
388                            if *v >= lst2[idx] {
389                                return false;
390                            }
391                        }
392                        true
393                    }
394                }
395                _ => false,
396            },
397        }
398    }
399
400    /// Returns `true` if values are not equivalent
401    fn cmp_neq(&self, val: &VarValue) -> bool {
402        !self.cmp_eq(val)
403    }
404    /// Returns `true` if this value is greater than or equal to `val`
405    ///
406    /// NOTE: for execution result the successful execution (exit code 0) is always greater
407    /// than failed one (exit code is not 0)
408    fn cmp_eq_or_greater(&self, val: &VarValue) -> bool {
409        !self.cmp_less(val)
410    }
411    /// Returns `true` if this value is less than or equal to `val`
412    ///
413    /// NOTE: for execution result the failed execution (exit code is not 0) is always less
414    /// than the successful one (exit code is 0)
415    fn cmp_eq_or_less(&self, val: &VarValue) -> bool {
416        !self.cmp_greater(val)
417    }
418    /// Generic comparison function that calls the correct method depending on comparison sign
419    pub(crate) fn cmp(&self, val: &VarValue, cmp_op: &str) -> bool {
420        match cmp_op {
421            "==" => self.cmp_eq(val),
422            "!=" => self.cmp_neq(val),
423            ">" => self.cmp_greater(val),
424            "<" => self.cmp_less(val),
425            ">=" => self.cmp_eq_or_greater(val),
426            "<=" => self.cmp_eq_or_less(val),
427            _ => unreachable!(),
428        }
429    }
430}
431
432/// Script variable
433pub struct Var {
434    /// variable's name
435    name: String,
436    /// value
437    value: VarValue,
438}
439impl Default for Var {
440    fn default() -> Self {
441        Var { name: String::from(""), value: VarValue::Undefined }
442    }
443}
444
445/// Variable manager: adds/removes variables, interpolates strings by substituting variable values
446pub(crate) struct VarMgr {
447    /// values from CLI - user defined ones. Used to initialize recipe local variables
448    pub(crate) free: Vec<String>,
449    /// list of current recipe's local variables
450    pub(crate) recipe_vars: Vec<Var>,
451    /// list of script global variables
452    vars: Vec<Var>,
453    /// verbosity level when displaying info for a user to standard output
454    verbosity: usize,
455    /// list of environment variables defined by the running script
456    pub(crate) env: HashMap<String, String>,
457}
458
459impl VarMgr {
460    pub(crate) fn new(verbosity: usize) -> Self {
461        VarMgr { recipe_vars: Vec::new(), vars: Vec::new(), free: Vec::new(), verbosity, env: HashMap::new() }
462    }
463
464    /// Change or creates a recipe local variable.
465    pub(crate) fn set_recipe_var(&mut self, name: &str, val: VarValue) {
466        output!(self.verbosity, 2, "Setting recipe var {}", name);
467        for v in self.recipe_vars.iter_mut() {
468            if v.name == name {
469                output!(self.verbosity, 2, "Changing recipe {} to {:?}", name, val);
470                v.value = val;
471                return;
472            }
473        }
474        output!(self.verbosity, 2, "New recipe var {}: {:?}", name, val);
475        self.recipe_vars.push(Var { name: name.to_string(), value: val });
476    }
477
478    /// First, it looks for recipe local variable. If it exists, its values changes. Otherwise,
479    /// it modifies or create a global variable.
480    pub(crate) fn set_var(&mut self, name: &str, val: VarValue) {
481        output!(self.verbosity, 2, "Setting a var {}", name);
482        for v in self.recipe_vars.iter_mut() {
483            if v.name == name {
484                output!(self.verbosity, 2, "Changing recipe {} to {:?}", name, val);
485                v.value = val;
486                return;
487            }
488        }
489        for v in self.vars.iter_mut() {
490            if v.name == name {
491                output!(self.verbosity, 2, "Changing var {} to {:?}", name, val);
492                v.value = val;
493                return;
494            }
495        }
496        output!(self.verbosity, 2, "New var {}: {:?}", name, val);
497        self.vars.push(Var { name: name.to_string(), value: val });
498    }
499
500    /// Returns a value of a variable. First it looks for a recipe local. If it does not exist,
501    /// looks for a global variable. The last check is to look for the environment variable.
502    /// Returns `Undefined` if no variable exists.
503    pub(crate) fn var(&self, name: &str) -> VarValue {
504        for v in self.recipe_vars.iter() {
505            if v.name == name {
506                output!(self.verbosity, 2, "Local recipe var {} found", name);
507                return v.value.clone();
508            }
509        }
510        for v in self.vars.iter() {
511            if v.name == name {
512                output!(self.verbosity, 2, "Global var {} found", name);
513                return v.value.clone();
514            }
515        }
516
517        if let Some(s) = self.env.get(name) {
518            output!(self.verbosity, 2, "Use environment variable from script {}", name);
519            return VarValue::Str(s.to_string());
520        }
521
522        if let Ok(s) = env::var(name) {
523            output!(self.verbosity, 2, "Use environment variable {}", name);
524            return VarValue::Str(s);
525        }
526
527        output!(self.verbosity, 2, "Variable {} not found", name);
528        VarValue::Undefined
529    }
530
531    /// Replaces variable names with its values in strings and shell command lines. A variable
532    /// name must be enclosed into curly braces and preceded with `$`.
533    ///
534    /// Example:
535    ///
536    /// msg = "Done"
537    /// echo "Message: ${msg}"
538    ///
539    /// Output: `Message: Done`
540    ///
541    ///To print `$` character just duplicate it or use a slash:
542    ///
543    /// msg = "Done"
544    /// echo "Message: $${msg}"
545    /// echo "Message: \${msg}
546    ///
547    /// Both echoes print out `Message: ${msg}".
548    ///
549    /// Argument `flat` determines how to interpolate multi-line/-item values:
550    ///
551    /// * `true` - join all lines with a space (for shell execution)
552    /// * `false` - join all lines with new line character (for `print`)
553    ///
554    /// Besides replacing variable names it replaces a few escape sequences: `\n`, `\\`, and `\t`.
555    pub(crate) fn interpolate(&self, in_str: &str, flat: bool) -> String {
556        let mut start_s: usize;
557        let mut start_d: usize;
558        let mut res = String::new();
559        let mut s_ptr = in_str;
560        let escapes: Vec<Escape> = vec![
561            Escape { what: "\\\\", with: "\\" },
562            Escape { what: "\\n", with: "\n" },
563            Escape { what: "\\t", with: "\t" },
564            Escape { what: "\\$", with: "$" },
565            Escape { what: "\\'", with: "'" },
566            Escape { what: "\\\"", with: "\"" },
567        ];
568
569        while !s_ptr.is_empty() {
570            start_d = s_ptr.find('$').unwrap_or(usize::MAX);
571            start_s = s_ptr.find('\\').unwrap_or(usize::MAX);
572
573            if start_s == usize::MAX && start_d == usize::MAX {
574                return res + s_ptr;
575            }
576
577            if start_s == usize::MAX || start_d < start_s {
578                res += &s_ptr[..start_d];
579                s_ptr = &s_ptr[start_d..];
580                // escaped '$'
581                if s_ptr.starts_with("$$") {
582                    res += "$";
583                    s_ptr = &s_ptr["$$".len()..];
584                    continue;
585                }
586
587                // stray '$' - skip it for now
588                if !s_ptr.starts_with("${") {
589                    res += "$";
590                    s_ptr = &s_ptr["$".len()..];
591                    continue;
592                }
593
594                // we have "${" - variable substitution starts
595                s_ptr = &s_ptr["${".len()..];
596                match s_ptr.find('}') {
597                    None => return res + "${" + s_ptr,
598                    Some(bp) => {
599                        let var_name = &s_ptr[..bp];
600                        if flat {
601                            res += self.var(var_name).to_flat_string().as_str();
602                        } else {
603                            res += self.var(var_name).to_string().as_str();
604                        }
605                        s_ptr = &s_ptr[(bp + "}".len())..];
606                    }
607                }
608                continue;
609            }
610
611            res += &s_ptr[..start_s];
612            s_ptr = &s_ptr[start_s..];
613            let mut escaped = false;
614            for esc in escapes.iter() {
615                if s_ptr.starts_with(esc.what) {
616                    res += esc.with;
617                    s_ptr = &s_ptr[esc.what.len()..];
618                    escaped = true;
619                    break;
620                }
621            }
622            if !escaped {
623                res += "\\";
624                s_ptr = &s_ptr["\\".len()..];
625            }
626        }
627        res
628    }
629}
630
631#[cfg(test)]
632mod var_test {
633    use super::*;
634
635    #[test]
636    fn var_mgr() {
637        let mut v = VarMgr::new(0);
638        v.set_var("abc", VarValue::Int(123));
639        v.recipe_vars.push(Var { name: "def".to_string(), value: VarValue::Int(10) });
640        let v1 = v.var("def");
641        assert_eq!(v1, VarValue::Int(10));
642        let v1 = v.var("abc");
643        assert_eq!(v1, VarValue::Int(123));
644        let v1 = v.var("abc2");
645        assert_eq!(v1, VarValue::Undefined);
646        v.recipe_vars.push(Var { name: "abc".to_string(), value: VarValue::Int(50) });
647        let v1 = v.var("abc");
648        assert_eq!(v1, VarValue::Int(50));
649        v.recipe_vars.clear();
650        let v1 = v.var("abc");
651        assert_eq!(v1, VarValue::Int(123));
652    }
653
654    #[test]
655    fn interpolate_no_matches() {
656        let mut v = VarMgr::new(0);
657        v.set_var("abc", VarValue::Str("123".to_string()));
658        // no brackets
659        let instr = "text $abc end";
660        let outstr = v.interpolate(instr, false);
661        assert_eq!(instr, &outstr);
662        // escaped $
663        let instr = "text $${abc} end";
664        let outstr = v.interpolate(instr, false);
665        assert_eq!("text ${abc} end", &outstr);
666        // inclosed variable name
667        let instr = "text ${abc end";
668        let outstr = v.interpolate(instr, false);
669        assert_eq!(instr, &outstr);
670        // empty string
671        let instr = "";
672        let outstr = v.interpolate(instr, false);
673        assert_eq!(instr, &outstr);
674    }
675
676    #[test]
677    fn interpolate_one_match() {
678        let mut v = VarMgr::new(0);
679        v.set_var("abc", VarValue::from("123"));
680        // escaped $
681        let instr = "text $$${abc} end";
682        let outstr = v.interpolate(instr, false);
683        assert_eq!("text $123 end", &outstr);
684        // no escaping
685        let instr = "text ${abc} end";
686        let outstr = v.interpolate(instr, false);
687        assert_eq!("text 123 end", &outstr);
688        // no escaping and no variable
689        let instr = "text ${abc2} end";
690        let outstr = v.interpolate(instr, false);
691        assert_eq!("text  end", &outstr);
692        // variable only
693        let instr = "${abc}";
694        let outstr = v.interpolate(instr, false);
695        assert_eq!("123", &outstr);
696        // non-existing variable only
697        let instr = "${abcd}";
698        let outstr = v.interpolate(instr, false);
699        assert_eq!("", &outstr);
700    }
701
702    #[test]
703    fn interpolate_few_matches() {
704        let mut v = VarMgr::new(0);
705        v.set_var("abc", VarValue::from("123"));
706        v.set_var("def", VarValue::from("test"));
707        // escaped $
708        let instr = "text ${def}$$${abc} end";
709        let outstr = v.interpolate(instr, false);
710        assert_eq!("text test$123 end", &outstr);
711        // no escaping
712        let instr = "text ${abc} end ${def}";
713        let outstr = v.interpolate(instr, false);
714        assert_eq!("text 123 end test", &outstr);
715        // no escaping and no variable
716        let instr = "${def} text ${abc2} end";
717        let outstr = v.interpolate(instr, false);
718        assert_eq!("test text  end", &outstr);
719        // variables only
720        let instr = "${def}${abc}";
721        let outstr = v.interpolate(instr, false);
722        assert_eq!("test123", &outstr);
723    }
724
725    #[test]
726    fn interpolate_mixed_matches() {
727        let mut v = VarMgr::new(0);
728        v.set_var("abc", VarValue::from("123"));
729        v.set_var("def", VarValue::from("test"));
730        // escaped $
731        let instr = "text ${def}$${abc} end";
732        let outstr = v.interpolate(instr, false);
733        assert_eq!("text test${abc} end", &outstr);
734        // no escaping
735        let instr = "text $abc end ${def}";
736        let outstr = v.interpolate(instr, false);
737        assert_eq!("text $abc end test", &outstr);
738    }
739
740    #[test]
741    fn unescaped() {
742        let v = VarMgr::new(0);
743        let ostr = v.interpolate("abcde 12345", false);
744        assert_eq!(&ostr, "abcde 12345");
745        let ostr = v.interpolate("", false);
746        assert_eq!(&ostr, "");
747        let ostr = v.interpolate("1234\\5678\\90", false);
748        assert_eq!(&ostr, "1234\\5678\\90");
749        let ostr = v.interpolate("\\t1234\\\\5678\\n90\\t", false);
750        assert_eq!(&ostr, "\t1234\\5678\n90\t");
751        // escaped quotes
752        let instr = "text \\\" test \\'";
753        let outstr = v.interpolate(instr, false);
754        assert_eq!("text \" test '", &outstr);
755    }
756
757    #[test]
758    fn mixed_interpolation() {
759        let mut v = VarMgr::new(0);
760        v.set_var("abc", VarValue::from("123"));
761        v.set_var("def", VarValue::from("test"));
762        // slash goes first
763        let instr = "\\t${def} text ${ab\\nc} end";
764        let outstr = v.interpolate(instr, false);
765        assert_eq!("\ttest text  end", &outstr);
766        // dollar goes first
767        let instr = "${def}\\ttext ${abc} end";
768        let outstr = v.interpolate(instr, false);
769        assert_eq!("test\ttext 123 end", &outstr);
770        // mixed escaping
771        let instr = "\\${def} ${abc} end";
772        let outstr = v.interpolate(instr, false);
773        assert_eq!("${def} 123 end", &outstr);
774        let instr = "$$\\$$${def} $$${abc}$$ end\\$";
775        let outstr = v.interpolate(instr, false);
776        assert_eq!("$$${def} $123$ end$", &outstr);
777    }
778}