[config] move to godotenv

This commit is contained in:
Alex Quick
2017-05-20 14:38:27 -04:00
parent 08aa2b9fa4
commit eeb6e78fa4
30 changed files with 1189 additions and 362 deletions

View File

@@ -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
}

View File

@@ -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 \

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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 "$@"
}

View File

@@ -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

View File

@@ -1,3 +1,5 @@
package: .
import:
- package: github.com/ryanuber/columnize
- package: github.com/alexquick/godotenv
version: feature-write-dotenv

View File

@@ -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":

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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
}

View 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)
}

View File

@@ -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)
}

View File

@@ -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 {

View 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)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -0,0 +1 @@
.DS_Store

View File

@@ -0,0 +1,8 @@
language: go
go:
- 1.8
os:
- linux
- osx

View 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.

View File

@@ -0,0 +1,127 @@
# 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)
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: [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78/m "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) 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](http://whoisjohnbarton.com) based off the tests/fixtures in the original library.

View 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()
}

View 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)
}
}

View File

@@ -0,0 +1,2 @@
export OPTION_A='postgres://localhost:5432/database?sslmode=disable'

View File

@@ -0,0 +1,2 @@
export OPTION_A=2
export OPTION_B='\n'

View File

@@ -0,0 +1,2 @@
INVALID LINE
foo=bar

View File

@@ -0,0 +1,7 @@
OPTION_A=1
OPTION_B=2
OPTION_C= 3
OPTION_D =4
OPTION_E = 5
OPTION_F =
OPTION_G=

View 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"

View 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
}

View 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)
}
}
}