
The big change is that the GeoJSON package has been completely rewritten to fix a few of geometry calculation bugs, increase performance, and to better follow the GeoJSON spec RFC 7946. GeoJSON updates - A LineString now requires at least two points. - All json members, even foreign, now persist with the object. - The bbox member persists too but is no longer used for geometry calculations. This is change in behavior. Previously Tile38 would treat the bbox as the object's physical rectangle. - Corrections to geometry intersects and within calculations. Faster spatial queries - The performance of Point-in-polygon and object intersect operations are greatly improved for complex polygons and line strings. It went from O(n) to roughly O(log n). - The same for all collection types with many children, including FeatureCollection, GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon. Codebase changes - The pkg directory has been renamed to internal - The GeoJSON internal package has been moved to a seperate repo at https://github.com/tidwall/geojson. It's now vendored. Please look out for higher memory usage for datasets using complex shapes. A complex shape is one that has 64 or more points. For these shapes it's expected that there will be increase of least 54 bytes per point.
492 lines
11 KiB
Go
492 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterh/liner"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/resp"
|
|
"github.com/tidwall/tile38/core"
|
|
"github.com/tidwall/tile38/internal/client"
|
|
)
|
|
|
|
func userHomeDir() string {
|
|
if runtime.GOOS == "windows" {
|
|
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
|
if home == "" {
|
|
home = os.Getenv("USERPROFILE")
|
|
}
|
|
return home
|
|
}
|
|
return os.Getenv("HOME")
|
|
}
|
|
|
|
var (
|
|
historyFile = filepath.Join(userHomeDir(), ".liner_example_history")
|
|
)
|
|
|
|
type connError struct {
|
|
OK bool `json:"ok"`
|
|
Err string `json:"err"`
|
|
}
|
|
|
|
var (
|
|
hostname = "127.0.0.1"
|
|
output = "json"
|
|
port = 9851
|
|
oneCommand string
|
|
raw bool
|
|
noprompt bool
|
|
tty bool
|
|
)
|
|
|
|
func showHelp() bool {
|
|
|
|
gitsha := ""
|
|
if core.GitSHA == "" || core.GitSHA == "0000000" {
|
|
gitsha = ""
|
|
} else {
|
|
gitsha = " (git:" + core.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, " --raw Use raw formatting for replies (default when STDOUT is not a tty)\n")
|
|
fmt.Fprintf(os.Stdout, " --noprompt Do not display a prompt\n")
|
|
fmt.Fprintf(os.Stdout, " --tty Force 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
|
|
}
|
|
|
|
func parseArgs() bool {
|
|
defer func() {
|
|
if v := recover(); v != nil {
|
|
if v, ok := v.(string); ok && v == "bad arg" {
|
|
showHelp()
|
|
}
|
|
}
|
|
}()
|
|
|
|
args := os.Args[1:]
|
|
readArg := func(arg string) string {
|
|
if len(args) == 0 {
|
|
panic("bad arg")
|
|
}
|
|
var narg = args[0]
|
|
args = args[1:]
|
|
return narg
|
|
}
|
|
badArg := func(arg string) 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" || arg == "-?" {
|
|
return showHelp()
|
|
}
|
|
if !strings.HasPrefix(arg, "-") {
|
|
args = append([]string{arg}, args...)
|
|
break
|
|
}
|
|
switch arg {
|
|
default:
|
|
return badArg(arg)
|
|
case "--raw":
|
|
raw = true
|
|
case "--tty":
|
|
tty = true
|
|
case "--noprompt":
|
|
noprompt = true
|
|
case "--resp":
|
|
output = "resp"
|
|
case "-h":
|
|
hostname = readArg(arg)
|
|
case "-p":
|
|
n, err := strconv.ParseUint(readArg(arg), 10, 16)
|
|
if err != nil {
|
|
return badArg(arg)
|
|
}
|
|
port = int(n)
|
|
}
|
|
}
|
|
oneCommand = strings.Join(args, " ")
|
|
return true
|
|
}
|
|
|
|
func refusedErrorString(addr string) string {
|
|
return fmt.Sprintf("Could not connect to Tile38 at %s: Connection refused", addr)
|
|
}
|
|
|
|
var groupsM = make(map[string][]string)
|
|
|
|
func main() {
|
|
if !parseArgs() {
|
|
return
|
|
}
|
|
|
|
if !raw && !tty && runtime.GOOS != "windows" {
|
|
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
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%d", hostname, port)
|
|
var conn *client.Conn
|
|
connDial := func() {
|
|
var err error
|
|
conn, err = client.Dial(addr)
|
|
if err != nil {
|
|
if _, ok := err.(net.Error); ok {
|
|
fmt.Fprintln(os.Stderr, refusedErrorString(addr))
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
if conn != nil {
|
|
if output == "resp" {
|
|
_, err := conn.Do("output resp")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
connDial()
|
|
livemode := false
|
|
aof := false
|
|
defer func() {
|
|
if livemode {
|
|
var err error
|
|
if aof {
|
|
_, err = io.Copy(os.Stdout, conn.Reader())
|
|
fmt.Fprintln(os.Stderr, "")
|
|
} else {
|
|
var msg []byte
|
|
for {
|
|
msg, err = conn.ReadMessage()
|
|
if err != nil {
|
|
break
|
|
}
|
|
fmt.Fprintln(os.Stderr, string(msg))
|
|
}
|
|
}
|
|
if err != nil && err != io.EOF {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
}
|
|
}
|
|
}()
|
|
|
|
line := liner.NewLiner()
|
|
defer line.Close()
|
|
|
|
var commands []string
|
|
for name, command := range core.Commands {
|
|
commands = append(commands, name)
|
|
groupsM[command.Group] = append(groupsM[command.Group], name)
|
|
}
|
|
sort.Strings(commands)
|
|
var groups []string
|
|
for group, arr := range groupsM {
|
|
groups = append(groups, "@"+group)
|
|
sort.Strings(arr)
|
|
groupsM[group] = arr
|
|
}
|
|
sort.Strings(groups)
|
|
|
|
line.SetMultiLineMode(false)
|
|
line.SetCtrlCAborts(true)
|
|
if !(noprompt && tty) {
|
|
line.SetCompleter(func(line string) (c []string) {
|
|
if strings.HasPrefix(strings.ToLower(line), "help ") {
|
|
var nitems []string
|
|
nline := strings.TrimSpace(line[5:])
|
|
if nline == "" || nline[0] == '@' {
|
|
for _, n := range groups {
|
|
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) {
|
|
nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToLower(n))
|
|
}
|
|
}
|
|
} else {
|
|
for _, n := range commands {
|
|
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) {
|
|
nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToUpper(n))
|
|
}
|
|
}
|
|
}
|
|
for _, n := range nitems {
|
|
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) {
|
|
c = append(c, n)
|
|
}
|
|
}
|
|
} else {
|
|
for _, n := range commands {
|
|
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) {
|
|
c = append(c, n)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
})
|
|
}
|
|
if f, err := os.Open(historyFile); err == nil {
|
|
line.ReadHistory(f)
|
|
f.Close()
|
|
}
|
|
defer func() {
|
|
if f, err := os.Create(historyFile); err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
} else {
|
|
line.WriteHistory(f)
|
|
f.Close()
|
|
}
|
|
}()
|
|
for {
|
|
var command string
|
|
var err error
|
|
if oneCommand == "" {
|
|
if raw || noprompt {
|
|
command, err = line.Prompt("")
|
|
} else {
|
|
if conn == nil {
|
|
command, err = line.Prompt("not connected> ")
|
|
} else {
|
|
command, err = line.Prompt(addr + "> ")
|
|
}
|
|
}
|
|
|
|
} else {
|
|
command = oneCommand
|
|
}
|
|
if err == nil {
|
|
nohist := strings.HasPrefix(command, " ")
|
|
command = strings.TrimSpace(command)
|
|
if command == "" {
|
|
if conn != nil {
|
|
_, err := conn.Do("pInG")
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr, refusedErrorString(addr))
|
|
}
|
|
}
|
|
} else {
|
|
if !nohist {
|
|
line.AppendHistory(command)
|
|
}
|
|
if (command[0] == 'e' || command[0] == 'E') && strings.ToLower(command) == "exit" {
|
|
return
|
|
}
|
|
if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" {
|
|
return
|
|
}
|
|
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 {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
aof = (command[0] == 'a' || command[0] == 'A') && strings.HasPrefix(strings.ToLower(command), "aof ")
|
|
tryAgain:
|
|
if conn == nil {
|
|
connDial()
|
|
if conn == nil {
|
|
continue
|
|
}
|
|
}
|
|
msg, err := conn.Do(command)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
} else {
|
|
conn = nil
|
|
goto tryAgain
|
|
}
|
|
return
|
|
}
|
|
switch strings.ToLower(command) {
|
|
case "output resp":
|
|
if string(msg) == "+OK\r\n" {
|
|
output = "resp"
|
|
}
|
|
case "output json":
|
|
if strings.HasPrefix(string(msg), `{"ok":true`) {
|
|
output = "json"
|
|
}
|
|
}
|
|
|
|
mustOutput := true
|
|
if oneCommand == "" && !strings.HasPrefix(string(msg), `{"ok":true`) {
|
|
var cerr connError
|
|
if err := json.Unmarshal(msg, &cerr); err == nil {
|
|
fmt.Fprintln(os.Stderr, "(error) "+cerr.Err)
|
|
mustOutput = false
|
|
}
|
|
} else if gjson.GetBytes(msg, "command").String() == "psubscribe" ||
|
|
gjson.GetBytes(msg, "command").String() == "subscribe" ||
|
|
string(msg) == client.LiveJSON {
|
|
fmt.Fprintln(os.Stderr, string(msg))
|
|
livemode = true
|
|
break // break out of prompt and just feed data to screen
|
|
}
|
|
if mustOutput {
|
|
if output == "resp" {
|
|
if !raw {
|
|
msg = convert2termresp(msg)
|
|
}
|
|
fmt.Fprintln(os.Stdout, string(msg))
|
|
} else {
|
|
if raw {
|
|
fmt.Fprintln(os.Stdout, string(msg))
|
|
} else {
|
|
fmt.Fprintln(os.Stdout, string(msg))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if err == liner.ErrPromptAborted {
|
|
return
|
|
} else if err == io.EOF {
|
|
os.Exit(0)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Error reading line: %s", err.Error())
|
|
}
|
|
if oneCommand != "" {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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)"
|
|
}
|
|
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 help(arg string) error {
|
|
var groupsA []string
|
|
for group := range groupsM {
|
|
groupsA = append(groupsA, "@"+group)
|
|
}
|
|
groups := "Groups: " + strings.Join(groupsA, ", ") + "\n"
|
|
|
|
if arg == "" {
|
|
fmt.Fprintf(os.Stderr, "tile38-cli %s (git:%s)\n", core.Version, core.GitSHA)
|
|
fmt.Fprintf(os.Stderr, `Type: "help @<group>" to get a list of commands in <group>`+"\n")
|
|
fmt.Fprintf(os.Stderr, ` "help <command>" for help on <command>`+"\n")
|
|
if !(noprompt && tty) {
|
|
fmt.Fprintf(os.Stderr, ` "help <tab>" to get a list of possible help topics`+"\n")
|
|
}
|
|
fmt.Fprintf(os.Stderr, ` "quit" to exit`+"\n")
|
|
if noprompt && tty {
|
|
fmt.Fprintf(os.Stderr, groups)
|
|
}
|
|
return nil
|
|
}
|
|
showGroups := false
|
|
found := false
|
|
if strings.HasPrefix(arg, "@") {
|
|
for _, command := range groupsM[arg[1:]] {
|
|
fmt.Fprintf(os.Stderr, "%s\n", core.Commands[command].TermOutput(" "))
|
|
found = true
|
|
}
|
|
if !found {
|
|
showGroups = true
|
|
}
|
|
} else {
|
|
if command, ok := core.Commands[strings.ToUpper(arg)]; ok {
|
|
fmt.Fprintf(os.Stderr, "%s\n", command.TermOutput(" "))
|
|
found = true
|
|
}
|
|
}
|
|
if showGroups {
|
|
if noprompt && tty {
|
|
fmt.Fprintf(os.Stderr, groups)
|
|
}
|
|
} else if !found {
|
|
if noprompt && tty {
|
|
help("")
|
|
}
|
|
}
|
|
return nil
|
|
}
|