// String substitution and expansion. package main import ( "strings" "unicode/utf8" ) // Expand a word. This includes substituting variables and handling quotes. func expand(input string, vars map[string][]string, expandBackticks bool) string { expanded := make([]byte, 0) var i, j int for i = 0; i < len(input); { j = i + strings.IndexAny(input[i:], "\"'`$\\") if j < 0 { expanded = append(expanded, input[i:]...) break } expanded = append(expanded, input[i:j]...) c, w := utf8.DecodeRuneInString(input[j:]) i = j + w var off int var out string switch c { case '\\': out, off = expandEscape(input[i:]) case '"': out, off = expandDoubleQuoted(input[i:], vars, expandBackticks) case '\'': out, off = expandSingleQuoted(input[i:]) case '`': if expandBackticks { out, off = expandBackQuoted(input[i:], vars) } else { out = input off = len(input) } case '$': out, off = expandSigil(input[i:], vars) } expanded = append(expanded, out...) i += off } return string(expanded) } // Expand following a '\\' func expandEscape(input string) (string, int) { c, w := utf8.DecodeRuneInString(input) return string(c), w } // Expand a double quoted string starting after a '\"' func expandDoubleQuoted(input string, vars map[string][]string, expandBackticks bool) (string, int) { // find the first non-escaped " j := 0 for { j = strings.IndexAny(input[j:], "\"\\") if j < 0 { break } _, w := utf8.DecodeRuneInString(input[j:]) j += w c, w := utf8.DecodeRuneInString(input[j:]) j += w if c == '"' { return expand(input[:j], vars, expandBackticks), (j + w) } if c == '\\' { if j+w < len(input) { j += w _, w := utf8.DecodeRuneInString(input[j:]) j += w } else { break } } } return input, len(input) } // Expand a single quoted string starting after a '\'' func expandSingleQuoted(input string) (string, int) { j := strings.Index(input, "'") if j < 0 { return input, len(input) } return input[:j], (j + 1) } // Expand something starting with at '$'. func expandSigil(input string, vars map[string][]string) (string, int) { c, w := utf8.DecodeRuneInString(input) var offset int var varname string if c == '{' { j := strings.IndexRune(input[w:], '}') if j < 0 { return input, len(input) } varname = input[w:j] offset = j + 1 } else { // try to match a variable name i := w j := i for j < len(input) { c, w = utf8.DecodeRuneInString(input) if !(isalpha(c) || c == '_' || (j > i && isdigit(c))) { break } j += w } if j > i { varname = input[i:j] } else { return input, len(input) } } if isValidVarName(varname) { varvals, ok := vars[varname] if ok { return strings.Join(varvals, " "), offset } } return input, len(input) } // Find and expand all sigils. func expandSigils(input string, vars map[string][]string) string { expanded := make([]byte, 0) for i := 0; i < len(input); { j := strings.IndexRune(input[i:], '$') if j < 0 { expanded = append(expanded, input[i:]...) break } ex, k := expandSigil(input[j+1:], vars) expanded = append(expanded, ex...) i = k } return string(expanded) } // Expand all unescaped '%' characters. func expandSuffixes(input string, stem string) string { expanded := make([]byte, 0) for i := 0; i < len(input); { j := strings.IndexAny(input[i:], "\\%") if j < 0 { expanded = append(expanded, input[i:]...) break } c, w := utf8.DecodeRuneInString(input[j:]) if c == '%' { expanded = append(expanded, stem...) i += w } else { j += w c, w := utf8.DecodeRuneInString(input[j:]) if c == '%' { expanded = append(expanded, '%') i = j + w } } } return string(expanded) } // TODO: expand RegexpRefs // Expand a backtick quoted string, by executing the contents. func expandBackQuoted(input string, vars map[string][]string) (string, int) { // TODO: expand sigils? j := strings.Index(input, "`") if j < 0 { return input, len(input) } output := executeRecipe("sh", nil, input[:j], false, false, true) return output, (j + 1) }