github.com/42z-io/confik@v0.0.2-0.20231103050132-21d8f377356c/env.go (about)

     1  package confik
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  )
    12  
    13  // loadEnvFile will locate and load the environment file into a map[string]string
    14  //
    15  // loadEnvFile will update the current environment with the files found in the environment file
    16  func loadEnvFile[T any](cfg Config[T]) (map[string]string, error) {
    17  	var envPath string
    18  	if cfg.EnvFilePath == "" {
    19  		foundPath, err := findEnvFile()
    20  		if err != nil {
    21  			return nil, err
    22  		}
    23  		envPath = foundPath
    24  	} else {
    25  		envPath = cfg.EnvFilePath
    26  	}
    27  
    28  	// no .env found or provided - return empty map
    29  	if envPath == "" {
    30  		envMap := make(map[string]string)
    31  		return envMap, nil
    32  	}
    33  
    34  	// check if the .env file exists
    35  	stat, err := os.Stat(envPath)
    36  	if err != nil {
    37  		if os.IsNotExist(err) {
    38  			return nil, fmt.Errorf("environment file does not exist: %s", envPath)
    39  		}
    40  		return nil, err
    41  	}
    42  
    43  	// check if the .env file is a directory
    44  	if stat.IsDir() {
    45  		return nil, fmt.Errorf("environment file is a directory: %s", envPath)
    46  	}
    47  
    48  	// open and parse the env file
    49  	file, err := os.Open(envPath)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	defer file.Close()
    54  
    55  	kv, err := parseEnvFile(file)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	// add the discovered environment variables in the environment file to the environment
    61  	for k, v := range kv {
    62  		_, exists := os.LookupEnv(k)
    63  		if cfg.EnvFileOverride || !exists {
    64  			os.Setenv(k, v)
    65  		}
    66  	}
    67  	return kv, nil
    68  }
    69  
    70  // findEnvFile will locate the .env file by looking in the current directory and recursing up the directory structure
    71  func findEnvFile() (string, error) {
    72  	// Start looking for the ".env" in the current directory
    73  	path, err := os.Getwd()
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  	var lastPath = filepath.Clean(path)
    78  	for {
    79  		var checkPath = filepath.Join(path, ".env")
    80  		stat, err := os.Stat(checkPath)
    81  		// If we cant find the ".env" in this directory look in the parent directory
    82  		if os.IsNotExist(err) {
    83  			path = filepath.Dir(path)
    84  			if path == lastPath {
    85  				return "", nil
    86  			}
    87  			lastPath = path
    88  		} else if err != nil {
    89  			return "", err
    90  		} else if stat.IsDir() {
    91  			return "", fmt.Errorf("environment file is a directory: %s", checkPath)
    92  		} else {
    93  			return checkPath, nil
    94  		}
    95  	}
    96  }
    97  
    98  // parseEnvVar will parse an environment variable in the format NAME=VALUE.
    99  func parseEnvVar(expression string) (string, string, error) {
   100  	// ensure format of the expression is correct
   101  	if !strings.Contains(expression, "=") {
   102  		return "", "", fmt.Errorf("invalid expression in env file: %s", expression)
   103  	}
   104  
   105  	// split the variable NAME=value [NAME, value]
   106  	parts := strings.SplitN(expression, "=", 2)
   107  	variable := strings.Trim(parts[0], " ")
   108  
   109  	// remove any quotes
   110  	unquoted, err := strconv.Unquote(parts[1])
   111  	if err != nil {
   112  		return variable, parts[1], nil
   113  	}
   114  	return variable, unquoted, nil
   115  }
   116  
   117  // parseEnvFile will convert an environment file into a map[string]string
   118  //
   119  // Expects the file in the format:
   120  //
   121  //	MY_VARIABLE=MY_NAME
   122  //	OTHER_VARIABLE="QUOTED_VALUE"
   123  //
   124  // Notes:
   125  //   - Quoted values will be unquoted
   126  //   - Blank lines will be ingored
   127  //   - Comments (starting with // or #) will be ignored
   128  //   - Whitespace around variables and their values will be stripped
   129  func parseEnvFile(reader io.Reader) (map[string]string, error) {
   130  	scanner := bufio.NewScanner(reader)
   131  	// map to store all the key => value pairs we find in the ".env" file
   132  	kv := make(map[string]string)
   133  	for scanner.Scan() {
   134  		expression := scanner.Text()
   135  		// ignore comments and blank lines
   136  		if strings.HasPrefix(expression, "#") || strings.HasPrefix(expression, "//") || strings.TrimSpace(expression) == "" {
   137  			continue
   138  		}
   139  
   140  		key, value, err := parseEnvVar(expression)
   141  		if err != nil {
   142  			return nil, err
   143  		}
   144  
   145  		// update the store
   146  		kv[strings.TrimSpace(key)] = strings.TrimSpace(value)
   147  	}
   148  	return kv, nil
   149  }