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 package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -13,6 +14,7 @@ import (
"strings" "strings"
"github.com/peterh/liner" "github.com/peterh/liner"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/client" "github.com/tidwall/tile38/client"
"github.com/tidwall/tile38/core" "github.com/tidwall/tile38/core"
) )
@ -39,9 +41,11 @@ type connError struct {
var ( var (
hostname = "127.0.0.1" hostname = "127.0.0.1"
output = "json"
port = 9851 port = 9851
oneCommand string oneCommand string
tokml bool tokml bool
raw bool
) )
func showHelp() 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, "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, "Usage: tile38-cli [OPTIONS] [cmd [arg [arg ...]]]\n")
fmt.Fprintf(os.Stdout, " -h <hostname> Server hostname (default: %s).\n", hostname) fmt.Fprintf(os.Stdout, " --raw Use raw formatting for replies (default when STDOUT is not a tty)\n")
fmt.Fprintf(os.Stdout, " -p <port> Server port (default: %d).\n", port) 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") fmt.Fprintf(os.Stdout, "\n")
return false return false
} }
@ -82,9 +88,10 @@ func parseArgs() bool {
fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg) fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg)
return false return false
} }
for len(args) > 0 { for len(args) > 0 {
arg := readArg("") arg := readArg("")
if arg == "--help" { if arg == "--help" || arg == "-?" {
return showHelp() return showHelp()
} }
if !strings.HasPrefix(arg, "-") { if !strings.HasPrefix(arg, "-") {
@ -96,6 +103,10 @@ func parseArgs() bool {
return badArg(arg) return badArg(arg)
case "-kml": case "-kml":
tokml = true tokml = true
case "--raw":
raw = true
case "--resp":
output = "resp"
case "-h": case "-h":
hostname = readArg(arg) hostname = readArg(arg)
case "-p": case "-p":
@ -120,6 +131,15 @@ func main() {
if !parseArgs() { if !parseArgs() {
return 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" { if len(oneCommand) > 0 && (oneCommand[0] == 'h' || oneCommand[0] == 'H') && strings.Split(strings.ToLower(oneCommand), " ")[0] == "help" {
showHelp() showHelp()
return return
@ -222,7 +242,13 @@ func main() {
f.Close() f.Close()
} }
}() }()
var raw bool if output == "resp" {
_, err := conn.Do("output resp")
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}
}
for { for {
var command string var command string
var err error var err error
@ -254,11 +280,6 @@ func main() {
if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" { if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" {
return 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")) { if (command[0] == 'h' || command[0] == 'H') && (strings.ToLower(command) == "help" || strings.HasPrefix(strings.ToLower(command), "help")) {
err = help(strings.TrimSpace(command[4:])) err = help(strings.TrimSpace(command[4:]))
if err != nil { if err != nil {
@ -266,11 +287,6 @@ func main() {
} }
continue 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 ") aof = (command[0] == 'a' || command[0] == 'A') && strings.HasPrefix(strings.ToLower(command), "aof ")
msg, err := conn.Do(command) msg, err := conn.Do(command)
if err != nil { if err != nil {
@ -296,11 +312,18 @@ func main() {
if mustOutput { if mustOutput {
if tokml { if tokml {
msg = convert2kml(msg) msg = convert2kml(msg)
} fmt.Fprintln(os.Stdout, string(msg))
if raw { } else if output == "resp" {
if !raw {
msg = convert2termresp(msg)
}
fmt.Fprintln(os.Stdout, string(msg)) fmt.Fprintln(os.Stdout, string(msg))
} else { } else {
fmt.Fprintln(os.Stdout, string(msg)) if raw {
fmt.Fprintln(os.Stdout, string(msg))
} else {
fmt.Fprintln(os.Stdout, string(msg))
}
} }
} }
} }
@ -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 { func convert2kml(msg []byte) []byte {
k := NewKML() k := NewKML()
var m map[string]interface{} 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") _, err = io.WriteString(w, res+"\r\n")
return err return err
case server.RESP: 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 return err
case server.Native: case server.Native:
_, err := fmt.Fprintf(w, "$%d %s\r\n", len(res), res) _, 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. // does not write to aof, but requires a write lock.
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
case "output":
// this is local connection operation. Locks not needed.
case "massinsert": case "massinsert":
// dev operation // dev operation
// ** danger zone ** // ** 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) res, d, err = c.cmdDrop(msg)
case "flushdb": case "flushdb":
res, d, err = c.cmdFlushDB(msg) res, d, err = c.cmdFlushDB(msg)
// case "sethook": case "sethook":
// err = c.cmdSetHook(nline) res, d, err = c.cmdSetHook(msg)
// resp = okResp() case "delhook":
// case "delhook": res, d, err = c.cmdDelHook(msg)
// err = c.cmdDelHook(nline) case "hooks":
// resp = okResp() res, err = c.cmdHooks(msg)
// case "hooks":
// err = c.cmdHooks(nline, w)
// case "massinsert": // case "massinsert":
// if !core.DevMode { // if !core.DevMode {
// err = fmt.Errorf("unknown command '%s'", cmd) // 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) res, err = c.cmdGet(msg)
case "keys": case "keys":
res, err = c.cmdKeys(msg) res, err = c.cmdKeys(msg)
case "output":
res, err = c.cmdOutput(msg)
// case "aof": // case "aof":
// err = c.cmdAOF(nline, w) // err = c.cmdAOF(nline, w)
// case "aofmd5": // case "aofmd5":

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"sort" "sort"
@ -42,7 +41,7 @@ type Hook struct {
Key string Key string
Name string Name string
Endpoints []Endpoint Endpoints []Endpoint
Command string Message *server.Message
Fence *liveFenceSwitches Fence *liveFenceSwitches
ScanWriter *scanWriter ScanWriter *scanWriter
} }
@ -156,70 +155,96 @@ func parseEndpoint(s string) (Endpoint, error) {
return endpoint, nil return endpoint, nil
} }
func (c *Controller) cmdSetHook(line string) (err error) { func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetailsT, err error) {
//start := time.Now() start := time.Now()
vs := msg.Values[1:]
var name, values, cmd string var name, values, cmd string
if line, name = token(line); name == "" { var ok bool
return errInvalidNumberOfArguments if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
} }
if line, values = token(line); values == "" { if vs, values, ok = tokenval(vs); !ok || values == "" {
return errInvalidNumberOfArguments return "", d, errInvalidNumberOfArguments
} }
var endpoints []Endpoint var endpoints []Endpoint
for _, value := range strings.Split(values, ",") { for _, value := range strings.Split(values, ",") {
endpoint, err := parseEndpoint(value) endpoint, err := parseEndpoint(value)
if err != nil { if err != nil {
log.Errorf("sethook: %v", err) log.Errorf("sethook: %v", err)
return errInvalidArgument(value) return "", d, errInvalidArgument(value)
} }
endpoints = append(endpoints, endpoint) endpoints = append(endpoints, endpoint)
} }
command := line commandvs := vs
if line, cmd = token(line); cmd == "" { if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return errInvalidNumberOfArguments return "", d, errInvalidNumberOfArguments
} }
cmdlc := strings.ToLower(cmd) cmdlc := strings.ToLower(cmd)
var types []string var types []string
switch cmdlc { switch cmdlc {
default: default:
return errInvalidArgument(cmd) return "", d, errInvalidArgument(cmd)
case "nearby": case "nearby":
types = nearbyTypes types = nearbyTypes
case "within", "intersects": case "within", "intersects":
types = withinOrIntersectsTypes types = withinOrIntersectsTypes
} }
var vs []resp.Value
panic("todo: assign vs correctly")
s, err := c.cmdSearchArgs(cmdlc, vs, types) s, err := c.cmdSearchArgs(cmdlc, vs, types)
if err != nil { if err != nil {
return err return "", d, err
} }
if !s.fence { if !s.fence {
return errors.New("missing FENCE argument") return "", d, errors.New("missing FENCE argument")
} }
s.cmd = cmdlc s.cmd = cmdlc
cmsg := &server.Message{}
*cmsg = *msg
cmsg.Values = commandvs
cmsg.Command = strings.ToLower(cmsg.Values[0].String())
hook := &Hook{ hook := &Hook{
Key: s.key, Key: s.key,
Name: name, Name: name,
Endpoints: endpoints, Endpoints: endpoints,
Fence: &s, Fence: &s,
Command: command, Message: cmsg,
} }
var wr bytes.Buffer var wr bytes.Buffer
var msg *server.Message hook.ScanWriter, err = c.newScanWriter(&wr, cmsg, s.key, s.output, s.precision, s.glob, s.limit, s.wheres, s.nofields)
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)
if err != nil { if err != nil {
return err return "", d, err
} }
// delete the previous hook // delete the previous hook
if h, ok := c.hooks[name]; ok { 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 { if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name) delete(hm, h.Name)
} }
delete(c.hooks, h.Name) delete(c.hooks, h.Name)
} }
d.updated = true
c.hooks[name] = hook c.hooks[name] = hook
hm, ok := c.hookcols[hook.Key] hm, ok := c.hookcols[hook.Key]
if !ok { if !ok {
@ -227,70 +252,121 @@ func (c *Controller) cmdSetHook(line string) (err error) {
c.hookcols[hook.Key] = hm c.hookcols[hook.Key] = hm
} }
hm[name] = hook 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 var name string
if line, name = token(line); name == "" { var ok bool
return errInvalidNumberOfArguments if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
} }
if line != "" { if len(vs) != 0 {
return errInvalidNumberOfArguments return "", d, errInvalidNumberOfArguments
} }
if h, ok := c.hooks[name]; ok { if h, ok := c.hooks[name]; ok {
if hm, ok := c.hookcols[h.Key]; ok { if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name) delete(hm, h.Name)
} }
delete(c.hooks, 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 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() start := time.Now()
vs := msg.Values[1:]
var pattern string var pattern string
if line, pattern = token(line); pattern == "" { var ok bool
return errInvalidNumberOfArguments if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return "", errInvalidNumberOfArguments
} }
if line != "" { if len(vs) != 0 {
return errInvalidNumberOfArguments return "", errInvalidNumberOfArguments
} }
var hooks []*Hook var hooks []*Hook
for name, hook := range c.hooks { 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) hooks = append(hooks, hook)
} else if err != nil {
return errInvalidArgument(pattern)
} }
} }
sort.Sort(hooksByName(hooks)) sort.Sort(hooksByName(hooks))
buf := &bytes.Buffer{} switch msg.OutputType {
buf.WriteString(`{"ok":true,"hooks":[`) case server.JSON:
for i, hook := range hooks { buf := &bytes.Buffer{}
if i > 0 { buf.WriteString(`{"ok":true,"hooks":[`)
buf.WriteByte(',') for i, hook := range hooks {
}
buf.WriteString(`{`)
buf.WriteString(`"name":` + jsonString(hook.Name))
buf.WriteString(`,"key":` + jsonString(hook.Key))
buf.WriteString(`,"endpoints":[`)
for i, endpoint := range hook.Endpoints {
if i > 0 { if i > 0 {
buf.WriteByte(',') buf.WriteByte(',')
} }
buf.WriteString(jsonString(endpoint.Original)) buf.WriteString(`{`)
} buf.WriteString(`"name":` + jsonString(hook.Name))
buf.WriteString(`],"command":` + jsonString(hook.Command)) buf.WriteString(`,"key":` + jsonString(hook.Key))
buf.WriteString(`}`) buf.WriteString(`,"endpoints":[`)
} for i, endpoint := range hook.Endpoints {
buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(jsonString(endpoint.Original))
}
buf.WriteString(`],"command":[`)
for i, v := range hook.Message.Values {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(v.String()))
}
w.Write(buf.Bytes()) buf.WriteString(`]}`)
return }
buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}")
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 { 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() defer conn.Close()
outputType := Null
rd := NewAnyReaderWriter(conn) rd := NewAnyReaderWriter(conn)
brd := rd.rd brd := rd.rd
for { for {
@ -114,6 +115,9 @@ func handleConn(
return return
} }
if msg != nil && msg.Command != "" { if msg != nil && msg.Command != "" {
if outputType != Null {
msg.OutputType = outputType
}
if msg.Command == "quit" { if msg.Command == "quit" {
if msg.OutputType == RESP { if msg.OutputType == RESP {
io.WriteString(conn, "+OK\r\n") io.WriteString(conn, "+OK\r\n")
@ -125,6 +129,7 @@ func handleConn(
log.Error(err) log.Error(err)
return return
} }
outputType = msg.OutputType
} else { } else {
conn.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n")) conn.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n"))
return return
@ -132,6 +137,7 @@ func handleConn(
if msg.ConnType == HTTP || msg.ConnType == WebSocket { if msg.ConnType == HTTP || msg.ConnType == WebSocket {
return return
} }
} }
} }