Skip to content

Commit

Permalink
Merge pull request #12 from m-mizutani/feature/overwrite
Browse files Browse the repository at this point in the history
feat(cli): Allow to override variable
  • Loading branch information
m-mizutani authored Jun 22, 2024
2 parents f0c8abf + 4cdaaa1 commit c72b5cd
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 19 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ You can specify arguments to specify loading environment in same manner with exe

## Advanced Usage

### Overriding environment variable

`zenv` can override environment variable by multiple `-e` option.

```sh
$ cat .env
POSTGRES_DB=your_local_db
POSTGRES_USER=test_user
$ cat .env.local
POSTGRES_DB=your_local_dev_db
$ zenv -e .env -e .env.local psql
# Access to your_local_dev_db with test_user
```

The priority for loading environment variables is as follows: first, the `-e` option, followed by additional `-e` options, and finally, the arguments of the `zenv` command.

```sh
$ cat .env1
COLOR=blue
$ cat .env2
COLOR=orange
$ zenv -e .env1 -e .env2 COLOR=red echo %COLOR
red
```

### Generate random secure value

`secret generate` subcommand can generate random value like token and save to KeyChain.
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ require (
github.com/google/uuid v1.3.0
github.com/keybase/go-keychain v0.0.0-20221221221913-9be78f6c498b
github.com/m-mizutani/goerr v0.1.8
github.com/m-mizutani/gt v0.0.4-0.20230223020823-2e7042cd92a6
github.com/m-mizutani/gt v0.0.10
github.com/mattn/go-shellwords v1.0.12
github.com/rs/zerolog v1.29.0
github.com/urfave/cli/v2 v2.24.4
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/keybase/go-keychain v0.0.0-20221221221913-9be78f6c498b h1:k2ZvAPXrDB1Q7fGRdUane+T08K+UaL96qH47Setr/7k=
github.com/keybase/go-keychain v0.0.0-20221221221913-9be78f6c498b/go.mod h1:TXh6wFVZNh4iuqbymzy4r3obmj8hTPDlLNnoKNIbvJE=
github.com/m-mizutani/goerr v0.1.8 h1:6UtsMmOkJsaYNtAsMNLvWIteZPl1NOxpKFYK5m65vpQ=
github.com/m-mizutani/goerr v0.1.8/go.mod h1:fQkXuu06q+oLlp4FkbiTFzI/N/+WAK/Mz1W5kPZ6yzs=
github.com/m-mizutani/gt v0.0.4-0.20230223020823-2e7042cd92a6 h1:aB3qT7U3VGuzOyoz/6QbwE7DxbrFWQBLlbOYwjSSBss=
github.com/m-mizutani/gt v0.0.4-0.20230223020823-2e7042cd92a6/go.mod h1:0MPYSfGBLmYjTduzADVmIqD58ELQ5IfBFiK/f0FmB3k=
github.com/m-mizutani/gt v0.0.10 h1:gJsRcZ0R0kcVAGeahwDAVBCDCwOA/tFw3N1/kh3DnAY=
github.com/m-mizutani/gt v0.0.10/go.mod h1:0MPYSfGBLmYjTduzADVmIqD58ELQ5IfBFiK/f0FmB3k=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
Expand Down
22 changes: 18 additions & 4 deletions pkg/controller/cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func WithUsecase(usecase *usecase.Usecase) Option {

func (x *Command) Run(args []string) error {
var appCfg model.Config
var dotEnvFiles cli.StringSlice

app := &cli.App{
Name: "zenv",
Expand All @@ -52,12 +53,19 @@ func (x *Command) Run(args []string) error {
Value: "zenv.",
},

&cli.StringFlag{
&cli.StringSliceFlag{
Name: "env-file",
Usage: "specify dotenv file",
Usage: "specify .env file",
Aliases: []string{"e"},
Destination: (*string)(&appCfg.DotEnvFile),
Value: (string)(model.DefaultDotEnvFilePath),
Destination: &dotEnvFiles,
Value: cli.NewStringSlice(string(model.DefaultDotEnvFilePath)),
},

&cli.StringFlag{
Name: "override",
Usage: "override .env file",
Aliases: []string{"o"},
Destination: (*string)(&appCfg.OverrideEnvFile),
},
},
Commands: []*cli.Command{
Expand All @@ -66,6 +74,12 @@ func (x *Command) Run(args []string) error {
},
Before: func(c *cli.Context) error {
x.usecase = x.usecase.Clone(usecase.WithConfig(&appCfg))

appCfg.DotEnvFiles = make([]types.FilePath, len(dotEnvFiles.Value()))
for i, v := range dotEnvFiles.Value() {
appCfg.DotEnvFiles[i] = types.FilePath(v)
}

return nil
},
Action: func(c *cli.Context) error {
Expand Down
3 changes: 2 additions & 1 deletion pkg/domain/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (

type Config struct {
KeychainNamespacePrefix types.NamespacePrefix
DotEnvFile types.FilePath
DotEnvFiles []types.FilePath
OverrideEnvFile types.FilePath
}

var envVarNameRegex = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
16 changes: 13 additions & 3 deletions pkg/usecase/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func (x *Usecase) loadEnvVar(arg types.Argument) ([]*model.EnvVar, error) {
func (x *Usecase) parseArgs(args types.Arguments) (types.Arguments, []*model.EnvVar, error) {
var envVars []*model.EnvVar

if x.config.DotEnvFile != "" {
loaded, err := loadDotEnv(x.config.DotEnvFile, x.client.ReadFile)
for _, dotEnvFile := range x.config.DotEnvFiles {
loaded, err := loadDotEnv(dotEnvFile, x.client.ReadFile)
if err != nil {
return nil, nil, err
}
Expand All @@ -76,7 +76,7 @@ func (x *Usecase) parseArgs(args types.Arguments) (types.Arguments, []*model.Env
if err != nil {
return nil, nil, err
} else if vars == nil {
return nil, nil, goerr.Wrap(types.ErrInvalidArgumentFormat, "in dotenv file").With("arg", arg).With("file", x.config.DotEnvFile)
return nil, nil, goerr.Wrap(types.ErrInvalidArgumentFormat, "in dotenv file").With("arg", arg).With("file", dotEnvFile)
}

envVars = append(envVars, vars...)
Expand Down Expand Up @@ -124,6 +124,16 @@ func (x *Usecase) parseArgs(args types.Arguments) (types.Arguments, []*model.Env
}
}

// Remove duplicated env vars. The last one is used.
unique := make(map[types.EnvKey]*model.EnvVar)
for _, v := range envVars {
unique[v.Key] = v
}
envVars = make([]*model.EnvVar, 0, len(unique))
for _, v := range unique {
envVars = append(envVars, v)
}

assigned := make([]*model.EnvVar, len(envVars))
for i, v := range envVars {
newVar := *v
Expand Down
96 changes: 93 additions & 3 deletions pkg/usecase/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package usecase_test

import (
"fmt"
"os"
"sort"
"testing"

"github.com/m-mizutani/gt"
Expand Down Expand Up @@ -76,12 +78,15 @@ func TestBasicExec(t *testing.T) {
func TestDotEnv(t *testing.T) {
t.Run("exec with dotenv file", func(t *testing.T) {
uc, mock := usecase.NewWithMock(usecase.WithConfig(&model.Config{
DotEnvFile: ".mydotenv",
DotEnvFiles: []types.FilePath{".mydotenv"},
}))
mock.ExecMock = func(vars []*model.EnvVar, args types.Arguments) error {
gt.Array(t, args).
Equal([]types.Argument{"this", "test"})

sort.Slice(vars, func(i, j int) bool {
return vars[i].Key < vars[j].Key
})
gt.Array(t, vars).Equal([]*model.EnvVar{
{
Key: "COLOR",
Expand Down Expand Up @@ -111,7 +116,7 @@ NUMBER=five

t.Run("error when invalid line in dotenv file", func(t *testing.T) {
uc, mock := usecase.NewWithMock(usecase.WithConfig(
&model.Config{DotEnvFile: ".env"},
&model.Config{DotEnvFiles: []types.FilePath{".env"}},
))

mock.ReadFileMock = func(filename types.FilePath) ([]byte, error) {
Expand All @@ -130,7 +135,7 @@ NUMBER=five
t.Run("something bad in reading dotenv", func(t *testing.T) {
err := fmt.Errorf("something bad")
uc, mock := usecase.NewWithMock(usecase.WithConfig(
&model.Config{DotEnvFile: ".env"},
&model.Config{DotEnvFiles: []types.FilePath{".env"}},
))
mock.ReadFileMock = func(filename types.FilePath) ([]byte, error) {
return nil, err
Expand Down Expand Up @@ -163,3 +168,88 @@ func TestReplacement(t *testing.T) {
}))
})
}

func TestOverride(t *testing.T) {
type testCase struct {
inputFiles []types.FilePath
envVars []*model.EnvVar
}

runTest := func(tc testCase) func(t *testing.T) {
return func(t *testing.T) {
var called int
uc, mock := usecase.NewWithMock(usecase.WithConfig(&model.Config{
DotEnvFiles: tc.inputFiles,
}))
mock.ReadFileMock = func(filename types.FilePath) ([]byte, error) {
return os.ReadFile(string(filename))
}

mock.ExecMock = func(vars []*model.EnvVar, args types.Arguments) error {
called++

sort.Slice(vars, func(i, j int) bool {
return vars[i].Key < vars[j].Key
})

gt.Array(t, vars).Equal(tc.envVars)
return nil
}

gt.NoError(t, uc.Exec(&model.ExecInput{
Args: types.Arguments{"this", "test"},
}))
gt.Equal(t, called, 1)
}
}

t.Run("no override env vars", runTest(testCase{
inputFiles: []types.FilePath{"testdata/basic.env"},
envVars: []*model.EnvVar{
{Key: "COLOR", Value: "blue"},
},
}))

t.Run("override env vars by one file", runTest(testCase{
inputFiles: []types.FilePath{
"testdata/basic.env",
"testdata/override1.env",
},
envVars: []*model.EnvVar{
{Key: "COLOR", Value: "orange"},
},
}))

t.Run("override env vars by two files", runTest(testCase{
inputFiles: []types.FilePath{
"testdata/basic.env",
"testdata/override1.env",
"testdata/override2.env",
},
envVars: []*model.EnvVar{
{Key: "COLOR", Value: "red"},
},
}))

t.Run("override env vars prioritize by order", runTest(testCase{
inputFiles: []types.FilePath{
"testdata/basic.env",
"testdata/override2.env",
"testdata/override1.env",
},
envVars: []*model.EnvVar{
{Key: "COLOR", Value: "orange"},
},
}))

t.Run("override env vars prioritize by order (2)", runTest(testCase{
inputFiles: []types.FilePath{
"testdata/override2.env",
"testdata/override1.env",
"testdata/basic.env",
},
envVars: []*model.EnvVar{
{Key: "COLOR", Value: "blue"},
},
}))
}
1 change: 1 addition & 0 deletions pkg/usecase/testdata/basic.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COLOR=blue
1 change: 1 addition & 0 deletions pkg/usecase/testdata/override1.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COLOR=orange
1 change: 1 addition & 0 deletions pkg/usecase/testdata/override2.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COLOR=red
4 changes: 2 additions & 2 deletions pkg/usecase/usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestGenerate(t *testing.T) {
mock.PutKeyChainValuesMock = func(envVars []*model.EnvVar, namespace types.Namespace) error {
gt.V(t, namespace).Equal("zenv.bridge")
gt.A(t, envVars).Length(1).
Elem(0, func(t testing.TB, v *model.EnvVar) {
At(0, func(t testing.TB, v *model.EnvVar) {
gt.Value(t, v.Key).Equal("SECRET")
gt.N(t, len(v.Value)).Equal(24)
})
Expand Down Expand Up @@ -153,7 +153,7 @@ func TestFileLoader(t *testing.T) {

func TestAssign(t *testing.T) {
uc, mock := usecase.NewWithMock(usecase.WithConfig(&model.Config{
DotEnvFile: ".env",
DotEnvFiles: []types.FilePath{".env"},
}))
mock.ReadFileMock = func(filename types.FilePath) ([]byte, error) {
return []byte("BLUE=%ORANGE"), nil
Expand Down

0 comments on commit c72b5cd

Please sign in to comment.