github.com/influx6/npkg@v0.8.8/nenv/envfile.go (about)

     1  package nenv
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  )
    11  
    12  const doubleQuoteSpecialChars = "\\\n\r\"!$`"
    13  
    14  func ReadDotEnvFile(filename string) (envMap map[string]string, err error) {
    15  	file, err := os.Open(filename)
    16  	if err != nil {
    17  		return
    18  	}
    19  	defer file.Close()
    20  
    21  	return ParseDotEnvReader(file)
    22  }
    23  
    24  // Parse reads an env file from io.Reader, returning a map of keys and values.
    25  func ParseDotEnvReader(r io.Reader) (envMap map[string]string, err error) {
    26  	envMap = make(map[string]string)
    27  
    28  	var lines []string
    29  	scanner := bufio.NewScanner(r)
    30  	for scanner.Scan() {
    31  		lines = append(lines, scanner.Text())
    32  	}
    33  
    34  	if err = scanner.Err(); err != nil {
    35  		return
    36  	}
    37  
    38  	for _, fullLine := range lines {
    39  		if !isIgnoredLine(fullLine) {
    40  			var key, value string
    41  			key, value, err = parseLine(fullLine, envMap)
    42  
    43  			if err != nil {
    44  				return
    45  			}
    46  			envMap[key] = value
    47  		}
    48  	}
    49  	return
    50  }
    51  
    52  var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`)
    53  
    54  func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
    55  	if len(line) == 0 {
    56  		err = errors.New("zero length string")
    57  		return
    58  	}
    59  
    60  	// ditch the comments (but keep quoted hashes)
    61  	if strings.Contains(line, "#") {
    62  		segmentsBetweenHashes := strings.Split(line, "#")
    63  		quotesAreOpen := false
    64  		var segmentsToKeep []string
    65  		for _, segment := range segmentsBetweenHashes {
    66  			if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
    67  				if quotesAreOpen {
    68  					quotesAreOpen = false
    69  					segmentsToKeep = append(segmentsToKeep, segment)
    70  				} else {
    71  					quotesAreOpen = true
    72  				}
    73  			}
    74  
    75  			if len(segmentsToKeep) == 0 || quotesAreOpen {
    76  				segmentsToKeep = append(segmentsToKeep, segment)
    77  			}
    78  		}
    79  
    80  		line = strings.Join(segmentsToKeep, "#")
    81  	}
    82  
    83  	firstEquals := strings.Index(line, "=")
    84  	firstColon := strings.Index(line, ":")
    85  	splitString := strings.SplitN(line, "=", 2)
    86  	if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
    87  		// this is a yaml-style line
    88  		splitString = strings.SplitN(line, ":", 2)
    89  	}
    90  
    91  	if len(splitString) != 2 {
    92  		err = errors.New("Can't separate key from value")
    93  		return
    94  	}
    95  
    96  	// Parse the key
    97  	key = splitString[0]
    98  	if strings.HasPrefix(key, "export") {
    99  		key = strings.TrimPrefix(key, "export")
   100  	}
   101  	key = strings.TrimSpace(key)
   102  
   103  	key = exportRegex.ReplaceAllString(splitString[0], "$1")
   104  
   105  	// Parse the value
   106  	value = parseValue(splitString[1], envMap)
   107  	return
   108  }
   109  
   110  var (
   111  	singleQuotesRegex  = regexp.MustCompile(`\A'(.*)'\z`)
   112  	doubleQuotesRegex  = regexp.MustCompile(`\A"(.*)"\z`)
   113  	escapeRegex        = regexp.MustCompile(`\\.`)
   114  	unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
   115  )
   116  
   117  func parseValue(value string, envMap map[string]string) string {
   118  
   119  	// trim
   120  	value = strings.Trim(value, " ")
   121  
   122  	// check if we've got quoted values or possible escapes
   123  	if len(value) > 1 {
   124  		singleQuotes := singleQuotesRegex.FindStringSubmatch(value)
   125  
   126  		doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value)
   127  
   128  		if singleQuotes != nil || doubleQuotes != nil {
   129  			// pull the quotes off the edges
   130  			value = value[1 : len(value)-1]
   131  		}
   132  
   133  		if doubleQuotes != nil {
   134  			// expand newlines
   135  			value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
   136  				c := strings.TrimPrefix(match, `\`)
   137  				switch c {
   138  				case "n":
   139  					return "\n"
   140  				case "r":
   141  					return "\r"
   142  				default:
   143  					return match
   144  				}
   145  			})
   146  			// unescape characters
   147  			value = unescapeCharsRegex.ReplaceAllString(value, "$1")
   148  		}
   149  
   150  		if singleQuotes == nil {
   151  			value = expandVariables(value, envMap)
   152  		}
   153  	}
   154  
   155  	return value
   156  }
   157  
   158  var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
   159  
   160  func expandVariables(v string, m map[string]string) string {
   161  	return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
   162  		submatch := expandVarRegex.FindStringSubmatch(s)
   163  
   164  		if submatch == nil {
   165  			return s
   166  		}
   167  		if submatch[1] == "\\" || submatch[2] == "(" {
   168  			return submatch[0][1:]
   169  		} else if submatch[4] != "" {
   170  			return m[submatch[4]]
   171  		}
   172  		return s
   173  	})
   174  }
   175  
   176  func isIgnoredLine(line string) bool {
   177  	trimmedLine := strings.TrimSpace(line)
   178  	return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
   179  }
   180  
   181  func doubleQuoteEscape(line string) string {
   182  	for _, c := range doubleQuoteSpecialChars {
   183  		toReplace := "\\" + string(c)
   184  		if c == '\n' {
   185  			toReplace = `\n`
   186  		}
   187  		if c == '\r' {
   188  			toReplace = `\r`
   189  		}
   190  		line = strings.Replace(line, string(c), toReplace, -1)
   191  	}
   192  	return line
   193  }