github.com/beornf/libcompose@v0.4.1-0.20210215180846-a59802c0f07c/config/interpolation.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/sirupsen/logrus"
     9  )
    10  
    11  var defaultValues = make(map[string]string)
    12  
    13  func isNum(c uint8) bool {
    14  	return c >= '0' && c <= '9'
    15  }
    16  
    17  func validVariableDefault(c uint8, line string, pos int) bool {
    18  	return (c == ':' && line[pos+1] == '-') || (c == '-')
    19  }
    20  
    21  func validVariableNameChar(c uint8) bool {
    22  	return c == '_' ||
    23  		c >= 'A' && c <= 'Z' ||
    24  		c >= 'a' && c <= 'z' ||
    25  		isNum(c)
    26  }
    27  
    28  func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) {
    29  	var buffer bytes.Buffer
    30  
    31  	for ; pos < len(line); pos++ {
    32  		c := line[pos]
    33  
    34  		switch {
    35  		case validVariableNameChar(c):
    36  			buffer.WriteByte(c)
    37  		default:
    38  			return mapping(buffer.String()), pos - 1, true
    39  		}
    40  	}
    41  
    42  	return mapping(buffer.String()), pos, true
    43  }
    44  
    45  func parseDefaultValue(line string, pos int) (string, int, bool) {
    46  	var buffer bytes.Buffer
    47  
    48  	// only skip :, :- and - at the beginning
    49  	for ; pos < len(line); pos++ {
    50  		c := line[pos]
    51  		if c == ':' || c == '-' {
    52  			continue
    53  		}
    54  		break
    55  	}
    56  	for ; pos < len(line); pos++ {
    57  		c := line[pos]
    58  		if c == '}' {
    59  			return buffer.String(), pos - 1, true
    60  		}
    61  		err := buffer.WriteByte(c)
    62  		if err != nil {
    63  			return "", pos, false
    64  		}
    65  	}
    66  	return "", 0, false
    67  }
    68  
    69  func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) {
    70  	var buffer bytes.Buffer
    71  
    72  	for ; pos < len(line); pos++ {
    73  		c := line[pos]
    74  
    75  		switch {
    76  		case c == '}':
    77  			bufferString := buffer.String()
    78  
    79  			if bufferString == "" {
    80  				return "", 0, false
    81  			}
    82  			return mapping(buffer.String()), pos, true
    83  		case validVariableNameChar(c):
    84  			buffer.WriteByte(c)
    85  		case validVariableDefault(c, line, pos):
    86  			defaultValue := ""
    87  			defaultValue, pos, _ = parseDefaultValue(line, pos)
    88  			defaultValues[buffer.String()] = defaultValue
    89  		default:
    90  			return "", 0, false
    91  		}
    92  	}
    93  
    94  	return "", 0, false
    95  }
    96  
    97  func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
    98  	c := line[pos]
    99  
   100  	switch {
   101  	case c == '$':
   102  		return "$", pos, true
   103  	case c == '{':
   104  		return parseVariableWithBraces(line, pos+1, mapping)
   105  	case !isNum(c) && validVariableNameChar(c):
   106  		// Variables can't start with a number
   107  		return parseVariable(line, pos, mapping)
   108  	default:
   109  		return "", 0, false
   110  	}
   111  }
   112  
   113  func parseLine(line string, mapping func(string) string) (string, bool) {
   114  	var buffer bytes.Buffer
   115  
   116  	for pos := 0; pos < len(line); pos++ {
   117  		c := line[pos]
   118  		switch {
   119  		case c == '$':
   120  			var replaced string
   121  			var success bool
   122  
   123  			replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
   124  
   125  			if !success {
   126  				return "", false
   127  			}
   128  
   129  			buffer.WriteString(replaced)
   130  		default:
   131  			buffer.WriteByte(c)
   132  		}
   133  	}
   134  
   135  	return buffer.String(), true
   136  }
   137  
   138  func parseConfig(key string, data *interface{}, mapping func(string) string) error {
   139  	switch typedData := (*data).(type) {
   140  	case string:
   141  		var success bool
   142  
   143  		*data, success = parseLine(typedData, mapping)
   144  
   145  		if !success {
   146  			return fmt.Errorf("Invalid interpolation format for key \"%s\": \"%s\"", key, typedData)
   147  		}
   148  	case []interface{}:
   149  		for k, v := range typedData {
   150  			err := parseConfig(key, &v, mapping)
   151  
   152  			if err != nil {
   153  				return err
   154  			}
   155  
   156  			typedData[k] = v
   157  		}
   158  	case map[interface{}]interface{}:
   159  		for k, v := range typedData {
   160  			err := parseConfig(key, &v, mapping)
   161  
   162  			if err != nil {
   163  				return err
   164  			}
   165  
   166  			typedData[k] = v
   167  		}
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // Interpolate replaces variables in a map entry
   174  func Interpolate(key string, data *interface{}, environmentLookup EnvironmentLookup) error {
   175  	return parseConfig(key, data, func(s string) string {
   176  		values := environmentLookup.Lookup(s, nil)
   177  
   178  		if len(values) == 0 {
   179  			if val, ok := defaultValues[s]; ok {
   180  				return val
   181  			}
   182  			logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
   183  			return ""
   184  		}
   185  
   186  		if strings.SplitN(values[0], "=", 2)[1] == "" {
   187  			if val, ok := defaultValues[s]; ok {
   188  				return val
   189  			}
   190  		}
   191  
   192  		// Use first result if many are given
   193  		value := values[0]
   194  
   195  		// Environment variables come in key=value format
   196  		// Return everything past first '='
   197  		return strings.SplitN(value, "=", 2)[1]
   198  	})
   199  }