This commit is contained in:
parent
af970769d7
commit
4fde7d8865
|
@ -93,6 +93,12 @@ You can provide default configuration flags to `act` by either creating a `./.ac
|
||||||
-P ubuntu-latest=nektos/act-environments-ubuntu:18.04
|
-P ubuntu-latest=nektos/act-environments-ubuntu:18.04
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additionally, act support loading environment variables from a `.env` file. The default is to look in the working directory for the file, but can be overriden by:
|
||||||
|
|
||||||
|
```
|
||||||
|
act --env-file my.env
|
||||||
|
```
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
Need help? Ask on [Gitter](https://gitter.im/nektos/act)!
|
Need help? Ask on [Gitter](https://gitter.im/nektos/act)!
|
||||||
|
|
|
@ -17,6 +17,7 @@ type Input struct {
|
||||||
dryrun bool
|
dryrun bool
|
||||||
forcePull bool
|
forcePull bool
|
||||||
noOutput bool
|
noOutput bool
|
||||||
|
envfile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Input) resolve(path string) string {
|
func (i *Input) resolve(path string) string {
|
||||||
|
@ -33,6 +34,11 @@ func (i *Input) resolve(path string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Envfile returns path to .env
|
||||||
|
func (i *Input) Envfile() string {
|
||||||
|
return i.resolve(i.envfile)
|
||||||
|
}
|
||||||
|
|
||||||
// Workdir returns path to workdir
|
// Workdir returns path to workdir
|
||||||
func (i *Input) Workdir() string {
|
func (i *Input) Workdir() string {
|
||||||
return i.resolve(".")
|
return i.resolve(".")
|
||||||
|
|
12
cmd/root.go
12
cmd/root.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
|
|
||||||
fswatch "github.com/andreaskoch/go-fswatch"
|
fswatch "github.com/andreaskoch/go-fswatch"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/nektos/act/pkg/runner"
|
"github.com/nektos/act/pkg/runner"
|
||||||
gitignore "github.com/sabhiram/go-gitignore"
|
gitignore "github.com/sabhiram/go-gitignore"
|
||||||
|
@ -45,7 +46,9 @@ func Execute(ctx context.Context, version string) {
|
||||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read")
|
||||||
rootCmd.SetArgs(args())
|
rootCmd.SetArgs(args())
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
@ -91,6 +94,15 @@ func setupLogging(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
|
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
envfile := input.Envfile()
|
||||||
|
if _, err := os.Stat(envfile); err == nil {
|
||||||
|
log.Debugf("Loading environment from %s", envfile)
|
||||||
|
err := godotenv.Load(envfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error loading environment from %s: %v", envfile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
|
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -18,6 +18,7 @@ require (
|
||||||
github.com/gorilla/mux v1.7.0 // indirect
|
github.com/gorilla/mux v1.7.0 // indirect
|
||||||
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
|
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||||
github.com/mgutz/str v1.2.0 // indirect
|
github.com/mgutz/str v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -58,6 +58,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8=
|
github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8=
|
||||||
|
|
|
@ -3,9 +3,11 @@ package runner
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -88,3 +90,53 @@ func TestRunEvent(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunEventSecrets(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
platforms := map[string]string{
|
||||||
|
"ubuntu-latest": "node:12.6-buster-slim",
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowPath := "secrets"
|
||||||
|
eventName := "push"
|
||||||
|
|
||||||
|
workdir, err := filepath.Abs("testdata")
|
||||||
|
assert.NilError(t, err, workflowPath)
|
||||||
|
|
||||||
|
_ = godotenv.Load(filepath.Join(workdir, workflowPath, ".env"))
|
||||||
|
|
||||||
|
secrets := make(map[string]string)
|
||||||
|
for _, secret := range []string{
|
||||||
|
"MY_SECRET",
|
||||||
|
"MULTILINE_SECRET",
|
||||||
|
"JSON_SECRET",
|
||||||
|
} {
|
||||||
|
if env, ok := os.LookupEnv(secret); ok && env != "" {
|
||||||
|
secrets[secret] = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerConfig := &Config{
|
||||||
|
Workdir: workdir,
|
||||||
|
EventName: eventName,
|
||||||
|
Platforms: platforms,
|
||||||
|
ReuseContainers: false,
|
||||||
|
Secrets: secrets,
|
||||||
|
}
|
||||||
|
runner, err := New(runnerConfig)
|
||||||
|
assert.NilError(t, err, workflowPath)
|
||||||
|
|
||||||
|
planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath))
|
||||||
|
assert.NilError(t, err, workflowPath)
|
||||||
|
|
||||||
|
plan := planner.PlanEvent(eventName)
|
||||||
|
|
||||||
|
err = runner.NewPlanExecutor(plan)(ctx)
|
||||||
|
assert.NilError(t, err, workflowPath)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
MY_SECRET=top-secret
|
||||||
|
MULTILINE_SECRET="foo\nbar\nbaz"
|
||||||
|
JSON_SECRET={"foo": "bar"}
|
|
@ -7,3 +7,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
echo '${{secrets.MY_SECRET}}' | grep 'top-secret'
|
echo '${{secrets.MY_SECRET}}' | grep 'top-secret'
|
||||||
|
- run: |
|
||||||
|
echo "${{secrets.MULTILINE_SECRET}}" | wc -l | grep 3
|
||||||
|
- run: |
|
||||||
|
echo '${{secrets.JSON_SECRET}}' | grep "{\"foo\": \"bar\"}"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright (c) 2013 John Barton
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
# GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||||
|
|
||||||
|
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
|
||||||
|
|
||||||
|
From the original Library:
|
||||||
|
|
||||||
|
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||||
|
>
|
||||||
|
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||||
|
|
||||||
|
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
|
||||||
|
|
||||||
|
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
As a library
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you want to use it as a bin command
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv/cmd/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Add your application configuration to your `.env` file in the root of your project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
S3_BUCKET=YOURS3BUCKET
|
||||||
|
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in your Go app you can do something like
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Bucket := os.Getenv("S3_BUCKET")
|
||||||
|
secretKey := os.Getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
// now do something with s3 or whatever
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
```
|
||||||
|
|
||||||
|
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||||
|
|
||||||
|
```go
|
||||||
|
_ = godotenv.Load("somerandomfile")
|
||||||
|
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# I am a comment and that is OK
|
||||||
|
SOME_VAR=someval
|
||||||
|
FOO=BAR # comments at line end are OK too
|
||||||
|
export BAR=BAZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Or finally you can do YAML(ish) style
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
FOO: bar
|
||||||
|
BAR: baz
|
||||||
|
```
|
||||||
|
|
||||||
|
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||||
|
|
||||||
|
```go
|
||||||
|
var myEnv map[string]string
|
||||||
|
myEnv, err := godotenv.Read()
|
||||||
|
|
||||||
|
s3Bucket := myEnv["S3_BUCKET"]
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from an `io.Reader` instead of a local file
|
||||||
|
|
||||||
|
```go
|
||||||
|
reader := getRemoteFile()
|
||||||
|
myEnv, err := godotenv.Parse(reader)
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from a `string` if you so desire
|
||||||
|
|
||||||
|
```go
|
||||||
|
content := getRemoteFileContent()
|
||||||
|
myEnv, err := godotenv.Unmarshal(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Mode
|
||||||
|
|
||||||
|
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||||
|
|
||||||
|
```
|
||||||
|
godotenv -f /some/path/to/.env some_command with some args
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||||
|
|
||||||
|
### Writing Env Files
|
||||||
|
|
||||||
|
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
err := godotenv.Write(env, "./.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
... or to a string
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
content, err := godotenv.Marshal(env)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
|
||||||
|
|
||||||
|
*code changes without tests will not be accepted*
|
||||||
|
|
||||||
|
1. Fork it
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create new Pull Request
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||||
|
|
||||||
|
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv)
|
||||||
|
|
||||||
|
## Who?
|
||||||
|
|
||||||
|
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.
|
|
@ -0,0 +1,346 @@
|
||||||
|
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||||
|
//
|
||||||
|
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
||||||
|
//
|
||||||
|
// The TL;DR is that you make a .env file that looks something like
|
||||||
|
//
|
||||||
|
// SOME_ENV_VAR=somevalue
|
||||||
|
//
|
||||||
|
// and then in your go code you can call
|
||||||
|
//
|
||||||
|
// godotenv.Load()
|
||||||
|
//
|
||||||
|
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||||
|
package godotenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||||
|
|
||||||
|
// Load will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Load without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Load("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
||||||
|
func Load(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, false)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Overload without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Overload("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
|
||||||
|
func Overload(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, true)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all env (with same file loading semantics as Load) but return values as
|
||||||
|
// a map rather than automatically writing values into env
|
||||||
|
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
individualEnvMap, individualErr := readFile(filename)
|
||||||
|
|
||||||
|
if individualErr != nil {
|
||||||
|
err = individualErr
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range individualEnvMap {
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||||
|
func Parse(r io.Reader) (envMap map[string]string, err error) {
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fullLine := range lines {
|
||||||
|
if !isIgnoredLine(fullLine) {
|
||||||
|
var key, value string
|
||||||
|
key, value, err = parseLine(fullLine, envMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||||
|
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||||
|
return Parse(strings.NewReader(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||||
|
// then executes the cmd specified.
|
||||||
|
//
|
||||||
|
// Simply hooks up os.Stdin/err/out to the command and calls Run()
|
||||||
|
//
|
||||||
|
// If you want more fine grained control over your command it's recommended
|
||||||
|
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
|
||||||
|
func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
||||||
|
Load(filenames...)
|
||||||
|
|
||||||
|
command := exec.Command(cmd, cmdArgs...)
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
command.Stdout = os.Stdout
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write serializes the given environment and writes it to a file
|
||||||
|
func Write(envMap map[string]string, filename string) error {
|
||||||
|
content, error := Marshal(envMap)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
file, error := os.Create(filename)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
_, err := file.WriteString(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||||
|
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||||
|
func Marshal(envMap map[string]string) (string, error) {
|
||||||
|
lines := make([]string, 0, len(envMap))
|
||||||
|
for k, v := range envMap {
|
||||||
|
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||||
|
}
|
||||||
|
sort.Strings(lines)
|
||||||
|
return strings.Join(lines, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenamesOrDefault(filenames []string) []string {
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
return []string{".env"}
|
||||||
|
}
|
||||||
|
return filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(filename string, overload bool) error {
|
||||||
|
envMap, err := readFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEnv := map[string]bool{}
|
||||||
|
rawEnv := os.Environ()
|
||||||
|
for _, rawEnvLine := range rawEnv {
|
||||||
|
key := strings.Split(rawEnvLine, "=")[0]
|
||||||
|
currentEnv[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range envMap {
|
||||||
|
if !currentEnv[key] || overload {
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(filename string) (envMap map[string]string, err error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return Parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
err = errors.New("zero length string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ditch the comments (but keep quoted hashes)
|
||||||
|
if strings.Contains(line, "#") {
|
||||||
|
segmentsBetweenHashes := strings.Split(line, "#")
|
||||||
|
quotesAreOpen := false
|
||||||
|
var segmentsToKeep []string
|
||||||
|
for _, segment := range segmentsBetweenHashes {
|
||||||
|
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||||
|
if quotesAreOpen {
|
||||||
|
quotesAreOpen = false
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
} else {
|
||||||
|
quotesAreOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Join(segmentsToKeep, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEquals := strings.Index(line, "=")
|
||||||
|
firstColon := strings.Index(line, ":")
|
||||||
|
splitString := strings.SplitN(line, "=", 2)
|
||||||
|
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
|
||||||
|
//this is a yaml-style line
|
||||||
|
splitString = strings.SplitN(line, ":", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitString) != 2 {
|
||||||
|
err = errors.New("Can't separate key from value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the key
|
||||||
|
key = splitString[0]
|
||||||
|
if strings.HasPrefix(key, "export") {
|
||||||
|
key = strings.TrimPrefix(key, "export")
|
||||||
|
}
|
||||||
|
key = strings.Trim(key, " ")
|
||||||
|
|
||||||
|
// Parse the value
|
||||||
|
value = parseValue(splitString[1], envMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(value string, envMap map[string]string) string {
|
||||||
|
|
||||||
|
// trim
|
||||||
|
value = strings.Trim(value, " ")
|
||||||
|
|
||||||
|
// check if we've got quoted values or possible escapes
|
||||||
|
if len(value) > 1 {
|
||||||
|
rs := regexp.MustCompile(`\A'(.*)'\z`)
|
||||||
|
singleQuotes := rs.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
rd := regexp.MustCompile(`\A"(.*)"\z`)
|
||||||
|
doubleQuotes := rd.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
if singleQuotes != nil || doubleQuotes != nil {
|
||||||
|
// pull the quotes off the edges
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if doubleQuotes != nil {
|
||||||
|
// expand newlines
|
||||||
|
escapeRegex := regexp.MustCompile(`\\.`)
|
||||||
|
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
|
||||||
|
c := strings.TrimPrefix(match, `\`)
|
||||||
|
switch c {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// unescape characters
|
||||||
|
e := regexp.MustCompile(`\\([^$])`)
|
||||||
|
value = e.ReplaceAllString(value, "$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if singleQuotes == nil {
|
||||||
|
value = expandVariables(value, envMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandVariables(v string, m map[string]string) string {
|
||||||
|
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
|
||||||
|
|
||||||
|
return r.ReplaceAllStringFunc(v, func(s string) string {
|
||||||
|
submatch := r.FindStringSubmatch(s)
|
||||||
|
|
||||||
|
if submatch == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if submatch[1] == "\\" || submatch[2] == "(" {
|
||||||
|
return submatch[0][1:]
|
||||||
|
} else if submatch[4] != "" {
|
||||||
|
return m[submatch[4]]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnoredLine(line string) bool {
|
||||||
|
trimmedLine := strings.Trim(line, " \n\t")
|
||||||
|
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doubleQuoteEscape(line string) string {
|
||||||
|
for _, c := range doubleQuoteSpecialChars {
|
||||||
|
toReplace := "\\" + string(c)
|
||||||
|
if c == '\n' {
|
||||||
|
toReplace = `\n`
|
||||||
|
}
|
||||||
|
if c == '\r' {
|
||||||
|
toReplace = `\r`
|
||||||
|
}
|
||||||
|
line = strings.Replace(line, string(c), toReplace, -1)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
|
@ -68,6 +68,8 @@ github.com/howeyc/gopass
|
||||||
github.com/inconshreveable/mousetrap
|
github.com/inconshreveable/mousetrap
|
||||||
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||||
github.com/jbenet/go-context/io
|
github.com/jbenet/go-context/io
|
||||||
|
# github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/joho/godotenv
|
||||||
# github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e
|
# github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e
|
||||||
github.com/kevinburke/ssh_config
|
github.com/kevinburke/ssh_config
|
||||||
# github.com/konsorten/go-windows-terminal-sequences v1.0.1
|
# github.com/konsorten/go-windows-terminal-sequences v1.0.1
|
||||||
|
|
Loading…
Reference in New Issue