From 824600d856149167e16ad2409d89825e8e439696 Mon Sep 17 00:00:00 2001 From: Sameer Jadav Date: Sat, 3 Aug 2024 14:43:35 +0530 Subject: [PATCH] feat: add ParseFile function --- README.md | 39 +++++++++++-- parser.go | 25 ++++++--- parser_test.go | 146 +++++++++++++++++++++++++++++++------------------ 3 files changed, 144 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 90fd415..0121b18 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# GoEnvParse +# EnvParse -[![Go Reference](https://pkg.go.dev/badge/github.com/SameerJadav/go-envparse.svg)](https://pkg.go.dev/github.com/SameerJadav/go-envparse) [![CI](https://github.com/SameerJadav/go-envparse/actions/workflows/ci.yml/badge.svg)](https://github.com/SameerJadav/go-envparse/actions/workflows/ci.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/SameerJadav/envparse.svg)](https://pkg.go.dev/github.com/SameerJadav/envparse) [![CI](https://github.com/SameerJadav/envparse/actions/workflows/ci.yml/badge.svg)](https://github.com/SameerJadav/envparse/actions/workflows/ci.yml) -GoEnvParse is a Go package for parsing environment variables from `.env` files. It provides a simple and efficient way to load environment variables from `.env` file into your Go applications. +EnvParse is a Go package designed for efficiently parsing environment variables from `.env` files. It provides a straightforward and performant way to load environment variables into your Go applications. ## Features - Parse environment files from any `io.Reader` source +- Parse environment files directly from a file - Handling of quoted values (double quotes, single quotes, and backticks) - Variable expansion in non-quoted and double-quoted values - Error reporting with line numbers for invalid syntax @@ -16,16 +17,20 @@ GoEnvParse is a Go package for parsing environment variables from `.env` files. - Double-quoted values are unescaped, including unicode characters - Single-quoted and backtick-quoted values are treated as literal strings - Variable expansion is performed in non-quoted and double-quoted values +- Any empty keys, commented lines (lines prefixed with `#`), and invalid lines (lines that are not comments and do not have an `=` sign) will not be parsed +- Inline comments (e.g., `KEY=VALUE#inline comment`) are removed from the value; if a value must contain a `#`, the value must be quoted (e.g., `KEY="VALUE#with hash"`) - Does not support multiline values ## Installation ```shell -go get github.com/SameerJadav/go-envparse +go get github.com/SameerJadav/envparse ``` ## Usage +Parse from `io.Reader` + ```go package main @@ -33,7 +38,7 @@ import ( "log" "os" - "github.com/SameerJadav/go-envparse" + "github.com/SameerJadav/envparse" ) func main() { @@ -54,6 +59,30 @@ func main() { } ``` +Parse from File + +```go +package main + +import ( + "log" + "os" + + "github.com/SameerJadav/envparse" +) + +func main() { + env, err := envparse.ParseFile(".env") + if err != nil { + log.Fatal(err) + } + + for key, value := range env { + os.Setenv(key, value) + } +} +``` + ## Contributing Contributions are welcome. Please open an issue or submit a pull request. diff --git a/parser.go b/parser.go index b162be4..8e8d0a3 100644 --- a/parser.go +++ b/parser.go @@ -2,6 +2,7 @@ package envparse import ( "bufio" + "bytes" "fmt" "io" "os" @@ -9,14 +10,9 @@ import ( "strings" ) -// Parse reads an env file from io.Reader, returning a map of keys-value pairs and an error. -// -// Only double-quoted values are escaped. Single-quoted and backquoted values -// are treated as literal strings. Variable expansion (${...} and $...) -// is performed in non-quoted and double-quoted values. -// -// Note: This function does not support multiline values. -// Each key-value pair must be on a single line. +// Parse reads an environment variables file from the provided io.Reader and +// returns a map of key-value pairs. The function also returns an error if +// any issues are encountered during parsing. func Parse(r io.Reader) (map[string]string, error) { result := make(map[string]string) scanner := bufio.NewScanner(r) @@ -78,6 +74,19 @@ func Parse(r io.Reader) (map[string]string, error) { return result, nil } +// ParseFile reads an environment variables file from the specified filename +// and returns a map of key-value pairs. The function also returns an error +// if any issues are encountered during file reading or parsing. +// +// This function uses the [Parse] function internally to process the file contents. +func ParseFile(filename string) (map[string]string, error) { + content, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read env file %s: %w", filename, err) + } + return Parse(bytes.NewReader(content)) +} + func isQuoted(value string) (byte, int, int, bool) { if len(value) < 2 { return 0, -1, -1, false diff --git a/parser_test.go b/parser_test.go index fda542d..4b3662d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,73 +1,113 @@ package envparse import ( + "bytes" "os" "testing" ) +var expected = map[string]string{ + "SIMPLE_VAR": "value", + "EMPTY": "", + "EMPTY_SINGLE_QUOTES": "", + "EMPTY_DOUBLE_QUOTES": "", + "EMPTY_BACKTICKS": "", + "QUOTED_VAR": "value", + "SINGLE_QUOTED_VAR": "value", + "BACKQUOTED_VAR": "value", + "DOUBLE_QUOTES_SPACED": " double quotes ", + "SINGLE_QUOTES_SPACED": " single quotes ", + "BACKQUOTE_SPACED": " back quotes ", + "UNQUOTED_WITH_SPACES": "this has spaces", + "NESTED_IN_DOUBLE": "This is a 'single quote' and a `backtick` inside double quotes", + "NESTED_IN_SINGLE": "This is a \"double quote\" and a `backtick` inside single quotes", + "NESTED_IN_BACKTICK": "This is a \"double quote\" and a 'single quote' inside backticks", + "ESCAPED_IN_DOUBLE": "This has \"escaped double quotes\"", + "ESCAPED_IN_SINGLE": "This has \\'escaped single quotes\\'", + "MIXED_QUOTES": "Double with 'single' and \"escaped double\" and `backtick`", + "UNQUOTED_NEWLINE": "This is a line\\nAnd this is another line", + "DOUBLE_QUOTED_NEWLINE": "This is in double quotes\nThis is a new line in double quotes", + "SINGLE_QUOTED_NEWLINE": "This is in single quotes\\nThis is a new line in single quotes", + "BACKTICK_QUOTED_NEWLINE": "This is in backticks\\nThis is a new line in backticks", + "VARIABLE_EXPANSION": "value/expansion", + "NESTED_EXPANSION": "value/expansion/nested", + "VARIABLE_EXPANSION_ALT": "value/expansion", + "NESTED_EXPANSION_ALT": "value/expansion/nested", + "DOUBLEQUOTED_VARIABLE_EXPANSION": "value/expansion", + "DOUBLEQUOTED_NESTED_EXPANSION": "value/expansion/nested", + "DOUBLEQUOTED_VARIABLE_EXPANSION_ALT": "value/expansion", + "DOUBLEQUOTED_NESTED_EXPANSION_ALT": "value/expansion/nested", + "SINGLEQUOTED_VARIABLE_EXPANSION": "${SIMPLE_VAR}/expansion", + "SINGLEQUOTED_NESTED_EXPANSION": "${VARIABLE_EXPANSION}/nested", + "SINGLEQUOTED_VARIABLE_EXPANSION_ALT": "$SIMPLE_VAR/expansion", + "SINGLEQUOTED_NESTED_EXPANSION_ALT": "$VARIABLE_EXPANSION/nested", + "BACKQUOTED_VARIABLE_EXPANSION": "${SIMPLE_VAR}/expansion", + "BACKQUOTED_NESTED_EXPANSION": "${VARIABLE_EXPANSION}/nested", + "BACKQUOTED_VARIABLE_EXPANSION_ALT": "$SIMPLE_VAR/expansion", + "BACKQUOTED_NESTED_EXPANSION_ALT": "$VARIABLE_EXPANSION/nested", + "UNMATCHED_DOUBLEQUOTE": "\"value", + "UNMATCHED_SINGLEQUOTE": "'value", + "UNMATCHED_BACKQUOTE": "`value", + "INLINE_COMMENTS": "value", + "INLINE_COMMENTS_DOUBLE_QUOTES": "inline comments outside of #doublequotes", + "INLINE_COMMENTS_SINGLE_QUOTES": "inline comments outside of #singlequotes", + "INLINE_COMMENTS_BACKQUOTES": "inline comments outside of #backticks", + "EXPORTED_VAR": "value", + "EQUAL_SIGNS": "equals==", + "SPACED_KEY": "value", +} + func TestParse(t *testing.T) { - expected := map[string]string{ - "SIMPLE_VAR": "value", - "EMPTY": "", - "EMPTY_SINGLE_QUOTES": "", - "EMPTY_DOUBLE_QUOTES": "", - "EMPTY_BACKTICKS": "", - "QUOTED_VAR": "value", - "SINGLE_QUOTED_VAR": "value", - "BACKQUOTED_VAR": "value", - "DOUBLE_QUOTES_SPACED": " double quotes ", - "SINGLE_QUOTES_SPACED": " single quotes ", - "BACKQUOTE_SPACED": " back quotes ", - "UNQUOTED_WITH_SPACES": "this has spaces", - "NESTED_IN_DOUBLE": "This is a 'single quote' and a `backtick` inside double quotes", - "NESTED_IN_SINGLE": "This is a \"double quote\" and a `backtick` inside single quotes", - "NESTED_IN_BACKTICK": "This is a \"double quote\" and a 'single quote' inside backticks", - "ESCAPED_IN_DOUBLE": "This has \"escaped double quotes\"", - "ESCAPED_IN_SINGLE": "This has \\'escaped single quotes\\'", - "MIXED_QUOTES": "Double with 'single' and \"escaped double\" and `backtick`", - "UNQUOTED_NEWLINE": "This is a line\\nAnd this is another line", - "DOUBLE_QUOTED_NEWLINE": "This is in double quotes\nThis is a new line in double quotes", - "SINGLE_QUOTED_NEWLINE": "This is in single quotes\\nThis is a new line in single quotes", - "BACKTICK_QUOTED_NEWLINE": "This is in backticks\\nThis is a new line in backticks", - "VARIABLE_EXPANSION": "value/expansion", - "NESTED_EXPANSION": "value/expansion/nested", - "VARIABLE_EXPANSION_ALT": "value/expansion", - "NESTED_EXPANSION_ALT": "value/expansion/nested", - "DOUBLEQUOTED_VARIABLE_EXPANSION": "value/expansion", - "DOUBLEQUOTED_NESTED_EXPANSION": "value/expansion/nested", - "DOUBLEQUOTED_VARIABLE_EXPANSION_ALT": "value/expansion", - "DOUBLEQUOTED_NESTED_EXPANSION_ALT": "value/expansion/nested", - "SINGLEQUOTED_VARIABLE_EXPANSION": "${SIMPLE_VAR}/expansion", - "SINGLEQUOTED_NESTED_EXPANSION": "${VARIABLE_EXPANSION}/nested", - "SINGLEQUOTED_VARIABLE_EXPANSION_ALT": "$SIMPLE_VAR/expansion", - "SINGLEQUOTED_NESTED_EXPANSION_ALT": "$VARIABLE_EXPANSION/nested", - "BACKQUOTED_VARIABLE_EXPANSION": "${SIMPLE_VAR}/expansion", - "BACKQUOTED_NESTED_EXPANSION": "${VARIABLE_EXPANSION}/nested", - "BACKQUOTED_VARIABLE_EXPANSION_ALT": "$SIMPLE_VAR/expansion", - "BACKQUOTED_NESTED_EXPANSION_ALT": "$VARIABLE_EXPANSION/nested", - "UNMATCHED_DOUBLEQUOTE": "\"value", - "UNMATCHED_SINGLEQUOTE": "'value", - "UNMATCHED_BACKQUOTE": "`value", - "INLINE_COMMENTS": "value", - "INLINE_COMMENTS_DOUBLE_QUOTES": "inline comments outside of #doublequotes", - "INLINE_COMMENTS_SINGLE_QUOTES": "inline comments outside of #singlequotes", - "INLINE_COMMENTS_BACKQUOTES": "inline comments outside of #backticks", - "EXPORTED_VAR": "value", - "EQUAL_SIGNS": "equals==", - "SPACED_KEY": "value", + content, err := os.ReadFile("test.env") + if err != nil { + t.Fatal(err) } - file, err := os.Open("test.env") + result, err := Parse(bytes.NewReader(content)) if err != nil { - t.Fatal(err) + t.Fatalf("Parse returned unexpected error: %v", err) } - defer file.Close() - result, err := Parse(file) + compareMaps(t, result, expected) +} + +func TestParseFile(t *testing.T) { + result, err := ParseFile("test.env") if err != nil { t.Fatalf("Parse returned unexpected error: %v", err) } + compareMaps(t, result, expected) +} + +func BenchmarkParse(b *testing.B) { + content, err := os.ReadFile("test.env") + if err != nil { + b.Fatal(err) + } + reader := bytes.NewReader(content) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = Parse(reader) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkParseFile(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := ParseFile("test.env") + if err != nil { + b.Fatal(err) + } + } +} + +func compareMaps(t *testing.T, result, expected map[string]string) { + t.Helper() + if len(result) != len(expected) { t.Errorf("Map size mismatch: got %d entries, want %d entries", len(result), len(expected)) }