[config] move to godotenv
This commit is contained in:
@@ -15,12 +15,12 @@ build_env_pre_build_buildpack() {
|
||||
dokku_log_info1 "Adding BUILD_ENV to build environment..."
|
||||
# create build env files for use in buildpacks like this:
|
||||
# https://github.com/niteoweb/heroku-buildpack-buildout/blob/5879fa3418f7d8e079f1aa5816ba1adde73f4948/bin/compile#L34
|
||||
local id=$(config_export app "$APP" --merged --bundle | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /tmp/env; cat | tar -x -C /tmp/env")
|
||||
local id=$(config_bundle --merged "$APP" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /tmp/env; cat | tar -x -C /tmp/env")
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
|
||||
# create build env for 'old style' buildpacks and dokku plugins
|
||||
local id=$(config_export app "$APP" --envfile --merged --escape-newlines | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "cat >> /app/.env")
|
||||
local id=$(config_export app "$APP" --format envfile --merged | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "cat >> /app/.env")
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ include ../../common.mk
|
||||
|
||||
GO_ARGS ?= -a
|
||||
|
||||
SUBCOMMANDS = subcommands/export subcommands/get subcommands/set subcommands/unset
|
||||
SUBCOMMANDS = subcommands/export subcommands/get subcommands/set subcommands/unset subcommands/keys subcommands/bundle
|
||||
|
||||
build-in-docker: clean
|
||||
docker run --rm \
|
||||
|
||||
@@ -1,20 +1,75 @@
|
||||
package config
|
||||
|
||||
import configenv "github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
import columnize "github.com/ryanuber/columnize"
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
//GetWithDefault gets value from app config
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
configenv "github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
columnize "github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
//GetWithDefault gets a value from a config. If appName is empty the global config is used.
|
||||
func GetWithDefault(appName string, key string, defaultValue string) string {
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
env, err := loadConfig(appName)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return env.GetDefault(key, defaultValue)
|
||||
}
|
||||
|
||||
//PrettyPrintLogEntries in columns
|
||||
func PrettyPrintLogEntries(prefix string, entries map[string]string) string {
|
||||
//SetMany variables in the environment. If appName is empty the global config is used. If restart is true the app is restarted.
|
||||
func SetMany(appName string, entries map[string]string, restart bool) {
|
||||
global := appName == ""
|
||||
env := GetConfig(appName, false)
|
||||
|
||||
keys := make([]string, 0, len(entries))
|
||||
|
||||
for k, v := range entries {
|
||||
env.Set(k, v)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
if len(entries) != 0 {
|
||||
common.LogInfo1("Setting config vars")
|
||||
fmt.Println(PrettyPrintEnvEntries(" ", entries))
|
||||
env.Write()
|
||||
args := append([]string{appName, "set"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
common.LogInfo1(fmt.Sprintf("Restarting app %s", appName))
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
}
|
||||
|
||||
//Unset a value in a config. If appName is empty the global config is used. If restart is true the app is restarted.
|
||||
func Unset(appName string, keys []string, restart bool) {
|
||||
global := appName == ""
|
||||
env := GetConfig(appName, false)
|
||||
var changed = false
|
||||
for _, k := range keys {
|
||||
common.LogInfo1(fmt.Sprintf("Unsetting %s", k))
|
||||
env.Unset(k)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
env.Write()
|
||||
args := append([]string{appName, "unset"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
common.LogInfo1(fmt.Sprintf("Restarting app %s", appName))
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
}
|
||||
|
||||
//PrettyPrintEnvEntries in columns
|
||||
func PrettyPrintEnvEntries(prefix string, entries map[string]string) string {
|
||||
colConfig := columnize.DefaultConfig()
|
||||
colConfig.Prefix = prefix
|
||||
colConfig.Delim = "\x00"
|
||||
@@ -24,3 +79,45 @@ func PrettyPrintLogEntries(prefix string, entries map[string]string) string {
|
||||
}
|
||||
return columnize.Format(lines, colConfig)
|
||||
}
|
||||
|
||||
//GetCommonArgs extracts common positional args (appName and keys)
|
||||
func GetCommonArgs(global bool, args []string) (string, []string) {
|
||||
nextArg := 0
|
||||
appName := ""
|
||||
if !global {
|
||||
if len(args) > 0 {
|
||||
appName = args[0]
|
||||
}
|
||||
if appName == "" {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
} else {
|
||||
nextArg++
|
||||
}
|
||||
}
|
||||
keys := args[nextArg:]
|
||||
return appName, keys
|
||||
}
|
||||
|
||||
//GetConfig for the given app (global config if appName is empty). Merge with global config if merged is true.
|
||||
func GetConfig(appName string, merged bool) *configenv.Env {
|
||||
env, err := loadConfig(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
if appName != "" && merged {
|
||||
global, err := configenv.LoadGlobal()
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
global.Merge(env)
|
||||
return global
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func loadConfig(appName string) (*configenv.Env, error) {
|
||||
if appName == "" || appName == "--global" {
|
||||
return configenv.LoadGlobal()
|
||||
}
|
||||
return configenv.LoadApp(appName)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ config_docker_args() {
|
||||
verify_app_name "$APP"
|
||||
|
||||
if ! is_image_herokuish_based "$IMAGE"; then
|
||||
local ENV_ARGS="$(config_export app "$APP" --prefix="-e" --separator=" " --merged)"
|
||||
local ENV_ARGS="$(config_export app "$APP" --format docker-args --merged)"
|
||||
echo -n "$STDIN $ENV_ARGS"
|
||||
else
|
||||
echo -n "$STDIN"
|
||||
|
||||
@@ -25,7 +25,7 @@ config_export() {
|
||||
|
||||
config_keys() {
|
||||
declare desc="returns keys for app or global"
|
||||
config_export "$@" "--keys"
|
||||
config_keys "$@"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -43,3 +43,8 @@ config_unset() {
|
||||
declare desc="set value of given config var"
|
||||
config_sub unset "$@"
|
||||
}
|
||||
|
||||
config_bundle() {
|
||||
declare desc="export tar bundle of config"
|
||||
config_sub bundle "$@"
|
||||
}
|
||||
|
||||
9
plugins/config/glide.lock
generated
9
plugins/config/glide.lock
generated
@@ -1,6 +1,7 @@
|
||||
hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149
|
||||
updated: 2017-01-03T17:16:50.97156327-08:00
|
||||
hash: 998d313aef819c8fa68d268d075079d281a7ccab0c3a16f6d8906b20c902f627
|
||||
updated: 2017-05-20T15:15:41.795778946-04:00
|
||||
imports:
|
||||
- name: github.com/alexquick/godotenv
|
||||
version: 147552bee9c6196b9388f15ab03fe15721264092
|
||||
- name: github.com/ryanuber/columnize
|
||||
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
|
||||
testImports: []
|
||||
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
|
||||
@@ -1,3 +1,5 @@
|
||||
package: .
|
||||
import:
|
||||
- package: github.com/ryanuber/columnize
|
||||
- package: github.com/alexquick/godotenv
|
||||
version: feature-write-dotenv
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
configenv "github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
columnize "github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
@@ -26,6 +24,8 @@ Additional commands:`
|
||||
config:set (<app>|--global) [--encoded] [--no-restart] KEY1=VALUE1 [KEY2=VALUE2 ...], Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...], Unset one or more config vars
|
||||
config:export (<app>|--global) [--envfile], Export a global or app environment
|
||||
config:keys (<app> | --global) [--merged], Show keys set in environment
|
||||
config:bundle (<app> | --global) [--merged], Bundle environment into tarfile
|
||||
`
|
||||
)
|
||||
|
||||
@@ -37,16 +37,8 @@ func main() {
|
||||
switch cmd {
|
||||
case "config", "config:show":
|
||||
target := flag.Arg(1)
|
||||
if target == "" {
|
||||
usage()
|
||||
common.LogFail("Please specify an app or --global")
|
||||
}
|
||||
env, err := configenv.NewFromTarget(target)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
} else {
|
||||
fmt.Println(config.PrettyPrintLogEntries("", env.Map()))
|
||||
}
|
||||
env := config.GetConfig(target, false)
|
||||
fmt.Print(config.PrettyPrintEnvEntries("", env.Map()))
|
||||
case "config:help":
|
||||
usage()
|
||||
case "help":
|
||||
|
||||
@@ -4,46 +4,75 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"archive/tar"
|
||||
|
||||
"os"
|
||||
|
||||
godotenv "github.com/alexquick/godotenv"
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
type ExportFormat int
|
||||
|
||||
const (
|
||||
Exports ExportFormat = iota
|
||||
Envfile
|
||||
DockerArgs
|
||||
)
|
||||
|
||||
//Env is a representation for global or app environment
|
||||
type Env struct {
|
||||
name string
|
||||
env map[string]string
|
||||
filename string
|
||||
EscapeNewlines bool
|
||||
name string
|
||||
filename string
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (e *Env) String() string {
|
||||
return e.EnvfileString()
|
||||
}
|
||||
|
||||
//EnvfileString returns the contents of this Env in ENVFILE format
|
||||
func (e *Env) Export(format ExportFormat) string {
|
||||
switch format {
|
||||
case Exports:
|
||||
return e.ExportfileString()
|
||||
case Envfile:
|
||||
return e.EnvfileString()
|
||||
case DockerArgs:
|
||||
return e.DockerArgsString()
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Unknown export format: %v", format))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//EnvfileString returns the contents of this Env in dotenv format
|
||||
func (e *Env) EnvfileString() string {
|
||||
return e.StringWithPrefixAndSeparator("", "\n")
|
||||
rep, _ := godotenv.WriteString(e.Map())
|
||||
return rep
|
||||
}
|
||||
|
||||
//ExportfileString returns the contents of this Env as bash exports
|
||||
func (e *Env) ExportfileString() string {
|
||||
return e.StringWithPrefixAndSeparator("export ", "\n")
|
||||
return e.stringWithPrefixAndSeparator("export ", "\n", true)
|
||||
}
|
||||
|
||||
//DockerArgsString gets the contents of this Env in the form -env=KEY=VALUE --env...
|
||||
func (e *Env) DockerArgsString() string {
|
||||
return e.stringWithPrefixAndSeparator("--env=", " ", true)
|
||||
}
|
||||
|
||||
//StringWithPrefixAndSeparator makes a string of the environment
|
||||
// with the given prefix and separator for each entry
|
||||
func (e *Env) StringWithPrefixAndSeparator(prefix string, separator string) string {
|
||||
func (e *Env) stringWithPrefixAndSeparator(prefix string, separator string, allowNewlines bool) string {
|
||||
keys := e.Keys()
|
||||
entries := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
v := SingleQuoteEscape(e.env[k])
|
||||
if e.EscapeNewlines {
|
||||
if !allowNewlines {
|
||||
v = strings.Replace(v, "\n", "'$'\\n''", -1)
|
||||
}
|
||||
entries[i] = fmt.Sprintf("%s%s='%s'", prefix, k, v)
|
||||
@@ -78,31 +107,45 @@ func (e *Env) ExportBundle(dest io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//NewFromTarget creates an env from the given target. Target is either "--global" or an app name
|
||||
func NewFromTarget(target string) (*Env, error) {
|
||||
if target == "--global" {
|
||||
return LoadGlobal()
|
||||
}
|
||||
return LoadApp(target)
|
||||
}
|
||||
|
||||
//LoadApp loads an environment for the given app
|
||||
func LoadApp(appName string) (*Env, error) {
|
||||
appfile, err := getAppFile(appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseEnv(appName, appfile)
|
||||
|
||||
if _, err := os.Stat(appfile); os.IsNotExist(err) {
|
||||
_, err := os.Create(appfile)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
return loadFromFile(appName, appfile)
|
||||
}
|
||||
|
||||
//LoadGlobal loads the global environmen
|
||||
func LoadGlobal() (*Env, error) {
|
||||
return parseEnv("global", getGlobalFile())
|
||||
return loadFromFile("global", getGlobalFile())
|
||||
}
|
||||
|
||||
//NewFromString creates an env from the given ENVFILE contents representation
|
||||
func NewFromString(rep string) (*Env, error) {
|
||||
return parseEnvFromReader("<unknown>", "", strings.NewReader(rep))
|
||||
envMap, err := godotenv.ReadFromReader(strings.NewReader(rep))
|
||||
env := &Env{
|
||||
"<unknown>",
|
||||
"",
|
||||
envMap,
|
||||
}
|
||||
return env, err
|
||||
}
|
||||
func loadFromFile(name string, filename string) (*Env, error) {
|
||||
envMap, err := godotenv.Read(filename)
|
||||
env := &Env{
|
||||
name,
|
||||
filename,
|
||||
envMap,
|
||||
}
|
||||
return env, err
|
||||
}
|
||||
|
||||
//Merge merges the given environment on top of the reciever
|
||||
@@ -172,13 +215,7 @@ func (e *Env) Write() error {
|
||||
if e.filename == "" {
|
||||
return errors.New("this Env was created unbound to a file")
|
||||
}
|
||||
file, err := os.Create(e.filename)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.WriteString(e.ExportfileString())
|
||||
return err
|
||||
return godotenv.Write(e.Map(), e.filename)
|
||||
}
|
||||
|
||||
func getAppFile(appName string) (string, error) {
|
||||
|
||||
@@ -11,39 +11,29 @@ func TestExportfileRoundtrip(t *testing.T) {
|
||||
env, err := NewFromString("HI='ho'\nFoo='Bar'\n\nBaz='BOFF'")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("Baz", "BOFF", "Foo", "Bar", "HI", "ho")))
|
||||
Expect(env.String()).To(Equal("Baz='BOFF'\nFoo='Bar'\nHI='ho'"))
|
||||
Expect(env.String()).To(Equal("Baz=\"BOFF\"\nFoo=\"Bar\"\nHI=\"ho\""))
|
||||
|
||||
env, err = NewFromString("\n export HI='h\\\no\\' \n")
|
||||
env, err = NewFromString(`export HI="h\no"`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "h\\\no\\")))
|
||||
Expect(env.String()).To(Equal("HI='h\\\no\\'"))
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "h\no")))
|
||||
Expect(env.String()).To(Equal(`HI="h\no"`))
|
||||
|
||||
env, err = NewFromString("\n export HI=ho\n")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho")))
|
||||
Expect(env.String()).To(Equal("HI='ho'"))
|
||||
|
||||
env, err = NewFromString("HI='ho'\nFOO=''\\''\nBAR='\\'''\\'''")
|
||||
env, err = NewFromString("HI='ho'\nFOO=\"'\\nBAR=''\"")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho", "FOO", "'\nBAR=''")))
|
||||
Expect(env.String()).To(Equal("FOO=''\\''\nBAR='\\'''\\'''\nHI='ho'"))
|
||||
Expect(env.String()).To(Equal("FOO=\"'\\nBAR=''\"\nHI=\"ho\""))
|
||||
|
||||
env, err = NewFromString("HI='ho\n'\n\nFOO='=bar\"\n'\\''\nbaz'")
|
||||
env, err = NewFromString("FOO='bar' ")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho\n", "FOO", "=bar\"\n'\nbaz")))
|
||||
Expect(env.String()).To(Equal("FOO='=bar\"\n'\\''\nbaz'\nHI='ho\n'"))
|
||||
Expect(env.Map()).To(Equal(pairs("FOO", "bar")))
|
||||
Expect(env.String()).To(Equal(`FOO="bar"`))
|
||||
|
||||
}
|
||||
|
||||
func TestExportfileErrors(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
|
||||
_, err := NewFromString("FOO='bar\\''") //single quotes are not escaped this way
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
_, err = NewFromString("F\nOO='bar'") //keys cannot have embedded newlines
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
_, err = NewFromString("FOO='bar' ") //no trailing content
|
||||
_, err := NewFromString("F\nOO='bar'") //keys cannot have embedded newlines
|
||||
Expect(err).To(HaveOccurred())
|
||||
}
|
||||
|
||||
@@ -57,9 +47,9 @@ func TestMerge(t *testing.T) {
|
||||
|
||||
func TestArrayExport(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
e, _ := NewFromString("BAR='BAZ'\nFOO='b'\\''ar '")
|
||||
Expect(e.StringWithPrefixAndSeparator("", " ")).To(Equal("BAR='BAZ' FOO='b'\\''ar '"))
|
||||
Expect(e.StringWithPrefixAndSeparator("-e", " ")).To(Equal("-eBAR='BAZ' -eFOO='b'\\''ar '"))
|
||||
e, _ := NewFromString("BAR='BAZ'\nFOO='b'ar '")
|
||||
Expect(e.EnvfileString()).To(Equal("BAR=\"BAZ\"\nFOO=\"b'ar \""))
|
||||
Expect(e.DockerArgsString()).To(Equal("--env=BAR='BAZ' --env=FOO='b'\\''ar '"))
|
||||
}
|
||||
|
||||
func pairs(vars ...string) map[string]string {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package configenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseEnv(name string, filename string) (*Env, error) {
|
||||
var file *os.File
|
||||
var err error
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
file, err = os.Create(filename)
|
||||
} else {
|
||||
file, err = os.Open(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return parseEnvFromReader(name, filename, file)
|
||||
}
|
||||
|
||||
func parseEnvFromReader(name string, filename string, reader io.Reader) (*Env, error) {
|
||||
//The file format looks like: `KEY='VALUE'`. Each pair is terminated with a newline.
|
||||
//All characters are valid for VALUE without escaping but one: the single quote.
|
||||
//A single quote literal is represented by ending the quoting
|
||||
//emitting a \' and resuming the quoting. So for VALUE to be "don't care":
|
||||
//`KEY='don'\''t care'`
|
||||
//The advantage of this is that it's easily parsed and sourced by bash correctly.
|
||||
|
||||
buffered := bufio.NewReader(reader)
|
||||
const (
|
||||
StateKey = iota
|
||||
StateValue = iota
|
||||
)
|
||||
env := &Env{name, map[string]string{}, filename, false}
|
||||
var buffer bytes.Buffer
|
||||
var state = StateKey
|
||||
var quoted = false
|
||||
var exportStyle = false
|
||||
var escaped = false
|
||||
var key = ""
|
||||
for char, _, err := buffered.ReadRune(); err == nil; char, _, err = buffered.ReadRune() {
|
||||
switch state {
|
||||
case StateKey:
|
||||
switch char {
|
||||
case ' ':
|
||||
if buffer.String() == "export" {
|
||||
exportStyle = true
|
||||
buffer.Truncate(0) //so we can read exportfiles as well as envfiles
|
||||
} else if buffer.Len() == 0 {
|
||||
continue //leading spaced are allowed but not encouraged
|
||||
} else {
|
||||
return nil, errors.New("keys cannot have spaces")
|
||||
}
|
||||
case '=':
|
||||
key = buffer.String()
|
||||
buffer.Truncate(0)
|
||||
state = StateValue
|
||||
case '\n':
|
||||
if buffer.Len() > 0 {
|
||||
return nil, errors.New("keys cannot contain newlines")
|
||||
}
|
||||
default:
|
||||
buffer.WriteRune(char)
|
||||
}
|
||||
case StateValue:
|
||||
switch char {
|
||||
case '\'':
|
||||
if escaped {
|
||||
buffer.WriteRune('\'')
|
||||
escaped = false
|
||||
} else {
|
||||
quoted = !quoted
|
||||
}
|
||||
case '\n':
|
||||
if quoted {
|
||||
buffer.WriteRune(char)
|
||||
} else {
|
||||
state = StateKey
|
||||
value := buffer.String()
|
||||
if exportStyle {
|
||||
value = strings.TrimSpace(value)
|
||||
}
|
||||
env.Set(key, value)
|
||||
buffer.Truncate(0)
|
||||
key = ""
|
||||
escaped = false
|
||||
quoted = false
|
||||
exportStyle = false
|
||||
}
|
||||
case '\\':
|
||||
if quoted {
|
||||
buffer.WriteRune(char)
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = true
|
||||
}
|
||||
default:
|
||||
if !quoted && !exportStyle {
|
||||
return nil, errors.New("trailing content for value of " + key)
|
||||
}
|
||||
if escaped {
|
||||
return nil, errors.New("invalid escape for " + key)
|
||||
}
|
||||
buffer.WriteRune(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
return nil, errors.New("unterminated escape at end")
|
||||
}
|
||||
if quoted {
|
||||
return nil, errors.New("unterminated quote at end")
|
||||
}
|
||||
|
||||
if state == StateValue {
|
||||
env.Set(key, buffer.String())
|
||||
} else {
|
||||
if buffer.Len() > 0 {
|
||||
return nil, errors.New("invalid trailing content: '" + buffer.String() + "'")
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
24
plugins/config/src/subcommands/bundle/bundle.go
Normal file
24
plugins/config/src/subcommands/bundle/bundle.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
args.Parse(os.Args[2:])
|
||||
|
||||
appName, trailingArgs := config.GetCommonArgs(*global, args.Args())
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
config := config.GetConfig(appName, *merged)
|
||||
config.ExportBundle(os.Stdout)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
@@ -16,66 +17,26 @@ func main() {
|
||||
args := flag.NewFlagSet("config:export", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
keys := args.Bool("keys", false, "--keys: export keys only")
|
||||
bundle := args.Bool("bundle", false, "--bundle: export as tar bundle")
|
||||
envfile := args.Bool("envfile", false, "--envfile: export as envfile rather than bash exports (--prefix='')")
|
||||
prefix := args.String("prefix", defaultPrefix, "--prefix: prefix")
|
||||
separator := args.String("separator", defaultSeparator, "--separator: separator")
|
||||
escapeNewlines := args.Bool("escape-newlines", false, "--escape-newlines: replace literal newlines with $'\n'")
|
||||
format := args.String("format", "exports", "--format: [ exports | envfile | docker-args ] which format to export as)")
|
||||
args.Parse(os.Args[2:])
|
||||
|
||||
appName := args.Arg(0)
|
||||
if appName == "" && !*global {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
appName, trailingArgs := config.GetCommonArgs(*global, args.Args())
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
|
||||
if *global {
|
||||
if appName != "" {
|
||||
common.LogFail("Trailing argument: " + appName)
|
||||
}
|
||||
if *merged == true {
|
||||
common.LogFail("Only app environments can be merged")
|
||||
}
|
||||
appName = "--global"
|
||||
env := config.GetConfig(appName, *merged)
|
||||
exportType := configenv.Exports
|
||||
switch *format {
|
||||
case "exports":
|
||||
exportType = configenv.Exports
|
||||
case "envfile":
|
||||
exportType = configenv.Envfile
|
||||
case "docker-args":
|
||||
exportType = configenv.DockerArgs
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Unknown export format: %v", *format))
|
||||
}
|
||||
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
|
||||
if *merged {
|
||||
global, err := configenv.LoadGlobal()
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
global.Merge(env)
|
||||
env = global
|
||||
}
|
||||
|
||||
if *keys {
|
||||
for _, k := range env.Keys() {
|
||||
fmt.Println(k)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *escapeNewlines {
|
||||
env.EscapeNewlines = true
|
||||
}
|
||||
|
||||
if *bundle {
|
||||
if *prefix != defaultPrefix || *separator != defaultSeparator || *envfile {
|
||||
common.LogFail("--bundle cannot be given with --envfile, --prefix, or --separator")
|
||||
}
|
||||
env.ExportBundle(os.Stdout)
|
||||
}
|
||||
if *envfile {
|
||||
if *prefix != defaultPrefix {
|
||||
common.LogFail("--prefix and --envfile cannot both be given")
|
||||
return
|
||||
}
|
||||
*prefix = ""
|
||||
}
|
||||
fmt.Println(env.StringWithPrefixAndSeparator(*prefix, *separator))
|
||||
exported := env.Export(exportType)
|
||||
fmt.Println(exported)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
@@ -15,28 +16,13 @@ func main() {
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
quoted := args.Bool("quoted", false, "--quoted: get the value quoted")
|
||||
args.Parse(os.Args[2:])
|
||||
appName := args.Arg(0)
|
||||
nextArg := 0
|
||||
if appName == "" && !*global {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
}
|
||||
|
||||
if *global {
|
||||
appName = "--global"
|
||||
} else {
|
||||
nextArg = 1
|
||||
appName, keys := config.GetCommonArgs(*global, args.Args())
|
||||
if len(keys) > 1 {
|
||||
common.LogFail(fmt.Sprintf("Unexpected argument(s): %v", keys[1:]))
|
||||
}
|
||||
value := config.GetWithDefault(appName, keys[0], "")
|
||||
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
|
||||
if args.NArg() > nextArg+1 {
|
||||
common.LogFail(fmt.Sprintf("Unexpected argument(s): %v", args.Args()[nextArg+1:]))
|
||||
}
|
||||
key := args.Arg(nextArg)
|
||||
value := env.GetDefault(key, "")
|
||||
if *quoted {
|
||||
fmt.Printf("'%s'", configenv.SingleQuoteEscape(value))
|
||||
} else {
|
||||
|
||||
26
plugins/config/src/subcommands/keys/keys.go
Normal file
26
plugins/config/src/subcommands/keys/keys.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
args.Parse(os.Args[2:])
|
||||
|
||||
appName, trailingArgs := config.GetCommonArgs(*global, args.Args())
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
config := config.GetConfig(appName, *merged)
|
||||
for _, k := range config.Keys() {
|
||||
fmt.Println(k)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
config "github.com/dokku/dokku/plugins/config"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
// set the given entries from the specified environment
|
||||
@@ -19,28 +18,10 @@ func main() {
|
||||
encoded := args.Bool("encoded", false, "--encoded: interpret VALUEs as base64")
|
||||
noRestart := args.Bool("no-restart", false, "--no-restart: no restart")
|
||||
args.Parse(os.Args[2:])
|
||||
shouldRestart := !*global && !*noRestart
|
||||
var nextArg = 0
|
||||
appName := args.Arg(0)
|
||||
if appName == "" && !*global {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
}
|
||||
appName, pairs := config.GetCommonArgs(*global, args.Args())
|
||||
|
||||
if *global {
|
||||
appName = "--global"
|
||||
} else {
|
||||
nextArg = 1
|
||||
}
|
||||
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
|
||||
entries := args.Args()[nextArg:]
|
||||
updated := make(map[string]string)
|
||||
for _, e := range entries {
|
||||
//log
|
||||
for _, e := range pairs {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
common.LogFail("Invalid env pair: " + e)
|
||||
@@ -53,21 +34,7 @@ func main() {
|
||||
}
|
||||
value = string(decoded)
|
||||
}
|
||||
env.Set(key, value)
|
||||
updated[key] = value
|
||||
}
|
||||
|
||||
if len(updated) != 0 {
|
||||
common.LogInfo1("Setting config vars")
|
||||
fmt.Println(config.PrettyPrintLogEntries(" ", updated))
|
||||
env.Write()
|
||||
args := append([]string{appName, "set"}, entries...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if shouldRestart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
common.LogInfo1(fmt.Sprintf("Restarting app %s", appName))
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
config.SetMany(appName, updated, !*noRestart)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"fmt"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
//unset the given entries from the given environment
|
||||
@@ -15,42 +12,8 @@ func main() {
|
||||
args := flag.NewFlagSet("config:unset", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
noRestart := args.Bool("no-restart", false, "--no-restart: no restart")
|
||||
|
||||
args.Parse(os.Args[2:])
|
||||
shouldRestart := !*global && !*noRestart
|
||||
var nextArg = 0
|
||||
appName := args.Arg(0)
|
||||
if appName == "" && !*global {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
}
|
||||
|
||||
if *global {
|
||||
appName = "--global"
|
||||
} else {
|
||||
nextArg = 1
|
||||
}
|
||||
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
|
||||
var changed = false
|
||||
keys := args.Args()[nextArg:]
|
||||
for _, k := range keys {
|
||||
common.LogInfo1(fmt.Sprintf("Unsetting %s", k))
|
||||
env.Unset(k)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
env.Write()
|
||||
args := append([]string{appName, "unset"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if shouldRestart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
common.LogInfo1(fmt.Sprintf("Restarting app %s", appName))
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
appName, keys := config.GetCommonArgs(*global, args.Args())
|
||||
config.Unset(appName, keys, !*noRestart)
|
||||
}
|
||||
|
||||
1
plugins/config/vendor/github.com/alexquick/godotenv/.gitignore
generated
vendored
Normal file
1
plugins/config/vendor/github.com/alexquick/godotenv/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
8
plugins/config/vendor/github.com/alexquick/godotenv/.travis.yml
generated
vendored
Normal file
8
plugins/config/vendor/github.com/alexquick/godotenv/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
23
plugins/config/vendor/github.com/alexquick/godotenv/LICENCE
generated
vendored
Normal file
23
plugins/config/vendor/github.com/alexquick/godotenv/LICENCE
generated
vendored
Normal file
@@ -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.
|
||||
|
||||
127
plugins/config/vendor/github.com/alexquick/godotenv/README.md
generated
vendored
Normal file
127
plugins/config/vendor/github.com/alexquick/godotenv/README.md
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
# GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/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"]
|
||||
```
|
||||
|
||||
### 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`
|
||||
|
||||
## 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
|
||||
|
||||
## CI
|
||||
|
||||
Linux: [](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) Windows: [](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](http://whoisjohnbarton.com) based off the tests/fixtures in the original library.
|
||||
15
plugins/config/vendor/github.com/alexquick/godotenv/autoload/autoload.go
generated
vendored
Normal file
15
plugins/config/vendor/github.com/alexquick/godotenv/autoload/autoload.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package autoload
|
||||
|
||||
/*
|
||||
You can just read the .env file on import just by doing
|
||||
|
||||
import _ "github.com/joho/godotenv/autoload"
|
||||
|
||||
And bob's your mother's brother
|
||||
*/
|
||||
|
||||
import "github.com/joho/godotenv"
|
||||
|
||||
func init() {
|
||||
godotenv.Load()
|
||||
}
|
||||
54
plugins/config/vendor/github.com/alexquick/godotenv/cmd/godotenv/cmd.go
generated
vendored
Normal file
54
plugins/config/vendor/github.com/alexquick/godotenv/cmd/godotenv/cmd.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var showHelp bool
|
||||
flag.BoolVar(&showHelp, "h", false, "show help")
|
||||
var rawEnvFilenames string
|
||||
flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
usage := `
|
||||
Run a process with a env setup from a .env file
|
||||
|
||||
godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS
|
||||
|
||||
ENV_FILE_PATHS: comma separated paths to .env files
|
||||
COMMAND_ARGS: command and args you want to run
|
||||
|
||||
example
|
||||
godotenv -f /path/to/something/.env,/another/path/.env fortune
|
||||
`
|
||||
// if no args or -h flag
|
||||
// print usage and return
|
||||
args := flag.Args()
|
||||
if showHelp || len(args) == 0 {
|
||||
fmt.Println(usage)
|
||||
return
|
||||
}
|
||||
|
||||
// load env
|
||||
var envFilenames []string
|
||||
if rawEnvFilenames != "" {
|
||||
envFilenames = strings.Split(rawEnvFilenames, ",")
|
||||
}
|
||||
|
||||
// take rest of args and "exec" them
|
||||
cmd := args[0]
|
||||
cmdArgs := args[1:]
|
||||
|
||||
err := godotenv.Exec(envFilenames, cmd, cmdArgs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/equals.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/equals.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export OPTION_A='postgres://localhost:5432/database?sslmode=disable'
|
||||
|
||||
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/exported.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/exported.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export OPTION_A=2
|
||||
export OPTION_B='\n'
|
||||
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/invalid1.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/invalid1.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
INVALID LINE
|
||||
foo=bar
|
||||
7
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/plain.env
generated
vendored
Normal file
7
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/plain.env
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
OPTION_A=1
|
||||
OPTION_B=2
|
||||
OPTION_C= 3
|
||||
OPTION_D =4
|
||||
OPTION_E = 5
|
||||
OPTION_F =
|
||||
OPTION_G=
|
||||
8
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/quoted.env
generated
vendored
Normal file
8
plugins/config/vendor/github.com/alexquick/godotenv/fixtures/quoted.env
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
OPTION_A='1'
|
||||
OPTION_B='2'
|
||||
OPTION_C=''
|
||||
OPTION_D='\n'
|
||||
OPTION_E="1"
|
||||
OPTION_F="2"
|
||||
OPTION_G=""
|
||||
OPTION_H="\n"
|
||||
304
plugins/config/vendor/github.com/alexquick/godotenv/godotenv.go
generated
vendored
Normal file
304
plugins/config/vendor/github.com/alexquick/godotenv/godotenv.go
generated
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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 avaiable through os.Getenv("SOME_ENV_VAR")
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 := WriteString(envMap)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
file, error := os.Create(filename)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
_, err := file.WriteString(content)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteString outputs the given environment as a dotenv-formatted environment file.
|
||||
//
|
||||
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||
func WriteString(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 ReadFromReader(file)
|
||||
}
|
||||
|
||||
func readString(content string) (envMap map[string]string, err error) {
|
||||
return ReadFromReader(strings.NewReader(content))
|
||||
}
|
||||
|
||||
func ReadFromReader(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)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
envMap[key] = value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseLine(line 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 for line: " + line)
|
||||
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])
|
||||
return
|
||||
}
|
||||
|
||||
func parseValue(value string) string {
|
||||
|
||||
// trim
|
||||
value = strings.Trim(value, " ")
|
||||
|
||||
// check if we've got quoted values
|
||||
if value != "" {
|
||||
first := string(value[0:1])
|
||||
last := string(value[len(value)-1:])
|
||||
if first == last && strings.ContainsAny(first, `"'`) {
|
||||
// pull the quotes off the edges
|
||||
value = value[1 : len(value)-1]
|
||||
// handle escapes
|
||||
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 c
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func isIgnoredLine(line string) bool {
|
||||
trimmedLine := strings.Trim(line, " \n\t")
|
||||
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||
}
|
||||
|
||||
func doubleQuoteEscape(line string) string {
|
||||
line = strings.Replace(line, `\`, `\\`, -1)
|
||||
line = strings.Replace(line, "\n", `\n`, -1)
|
||||
line = strings.Replace(line, "\r", `\r`, -1)
|
||||
line = strings.Replace(line, `"`, `\"`, -1)
|
||||
return line
|
||||
}
|
||||
352
plugins/config/vendor/github.com/alexquick/godotenv/godotenv_test.go
generated
vendored
Normal file
352
plugins/config/vendor/github.com/alexquick/godotenv/godotenv_test.go
generated
vendored
Normal file
@@ -0,0 +1,352 @@
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var noopPresets = make(map[string]string)
|
||||
|
||||
func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
|
||||
key, value, _ := parseLine(rawEnvLine)
|
||||
if key != expectedKey || value != expectedValue {
|
||||
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) {
|
||||
// first up, clear the env
|
||||
os.Clearenv()
|
||||
|
||||
for k, v := range presets {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
err := loader(envFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading %v", envFileName)
|
||||
}
|
||||
|
||||
for k := range expectedValues {
|
||||
envValue := os.Getenv(k)
|
||||
v := expectedValues[k]
|
||||
if envValue != v {
|
||||
t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
|
||||
err := Load()
|
||||
pathError := err.(*os.PathError)
|
||||
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
|
||||
t.Errorf("Didn't try and open .env by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
|
||||
err := Overload()
|
||||
pathError := err.(*os.PathError)
|
||||
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
|
||||
t.Errorf("Didn't try and open .env by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileNotFound(t *testing.T) {
|
||||
err := Load("somefilethatwillneverexistever.env")
|
||||
if err == nil {
|
||||
t.Error("File wasn't found but Load didn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverloadFileNotFound(t *testing.T) {
|
||||
err := Overload("somefilethatwillneverexistever.env")
|
||||
if err == nil {
|
||||
t.Error("File wasn't found but Overload didn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPlainEnv(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "3",
|
||||
"OPTION_D": "4",
|
||||
"OPTION_E": "5",
|
||||
"OPTION_F": "",
|
||||
"OPTION_G": "",
|
||||
}
|
||||
|
||||
envMap, err := Read(envFileName)
|
||||
if err != nil {
|
||||
t.Error("Error reading file")
|
||||
}
|
||||
|
||||
if len(envMap) != len(expectedValues) {
|
||||
t.Error("Didn't get the right size map back")
|
||||
}
|
||||
|
||||
for key, value := range expectedValues {
|
||||
if envMap[key] != value {
|
||||
t.Error("Read got one of the keys wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDoesNotOverride(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
|
||||
// ensure NO overload
|
||||
presets := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
"OPTION_B": "",
|
||||
}
|
||||
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
"OPTION_B": "",
|
||||
}
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
|
||||
}
|
||||
|
||||
func TestOveroadDoesOverride(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
|
||||
// ensure NO overload
|
||||
presets := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
}
|
||||
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
}
|
||||
loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)
|
||||
}
|
||||
|
||||
func TestLoadPlainEnv(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "3",
|
||||
"OPTION_D": "4",
|
||||
"OPTION_E": "5",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadExportedEnv(t *testing.T) {
|
||||
envFileName := "fixtures/exported.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "2",
|
||||
"OPTION_B": "\n",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadEqualsEnv(t *testing.T) {
|
||||
envFileName := "fixtures/equals.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "postgres://localhost:5432/database?sslmode=disable",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadQuotedEnv(t *testing.T) {
|
||||
envFileName := "fixtures/quoted.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "",
|
||||
"OPTION_D": "\n",
|
||||
"OPTION_E": "1",
|
||||
"OPTION_F": "2",
|
||||
"OPTION_G": "",
|
||||
"OPTION_H": "\n",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestActualEnvVarsAreLeftAlone(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("OPTION_A", "actualenv")
|
||||
_ = Load("fixtures/plain.env")
|
||||
|
||||
if os.Getenv("OPTION_A") != "actualenv" {
|
||||
t.Error("An ENV var set earlier was overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsing(t *testing.T) {
|
||||
// unquoted values
|
||||
parseAndCompare(t, "FOO=bar", "FOO", "bar")
|
||||
|
||||
// parses values with spaces around equal sign
|
||||
parseAndCompare(t, "FOO =bar", "FOO", "bar")
|
||||
parseAndCompare(t, "FOO= bar", "FOO", "bar")
|
||||
|
||||
// parses double quoted values
|
||||
parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
|
||||
|
||||
// parses single quoted values
|
||||
parseAndCompare(t, "FOO='bar'", "FOO", "bar")
|
||||
|
||||
// parses escaped double quotes
|
||||
parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
|
||||
|
||||
// parses single quotes inside double quotes
|
||||
parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
|
||||
|
||||
// parses yaml style options
|
||||
parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
|
||||
|
||||
//parses yaml values with equal signs
|
||||
parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")
|
||||
|
||||
// parses non-yaml options with colons
|
||||
parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B")
|
||||
|
||||
// parses export keyword
|
||||
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
|
||||
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\n")
|
||||
|
||||
// it 'expands newlines in quoted strings' do
|
||||
// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
|
||||
parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz")
|
||||
|
||||
// it 'parses varibales with "." in the name' do
|
||||
// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')
|
||||
parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar")
|
||||
|
||||
// it 'parses varibales with several "=" in the value' do
|
||||
// expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=')
|
||||
parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=")
|
||||
|
||||
// it 'strips unquoted values' do
|
||||
// expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar '
|
||||
parseAndCompare(t, "FOO=bar ", "FOO", "bar")
|
||||
|
||||
// it 'ignores inline comments' do
|
||||
// expect(env("foo=bar # this is foo")).to eql('foo' => 'bar')
|
||||
parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar")
|
||||
|
||||
// it 'allows # in quoted value' do
|
||||
// expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz')
|
||||
parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz")
|
||||
parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz")
|
||||
parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang")
|
||||
|
||||
// it 'parses # in quoted values' do
|
||||
// expect(env('foo="ba#r"')).to eql('foo' => 'ba#r')
|
||||
// expect(env("foo='ba#r'")).to eql('foo' => 'ba#r')
|
||||
parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
|
||||
parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
|
||||
|
||||
//newlines and backslashes should be escaped
|
||||
parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz")
|
||||
parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz")
|
||||
parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz")
|
||||
|
||||
// it 'throws an error if line format is incorrect' do
|
||||
// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)
|
||||
badlyFormattedLine := "lol$wut"
|
||||
_, _, err := parseLine(badlyFormattedLine)
|
||||
if err == nil {
|
||||
t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinesToIgnore(t *testing.T) {
|
||||
// it 'ignores empty lines' do
|
||||
// expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz')
|
||||
if !isIgnoredLine("\n") {
|
||||
t.Error("Line with nothing but line break wasn't ignored")
|
||||
}
|
||||
|
||||
if !isIgnoredLine("\t\t ") {
|
||||
t.Error("Line full of whitespace wasn't ignored")
|
||||
}
|
||||
|
||||
// it 'ignores comment lines' do
|
||||
// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar')
|
||||
if !isIgnoredLine("# comment") {
|
||||
t.Error("Comment wasn't ignored")
|
||||
}
|
||||
|
||||
if !isIgnoredLine("\t#comment") {
|
||||
t.Error("Indented comment wasn't ignored")
|
||||
}
|
||||
|
||||
// make sure we're not getting false positives
|
||||
if isIgnoredLine(`export OPTION_B='\n'`) {
|
||||
t.Error("ignoring a perfectly valid line to parse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorReadDirectory(t *testing.T) {
|
||||
envFileName := "fixtures/"
|
||||
envMap, err := Read(envFileName)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", envMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorParsing(t *testing.T) {
|
||||
envFileName := "fixtures/invalid1.env"
|
||||
envMap, err := Read(envFileName)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", envMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
writeAndCompare := func(env string, expected string) {
|
||||
envMap, _ := readString(env)
|
||||
actual, _ := WriteString(envMap)
|
||||
if expected != actual {
|
||||
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
|
||||
}
|
||||
}
|
||||
//just test some single lines to show the general idea
|
||||
//TestRoundtrip makes most of the good assertions
|
||||
|
||||
//values are always double-quoted
|
||||
writeAndCompare(`key=value`, `key="value"`)
|
||||
//double-quotes are escaped
|
||||
writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
|
||||
//but single quotes are left alone
|
||||
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
|
||||
// newlines and backslashes are escaped
|
||||
writeAndCompare(`foo="ba\n\r\\r!"`, `foo="ba\n\r\\r!"`)
|
||||
}
|
||||
|
||||
func TestRoundtrip(t *testing.T) {
|
||||
fixtures := []string{"equals.env", "exported.env", "invalid1.env", "plain.env", "quoted.env"}
|
||||
for _, fixture := range fixtures {
|
||||
fixtureFilename := fmt.Sprintf("fixtures/%s", fixture)
|
||||
env, err := readFile(fixtureFilename)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rep, err := WriteString(env)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roundtripped, err := readString(rep)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(env, roundtripped) {
|
||||
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user