diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b2dd6d5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v -coverprofile=cover.out -shuffle on ./... + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - name: Lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 559afa5..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Go - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Check out - uses: actions/checkout@v2 - - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: ^1.16 - - - name: Get dependencies - run: go get -v -t -d ./... - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v -coverprofile=coverage.txt -shuffle on ./... - - - name: Coverage - uses: codecov/codecov-action@v1.0.13 - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Check out - uses: actions/checkout@v2 - - - name: Lint - uses: golangci/golangci-lint-action@v2 - with: - version: latest diff --git a/README.md b/README.md index b5f4362..9165183 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Made in 🇩🇰 by [maragu](https://www.maragu.dk), maker of [online Go courses ## Usage ```shell -go get -u github.com/maragudk/env +go get github.com/maragudk/env ``` ```go diff --git a/env.go b/env.go index 7ecce5c..5b8ad69 100644 --- a/env.go +++ b/env.go @@ -10,11 +10,6 @@ import ( "time" ) -const ( - charComment = '#' - // TODO: handle `prefix`, i.e. single and double quote -) - // GetStringOrDefault value. func GetStringOrDefault(name, defaultV string) string { v, ok := os.LookupEnv(name) @@ -78,9 +73,17 @@ func Load(paths ...string) error { for s.Scan() { i++ line := s.Text() - if line[0] == charComment { + + // Blank lines + if line == "" { continue } + + // Comments + if strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) if len(parts) < 2 { return fmt.Errorf("missing equal sign on line %v in %v", i, path) diff --git a/env_test.go b/env_test.go index eb02f66..6f04145 100644 --- a/env_test.go +++ b/env_test.go @@ -5,148 +5,150 @@ import ( "testing" "time" + "github.com/maragudk/is" + "github.com/maragudk/env" - "github.com/matryer/is" ) func TestGetStringOrDefault(t *testing.T) { t.Run("gets the string value from the environment", func(t *testing.T) { - is := is.New(t) defer setenv("hat", "party")() v := env.GetStringOrDefault("hat", "regular") - is.Equal("party", v) + is.Equal(t, "party", v) }) t.Run("gets the default value if not set", func(t *testing.T) { - is := is.New(t) v := env.GetStringOrDefault("hat", "regular") - is.Equal("regular", v) + is.Equal(t, "regular", v) }) } func TestGetIntOrDefault(t *testing.T) { t.Run("gets the int value from the environment", func(t *testing.T) { - is := is.New(t) defer setenv("hats", "2")() v := env.GetIntOrDefault("hats", 1) - is.Equal(2, v) + is.Equal(t, 2, v) }) t.Run("gets the default value if not set", func(t *testing.T) { - is := is.New(t) v := env.GetIntOrDefault("hats", 1) - is.Equal(1, v) + is.Equal(t, 1, v) }) t.Run("gets the default value if not an int", func(t *testing.T) { - is := is.New(t) defer setenv("hats", "notanumber")() v := env.GetIntOrDefault("hats", 1) - is.Equal(1, v) + is.Equal(t, 1, v) }) } func TestGetBoolOrDefault(t *testing.T) { t.Run("gets the bool value from the environment", func(t *testing.T) { - is := is.New(t) defer setenv("hats", "true")() v := env.GetBoolOrDefault("hats", true) - is.Equal(true, v) + is.Equal(t, true, v) }) t.Run("gets the default value if not set", func(t *testing.T) { - is := is.New(t) v := env.GetBoolOrDefault("hats", false) - is.Equal(false, v) + is.Equal(t, false, v) }) t.Run("gets the default value if not a bool", func(t *testing.T) { - is := is.New(t) defer setenv("hats", "notabool")() v := env.GetBoolOrDefault("hats", false) - is.Equal(false, v) + is.Equal(t, false, v) }) } func TestGetDurationOrDefault(t *testing.T) { t.Run("gets the duration value from the environment", func(t *testing.T) { - is := is.New(t) defer setenv("wearhatfor", "1m")() v := env.GetDurationOrDefault("wearhatfor", time.Second) - is.Equal(time.Minute, v) + is.Equal(t, time.Minute, v) }) t.Run("gets the default value if not set", func(t *testing.T) { - is := is.New(t) v := env.GetDurationOrDefault("wearhatfor", time.Second) - is.Equal(time.Second, v) + is.Equal(t, time.Second, v) }) t.Run("gets the default value if not a bool", func(t *testing.T) { - is := is.New(t) defer setenv("wearhatfor", "notaduration")() v := env.GetDurationOrDefault("wearhatfor", time.Second) - is.Equal(time.Second, v) + is.Equal(t, time.Second, v) }) } func TestLoad(t *testing.T) { t.Run("loads an environment file", func(t *testing.T) { - is := is.New(t) defer unsetenv("hat", "hats", "equals") err := env.Load("testdata/env") - is.NoErr(err) + is.NotError(t, err) + hat := env.GetStringOrDefault("hat", "regular") + is.Equal(t, "party", hat) + hats := env.GetIntOrDefault("hats", 1) + is.Equal(t, 2, hats) + }) + + t.Run("loads multiple environment files, and later files take precedence", func(t *testing.T) { + defer unsetenv("hat", "hats", "equals") + err := env.Load("testdata/env", "testdata/env2") + is.NotError(t, err) hat := env.GetStringOrDefault("hat", "regular") - is.Equal("party", hat) + is.Equal(t, "party", hat) hats := env.GetIntOrDefault("hats", 1) - is.Equal(2, hats) + is.Equal(t, 3, hats) + }) + + t.Run("ignores blank lines", func(t *testing.T) { + defer unsetenv("hat", "hats") + err := env.Load("testdata/blank") + is.NotError(t, err) + hat := env.GetStringOrDefault("hat", "regular") + is.Equal(t, "party", hat) }) t.Run("errors on bad file", func(t *testing.T) { - is := is.New(t) err := env.Load("testdata/invalid") - is.True(err != nil) - is.Equal("missing equal sign on line 1 in testdata/invalid", err.Error()) + is.True(t, err != nil) + is.Equal(t, "missing equal sign on line 1 in testdata/invalid", err.Error()) }) - t.Run("ignore comments in environment file", func(t *testing.T) { - is := is.New(t) - defer unsetenv("hat", "hats", "equals") - err := env.Load("testdata/with_comment") - is.NoErr(err) - equals := env.GetStringOrDefault("equals", "") - is.Equal("somethingwithequalsafter=", equals) + t.Run("ignores comments in environment file", func(t *testing.T) { + defer unsetenv("hat") + err := env.Load("testdata/comments") + is.NotError(t, err) + hat := env.GetStringOrDefault("hat", "regular") + is.Equal(t, "party", hat) }) t.Run("gets the string value including equal signs", func(t *testing.T) { - is := is.New(t) defer unsetenv("hat", "hats", "equals") err := env.Load("testdata/env") - is.NoErr(err) + is.NotError(t, err) equals := env.GetStringOrDefault("equals", "") - is.Equal("somethingwithequalsafter=", equals) + is.Equal(t, "somethingwithequalsafter=", equals) }) } func TestMustLoad(t *testing.T) { t.Run("loads an environment file", func(t *testing.T) { - is := is.New(t) defer unsetenv("hat", "hats", "equals") env.MustLoad("testdata/env") hat := env.GetStringOrDefault("hat", "regular") - is.Equal("party", hat) + is.Equal(t, "party", hat) hats := env.GetIntOrDefault("hats", 1) - is.Equal(2, hats) + is.Equal(t, 2, hats) }) t.Run("panics on no such file", func(t *testing.T) { - is := is.New(t) recovered := false defer func() { if err := recover(); err != nil { recovered = true } - is.True(recovered) + is.True(t, recovered) }() env.MustLoad() }) diff --git a/go.mod b/go.mod index 31d07c8..566f286 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/maragudk/env -go 1.16 +go 1.18 -require github.com/matryer/is v1.4.0 +require github.com/maragudk/is v0.1.0 diff --git a/go.sum b/go.sum index ddd6bbf..9846cae 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/maragudk/is v0.1.0 h1:obq9anZNmOYcaNbeT0LMyjIexdNeYTw/TLAPD/BnZHA= +github.com/maragudk/is v0.1.0/go.mod h1:W/r6+TpnISu+a88OLXQy5JQGCOhXQXXLD2e5b4xMn5c= diff --git a/testdata/blank b/testdata/blank new file mode 100644 index 0000000..5a98ea5 --- /dev/null +++ b/testdata/blank @@ -0,0 +1,3 @@ +hat=party + +hats=3 diff --git a/testdata/comments b/testdata/comments new file mode 100644 index 0000000..81fa14b --- /dev/null +++ b/testdata/comments @@ -0,0 +1,3 @@ +# this a comment +hat=party +# this another comment diff --git a/testdata/env2 b/testdata/env2 new file mode 100644 index 0000000..1bf68d8 --- /dev/null +++ b/testdata/env2 @@ -0,0 +1 @@ +hats=3 diff --git a/testdata/with_comment b/testdata/with_comment deleted file mode 100644 index 094d347..0000000 --- a/testdata/with_comment +++ /dev/null @@ -1,5 +0,0 @@ -# this a comment, and it should be ignored!!!! -hat=party -hats=2 -equals=somethingwithequalsafter= -# this another comment and should be ignored!!!