diff --git a/GNUmakefile b/GNUmakefile index b61e513..05770ae 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,7 +1,26 @@ BINARY=epcc +VERSION=NIGHTLY build: - go build -o ./bin/${BINARY} ./cmd/ + go build -o ./bin/${BINARY} ./ + +release-some: + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ./bin/${VERSION}/darwin_amd64/${BINARY} + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o ./bin/${VERSION}/darwin_arm64/${BINARY} + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ./bin/${VERSION}/windows_amd64/${BINARY}.exe + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/${VERSION}/linux_amd64/${BINARY} + + +release: release-some + CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -o ./bin/${VERSION}/windows_386/${BINARY}.exe + CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -o ./bin/${VERSION}/freebsd_386/${BINARY} + CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o ./bin/${VERSION}/freebsd_amd64/${BINARY} + CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build -o ./bin/${VERSION}/freebsd_arm/${BINARY} + CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -o ./bin/${VERSION}/linux_386/${BINARY} + CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o ./bin/${VERSION}/linux_arm/${BINARY} + CGO_ENABLED=0 GOOS=openbsd GOARCH=386 go build -o ./bin/${VERSION}/openbsd_386/${BINARY} + CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -o ./bin/${VERSION}/openbsd_amd64/${BINARY} + CGO_ENABLED=0 GOOS=solaris GOARCH=amd64 go build -o ./bin/${VERSION}/solaris_amd64/${BINARY} clean: rm -rf bin || true \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 37f250a..88c6ca2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/caarlos0/env/v6" "github.com/elasticpath/epcc-cli/config" + "github.com/elasticpath/epcc-cli/external/json" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -26,6 +27,8 @@ func init() { get, ) + testJson.Flags().BoolVarP(&noWrapping, "no-wrapping", "", false, "if set, we won't wrap the output the json in a data tag") + rootCmd.PersistentFlags().BoolVarP(&json.MonochromeOutput, "monochrome-output", "M", false, "By default, epcc will output using colors if the terminal supports this. Use this option to disable it.") } var rootCmd = &cobra.Command{ diff --git a/cmd/test-json.go b/cmd/test-json.go index c74add1..d54d7a4 100644 --- a/cmd/test-json.go +++ b/cmd/test-json.go @@ -1,19 +1,20 @@ package cmd import ( - "fmt" "github.com/elasticpath/epcc-cli/external/json" "github.com/spf13/cobra" ) +var noWrapping bool var testJson = &cobra.Command{ Use: "test-json [KEY_1] [VAL_1] [KEY_2] [VAL_2] ...", Short: "Prints the resulting json for what a command will look like", RunE: func(cmd *cobra.Command, args []string) error { - res, err := json.ToJson(args) + res, err := json.ToJson(args, noWrapping) if res != "" { - fmt.Println(res) + json.PrintJson(res) + } return err diff --git a/external/json/encoder.go b/external/json/encoder.go new file mode 100644 index 0000000..2aef63e --- /dev/null +++ b/external/json/encoder.go @@ -0,0 +1,344 @@ +package json + +import ( + "bytes" + "fmt" + wincolor "github.com/gookit/color" + "io" + "math" + "math/big" + "runtime" + "sort" + "strconv" + "unicode/utf8" +) + +// This whole file is 98% based on being copied from https://github.com/itchyny/gojq/blob/main/cli/encoder.go +// https://github.com/itchyny/gojq/blob/main/cli/color.go + +type encoder struct { + out io.Writer + w *bytes.Buffer + tab bool + indent int + depth int + buf [64]byte +} + +type colorInfo struct { + unix []byte + win wincolor.Color + winReset bool +} + +func setColor(buf *bytes.Buffer, color colorInfo) { + if !MonochromeOutput { + if runtime.GOOS == "windows" { + if color.winReset { + _, err := wincolor.Reset() + if err != nil { + panic(fmt.Errorf("Could not set colors on windows, try running with -M, %w", err)) + } + } else { + _, err := wincolor.Set(color.win) + if err != nil { + panic(fmt.Errorf("Could not set colors on windows, try running with -M, %w", err)) + } + } + + } else { + buf.Write(color.unix) + } + + } +} + +func newColor(unix string, win wincolor.Color, winReset bool) colorInfo { + var colorByte = []byte(nil) + if unix != "" { + colorByte = []byte("\x1b[" + unix + "m") + } + return colorInfo{ + unix: colorByte, + win: win, + winReset: winReset, + } +} + +var ( + resetColor = newColor("0", wincolor.FgDefault, true) // Reset + nullColor = newColor("90", wincolor.FgLightWhite, false) // Bright black + falseColor = newColor("33", wincolor.FgYellow, false) // Yellow + trueColor = newColor("33", wincolor.FgYellow, false) // Yellow + numberColor = newColor("36", wincolor.FgCyan, false) // Cyan + stringColor = newColor("32", wincolor.FgGreen, false) // Green + objectKeyColor = newColor("34;1", wincolor.FgBlue, false) // Bold Blue + unimportantObjectKeyColor = newColor("34", wincolor.FgLightBlue, false) // Blue + importantObjectKeyColor = newColor("35;1", wincolor.FgMagenta, false) // Bold Purple + urgentObjectKeyColor = newColor("31;1", wincolor.FgRed, false) // Bold Red + arrayColor = newColor("", wincolor.FgDefault, false) // No color + objectColor = newColor("", wincolor.FgDefault, false) // No color +) + +func NewEncoder(tab bool, indent int) *encoder { + // reuse the buffer in multiple calls of marshal + return &encoder{w: new(bytes.Buffer), tab: tab, indent: indent} +} + +func (e *encoder) Marshal(v interface{}, w io.Writer) error { + e.out = w + e.encode(v) + _, err := w.Write(e.w.Bytes()) + e.w.Reset() + return err +} + +func (e *encoder) encode(v interface{}) { + switch v := v.(type) { + case nil: + e.write([]byte("null"), &nullColor) + case bool: + if v { + e.write([]byte("true"), &trueColor) + } else { + e.write([]byte("false"), &falseColor) + } + case int: + e.write(strconv.AppendInt(e.buf[:0], int64(v), 10), &numberColor) + case float64: + e.encodeFloat64(v) + case *big.Int: + e.write(v.Append(e.buf[:0], 10), &numberColor) + case string: + e.encodeString(v, &stringColor) + case []interface{}: + e.encodeArray(v) + case map[string]interface{}: + e.encodeMap(v) + default: + panic(fmt.Sprintf("invalid value: %v", v)) + } + if e.w.Len() > 8*1024 { + e.out.Write(e.w.Bytes()) + e.w.Reset() + } +} + +// ref: floatEncoder in encoding/json +func (e *encoder) encodeFloat64(f float64) { + if math.IsNaN(f) { + e.write([]byte("null"), &nullColor) + return + } + if f >= math.MaxFloat64 { + f = math.MaxFloat64 + } else if f <= -math.MaxFloat64 { + f = -math.MaxFloat64 + } + fmt := byte('f') + if x := math.Abs(f); x != 0 && x < 1e-6 || x >= 1e21 { + fmt = 'e' + } + buf := strconv.AppendFloat(e.buf[:0], f, fmt, -1, 64) + if fmt == 'e' { + // clean up e-09 to e-9 + if n := len(buf); n >= 4 && buf[n-4] == 'e' && buf[n-3] == '-' && buf[n-2] == '0' { + buf[n-2] = buf[n-1] + buf = buf[:n-1] + } + } + e.write(buf, &numberColor) +} + +// ref: encodeState#string in encoding/json +func (e *encoder) encodeString(s string, color *colorInfo) { + if color != nil { + setColor(e.w, *color) + } + e.w.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if ' ' <= b && b <= '~' && b != '"' && b != '\\' { + i++ + continue + } + if start < i { + e.w.WriteString(s[start:i]) + } + e.w.WriteByte('\\') + switch b { + case '\\', '"': + e.w.WriteByte(b) + case '\b': + e.w.WriteByte('b') + case '\f': + e.w.WriteByte('f') + case '\n': + e.w.WriteByte('n') + case '\r': + e.w.WriteByte('r') + case '\t': + e.w.WriteByte('t') + default: + const hex = "0123456789abcdef" + e.w.WriteString("u00") + e.w.WriteByte(hex[b>>4]) + e.w.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + e.w.WriteString(s[start:i]) + } + e.w.WriteString(`\ufffd`) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + e.w.WriteString(s[start:]) + } + e.w.WriteByte('"') + if color != nil { + setColor(e.w, resetColor) + } +} + +func (e *encoder) encodeArray(vs []interface{}) { + e.writeByte('[', &arrayColor) + e.depth += e.indent + for i, v := range vs { + if i > 0 { + e.writeByte(',', &arrayColor) + } + if e.indent != 0 { + e.writeIndent() + } + e.encode(v) + } + e.depth -= e.indent + if len(vs) > 0 && e.indent != 0 { + e.writeIndent() + } + e.writeByte(']', &arrayColor) +} + +func (e *encoder) encodeMap(vs map[string]interface{}) { + e.writeByte('{', &objectColor) + e.depth += e.indent + type keyVal struct { + key string + val interface{} + } + kvs := make([]keyVal, len(vs)) + var i int + for k, v := range vs { + kvs[i] = keyVal{k, v} + i++ + } + sort.Slice(kvs, func(i, j int) bool { + + if kvs[i].key == "type" { + return true + } else if kvs[j].key == "type" { + return false + } + + if kvs[i].key == "id" { + return true + } else if kvs[j].key == "id" { + return false + } + + return kvs[i].key < kvs[j].key + }) + for i, kv := range kvs { + if i > 0 { + e.writeByte(',', &objectColor) + } + if e.indent != 0 { + e.writeIndent() + } + + keyColorToUse := objectKeyColor + switch kv.key { + case "data": + fallthrough + case "id": + fallthrough + case "attributes": + fallthrough + case "type": + keyColorToUse = unimportantObjectKeyColor + case "name": + fallthrough + case "email": + keyColorToUse = importantObjectKeyColor + case "error": + fallthrough + case "errors": + keyColorToUse = urgentObjectKeyColor + + } + + e.encodeString(kv.key, &keyColorToUse) + e.writeByte(':', &objectColor) + if e.indent != 0 { + e.w.WriteByte(' ') + } + e.encode(kv.val) + } + e.depth -= e.indent + if len(vs) > 0 && e.indent != 0 { + e.writeIndent() + } + e.writeByte('}', &objectColor) +} + +func (e *encoder) writeIndent() { + e.w.WriteByte('\n') + if n := e.depth; n > 0 { + if e.tab { + const tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" + for n > len(tabs) { + e.w.Write([]byte(tabs)) + n -= len(tabs) + } + e.w.Write([]byte(tabs)[:n]) + } else { + const spaces = " " + for n > len(spaces) { + e.w.Write([]byte(spaces)) + n -= len(spaces) + } + e.w.Write([]byte(spaces)[:n]) + } + } +} + +func (e *encoder) writeByte(b byte, color *colorInfo) { + if color == nil { + e.w.WriteByte(b) + } else { + setColor(e.w, *color) + e.w.WriteByte(b) + setColor(e.w, resetColor) + } +} + +func (e *encoder) write(bs []byte, color *colorInfo) { + if color == nil { + e.w.Write(bs) + } else { + setColor(e.w, *color) + e.w.Write(bs) + setColor(e.w, resetColor) + } +} diff --git a/external/json/print_json.go b/external/json/print_json.go new file mode 100644 index 0000000..ce16551 --- /dev/null +++ b/external/json/print_json.go @@ -0,0 +1,30 @@ +package json + +import ( + gojson "encoding/json" + "github.com/mattn/go-isatty" + "os" +) + +var MonochromeOutput = false + +func PrintJson(json string) error { + // Adapted from gojq + if os.Getenv("TERM") == "dumb" { + MonochromeOutput = true + } else { + colorCapableTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) + if !colorCapableTerminal { + MonochromeOutput = true + } + } + + var v interface{} + + err := gojson.Unmarshal([]byte(json), &v) + + e := NewEncoder(false, 2) + + err = e.Marshal(v, os.Stdout) + return err +} diff --git a/external/json/to_json.go b/external/json/to_json.go index 534c6ae..368fbe8 100644 --- a/external/json/to_json.go +++ b/external/json/to_json.go @@ -8,7 +8,7 @@ import ( "regexp" ) -func ToJson(args []string) (string, error) { +func ToJson(args []string, noWrapping bool) (string, error) { if len(args)%2 == 1 { return "", fmt.Errorf("The number arguments %d supplied isn't even, json should be passed in key value pairs", len(args)) @@ -31,7 +31,10 @@ func ToJson(args []string) (string, error) { } - result, err = runJQ(`{ "data": . }`, result) + if !noWrapping { + result, err = runJQ(`{ "data": . }`, result) + } + jsonStr, err := gojson.Marshal(result) return string(jsonStr), err diff --git a/go.mod b/go.mod index 18ae1c3..674b8c3 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,12 @@ require ( require ( github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/gookit/color v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/itchyny/timefmt-go v0.1.3 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/spf13/afero v1.6.0 // indirect @@ -24,6 +26,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index caadd22..9b5b387 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -18,6 +20,7 @@ github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrn github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -47,13 +50,17 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= @@ -71,5 +78,6 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=