From 7c921054c33f902fa71613be2fcfafdc917e1270 Mon Sep 17 00:00:00 2001 From: GINTOAHC Date: Thu, 6 Jun 2024 17:05:16 +0800 Subject: [PATCH] refactor: Implement our own dotenv --- dotenv/dotenv.go | 87 ++++++++++++++++++++++++++++++++++ dotenv/dotenv_test.go | 25 ++++++++++ dotenv/env_test | 5 ++ dotenv/env_test_error | 6 +++ go.mod | 2 - go.sum | 2 - internal/config/config.go | 4 +- internal/helper/helper_test.go | 7 ++- 8 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 dotenv/dotenv.go create mode 100644 dotenv/dotenv_test.go create mode 100644 dotenv/env_test create mode 100644 dotenv/env_test_error diff --git a/dotenv/dotenv.go b/dotenv/dotenv.go new file mode 100644 index 0000000..745850b --- /dev/null +++ b/dotenv/dotenv.go @@ -0,0 +1,87 @@ +package dotenv + +import ( + "bufio" + "errors" + "fmt" + "log/slog" + "os" + "regexp" + "strings" +) + +func load(path string) error { + varRegexp := regexp.MustCompile(`\${([a-zA-Z0-9_]+)}`) // Match ${VAR} + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + // Read the file + scanner := bufio.NewScanner(file) // Create a scanner + envMap := make(map[string]string) // Map to store the environment variables + var line, k, v string // Variables to store the line, key and value + + for scanner.Scan() { + line = scanner.Text() + if varRegexp.MatchString(line) { + // Check if the variable is defined (warn if not) + varName := varRegexp.FindString(line)[2 : len(varRegexp.FindString(line))-1] + if _, ok := envMap[varName]; !ok { + slog.Warn(fmt.Sprintf("Undefined variable: %s in line %s", varRegexp.FindString(line), line)) + } + line = varRegexp.ReplaceAllStringFunc(line, func(s string) string { + return envMap[s[2:len(s)-1]] + }) + } + // Parse the line + k, v, err = parse(line) + if err != nil { + return errors.New(fmt.Sprintf("Invalid line: %s", line)) + } + if k == "" || v == "" { // Skip empty lines + continue + } + envMap[k] = v + } + // Set the environment variables + for k, v := range envMap { + os.Setenv(k, v) + } + return nil +} + +func parse(line string) (string, string, error) { + s := strings.TrimSpace(line) // Trim the line + if strings.HasPrefix(s, "#") { // Check if the line is a comment + return "", "", nil + } + if !strings.Contains(s, "=") { // Check if the line is a key value pair + return "", "", errors.New("No '=' in line") + } + keyValuePair := regexp.MustCompile(`\s*=\s*`).Split(s, 2) // Split the line by the first '=' + if len(keyValuePair) == 2 { + // Return key and value (remove comments if any) + value := strings.Split(keyValuePair[1], "#")[0] + // Remove quotes if any + if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { + value = value[1 : len(value)-1] + } + return strings.TrimSpace(keyValuePair[0]), strings.TrimSpace(value), nil + } + return "", "", nil +} + +func Load(paths ...string) error { + if len(paths) == 0 { + paths = append(paths, ".env") + } + for _, path := range paths { + err := load(path) + if err != nil { + return err + } + } + return nil +} diff --git a/dotenv/dotenv_test.go b/dotenv/dotenv_test.go new file mode 100644 index 0000000..4c41f1b --- /dev/null +++ b/dotenv/dotenv_test.go @@ -0,0 +1,25 @@ +package dotenv + +import ( + "os" + "testing" +) + +func TestDotenv(t *testing.T) { + err := Load("./env_test") + t.Log("DOTENV_TEST_VAR_FIRST:", os.Getenv("DOTENV_TEST_VAR_FIRST")) + t.Log("DOTENV_TEST:", os.Getenv("DOTENV_TEST")) + t.Log("DOTENV_TEST_WITH_QUOTE:", os.Getenv("DOTENV_TEST_WITH_QUOTE")) + t.Log("DOTENV_TEST_WITH_SPACE:", os.Getenv("DOTENV_TEST_WITH_SPACE")) + t.Log("DOTENV_TEST_WITH_VAR:", os.Getenv("DOTENV_TEST_WITH_VAR")) + if err != nil { + t.Error("Error loading .env file:", err) + } else { + t.Log("Success loading .env file") + } +} + +func TestLogError(t *testing.T) { + err := Load("./env_test_error") + t.Log("Error:", err) +} diff --git a/dotenv/env_test b/dotenv/env_test new file mode 100644 index 0000000..ddc40c5 --- /dev/null +++ b/dotenv/env_test @@ -0,0 +1,5 @@ +DOTENV_TEST=testenv +DOTENV_TEST_WITH_QUOTE="testenv_withquote" +DOTENV_TEST_WITH_SPACE = "testenv_withspace" +VAR = var_here +DOTENV_TEST_WITH_VAR = test_withvar_${VAR} diff --git a/dotenv/env_test_error b/dotenv/env_test_error new file mode 100644 index 0000000..61b47c7 --- /dev/null +++ b/dotenv/env_test_error @@ -0,0 +1,6 @@ +VAR = var_here +VAR = var_here +VAR = var_here +VAR = var_here +VAR = var_here +DOTENV_ERROR error diff --git a/go.mod b/go.mod index 866d2e8..44e15c1 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module past-papers-web go 1.22.2 - -require github.com/joho/godotenv v1.5.1 // indirect diff --git a/go.sum b/go.sum index d61b19e..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/internal/config/config.go b/internal/config/config.go index f3bb856..1bcbf0f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,7 @@ import ( "log" "os" - "github.com/joho/godotenv" + "past-papers-web/dotenv" ) type Config struct { @@ -14,7 +14,7 @@ type Config struct { } func NewConfig() *Config { - err := godotenv.Load() + err := dotenv.Load() if err != nil { log.Fatal("Error loading .env file") } diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index d6d3e2b..67f6acf 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -3,15 +3,14 @@ package helper import ( b64 "encoding/base64" "os" - "past-papers-web/internal/config" "testing" - "github.com/joho/godotenv" + "past-papers-web/dotenv" + "past-papers-web/internal/config" ) func TestUploadCombos(t *testing.T) { - - err := godotenv.Load() + err := dotenv.Load("./.env") // Load .env under same directory if err != nil { t.Log("Error loading .env file") }