optional output formats

This commit is contained in:
Josh Baker 2016-03-29 12:29:15 -07:00
parent 4e2bbf1c33
commit 4ce4e1af71
5 changed files with 294 additions and 81 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
@ -13,6 +14,7 @@ import (
"strings"
"github.com/peterh/liner"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/client"
"github.com/tidwall/tile38/core"
)
@ -39,9 +41,11 @@ type connError struct {
var (
hostname = "127.0.0.1"
output = "json"
port = 9851
oneCommand string
tokml bool
raw bool
)
func showHelp() bool {
@ -54,8 +58,10 @@ func showHelp() bool {
}
fmt.Fprintf(os.Stdout, "tile38-cli %s%s\n\n", core.Version, gitsha)
fmt.Fprintf(os.Stdout, "Usage: tile38-cli [OPTIONS] [cmd [arg [arg ...]]]\n")
fmt.Fprintf(os.Stdout, " -h <hostname> Server hostname (default: %s).\n", hostname)
fmt.Fprintf(os.Stdout, " -p <port> Server port (default: %d).\n", port)
fmt.Fprintf(os.Stdout, " --raw Use raw formatting for replies (default when STDOUT is not a tty)\n")
fmt.Fprintf(os.Stdout, " --resp Use RESP output formatting (default is JSON output)\n")
fmt.Fprintf(os.Stdout, " -h <hostname> Server hostname (default: %s)\n", hostname)
fmt.Fprintf(os.Stdout, " -p <port> Server port (default: %d)\n", port)
fmt.Fprintf(os.Stdout, "\n")
return false
}
@ -82,9 +88,10 @@ func parseArgs() bool {
fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg)
return false
}
for len(args) > 0 {
arg := readArg("")
if arg == "--help" {
if arg == "--help" || arg == "-?" {
return showHelp()
}
if !strings.HasPrefix(arg, "-") {
@ -96,6 +103,10 @@ func parseArgs() bool {
return badArg(arg)
case "-kml":
tokml = true
case "--raw":
raw = true
case "--resp":
output = "resp"
case "-h":
hostname = readArg(arg)
case "-p":
@ -120,6 +131,15 @@ func main() {
if !parseArgs() {
return
}
if !raw {
fi, err := os.Stdout.Stat()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}
raw = (fi.Mode() & os.ModeCharDevice) == 0
}
if len(oneCommand) > 0 && (oneCommand[0] == 'h' || oneCommand[0] == 'H') && strings.Split(strings.ToLower(oneCommand), " ")[0] == "help" {
showHelp()
return
@ -222,7 +242,13 @@ func main() {
f.Close()
}
}()
var raw bool
if output == "resp" {
_, err := conn.Do("output resp")
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}
}
for {
var command string
var err error
@ -254,11 +280,6 @@ func main() {
if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" {
return
}
if (command[0] == 'r' || command[0] == 'R') && strings.ToLower(command) == "raw" {
raw = true
fmt.Fprintln(os.Stderr, "raw mode is ON")
continue
}
if (command[0] == 'h' || command[0] == 'H') && (strings.ToLower(command) == "help" || strings.HasPrefix(strings.ToLower(command), "help")) {
err = help(strings.TrimSpace(command[4:]))
if err != nil {
@ -266,11 +287,6 @@ func main() {
}
continue
}
if (command[0] == 'p' || command[0] == 'P') && strings.ToLower(command) == "pretty" {
raw = false
fmt.Fprintln(os.Stderr, "raw mode is OFF")
continue
}
aof = (command[0] == 'a' || command[0] == 'A') && strings.HasPrefix(strings.ToLower(command), "aof ")
msg, err := conn.Do(command)
if err != nil {
@ -296,7 +312,13 @@ func main() {
if mustOutput {
if tokml {
msg = convert2kml(msg)
fmt.Fprintln(os.Stdout, string(msg))
} else if output == "resp" {
if !raw {
msg = convert2termresp(msg)
}
fmt.Fprintln(os.Stdout, string(msg))
} else {
if raw {
fmt.Fprintln(os.Stdout, string(msg))
} else {
@ -304,6 +326,7 @@ func main() {
}
}
}
}
} else if err == liner.ErrPromptAborted {
return
} else {
@ -315,6 +338,67 @@ func main() {
}
}
func convert2termresp(msg []byte) []byte {
rd := resp.NewReader(bytes.NewBuffer(msg))
out := ""
for {
v, _, err := rd.ReadValue()
if err != nil {
break
}
out += convert2termrespval(v, 0)
}
return []byte(strings.TrimSpace(out))
}
func convert2termrespval(v resp.Value, spaces int) string {
switch v.Type() {
default:
return v.String()
case resp.BulkString:
if v.IsNull() {
return "(nil)"
} else {
return "\"" + v.String() + "\""
}
case resp.Integer:
return "(integer) " + v.String()
case resp.Error:
return "(error) " + v.String()
case resp.Array:
arr := v.Array()
if len(arr) == 0 {
return "(empty list or set)"
}
out := ""
nspaces := spaces + numlen(len(arr))
for i, v := range arr {
if i > 0 {
out += strings.Repeat(" ", spaces)
}
iout := strings.TrimSpace(convert2termrespval(v, nspaces+2))
out += fmt.Sprintf("%d) %s\n", i+1, iout)
}
return out
}
}
func numlen(n int) int {
l := 1
if n < 0 {
l++
n = n * -1
}
for i := 0; i < 1000; i++ {
if n < 10 {
break
}
l++
n = n / 10
}
return l
}
func convert2kml(msg []byte) []byte {
k := NewKML()
var m map[string]interface{}

View File

@ -190,7 +190,12 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
_, err = io.WriteString(w, res+"\r\n")
return err
case server.RESP:
_, err := io.WriteString(w, res)
var err error
if msg.OutputType == server.JSON {
_, err = fmt.Fprintf(w, "$%d\r\n%s\r\n", len(res), res)
} else {
_, err = io.WriteString(w, res)
}
return err
case server.Native:
_, err := fmt.Fprintf(w, "$%d %s\r\n", len(res), res)
@ -278,6 +283,8 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
// does not write to aof, but requires a write lock.
c.mu.Lock()
defer c.mu.Unlock()
case "output":
// this is local connection operation. Locks not needed.
case "massinsert":
// dev operation
// ** danger zone **
@ -340,14 +347,12 @@ func (c *Controller) command(msg *server.Message, w io.Writer) (res string, d co
res, d, err = c.cmdDrop(msg)
case "flushdb":
res, d, err = c.cmdFlushDB(msg)
// case "sethook":
// err = c.cmdSetHook(nline)
// resp = okResp()
// case "delhook":
// err = c.cmdDelHook(nline)
// resp = okResp()
// case "hooks":
// err = c.cmdHooks(nline, w)
case "sethook":
res, d, err = c.cmdSetHook(msg)
case "delhook":
res, d, err = c.cmdDelHook(msg)
case "hooks":
res, err = c.cmdHooks(msg)
// case "massinsert":
// if !core.DevMode {
// err = fmt.Errorf("unknown command '%s'", cmd)
@ -376,6 +381,8 @@ func (c *Controller) command(msg *server.Message, w io.Writer) (res string, d co
res, err = c.cmdGet(msg)
case "keys":
res, err = c.cmdKeys(msg)
case "output":
res, err = c.cmdOutput(msg)
// case "aof":
// err = c.cmdAOF(nline, w)
// case "aofmd5":

View File

@ -4,7 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
@ -42,7 +41,7 @@ type Hook struct {
Key string
Name string
Endpoints []Endpoint
Command string
Message *server.Message
Fence *liveFenceSwitches
ScanWriter *scanWriter
}
@ -156,70 +155,96 @@ func parseEndpoint(s string) (Endpoint, error) {
return endpoint, nil
}
func (c *Controller) cmdSetHook(line string) (err error) {
//start := time.Now()
func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetailsT, err error) {
start := time.Now()
vs := msg.Values[1:]
var name, values, cmd string
if line, name = token(line); name == "" {
return errInvalidNumberOfArguments
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
}
if line, values = token(line); values == "" {
return errInvalidNumberOfArguments
if vs, values, ok = tokenval(vs); !ok || values == "" {
return "", d, errInvalidNumberOfArguments
}
var endpoints []Endpoint
for _, value := range strings.Split(values, ",") {
endpoint, err := parseEndpoint(value)
if err != nil {
log.Errorf("sethook: %v", err)
return errInvalidArgument(value)
return "", d, errInvalidArgument(value)
}
endpoints = append(endpoints, endpoint)
}
command := line
if line, cmd = token(line); cmd == "" {
return errInvalidNumberOfArguments
commandvs := vs
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return "", d, errInvalidNumberOfArguments
}
cmdlc := strings.ToLower(cmd)
var types []string
switch cmdlc {
default:
return errInvalidArgument(cmd)
return "", d, errInvalidArgument(cmd)
case "nearby":
types = nearbyTypes
case "within", "intersects":
types = withinOrIntersectsTypes
}
var vs []resp.Value
panic("todo: assign vs correctly")
s, err := c.cmdSearchArgs(cmdlc, vs, types)
if err != nil {
return err
return "", d, err
}
if !s.fence {
return errors.New("missing FENCE argument")
return "", d, errors.New("missing FENCE argument")
}
s.cmd = cmdlc
cmsg := &server.Message{}
*cmsg = *msg
cmsg.Values = commandvs
cmsg.Command = strings.ToLower(cmsg.Values[0].String())
hook := &Hook{
Key: s.key,
Name: name,
Endpoints: endpoints,
Fence: &s,
Command: command,
Message: cmsg,
}
var wr bytes.Buffer
var msg *server.Message
panic("todo: cmdSetHook message must be defined")
hook.ScanWriter, err = c.newScanWriter(&wr, msg, s.key, s.output, s.precision, s.glob, s.limit, s.wheres, s.nofields)
hook.ScanWriter, err = c.newScanWriter(&wr, cmsg, s.key, s.output, s.precision, s.glob, s.limit, s.wheres, s.nofields)
if err != nil {
return err
return "", d, err
}
// delete the previous hook
if h, ok := c.hooks[name]; ok {
// lets see if the previous hook matches the new hook
if h.Key == hook.Key && h.Name == hook.Name {
if len(h.Endpoints) == len(hook.Endpoints) {
match := true
for i, endpoint := range h.Endpoints {
if endpoint.Original != hook.Endpoints[i].Original {
match = false
break
}
}
if match && resp.ArrayValue(h.Message.Values).Equals(resp.ArrayValue(hook.Message.Values)) {
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":0\r\n", d, nil
}
}
}
}
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
}
d.updated = true
c.hooks[name] = hook
hm, ok := c.hookcols[hook.Key]
if !ok {
@ -227,47 +252,72 @@ func (c *Controller) cmdSetHook(line string) (err error) {
c.hookcols[hook.Key] = hm
}
hm[name] = hook
return nil
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":1\r\n", d, nil
}
return "", d, nil
}
func (c *Controller) cmdDelHook(line string) (err error) {
func (c *Controller) cmdDelHook(msg *server.Message) (res string, d commandDetailsT, err error) {
start := time.Now()
vs := msg.Values[1:]
var name string
if line, name = token(line); name == "" {
return errInvalidNumberOfArguments
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
}
if line != "" {
return errInvalidNumberOfArguments
if len(vs) != 0 {
return "", d, errInvalidNumberOfArguments
}
if h, ok := c.hooks[name]; ok {
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
d.updated = true
}
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
if d.updated {
return ":1\r\n", d, nil
} else {
return ":0\r\n", d, nil
}
}
return
}
func (c *Controller) cmdHooks(line string, w io.Writer) (err error) {
func (c *Controller) cmdHooks(msg *server.Message) (res string, err error) {
start := time.Now()
vs := msg.Values[1:]
var pattern string
if line, pattern = token(line); pattern == "" {
return errInvalidNumberOfArguments
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return "", errInvalidNumberOfArguments
}
if line != "" {
return errInvalidNumberOfArguments
if len(vs) != 0 {
return "", errInvalidNumberOfArguments
}
var hooks []*Hook
for name, hook := range c.hooks {
if ok, err := globMatch(pattern, name); err == nil && ok {
match, _ := globMatch(pattern, name)
if match {
hooks = append(hooks, hook)
} else if err != nil {
return errInvalidArgument(pattern)
}
}
sort.Sort(hooksByName(hooks))
switch msg.OutputType {
case server.JSON:
buf := &bytes.Buffer{}
buf.WriteString(`{"ok":true,"hooks":[`)
for i, hook := range hooks {
@ -284,13 +334,39 @@ func (c *Controller) cmdHooks(line string, w io.Writer) (err error) {
}
buf.WriteString(jsonString(endpoint.Original))
}
buf.WriteString(`],"command":` + jsonString(hook.Command))
buf.WriteString(`}`)
buf.WriteString(`],"command":[`)
for i, v := range hook.Message.Values {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(v.String()))
}
buf.WriteString(`]}`)
}
buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}")
w.Write(buf.Bytes())
return
return buf.String(), nil
case server.RESP:
var vals []resp.Value
for _, hook := range hooks {
var hvals []resp.Value
hvals = append(hvals, resp.StringValue(hook.Name))
hvals = append(hvals, resp.StringValue(hook.Key))
var evals []resp.Value
for _, endpoint := range hook.Endpoints {
evals = append(evals, resp.StringValue(endpoint.Original))
}
hvals = append(hvals, resp.ArrayValue(evals))
hvals = append(hvals, resp.ArrayValue(hook.Message.Values))
vals = append(vals, resp.ArrayValue(hvals))
}
data, err := resp.ArrayValue(vals).MarshalRESP()
if err != nil {
return "", err
}
return string(data), nil
}
return "", nil
}
func (c *Controller) sendHTTPMessage(endpoint Endpoint, msg []byte) error {

40
controller/output.go Normal file
View File

@ -0,0 +1,40 @@
package controller
import (
"strings"
"time"
"github.com/tidwall/tile38/controller/server"
)
func (c *Controller) cmdOutput(msg *server.Message) (res string, err error) {
start := time.Now()
vs := msg.Values[1:]
var arg string
var ok bool
if len(vs) != 0 {
if vs, arg, ok = tokenval(vs); !ok || arg == "" {
return "", errInvalidNumberOfArguments
}
// Setting the original message output type will be picked up by the
// server prior to the next command being executed.
switch strings.ToLower(arg) {
default:
return "", errInvalidArgument(arg)
case "json":
msg.OutputType = server.JSON
case "resp":
msg.OutputType = server.RESP
}
return server.OKMessage(msg, start), nil
}
// return the output
switch msg.OutputType {
default:
return "", nil
case server.JSON:
return `{"ok":true,"output":"json","elapsed":` + time.Now().Sub(start).String() + `}`, nil
case server.RESP:
return "$4\r\nresp\r\n", nil
}
}

View File

@ -98,6 +98,7 @@ func handleConn(
}
}
defer conn.Close()
outputType := Null
rd := NewAnyReaderWriter(conn)
brd := rd.rd
for {
@ -114,6 +115,9 @@ func handleConn(
return
}
if msg != nil && msg.Command != "" {
if outputType != Null {
msg.OutputType = outputType
}
if msg.Command == "quit" {
if msg.OutputType == RESP {
io.WriteString(conn, "+OK\r\n")
@ -125,6 +129,7 @@ func handleConn(
log.Error(err)
return
}
outputType = msg.OutputType
} else {
conn.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n"))
return
@ -132,6 +137,7 @@ func handleConn(
if msg.ConnType == HTTP || msg.ConnType == WebSocket {
return
}
}
}