[config] first cut of config rewritten in golang
still to do: * Rework disk format to be okay with quotes * Fix Dockerfile deployments * Dedup code in the subcommands * Add --encode * Add deploytests for the new stuff * Restore some log messages (should I make some helpers in common?) * Restore some functions that may be used by 3d-party plugins
This commit is contained in:
2
plugins/config/.gitignore
vendored
Normal file
2
plugins/config/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/commands
|
||||
/subcommands/*
|
||||
32
plugins/config/Makefile
Normal file
32
plugins/config/Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
.PHONY: build-in-docker build clean
|
||||
|
||||
GO_REPO_ROOT ?= /go/src/github.com/dokku/dokku
|
||||
BUILD_IMAGE ?= golang:1.7.1
|
||||
|
||||
build-in-docker: clean
|
||||
docker run --rm \
|
||||
-v $$PWD/../..:$(GO_REPO_ROOT) \
|
||||
-w $(GO_REPO_ROOT)/plugins/config \
|
||||
$(BUILD_IMAGE) \
|
||||
bash -c "make build" || exit $$?
|
||||
|
||||
build: commands subcommands
|
||||
subcommands: subcommands/export subcommands/set subcommands/get subcommands/unset
|
||||
|
||||
commands: **/**/commands.go
|
||||
go build -a -o commands src/commands/commands.go
|
||||
|
||||
subcommands/export: **/**/**/export.go
|
||||
go build -a -o subcommands/export src/subcommands/export/export.go
|
||||
|
||||
subcommands/set: **/**/**/set.go
|
||||
go build -a -o subcommands/set src/subcommands/set/set.go
|
||||
|
||||
subcommands/get: **/**/**/get.go
|
||||
go build -a -o subcommands/get src/subcommands/get/get.go
|
||||
|
||||
subcommands/unset: **/**/**/unset.go
|
||||
go build -a -o subcommands/unset src/subcommands/unset/unset.go
|
||||
|
||||
clean:
|
||||
rm -rf commands subcommands
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
[[ " help config:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
case "$1" in
|
||||
help | config:help)
|
||||
help_content_func () {
|
||||
declare desc="return config plugin help content"
|
||||
cat<<help_content
|
||||
config (<app>|--global), Display all global or app-specific config vars
|
||||
config:get (<app>|--global) KEY, Display a global or app-specific config value
|
||||
config:set (<app>|--global) KEY1=VALUE1 [KEY2=VALUE2 ...], Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...], Unset one or more config vars
|
||||
help_content
|
||||
}
|
||||
|
||||
if [[ $1 = "config:help" ]] ; then
|
||||
echo -e 'Usage: dokku config[:COMMAND] (<app>|--global)'
|
||||
echo ''
|
||||
echo 'Manage global or app-specific config vars.'
|
||||
echo ''
|
||||
echo 'Additional commands:'
|
||||
help_content_func | sort | column -c2 -t -s,
|
||||
else
|
||||
help_content_func
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
|
||||
;;
|
||||
|
||||
esac
|
||||
@@ -6,251 +6,24 @@ config_export() {
|
||||
declare desc="returns export command for config variable of specified type (app/global)"
|
||||
local CONFIG_TYPE="$1"
|
||||
local APP="$2"
|
||||
local ENV_FILE="$DOKKU_ROOT/$APP/ENV"
|
||||
|
||||
[[ $CONFIG_TYPE == "global" ]] && local ENV_FILE="$DOKKU_ROOT/ENV"
|
||||
[[ ! -f $ENV_FILE ]] && return 0
|
||||
[[ ! -s $ENV_FILE ]] && return 0
|
||||
|
||||
local VARS=$(grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | cut -d" " -f2-)
|
||||
echo "$VARS" | awk '{print "export " $0}'
|
||||
if [[ $CONFIG_TYPE == "global" ]]; then
|
||||
APP="--global"
|
||||
fi
|
||||
dokku config:export "$APP"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_sanitized_config_args() {
|
||||
local desc="return sanitized arguments for config plugin"
|
||||
local SANITIZED_ARGS=("$@")
|
||||
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--global/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--no-restart/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--export/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--shell/}")
|
||||
|
||||
echo "${SANITIZED_ARGS[@]}"
|
||||
}
|
||||
|
||||
config_parse_args() {
|
||||
declare desc="parse config plugin args"
|
||||
unset APP ENV_FILE DOKKU_CONFIG_TYPE DOKKU_CONFIG_RESTART DOKKU_CONFIG_EXPORT
|
||||
|
||||
for var; do
|
||||
if [[ "$var" == "--global" ]]; then
|
||||
ENV_FILE="$DOKKU_ROOT/ENV"
|
||||
DOKKU_CONFIG_TYPE="global"
|
||||
DOKKU_CONFIG_RESTART=false
|
||||
fi
|
||||
if [[ "$var" == "--no-restart" ]]; then
|
||||
DOKKU_CONFIG_RESTART=false
|
||||
fi
|
||||
if [[ "$var" == "--export" ]]; then
|
||||
DOKKU_CONFIG_EXPORT=true
|
||||
fi
|
||||
if [[ "$var" == "--shell" ]]; then
|
||||
DOKKU_CONFIG_SHELL=true
|
||||
fi
|
||||
done
|
||||
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
DOKKU_CONFIG_TYPE=${DOKKU_CONFIG_TYPE:="app"}
|
||||
DOKKU_CONFIG_RESTART=${DOKKU_CONFIG_RESTART:=true}
|
||||
|
||||
if [[ "$DOKKU_CONFIG_TYPE" == "app" ]]; then
|
||||
if [[ -z $2 ]]; then
|
||||
dokku_log_fail "Please specify an app to run the command on"
|
||||
else
|
||||
verify_app_name "$2"
|
||||
APP=${APP:="$2"}
|
||||
ENV_FILE=${ENV_FILE:="$DOKKU_ROOT/$APP/ENV"}
|
||||
fi
|
||||
fi
|
||||
export APP ENV_FILE DOKKU_CONFIG_TYPE DOKKU_CONFIG_RESTART DOKKU_CONFIG_EXPORT
|
||||
}
|
||||
|
||||
config_create () {
|
||||
declare desc="create config env file"
|
||||
local ENV_FILE=$1
|
||||
[[ -f $ENV_FILE ]] || touch "$ENV_FILE"
|
||||
}
|
||||
|
||||
config_styled_hash () {
|
||||
declare desc="internal config hash"
|
||||
local vars="$1"
|
||||
local prefix="$2"
|
||||
local longest=""
|
||||
|
||||
while read -r word; do
|
||||
local KEY=$(echo "$word" | cut -d"=" -f1)
|
||||
if [[ ${#KEY} -gt ${#longest} ]]; then
|
||||
local longest=$KEY
|
||||
fi
|
||||
done <<< "$vars"
|
||||
|
||||
while read -r word; do
|
||||
local KEY=$(echo "$word" | cut -d"=" -f1)
|
||||
local VALUE=$(echo "$word" | cut -d"=" -f2- | sed -e "s/^'//" -e "s/'$//" -e "s/\$$//g")
|
||||
|
||||
local num_zeros=$((${#longest} - ${#KEY}))
|
||||
local zeros=" "
|
||||
while [[ $num_zeros -gt 0 ]]; do
|
||||
local zeros="$zeros "
|
||||
local num_zeros=$((num_zeros - 1))
|
||||
done
|
||||
echo "$prefix$KEY:$zeros$VALUE"
|
||||
done <<< "$vars"
|
||||
}
|
||||
|
||||
config_write() {
|
||||
declare desc="writes config vars out to appropriate file path"
|
||||
local ENV_TEMP="$1"
|
||||
local ENV_FILE_TEMP="${ENV_FILE}.tmp"
|
||||
|
||||
echo "$ENV_TEMP" | sed '/^$/d' | sort > "$ENV_FILE_TEMP"
|
||||
if ! cmp -s "$ENV_FILE" "$ENV_FILE_TEMP"; then
|
||||
cp -f "$ENV_FILE_TEMP" "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
rm -f "$ENV_FILE_TEMP"
|
||||
}
|
||||
|
||||
config_all() {
|
||||
declare desc="print or export config vars"
|
||||
[[ "$1" == "config" ]] || set -- "config" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
[[ $DOKKU_CONFIG_EXPORT ]] && config_export "$DOKKU_CONFIG_TYPE" "$APP" && return 0
|
||||
|
||||
[[ $APP ]] && local DOKKU_CONFIG_TYPE=$APP
|
||||
[[ ! -s $ENV_FILE ]] && dokku_log_fail "no config vars for $DOKKU_CONFIG_TYPE"
|
||||
|
||||
local VARS=$(grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | cut -d" " -f2-)
|
||||
|
||||
[[ $DOKKU_CONFIG_SHELL ]] && echo "$VARS" && return 0
|
||||
|
||||
dokku_log_info2_quiet "$DOKKU_CONFIG_TYPE config vars"
|
||||
config_styled_hash "$VARS"
|
||||
}
|
||||
|
||||
config_get() {
|
||||
declare desc="get value of given config var"
|
||||
[[ "$1" == "config:get" ]] || set -- "config:get" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
if [[ -z $2 ]]; then
|
||||
echo "Usage: dokku config:get APP KEY"
|
||||
echo "Must specify KEY."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
if [[ ! -s $ENV_FILE ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local KEY="${*: -1}"
|
||||
|
||||
grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | grep "^export $KEY=" | cut -d"=" -f2- | sed -e "s/^'//" -e "s/'$//"
|
||||
dokku config:get "$@"
|
||||
}
|
||||
|
||||
config_set() {
|
||||
declare desc="set value of given config var"
|
||||
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
|
||||
|
||||
[[ "$1" == "config:set" ]] || set -- "config:set" "$@"
|
||||
config_parse_args "$@"
|
||||
|
||||
if [[ -z "${*:3}" ]]; then
|
||||
echo "Usage: dokku config:set APP KEY1=VALUE1 [KEY2=VALUE2 ...]"
|
||||
echo "Whitespaces need to be escaped, i.e. KEY=\"VAL\ WITH\ SPACES\""
|
||||
echo "Must specify KEY and VALUE to set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
local ENV_ADD=""
|
||||
local ENV_TEMP=$(cat "${ENV_FILE}")
|
||||
local RESTART_APP=false
|
||||
shift 2
|
||||
|
||||
for var; do
|
||||
if [[ $var == "--global" ]] || [[ $var == "--no-restart" ]] || ([[ $APP ]] && [[ $var == "$APP" ]]); then
|
||||
shift 1
|
||||
elif [[ $var != *"="* ]]; then
|
||||
echo "Usage: dokku config:set APP KEY1=VALUE1 [KEY2=VALUE2 ...]"
|
||||
echo "Whitespaces need to be escaped, i.e. KEY=\"VAL\ WITH\ SPACES\""
|
||||
echo "Must specify KEY and VALUE to set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for var; do
|
||||
local KEY=$(echo ${var} | cut -d"=" -f1)
|
||||
local VALUE=$(echo ${var} | cut -d"=" -f2-)
|
||||
|
||||
if [[ $KEY =~ [a-zA-Z_][a-zA-Z0-9_]* ]]; then
|
||||
local RESTART_APP=true
|
||||
local ENV_TEMP=$(echo "${ENV_TEMP}" | sed "/^export $KEY=/ d")
|
||||
local ENV_TEMP="${ENV_TEMP}
|
||||
export $KEY='$VALUE'"
|
||||
local ENV_ADD=$(echo -e "${ENV_ADD}" | sed "/^$KEY=/ d")
|
||||
local ENV_ADD="${ENV_ADD}$
|
||||
${var}"
|
||||
fi
|
||||
done
|
||||
local ENV_ADD=$(echo "$ENV_ADD" | tail -n +2) #remove first empty line
|
||||
|
||||
if [[ $RESTART_APP ]]; then
|
||||
dokku_log_info1 "Setting config vars"
|
||||
config_styled_hash "$ENV_ADD" " "
|
||||
|
||||
config_write "$ENV_TEMP"
|
||||
plugn trigger post-config-update "$APP" "set" "$@"
|
||||
fi
|
||||
|
||||
[[ $APP ]] && local DOKKU_APP_RESTORE=$(config_get "$APP" DOKKU_APP_RESTORE || true)
|
||||
if [[ "$DOKKU_CONFIG_RESTART" == "true" ]] && [[ "$DOKKU_APP_RESTORE" != 0 ]]; then
|
||||
dokku_log_info1 "Restarting app $APP"
|
||||
ps_restart "$APP"
|
||||
fi
|
||||
dokku config:set "$@"
|
||||
}
|
||||
|
||||
config_unset() {
|
||||
declare desc="unset given config var"
|
||||
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
|
||||
|
||||
[[ "$1" == "config:unset" ]] || set -- "config:unset" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
if [[ -z $2 ]]; then
|
||||
echo "Usage: dokku config:unset APP KEY1 [KEY2 ...]"
|
||||
echo "Must specify KEY to unset."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
local ENV_TEMP=$(cat "${ENV_FILE}")
|
||||
local VARS="${*:2}"
|
||||
|
||||
for var in $VARS; do
|
||||
dokku_log_info1 "Unsetting $var"
|
||||
local ENV_TEMP=$(echo "${ENV_TEMP}" | sed "/^export $var=/ d")
|
||||
|
||||
config_write "$ENV_TEMP"
|
||||
done
|
||||
|
||||
plugn trigger post-config-update "$APP" "unset" "$@"
|
||||
|
||||
[[ $APP ]] && local DOKKU_APP_RESTORE=$(config_get "$APP" DOKKU_APP_RESTORE || true)
|
||||
if [[ "$DOKKU_CONFIG_RESTART" == "true" ]] && [[ "$DOKKU_APP_RESTORE" != 0 ]]; then
|
||||
dokku_log_info1 "Restarting app $APP"
|
||||
ps_restart "$APP"
|
||||
fi
|
||||
declare desc="set value of given config var"
|
||||
dokku config:unset "$@"
|
||||
}
|
||||
|
||||
66
plugins/config/src/commands/commands.go
Normal file
66
plugins/config/src/commands/commands.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
configenv "github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
columnize "github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
const (
|
||||
helpHeader = `Usage: dokku config [<app>|--global]
|
||||
|
||||
Display all global or app-specific config vars
|
||||
|
||||
Additional commands:`
|
||||
|
||||
helpContent = `
|
||||
config:get (<app>|--global) KEY, Display a global or app-specific config value
|
||||
config:set (<app>|--global) [--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
|
||||
`
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
switch cmd {
|
||||
case "config":
|
||||
target := flag.Arg(1)
|
||||
env, err := configenv.NewFromTarget(target)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
} else {
|
||||
fmt.Println(env.String())
|
||||
}
|
||||
case "config:help":
|
||||
usage()
|
||||
case "help":
|
||||
fmt.Print(helpContent)
|
||||
default:
|
||||
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
|
||||
if err != nil {
|
||||
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
|
||||
dokkuNotImplementExitCode = 10
|
||||
}
|
||||
os.Exit(dokkuNotImplementExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
config := columnize.DefaultConfig()
|
||||
config.Delim = ","
|
||||
config.Prefix = "\t"
|
||||
config.Empty = ""
|
||||
content := strings.Split(helpContent, "\n")[1:]
|
||||
fmt.Println(helpHeader)
|
||||
fmt.Println(columnize.Format(content, config))
|
||||
}
|
||||
139
plugins/config/src/configenv/configenv.go
Normal file
139
plugins/config/src/configenv/configenv.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package configenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
//Env is a representation for global or app environment
|
||||
type Env struct {
|
||||
name string
|
||||
env map[string]string
|
||||
filename string
|
||||
}
|
||||
|
||||
func (e *Env) String() string {
|
||||
return e.EnvfileString()
|
||||
}
|
||||
|
||||
//EnvfileString returns the contents of this Env in ENVFILE format
|
||||
func (e *Env) EnvfileString() string {
|
||||
return e.stringWithEntryPrefix("")
|
||||
}
|
||||
|
||||
//ExportfileString returns the contents of this Env as bash exports
|
||||
func (e *Env) ExportfileString() string {
|
||||
return e.stringWithEntryPrefix("export ")
|
||||
}
|
||||
|
||||
func (e *Env) stringWithEntryPrefix(prefix string) string {
|
||||
keys := e.Keys()
|
||||
sort.Strings(keys)
|
||||
entries := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
entries[i] = fmt.Sprintf("%s%s='%s'", prefix, k, escape(e.env[k]))
|
||||
}
|
||||
return strings.Join(entries, "\n")
|
||||
}
|
||||
|
||||
func escape(value string) string { //TODO: improve this
|
||||
return strings.Replace(value, "'", "\\'", -1)
|
||||
}
|
||||
|
||||
//NewFromTarget creates an env from the given target. Tareget is either "--global" or an app name
|
||||
func NewFromTarget(target string) (*Env, error) {
|
||||
if target == "--global" {
|
||||
return parseEnv("global", getGlobalFile())
|
||||
}
|
||||
appfile, err := getAppFile(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseEnv(target, appfile)
|
||||
}
|
||||
|
||||
//NewFromString creates an env from the given ENVFILE contents representation
|
||||
func NewFromString(rep string) (*Env, error) {
|
||||
return parseEnvFromReader("<unknown>", "", strings.NewReader(rep))
|
||||
}
|
||||
|
||||
//Set an environment variable
|
||||
func (e *Env) Set(key string, value string) {
|
||||
e.env[key] = value
|
||||
}
|
||||
|
||||
//Unset an environment variable
|
||||
func (e *Env) Unset(key string) {
|
||||
delete(e.env, key)
|
||||
}
|
||||
|
||||
//Keys gets the keys in this environment
|
||||
func (e *Env) Keys() []string {
|
||||
keys := make([]string, 0, len(e.env))
|
||||
for k := range e.env {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
//Get an environment variable
|
||||
func (e *Env) Get(key string) (string, bool) {
|
||||
v, ok := e.env[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
//GetDefault an environment variable or a default if it doesnt exist
|
||||
func (e *Env) GetDefault(key string, defaultValue string) string {
|
||||
v, ok := e.env[key]
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
//GetBoolDefault gets the bool value of the given key with the given default
|
||||
//right now that is evaluated as `value != "0"`
|
||||
func (e *Env) GetBoolDefault(key string, defaultValue bool) bool {
|
||||
v, ok := e.Get(key)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return v != "0"
|
||||
}
|
||||
|
||||
//Map return the Env as a map
|
||||
func (e *Env) Map() map[string]string {
|
||||
return e.env
|
||||
}
|
||||
|
||||
//Write an Env back to the file it was read from as an exportfile
|
||||
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
|
||||
}
|
||||
|
||||
func getAppFile(appName string) (string, error) {
|
||||
err := common.VerifyAppName(appName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"), nil
|
||||
}
|
||||
|
||||
func getGlobalFile() string {
|
||||
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV")
|
||||
}
|
||||
42
plugins/config/src/configenv/configenv_test.go
Normal file
42
plugins/config/src/configenv/configenv_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package configenv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
env, err := NewFromString("HI=ho")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho")))
|
||||
Expect(env.String()).To(Equal("HI='ho'"))
|
||||
|
||||
env, err = NewFromString("HI=h\\o\\'")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "h\\o'")))
|
||||
Expect(env.String()).To(Equal("HI='h\\o\\''"))
|
||||
|
||||
env, err = NewFromString("HI=ho\nFOO=bar\n\n")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho", "FOO", "bar")))
|
||||
Expect(env.String()).To(Equal("FOO='bar'\nHI='ho'"))
|
||||
|
||||
env, err = NewFromString("HI='ho\n'\n\nFOO='bar\\'\nbaz'")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho\n", "FOO", "bar'\nbaz")))
|
||||
Expect(env.String()).To(Equal("FOO='bar\\'\nbaz'\nHI='ho\n'"))
|
||||
}
|
||||
|
||||
func pairs(vars ...string) map[string]string {
|
||||
res := map[string]string{}
|
||||
var i = 0
|
||||
for i < len(vars)-1 {
|
||||
key := vars[i]
|
||||
value := vars[i+1]
|
||||
res[key] = value
|
||||
i += 2
|
||||
}
|
||||
return res
|
||||
}
|
||||
119
plugins/config/src/configenv/parsing.go
Normal file
119
plugins/config/src/configenv/parsing.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package configenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
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'. Eait pair is terminated with a newline.
|
||||
//All characters are valid for VALUE without escaping
|
||||
//but one: the single quote and backslash. To represent a single quote emit \'. To represent
|
||||
//a literal backslash emit \\. In any other case, the backslash is interpreted literally.
|
||||
//KEY='value
|
||||
//one'
|
||||
//KEY='val\\ue\\'' etc..
|
||||
|
||||
//TODO: We might want to rework this to use shell-style quoting for single quotes
|
||||
buffered := bufio.NewReader(reader)
|
||||
const (
|
||||
StateKey = iota
|
||||
StateValue = iota
|
||||
)
|
||||
env := &Env{name, map[string]string{}, filename}
|
||||
var buffer bytes.Buffer
|
||||
var state = StateKey
|
||||
var quoted = 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" {
|
||||
buffer.Truncate(0) //so we can read exportfiles too
|
||||
} else {
|
||||
return nil, errors.New("Env keys cannot have spaces")
|
||||
}
|
||||
case '=':
|
||||
key = buffer.String()
|
||||
buffer.Truncate(0)
|
||||
state = StateValue
|
||||
case '\n':
|
||||
if buffer.Len() > 0 {
|
||||
return nil, errors.New("Invalid newline after: " + buffer.String())
|
||||
}
|
||||
default:
|
||||
buffer.WriteRune(char)
|
||||
}
|
||||
case StateValue:
|
||||
switch char {
|
||||
case '\'':
|
||||
if escaped {
|
||||
buffer.WriteRune('\'')
|
||||
escaped = false
|
||||
} else {
|
||||
quoted = !quoted
|
||||
}
|
||||
case '\n':
|
||||
if escaped || quoted {
|
||||
buffer.WriteRune(char)
|
||||
} else {
|
||||
state = StateKey
|
||||
env.Set(key, buffer.String())
|
||||
buffer.Truncate(0)
|
||||
key = ""
|
||||
escaped = false
|
||||
quoted = false
|
||||
}
|
||||
case '\\':
|
||||
if escaped {
|
||||
buffer.WriteRune(char)
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = true
|
||||
}
|
||||
default:
|
||||
if escaped {
|
||||
buffer.WriteRune('\\')
|
||||
escaped = false
|
||||
}
|
||||
buffer.WriteRune(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
return nil, errors.New("Unterminated escape")
|
||||
}
|
||||
if quoted {
|
||||
return nil, errors.New("Unterminated quote")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
6
plugins/config/src/glide.lock
generated
Normal file
6
plugins/config/src/glide.lock
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149
|
||||
updated: 2017-01-03T17:16:50.97156327-08:00
|
||||
imports:
|
||||
- name: github.com/ryanuber/columnize
|
||||
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
|
||||
testImports: []
|
||||
3
plugins/config/src/glide.yaml
Normal file
3
plugins/config/src/glide.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
package: .
|
||||
import:
|
||||
- package: github.com/ryanuber/columnize
|
||||
40
plugins/config/src/subcommands/export/export.go
Normal file
40
plugins/config/src/subcommands/export/export.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
// print the environment to stdout
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:export", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
envfile := args.Bool("envfile", false, "--envfile: export as envfile rather than bash exports")
|
||||
args.Parse(os.Args[2:])
|
||||
|
||||
appName := args.Arg(0)
|
||||
if appName == "" && !*global {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
}
|
||||
|
||||
if *global {
|
||||
if appName != "" {
|
||||
common.LogFail("Trailing argument: " + appName)
|
||||
}
|
||||
appName = "--global"
|
||||
}
|
||||
|
||||
env, err := configenv.NewFromTarget(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
if *envfile {
|
||||
fmt.Println(env.EnvfileString())
|
||||
} else {
|
||||
fmt.Println(env.ExportfileString())
|
||||
}
|
||||
}
|
||||
39
plugins/config/src/subcommands/get/get.go
Normal file
39
plugins/config/src/subcommands/get/get.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
// get the given entries from the specified environment
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Printf("%s", env.GetDefault(key, ""))
|
||||
}
|
||||
61
plugins/config/src/subcommands/set/set.go
Normal file
61
plugins/config/src/subcommands/set/set.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
// set the given entries from the specified environment
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:set", 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
|
||||
entries := args.Args()[nextArg:]
|
||||
for _, e := range entries {
|
||||
//log
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
common.LogFail("Invalid env pair: " + e)
|
||||
}
|
||||
env.Set(parts[0], parts[1])
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
//log
|
||||
env.Write()
|
||||
args := append([]string{appName, "set"}, entries...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if shouldRestart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
}
|
||||
54
plugins/config/src/subcommands/unset/unset.go
Normal file
54
plugins/config/src/subcommands/unset/unset.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
common "github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/config/src/configenv"
|
||||
)
|
||||
|
||||
//unset the given entries from the given environment
|
||||
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 {
|
||||
//log
|
||||
env.Unset(k)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
//log
|
||||
env.Write()
|
||||
args := append([]string{appName, "unset"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
|
||||
if shouldRestart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
cmd := common.NewTokenizedShellCmd("dokku", "ps:restart", appName)
|
||||
cmd.Execute()
|
||||
}
|
||||
}
|
||||
3
plugins/config/src/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
3
plugins/config/src/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
language: go
|
||||
go:
|
||||
- tip
|
||||
20
plugins/config/src/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
20
plugins/config/src/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016 Ryan Uber
|
||||
|
||||
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.
|
||||
69
plugins/config/src/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
69
plugins/config/src/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
Columnize
|
||||
=========
|
||||
|
||||
Easy column-formatted output for golang
|
||||
|
||||
[](https://travis-ci.org/ryanuber/columnize)
|
||||
[](https://godoc.org/github.com/ryanuber/columnize)
|
||||
|
||||
Columnize is a really small Go package that makes building CLI's a little bit
|
||||
easier. In some CLI designs, you want to output a number similar items in a
|
||||
human-readable way with nicely aligned columns. However, figuring out how wide
|
||||
to make each column is a boring problem to solve and eats your valuable time.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
output := []string{
|
||||
"Name | Gender | Age",
|
||||
"Bob | Male | 38",
|
||||
"Sally | Female | 26",
|
||||
}
|
||||
result := columnize.SimpleFormat(output)
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, you just pass in a list of strings. And the result:
|
||||
|
||||
```
|
||||
Name Gender Age
|
||||
Bob Male 38
|
||||
Sally Female 26
|
||||
```
|
||||
|
||||
Columnize is tolerant of missing or empty fields, or even empty lines, so
|
||||
passing in extra lines for spacing should show up as you would expect.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Columnize is configured using a `Config`, which can be obtained by calling the
|
||||
`DefaultConfig()` method. You can then tweak the settings in the resulting
|
||||
`Config`:
|
||||
|
||||
```
|
||||
config := columnize.DefaultConfig()
|
||||
config.Delim = "|"
|
||||
config.Glue = " "
|
||||
config.Prefix = ""
|
||||
config.Empty = ""
|
||||
```
|
||||
|
||||
* `Delim` is the string by which columns of **input** are delimited
|
||||
* `Glue` is the string by which columns of **output** are delimited
|
||||
* `Prefix` is a string by which each line of **output** is prefixed
|
||||
* `Empty` is a string used to replace blank values found in output
|
||||
|
||||
You can then pass the `Config` in using the `Format` method (signature below) to
|
||||
have text formatted to your liking.
|
||||
|
||||
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.
|
||||
178
plugins/config/src/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
178
plugins/config/src/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
package columnize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config can be used to tune certain parameters which affect the way
|
||||
// in which Columnize will format output text.
|
||||
type Config struct {
|
||||
// The string by which the lines of input will be split.
|
||||
Delim string
|
||||
|
||||
// The string by which columns of output will be separated.
|
||||
Glue string
|
||||
|
||||
// The string by which columns of output will be prefixed.
|
||||
Prefix string
|
||||
|
||||
// A replacement string to replace empty fields
|
||||
Empty string
|
||||
}
|
||||
|
||||
// DefaultConfig returns a *Config with default values.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Delim: "|",
|
||||
Glue: " ",
|
||||
Prefix: "",
|
||||
Empty: "",
|
||||
}
|
||||
}
|
||||
|
||||
// MergeConfig merges two config objects together and returns the resulting
|
||||
// configuration. Values from the right take precedence over the left side.
|
||||
func MergeConfig(a, b *Config) *Config {
|
||||
var result Config = *a
|
||||
|
||||
// Return quickly if either side was nil
|
||||
if a == nil || b == nil {
|
||||
return &result
|
||||
}
|
||||
|
||||
if b.Delim != "" {
|
||||
result.Delim = b.Delim
|
||||
}
|
||||
if b.Glue != "" {
|
||||
result.Glue = b.Glue
|
||||
}
|
||||
if b.Prefix != "" {
|
||||
result.Prefix = b.Prefix
|
||||
}
|
||||
if b.Empty != "" {
|
||||
result.Empty = b.Empty
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// stringFormat, given a set of column widths and the number of columns in
|
||||
// the current line, returns a sprintf-style format string which can be used
|
||||
// to print output aligned properly with other lines using the same widths set.
|
||||
func stringFormat(c *Config, widths []int, columns int) string {
|
||||
// Create the buffer with an estimate of the length
|
||||
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
|
||||
|
||||
// Start with the prefix, if any was given. The buffer will not return an
|
||||
// error so it does not need to be handled
|
||||
buf.WriteString(c.Prefix)
|
||||
|
||||
// Create the format string from the discovered widths
|
||||
for i := 0; i < columns && i < len(widths); i++ {
|
||||
if i == columns-1 {
|
||||
buf.WriteString("%s\n")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// elementsFromLine returns a list of elements, each representing a single
|
||||
// item which will belong to a column of output.
|
||||
func elementsFromLine(config *Config, line string) []interface{} {
|
||||
seperated := strings.Split(line, config.Delim)
|
||||
elements := make([]interface{}, len(seperated))
|
||||
for i, field := range seperated {
|
||||
value := strings.TrimSpace(field)
|
||||
|
||||
// Apply the empty value, if configured.
|
||||
if value == "" && config.Empty != "" {
|
||||
value = config.Empty
|
||||
}
|
||||
elements[i] = value
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
// runeLen calculates the number of visible "characters" in a string
|
||||
func runeLen(s string) int {
|
||||
l := 0
|
||||
for _ = range s {
|
||||
l++
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// widthsFromLines examines a list of strings and determines how wide each
|
||||
// column should be considering all of the elements that need to be printed
|
||||
// within it.
|
||||
func widthsFromLines(config *Config, lines []string) []int {
|
||||
widths := make([]int, 0, 8)
|
||||
|
||||
for _, line := range lines {
|
||||
elems := elementsFromLine(config, line)
|
||||
for i := 0; i < len(elems); i++ {
|
||||
l := runeLen(elems[i].(string))
|
||||
if len(widths) <= i {
|
||||
widths = append(widths, l)
|
||||
} else if widths[i] < l {
|
||||
widths[i] = l
|
||||
}
|
||||
}
|
||||
}
|
||||
return widths
|
||||
}
|
||||
|
||||
// Format is the public-facing interface that takes a list of strings and
|
||||
// returns nicely aligned column-formatted text.
|
||||
func Format(lines []string, config *Config) string {
|
||||
conf := MergeConfig(DefaultConfig(), config)
|
||||
widths := widthsFromLines(conf, lines)
|
||||
|
||||
// Estimate the buffer size
|
||||
glueSize := len(conf.Glue)
|
||||
var size int
|
||||
for _, w := range widths {
|
||||
size += w + glueSize
|
||||
}
|
||||
size *= len(lines)
|
||||
|
||||
// Create the buffer
|
||||
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||
|
||||
// Create a cache for the string formats
|
||||
fmtCache := make(map[int]string, 16)
|
||||
|
||||
// Create the formatted output using the format string
|
||||
for _, line := range lines {
|
||||
elems := elementsFromLine(conf, line)
|
||||
|
||||
// Get the string format using cache
|
||||
numElems := len(elems)
|
||||
stringfmt, ok := fmtCache[numElems]
|
||||
if !ok {
|
||||
stringfmt = stringFormat(conf, widths, numElems)
|
||||
fmtCache[numElems] = stringfmt
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, stringfmt, elems...)
|
||||
}
|
||||
|
||||
// Get the string result
|
||||
result := buf.String()
|
||||
|
||||
// Remove trailing newline without removing leading/trailing space
|
||||
if n := len(result); n > 0 && result[n-1] == '\n' {
|
||||
result = result[:n-1]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SimpleFormat is a convenience function to format text with the defaults.
|
||||
func SimpleFormat(lines []string) string {
|
||||
return Format(lines, nil)
|
||||
}
|
||||
306
plugins/config/src/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
306
plugins/config/src/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
package columnize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
crand "crypto/rand"
|
||||
)
|
||||
|
||||
func TestListOfStringsInput(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyLinesOutput(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeadingSpacePreserved(t *testing.T) {
|
||||
input := []string{
|
||||
"| Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := " Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnWidthCalculator(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"Longer than A | Longer than B | Longer than C",
|
||||
"short | short | short",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "Longer than A Longer than B Longer than C\n"
|
||||
expected += "short short short"
|
||||
|
||||
if output != expected {
|
||||
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||
t.Fatalf("\n%s", printableProof)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
|
||||
"short | short | short",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
|
||||
expected += "short short short"
|
||||
|
||||
if output != expected {
|
||||
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||
t.Fatalf("\n%s", printableProof)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkColumnWidthCalculator(b *testing.B) {
|
||||
// Generate the input
|
||||
input := []string{
|
||||
"UUID A | UUID B | UUID C | Column D | Column E",
|
||||
}
|
||||
|
||||
format := "%s|%s|%s|%s"
|
||||
short := "short"
|
||||
|
||||
uuid := func() string {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := crand.Read(buf); err != nil {
|
||||
panic(fmt.Errorf("failed to read random bytes: %v", err))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
||||
buf[0:4],
|
||||
buf[4:6],
|
||||
buf[6:8],
|
||||
buf[8:10],
|
||||
buf[10:16])
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
|
||||
input = append(input, l)
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Format(input, config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariedInputSpacing(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A |Column B| Column C",
|
||||
"x|y| z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmatchedColumnCounts(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"Value A | Value B",
|
||||
"Value A | Value B | Value C | Value D",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "Value A Value B\n"
|
||||
expected += "Value A Value B Value C Value D"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternateDelimiter(t *testing.T) {
|
||||
input := []string{
|
||||
"Column | A % Column | B % Column | C",
|
||||
"Value A % Value B % Value C",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Delim = "%"
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column | A Column | B Column | C\n"
|
||||
expected += "Value A Value B Value C"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternateSpacingString(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Glue = " "
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleFormat(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
output := SimpleFormat(input)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternatePrefixString(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Prefix = " "
|
||||
output := Format(input, config)
|
||||
|
||||
expected := " Column A Column B Column C\n"
|
||||
expected += " x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFieldReplacement(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Empty = "<none>"
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x <none> z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyConfigValues(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
output := Format(input, &config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfig(t *testing.T) {
|
||||
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
|
||||
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
|
||||
conf3 := &Config{Delim: "c", Prefix: "c"}
|
||||
|
||||
m := MergeConfig(conf1, conf2)
|
||||
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
|
||||
t.Fatalf("bad: %#v", m)
|
||||
}
|
||||
|
||||
m = MergeConfig(conf1, conf3)
|
||||
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
|
||||
t.Fatalf("bad: %#v", m)
|
||||
}
|
||||
|
||||
m = MergeConfig(conf1, nil)
|
||||
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||
t.Fatalf("bad: %#v", m)
|
||||
}
|
||||
|
||||
m = MergeConfig(conf1, &Config{})
|
||||
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||
t.Fatalf("bad: %#v", m)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_main_cmd() {
|
||||
declare desc="print config vars for app via command line"
|
||||
local cmd="config"
|
||||
config_all "$@"
|
||||
}
|
||||
|
||||
config_main_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_get_cmd() {
|
||||
declare desc="get specified config vars from app via command line"
|
||||
local cmd="config:get"
|
||||
config_get "$@"
|
||||
}
|
||||
|
||||
config_get_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_set_cmd() {
|
||||
declare desc="set specified config vars for app via command line"
|
||||
local cmd="config:set"
|
||||
config_set "$@"
|
||||
}
|
||||
|
||||
config_set_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_unset_cmd() {
|
||||
declare desc="unset specified config vars for app via command line"
|
||||
local cmd="config:unset"
|
||||
config_unset "$@"
|
||||
}
|
||||
|
||||
config_unset_cmd "$@"
|
||||
Reference in New Issue
Block a user