diff --git a/.gitignore b/.gitignore index 412178bf..c1b55918 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ rqlited **/rqlited !**/rqlited/ +rqlite-cli # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o @@ -36,4 +37,3 @@ _testmain.go *.exe *.test -rqlite diff --git a/cmd/rqlite/README.md b/cmd/rqlite/README.md index 4c6afbda..b8d005b0 100644 --- a/cmd/rqlite/README.md +++ b/cmd/rqlite/README.md @@ -1 +1,60 @@ -This is the rqlite CLI. +# rqlite-cli + +`rqlite-cli` is a command line tool for connecting rqlited. + +## Build + +```sh +go build -o rqlite-cli +``` + +## Usage + +```sh +$> ./rqlite-cli -h +Options: + + -h, --help + display help + + -P, --scheme[=http] + protocol scheme(http or https) + + -H, --host[=127.0.0.1] + rqlited host address + + -p, --port[=4001] + rqlited listening http(s) port +``` + +## Example + +```sh +# start rqlited +$> rqlited ~/node.1 + +# start rqlite-cli terminal +$> ./rqlite-cli +# now, we have enter the rqlite-cli terminal +127.0.0.1:4001> create table foo (id integer not null primary key, name text) +{ + "last_insert_id": 2, + "rows_affected": 1, + "time": 0.00019249700000000002 +} +127.0.0.1:4001> insert into foo(name) values("fiona") +{ + "last_insert_id": 1, + "rows_affected": 1, + "time": 0.000155756 +} +127.0.0.1:4001> select * from foo ++----+-------+ +| id | name | ++----+-------+ +| 1 | fiona | ++----+-------+ +127.0.0.1:4001> quit +bye~ +$> +``` diff --git a/cmd/rqlite/execute.go b/cmd/rqlite/execute.go new file mode 100644 index 00000000..fec3309d --- /dev/null +++ b/cmd/rqlite/execute.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + "github.com/mkideal/cli" +) + +// Result represents execute result +type Result struct { + LastInsertID int `json:"last_insert_id,omitempty"` + RowsAffected int `json:"rows_affected,omitempty"` + Time float64 `json:"time,omitempty"` + Error string `json:"error,omitempty"` +} + +type executeResponse struct { + Results []*Result `json:"results,omitempty"` + Error string `json:"error,omitempty"` + Time float64 `json:"time,omitempty"` +} + +func execute(ctx *cli.Context, cmd, line string, argv *argT) error { + urlStr := fmt.Sprintf("%s://%s:%d/db/execute?pretty&timings", argv.Protocol, argv.Host, argv.Port) + ret := &executeResponse{} + if err := sendRequest(ctx, urlStr, line, ret); err != nil { + return err + } + if ret.Error != "" { + return fmt.Errorf(ret.Error) + } + if len(ret.Results) != 1 { + // What's happen? ret.Results.length MUST be 1 + return fmt.Errorf("unexpected results length: %d", len(ret.Results)) + } + + result := ret.Results[0] + ctx.JSONIndentln(result, "", " ") + return nil +} diff --git a/cmd/rqlite/main.go b/cmd/rqlite/main.go index d5d2692c..417c9226 100644 --- a/cmd/rqlite/main.go +++ b/cmd/rqlite/main.go @@ -1,9 +1,89 @@ package main import ( + "encoding/json" "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/Bowery/prompt" + "github.com/mkideal/cli" ) +type argT struct { + cli.Helper + Protocol string `cli:"s,scheme" usage:"protocol scheme(http or https)" dft:"http"` + Host string `cli:"H,host" usage:"rqlited host address" dft:"127.0.0.1"` + Port uint16 `cli:"p,port" usage:"rqlited listening http(s) port" dft:"4001"` +} + func main() { - fmt.Println("I am the rqlite CLI!") + cli.SetUsageStyle(cli.ManualStyle) + cli.Run(new(argT), func(ctx *cli.Context) error { + argv := ctx.Argv().(*argT) + if argv.Help { + ctx.WriteUsage() + return nil + } + + prefix := fmt.Sprintf("%s:%d> ", argv.Host, argv.Port) + FOR_READ: + for { + line, err := prompt.Basic(prefix, false) + if err != nil { + return err + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + var ( + index = strings.Index(line, " ") + cmd = line + ) + if index >= 0 { + cmd = line[:index] + } + cmd = strings.ToUpper(cmd) + switch cmd { + case "QUIT", "EXIT": + break FOR_READ + case "SELECT": + err = query(ctx, cmd, line, argv) + default: + err = execute(ctx, cmd, line, argv) + } + if err != nil { + ctx.String("%s %v\n", ctx.Color().Red("ERR!"), err) + } + } + ctx.String("bye~\n") + return nil + }) +} + +func makeJSONBody(line string) string { + data, err := json.MarshalIndent([]string{line}, "", " ") + if err != nil { + return "" + } + return string(data) +} + +func sendRequest(ctx *cli.Context, urlStr string, line string, ret interface{}) error { + data := makeJSONBody(line) + resp, err := http.Post(urlStr, "application/json", strings.NewReader(data)) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if err := json.Unmarshal(body, ret); err != nil { + return fmt.Errorf(string(body)) + } + return nil } diff --git a/cmd/rqlite/query.go b/cmd/rqlite/query.go new file mode 100644 index 00000000..e3a1bb09 --- /dev/null +++ b/cmd/rqlite/query.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + + "github.com/mkideal/cli" + "github.com/mkideal/pkg/textutil" +) + +// Rows represents query result +type Rows struct { + Columns []string `json:"columns"` + Types []string `json:"types"` + Values [][2]interface{} `json:"values"` + Time float64 `json:"time"` + Error string `json:"error,omitempty"` +} + +// RowCount implements textutil.Table interface +func (r *Rows) RowCount() int { + return len(r.Values) + 1 +} + +// ColCount implements textutil.Table interface +func (r *Rows) ColCount() int { + return len(r.Columns) +} + +// Get implements textutil.Table interface +func (r *Rows) Get(i, j int) string { + if i == 0 { + if j >= len(r.Columns) { + return "" + } + return r.Columns[j] + } + if i-1 >= len(r.Values) { + return "NULL" + } + if j >= len(r.Values[i-1]) { + return "NULL" + } + return fmt.Sprintf("%v", r.Values[i-1][j]) +} + +func (r *Rows) validate() error { + if r.Error != "" { + return fmt.Errorf(r.Error) + } + if r.Columns == nil || r.Types == nil || r.Values == nil { + return fmt.Errorf("unexpected result") + } + return nil +} + +// headerRenderStyle render the header of result +type headerRenderStyle struct { + textutil.DefaultStyle +} + +func (render headerRenderStyle) CellRender(row, col int, cell string, cw *textutil.ColorWriter) { + if row != 0 { + fmt.Fprintf(cw, cell) + } else { + fmt.Fprintf(cw, cw.Color.Cyan(cell)) + } +} + +var headerRender = &headerRenderStyle{} + +type queryResponse struct { + Results []*Rows `json:"results"` + Error string `json:"error,omitempty"` + Time float64 `json:"time"` +} + +func query(ctx *cli.Context, cmd, line string, argv *argT) error { + urlStr := fmt.Sprintf("%s://%s:%d/db/query?pretty&timings", argv.Protocol, argv.Host, argv.Port) + ret := &queryResponse{} + if err := sendRequest(ctx, urlStr, line, ret); err != nil { + return err + } + if ret.Error != "" { + return fmt.Errorf(ret.Error) + } + if len(ret.Results) != 1 { + // NOTE:What's happen? ret.Results.length MUST be 1 + return fmt.Errorf("unexpected results length: %d", len(ret.Results)) + } + + result := ret.Results[0] + if err := result.validate(); err != nil { + return err + } + textutil.WriteTable(ctx, result, headerRender) + return nil +}