1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// -*- coding: utf-8 -*-
// vi: set sts=4 ts=4 sw=4 et ft=rust:

//! Provides POSIX style substition.
//!
//! I saying POSIX style like `${var}` or `$var` formatted strings.
//! > Main refering souce is Bash parameter substition.

/// Parse src and substitute found variables with result of `mapfn`.
///
/// # Examples
///
/// ```
/// use varsun::posix::substitute;
///
/// let src = "foo is $foo.";
/// let res = substitute(src, |name: &str| -> Option<String> {
///     match name {
///         "foo" => Some("FOO!!".to_string()),
///         _     => None,      // If None returns, variable replaces with "" (empty string).
///     }
/// });
/// ```
pub fn substitute<F>(src: &str, mapfn: F) -> String where F: Fn(&str) -> Option<String> {
    let mut dst = String::new();
    let mut chs = src.chars();

    // Temporaries.
    let mut varname = String::new();
    let mut started = false;
    let mut escaped = false;
    let mut bracket = false;

    while let Some(ch) = chs.next() {
        if started {
            if varname.is_empty() && ch == '{' {
                // Open bracket.
                bracket = true;
                continue;
            }

            // Get varname
            if bracket {
                if ch == '}' {
                    // Close bracket.
                    bracket = false;
                    started = false;

                    // Call mapping-function.
                    if let Some(val) = mapfn(varname.as_str()) {
                        // OK
                        dst.push_str(val.as_str());
                    }

                    // Reset varname.
                    varname.clear();
                } else {
                    // Inside of brackets, allow any characters.
                    // FIXME Incompatible with POSIX.
                    varname.push(ch);
                }
                continue;
            } else {
                // Check character range.
                if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '_') {
                    // Alphabets or Underbar.
                    varname.push(ch);
                    continue;
                } else if !varname.is_empty() && (ch >= '0' && ch <= '9') {
                    // ch is not first, allow numbers.
                    varname.push(ch);
                    continue;
                } else {
                    // ch cannot use for varname.
                    // (end of varname)
                    started = false;

                    // Call mapping-function.
                    if let Some(val) = mapfn(varname.as_str()) {
                        // OK
                        dst.push_str(val.as_str());
                    }

                    // Reset varname.
                    varname.clear();

                    // Pass throught to Check blocks.
                    // Why?
                    //   if ch is '$', this ch is start point of next variable.
                }
            }
        }

        // ---- Check Blocks ----

        // Check escape-seq.
        if !escaped && ch == '\\' {
            escaped = true;
            continue;
        }

        // Push to dest if escaped.
        if escaped {
            escaped = false;
            dst.push(ch);
            continue;
        }

        // Start of placeholder?
        if ch == '$' {
            started = true;
        } else {
            dst.push(ch);
        }
    }

    // varname still alive, replace of remove varname block.
    if !varname.is_empty() {
        if !bracket {
            // Replace mapped value.
            if let Some(val) = mapfn(varname.as_str()) {
                dst.push_str(val.as_str());
            }
        }
        varname.clear();

        // Opened bracket not closed, removed it.
    }

    return dst;
}

/// Substitute environment variable by POSIX format.
pub fn substenvar(src: &str) -> String {
    return self::substitute(src, super::envar);
}

#[cfg(test)]
mod tests {
    fn mapfn(name: &str) -> Option<String> {
        match name {
            "foo" => Some("foo!!".to_string()),
            "bar" => Some("!bar!".to_string()),
            "baz" => Some("-baz-".to_string()),
            _     => Some("( ・ω・)?".to_string()),
        }
    }

    #[test]
    fn substitute_basic() {
        assert_eq!("foo!!", super::substitute("$foo", mapfn));
        assert_eq!("!bar!", super::substitute("${bar}", mapfn));
        assert_eq!("-baz-", super::substitute("$baz", mapfn));
        assert_eq!("foo is foo!!", super::substitute("foo is $foo", mapfn));
        assert_eq!("!bar! not ( ・ω・)?", super::substitute("${bar} not $foobar", mapfn));
        assert_eq!("$foo is foo!!", super::substitute("\\$foo is $foo", mapfn));
        assert_eq!("foo!!!bar!-baz-", super::substitute("$foo${bar}$baz", mapfn));
    }

    #[test]
    fn substitute_escape() {
        assert_eq!("foo!!", super::substitute("$foo", mapfn));
        assert_eq!("$foo", super::substitute("\\$foo", mapfn));
        assert_eq!("\\foo!!", super::substitute("\\\\$foo", mapfn));
        assert_eq!("\\${foo}", super::substitute("\\\\\\${foo}", mapfn));
    }

    //#[bench]
    //fn substitute_bench(b: &mut Bencher) {
    //    b.iter(|| super::substitute("$foo${bar}$baz", mapfn));
    //}

    #[test]
    fn substenvar_basic() {
        ::std::env::set_var("FOO", "foo, foo!");
        ::std::env::set_var("BAR", "foobar");

        assert_eq!("foo, foo!", super::substenvar("$FOO"));
        assert_eq!("foobar says 'foo, foo!'", super::substenvar("${BAR} says '$FOO'"));
        assert_eq!("", super::substenvar("$BAZ"));
        assert_eq!("foobar provided by $BAR", super::substenvar("$BAR provided by \\$BAR"));
    }
}