package config import ( "bytes" "fmt" "strings" "github.com/sirupsen/logrus" ) var defaultValues = make(map[string]string) func isNum(c uint8) bool { return c >= '0' && c <= '9' } func validVariableDefault(c uint8, line string, pos int) bool { return (c == ':' && line[pos+1] == '-') || (c == '-') } func validVariableNameChar(c uint8) bool { return c == '_' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || isNum(c) } func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) { var buffer bytes.Buffer for ; pos < len(line); pos++ { c := line[pos] switch { case validVariableNameChar(c): buffer.WriteByte(c) default: return mapping(buffer.String()), pos - 1, true } } return mapping(buffer.String()), pos, true } func parseDefaultValue(line string, pos int) (string, int, bool) { var buffer bytes.Buffer // only skip :, :- and - at the beginning for ; pos < len(line); pos++ { c := line[pos] if c == ':' || c == '-' { continue } break } for ; pos < len(line); pos++ { c := line[pos] if c == '}' { return buffer.String(), pos - 1, true } err := buffer.WriteByte(c) if err != nil { return "", pos, false } } return "", 0, false } func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) { var buffer bytes.Buffer for ; pos < len(line); pos++ { c := line[pos] switch { case c == '}': bufferString := buffer.String() if bufferString == "" { return "", 0, false } return mapping(buffer.String()), pos, true case validVariableNameChar(c): buffer.WriteByte(c) case validVariableDefault(c, line, pos): defaultValue := "" defaultValue, pos, _ = parseDefaultValue(line, pos) defaultValues[buffer.String()] = defaultValue default: return "", 0, false } } return "", 0, false } func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) { c := line[pos] switch { case c == '$': return "$", pos, true case c == '{': return parseVariableWithBraces(line, pos+1, mapping) case !isNum(c) && validVariableNameChar(c): // Variables can't start with a number return parseVariable(line, pos, mapping) default: return "", 0, false } } func parseLine(line string, mapping func(string) string) (string, bool) { var buffer bytes.Buffer for pos := 0; pos < len(line); pos++ { c := line[pos] switch { case c == '$': var replaced string var success bool replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping) if !success { return "", false } buffer.WriteString(replaced) default: buffer.WriteByte(c) } } return buffer.String(), true } func parseConfig(key string, data *interface{}, mapping func(string) string) error { switch typedData := (*data).(type) { case string: var success bool *data, success = parseLine(typedData, mapping) if !success { return fmt.Errorf("Invalid interpolation format for key \"%s\": \"%s\"", key, typedData) } case []interface{}: for k, v := range typedData { err := parseConfig(key, &v, mapping) if err != nil { return err } typedData[k] = v } case map[interface{}]interface{}: for k, v := range typedData { err := parseConfig(key, &v, mapping) if err != nil { return err } typedData[k] = v } } return nil } // Interpolate replaces variables in a map entry func Interpolate(key string, data *interface{}, environmentLookup EnvironmentLookup) error { return parseConfig(key, data, func(s string) string { values := environmentLookup.Lookup(s, nil) if len(values) == 0 { if val, ok := defaultValues[s]; ok { return val } logrus.Warnf("The %s variable is not set. Substituting a blank string.", s) return "" } if strings.SplitN(values[0], "=", 2)[1] == "" { if val, ok := defaultValues[s]; ok { return val } } // Use first result if many are given value := values[0] // Environment variables come in key=value format // Return everything past first '=' return strings.SplitN(value, "=", 2)[1] }) }