[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:
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user