Restructure sb tool to allow for top-level calls
Previously all commands would be called via
```
$ sb <flags> sh
? <command>
```
This patch allows commands of the form
```
$ sb <flags> <command
```
These commands are understood by cmdline, so you can
```
$ sb help command
```
or
```
$ sb <flags> sh
? help command
```
Change-Id: Ia6bb970401eecba57c4a5b5570550149182cb841
diff --git a/cmd/sb/commands/commands.go b/cmd/sb/commands/commands.go
new file mode 100644
index 0000000..21efbc1
--- /dev/null
+++ b/cmd/sb/commands/commands.go
@@ -0,0 +1,74 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package commands
+
+import (
+ "fmt"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/syncbase"
+ "v.io/x/lib/cmdline"
+ "v.io/x/ref/cmd/sb/dbutil"
+ "v.io/x/ref/lib/v23cmd"
+)
+
+var Commands = []*cmdline.Command{
+ cmdDump,
+ cmdMakeDemo,
+ cmdSelect,
+}
+
+var (
+ commandCtx *context.T
+ commandDb syncbase.Database
+)
+
+func SetCtx(ctx *context.T) {
+ commandCtx = ctx
+}
+
+func SetDB(db syncbase.Database) {
+ commandDb = db
+}
+
+type sbHandler func(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error
+
+func SbRunner(handler sbHandler) cmdline.Runner {
+ return v23cmd.RunnerFuncWithInit(func(ctx *context.T, env *cmdline.Env, args []string) error {
+ db := commandDb // Set in shell handler.
+ if db == nil {
+ var err error
+ if db, err = dbutil.OpenDB(ctx); err != nil {
+ return err
+ }
+ }
+ return handler(ctx, db, env, args)
+ }, func() (*context.T, v23.Shutdown, error) {
+ if commandCtx != nil {
+ return commandCtx, func() {}, nil
+ }
+ return v23.TryInit()
+ })
+}
+
+func GetCommand(name string) (*cmdline.Command, error) {
+ for _, cmd := range Commands {
+ if cmd.Name == name {
+ return cmd, nil
+ }
+ }
+
+ return nil, fmt.Errorf("no command %q", name)
+}
+
+func PrintUsage(command *cmdline.Command) {
+ fmt.Println(command.Long)
+ fmt.Println()
+ fmt.Println("Usage:")
+ fmt.Printf("\t%s [flags] %s\n", command.Name, command.ArgsName)
+ fmt.Println()
+ fmt.Println(command.ArgsLong)
+}
diff --git a/cmd/sb/commands/dump.go b/cmd/sb/commands/dump.go
new file mode 100644
index 0000000..324dc3b
--- /dev/null
+++ b/cmd/sb/commands/dump.go
@@ -0,0 +1,87 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package commands
+
+import (
+ "fmt"
+ "io"
+
+ "v.io/v23/context"
+ "v.io/v23/syncbase"
+ "v.io/x/lib/cmdline"
+
+ // TODO(zsterling): This is a temporary hack-around to avoid a permissions issue.
+ // Investigate why this is needed and how to get rid of it.
+ _ "v.io/x/ref/cmd/sb/internal/demodb"
+)
+
+var cmdDump = &cmdline.Command{
+ Name: "dump",
+ Short: "Print a dump of the database",
+ Long: `Print a dump of the database.`,
+ Runner: SbRunner(dumpDatabase),
+}
+
+func dumpDatabase(ctx *context.T, d syncbase.Database, env *cmdline.Env, _ []string) error {
+ w := env.Stdout
+ var errors []error
+ if err := dumpCollections(ctx, w, d); err != nil {
+ errors = append(errors, fmt.Errorf("failed dumping collections: %v", err))
+ }
+ if err := dumpSyncgroups(ctx, w, d); err != nil {
+ errors = append(errors, fmt.Errorf("failed dumping syncgroups: %v", err))
+ }
+ return mergeErrors(errors)
+}
+
+func dumpCollections(ctx *context.T, w io.Writer, d syncbase.Database) error {
+ collections, err := d.ListCollections(ctx)
+ if err != nil {
+ return fmt.Errorf("failed listing collections: %v", err)
+ }
+ var errs []error
+ for _, collection := range collections {
+ fmt.Fprintf(w, "collection: %v\n", collection)
+ // TODO(ivanpi): Queries currently support only the default user blessing.
+ if err := queryExec(ctx, w, d, fmt.Sprintf("select k, v from %s", collection.Name)); err != nil {
+ errs = append(errs, fmt.Errorf("> %v: %v", collection, err))
+ }
+ }
+ if len(errs) > 0 {
+ return fmt.Errorf("failed dumping %d of %d collections:\n%v", len(errs), len(collections), mergeErrors(errs))
+ }
+ return nil
+}
+
+func dumpSyncgroups(ctx *context.T, w io.Writer, d syncbase.Database) error {
+ sgIds, err := d.ListSyncgroups(ctx)
+ if err != nil {
+ return fmt.Errorf("failed listing syncgroups: %v", err)
+ }
+ var errs []error
+ for _, sgId := range sgIds {
+ fmt.Fprintf(w, "syncgroup: %+v\n", sgId)
+ if spec, version, err := d.SyncgroupForId(sgId).GetSpec(ctx); err != nil {
+ errs = append(errs, err)
+ } else {
+ fmt.Fprintf(w, "%+v (version: \"%s\")\n", spec, version)
+ }
+ }
+ if len(errs) > 0 {
+ return fmt.Errorf("failed dumping %d of %d syncgroups:\n%v", len(errs), len(sgIds), mergeErrors(errs))
+ }
+ return nil
+}
+
+func mergeErrors(errs []error) error {
+ if len(errs) == 0 {
+ return nil
+ }
+ err := errs[0]
+ for _, e := range errs[1:] {
+ err = fmt.Errorf("%v\n%v", err, e)
+ }
+ return err
+}
diff --git a/cmd/sb/commands/make-demo.go b/cmd/sb/commands/make-demo.go
new file mode 100644
index 0000000..afb7543
--- /dev/null
+++ b/cmd/sb/commands/make-demo.go
@@ -0,0 +1,30 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package commands
+
+import (
+ "fmt"
+
+ "v.io/v23/context"
+ "v.io/v23/syncbase"
+ "v.io/x/lib/cmdline"
+ "v.io/x/ref/cmd/sb/internal/demodb"
+)
+
+var cmdMakeDemo = &cmdline.Command{
+ Name: "make-demo",
+ Short: "Populate the db with dummy data",
+ Long: `Populate the db with dummy data.`,
+ Runner: SbRunner(makeDemoDB),
+}
+
+func makeDemoDB(ctx *context.T, db syncbase.Database, env *cmdline.Env, _ []string) error {
+ w := env.Stdout
+ if err := demodb.PopulateDemoDB(ctx, db); err != nil {
+ return fmt.Errorf("failed making demo collections: %v", err)
+ }
+ fmt.Fprintln(w, "Demo collections created and populated.")
+ return nil
+}
diff --git a/cmd/sb/commands/output-format.go b/cmd/sb/commands/output-format.go
new file mode 100644
index 0000000..6869843
--- /dev/null
+++ b/cmd/sb/commands/output-format.go
@@ -0,0 +1,53 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package commands
+
+import (
+ "flag"
+ "fmt"
+ "io"
+
+ "v.io/x/ref/cmd/sb/internal/writer"
+)
+
+type formatFlag string
+
+func (f *formatFlag) Set(s string) error {
+ for _, v := range []string{"table", "csv", "json"} {
+ if s == v {
+ *f = formatFlag(s)
+ return nil
+ }
+ }
+ return fmt.Errorf("unsupported -format %q", s)
+}
+
+func (f *formatFlag) String() string {
+ return string(*f)
+}
+
+func (f formatFlag) NewWriter(w io.Writer) writer.FormattingWriter {
+ switch f {
+ case "table":
+ return writer.NewTableWriter(w)
+ case "csv":
+ return writer.NewCSVWriter(w, flagCSVDelimiter)
+ case "json":
+ return writer.NewJSONWriter(w)
+ default:
+ panic("unexpected format:" + f)
+ }
+ return nil
+}
+
+var (
+ flagFormat formatFlag = "table"
+ flagCSVDelimiter string
+)
+
+func init() {
+ flag.Var(&flagFormat, "format", "Output format. 'table': human-readable table; 'csv': comma-separated values, use -csv-delimiter to control the delimiter; 'json': JSON objects.")
+ flag.StringVar(&flagCSVDelimiter, "csv-delimiter", ",", "The delimiter used for the csv output format.")
+}
diff --git a/cmd/sb/commands/select.go b/cmd/sb/commands/select.go
new file mode 100644
index 0000000..5207068
--- /dev/null
+++ b/cmd/sb/commands/select.go
@@ -0,0 +1,60 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package commands
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ "v.io/v23/context"
+ "v.io/v23/syncbase"
+ "v.io/x/lib/cmdline"
+)
+
+var cmdSelect = &cmdline.Command{
+ // TODO(zsterling): Wrap select in exec and add more general queries/deletes.
+ Name: "select",
+ Short: "Display particular rows, or parts of rows",
+ Long: `
+Display particular rows, or parts of rows.
+`,
+ ArgsName: "<field_1,field_2,...> from <collection> [where <field> = <value>]",
+ ArgsLong: `
+<field_n> specifies a field in the collection <collection>.
+`,
+ Runner: SbRunner(selectFunc),
+}
+
+func selectFunc(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+ return queryExec(ctx, env.Stdout, db, "select "+strings.Join(args, " "))
+}
+
+// Split an error message into an offset and the remaining (i.e., rhs of offset) message.
+// The convention for syncql is "<module><optional-rpc>[offset]<remaining-message>".
+func splitError(err error) (int64, string) {
+ errMsg := err.Error()
+ idx1 := strings.Index(errMsg, "[")
+ idx2 := strings.Index(errMsg, "]")
+ if idx1 == -1 || idx2 == -1 {
+ return 0, errMsg
+ }
+ offsetString := errMsg[idx1+1 : idx2]
+ offset, err := strconv.ParseInt(offsetString, 10, 64)
+ if err != nil {
+ return 0, errMsg
+ }
+ return offset, errMsg[idx2+1:]
+}
+
+func queryExec(ctx *context.T, w io.Writer, d syncbase.Database, q string) error {
+ columnNames, rs, err := d.Exec(ctx, q)
+ if err != nil {
+ off, msg := splitError(err)
+ return fmt.Errorf("\n%s\n%s^\n%d: %s", q, strings.Repeat(" ", int(off)), off+1, msg)
+ }
+ return flagFormat.NewWriter(w).Write(columnNames, rs)
+}
diff --git a/cmd/sb/dbutil/dbutil.go b/cmd/sb/dbutil/dbutil.go
new file mode 100644
index 0000000..330444b
--- /dev/null
+++ b/cmd/sb/dbutil/dbutil.go
@@ -0,0 +1,56 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dbutil
+
+import (
+ "flag"
+ "fmt"
+
+ "v.io/v23/context"
+ wire "v.io/v23/services/syncbase"
+ "v.io/v23/syncbase"
+)
+
+var (
+ flagCreateIfAbsent bool
+ flagService string
+ flagBlessing string
+ flagDbId string
+)
+
+func init() {
+ flag.BoolVar(&flagCreateIfAbsent, "create-if-absent", false, "Create the target database if it doesn't exist.")
+ flag.StringVar(&flagService, "service", "", "Syncbase service to connect to.")
+ flag.StringVar(&flagDbId, "db", "", "Id of database to connect to.")
+}
+
+// OpenDB is a user-friendly wrapper for openDB.
+func OpenDB(ctx *context.T) (syncbase.Database, error) {
+ // Open a connection to syncbase
+ sbService := syncbase.NewService(flagService)
+ dbId, err := wire.ParseId(flagDbId)
+ if err != nil {
+ return nil, err
+ }
+ return openDB(ctx, sbService, dbId, flagCreateIfAbsent)
+}
+
+// openDB opens the db at sbService specified by id.
+func openDB(ctx *context.T, sbService syncbase.Service, id wire.Id, createIfAbsent bool) (syncbase.Database, error) {
+ db := sbService.DatabaseForId(id, nil)
+ if exists, err := db.Exists(ctx); err != nil {
+ return nil, fmt.Errorf("failed checking for db %q: %v", db.FullName(), err)
+ } else if !exists {
+ if createIfAbsent {
+ if err := db.Create(ctx, nil); err != nil {
+ return nil, fmt.Errorf("%s\nDeveloper's note: double-check your permissions!", err)
+ }
+ } else {
+ return nil, fmt.Errorf("db %q does not exist", db.FullName())
+ }
+ }
+
+ return db, nil
+}
diff --git a/cmd/sb/doc.go b/cmd/sb/doc.go
index 72a9d2b..a43af09 100644
--- a/cmd/sb/doc.go
+++ b/cmd/sb/doc.go
@@ -2,62 +2,85 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Sb is a syncbase general-purpose client and management utility.
-// It currently supports syncQL (the syncbase query language).
-//
-// The 'sh' command connects to a specified database on a syncbase instance,
-// creating it if it does not exist if -create-missing is specified.
-// The user can then enter the following at the command line:
-// 1. dump - to get a dump of the database
-// 2. a syncQL select statement - which is executed and results printed to stdout
-// 3. a syncQL delete statement - which is executed to delete k/v pairs from a collection
-// 4. destroy {db|collection|syncgroup} <identifier> - to destroy a syncbase object
-// 5. make-demo - to create demo collections in the database to experiment with, equivalent to -make-demo flag
-// 5. exit (or quit) - to exit the program
-//
-// When the shell is running non-interactively (stdin not connected to a tty),
-// errors cause the shell to exit with a non-zero status.
-//
-// To build client:
-// jiri go install v.io/x/ref/cmd/sb
-//
-// To run client:
-// $JIRI_ROOT/release/go/bin/sb sh <app_blessing> <db_name>
-//
-// Sample run (assuming a syncbase service is mounted at '/:8101/syncbase',
-// otherwise specify using -service flag):
-// > $JIRI_ROOT/release/go/bin/sb sh -create-missing -make-demo -format=csv demoapp demodb
-// ? select v.Name, v.Address.State from Customers where Type(v) = "Customer";
-// v.Name,v.Address.State
-// John Smith,CA
-// Bat Masterson,IA
-// ? select v.CustId, v.InvoiceNum, v.ShipTo.Zip, v.Amount from Customers where Type(v) = "Invoice" and v.Amount > 100;
-// v.CustId,v.InvoiceNum,v.ShipTo.Zip,v.Amount
-// 2,1001,50055,166
-// 2,1002,50055,243
-// 2,1004,50055,787
-// ? select k, v fro Customers;
-// Error:
-// select k, v fro Customers
-// ^
-// 13: Expected 'from', found fro.
-// ? select k, v from Customers;
-// k,v
-// 001,"{Name: ""John Smith"", Id: 1, Active: true, Address: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}, Credit: {Agency: Equifax, Report: EquifaxReport: {Rating: 65}}}"
-// 001001,"{CustId: 1, InvoiceNum: 1000, Amount: 42, ShipTo: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
-// 001002,"{CustId: 1, InvoiceNum: 1003, Amount: 7, ShipTo: {Street: ""2 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
-// 001003,"{CustId: 1, InvoiceNum: 1005, Amount: 88, ShipTo: {Street: ""3 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
-// 002,"{Name: ""Bat Masterson"", Id: 2, Active: true, Address: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}, Credit: {Agency: TransUnion, Report: TransUnionReport: {Rating: 80}}}"
-// 002001,"{CustId: 2, InvoiceNum: 1001, Amount: 166, ShipTo: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
-// 002002,"{CustId: 2, InvoiceNum: 1002, Amount: 243, ShipTo: {Street: ""888 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
-// 002003,"{CustId: 2, InvoiceNum: 1004, Amount: 787, ShipTo: {Street: ""999 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
-// 002004,"{CustId: 2, InvoiceNum: 1006, Amount: 88, ShipTo: {Street: ""101010 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
-// ? delete from Customers where k = "001002";
-// +-------+
-// | Count |
-// +-------+
-// | 1 |
-// +-------+
-// ? exit;
-// >
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Syncbase general-purpose client and management utility. Supports starting a
+syncQL shell and executing the commands at top level.
+
+Usage:
+ sb [flags] <command>
+
+The sb commands are:
+ sh Start a syncQL shell
+ dump Print a dump of the database
+ make-demo Populate the db with dummy data
+ select Display particular rows, or parts of rows
+ help Display help for commands or topics
+
+The global flags are:
+ -create-if-absent=false
+ Create the target database if it doesn't exist.
+ -csv-delimiter=,
+ The delimiter used for the csv output format.
+ -db=
+ Id of database to connect to.
+ -format=table
+ Output format. 'table': human-readable table; 'csv': comma-separated values,
+ use -csv-delimiter to control the delimiter; 'json': JSON objects.
+ -service=
+ Syncbase service to connect to.
+
+ -alsologtostderr=true
+ log to standard error as well as files
+ -log_backtrace_at=:0
+ when logging hits line file:N, emit a stack trace
+ -log_dir=
+ if non-empty, write log files to this directory
+ -logtostderr=false
+ log to standard error instead of files
+ -max_stack_buf_size=4292608
+ max size in bytes of the buffer to use for logging stack traces
+ -metadata=<just specify -metadata to activate>
+ Displays metadata for the program and exits.
+ -stderrthreshold=2
+ logs at or above this threshold go to stderr
+ -time=false
+ Dump timing information to stderr before exiting the program.
+ -v=0
+ log level for V logs
+ -v23.credentials=
+ directory to use for storing security credentials
+ -v23.i18n-catalogue=
+ 18n catalogue files to load, comma separated
+ -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101]
+ local namespace root; can be repeated to provided multiple roots
+ -v23.proxy=
+ object name of proxy service to use to export services across network
+ boundaries
+ -v23.tcp.address=
+ address to listen on
+ -v23.tcp.protocol=wsh
+ protocol to listen with
+ -v23.vtrace.cache-size=1024
+ The number of vtrace traces to store in memory.
+ -v23.vtrace.collect-regexp=
+ Spans and annotations that match this regular expression will trigger trace
+ collection.
+ -v23.vtrace.dump-on-shutdown=true
+ If true, dump all stored traces on runtime shutdown.
+ -v23.vtrace.sample-rate=0
+ Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -v23.vtrace.v=0
+ The verbosity level of the log messages to be captured in traces
+ -vmodule=
+ comma-separated list of globpattern=N settings for filename-filtered logging
+ (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or
+ *az or b* but not by bar/baz or baz.go or az or b.*
+ -vpath=
+ comma-separated list of regexppattern=N settings for file pathname-filtered
+ logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns
+ foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az
+*/
package main
diff --git a/cmd/sb/internal/reader/reader.go b/cmd/sb/internal/reader/reader.go
index 930c140..2bff765 100644
--- a/cmd/sb/internal/reader/reader.go
+++ b/cmd/sb/internal/reader/reader.go
@@ -40,6 +40,10 @@
// GetQuery returns an entire query where queries are delimited by semicolons.
// GetQuery returns the error io.EOF when there is no more input.
func (t *T) GetQuery() (string, error) {
+ return t.GetQueryWithTerminator(';')
+}
+
+func (t *T) GetQueryWithTerminator(terminator rune) (string, error) {
if t.s.Peek() == scanner.EOF {
input, err := t.prompt.InitialPrompt()
if err != nil {
@@ -51,11 +55,14 @@
WholeQuery:
for true {
for tok := t.s.Scan(); tok != scanner.EOF; tok = t.s.Scan() {
- if tok == ';' {
+ if tok == terminator {
break WholeQuery
}
query += t.s.TokenText()
}
+ if terminator == '\n' {
+ break
+ }
input, err := t.prompt.ContinuePrompt()
if err != nil {
return "", err
@@ -63,7 +70,7 @@
t.initScanner(input)
query += "\n" // User started a new line.
}
- t.prompt.AppendHistory(query + ";")
+ t.prompt.AppendHistory(query + string(terminator))
return query, nil
}
diff --git a/cmd/sb/main.go b/cmd/sb/main.go
index d0b23a6..687565a 100644
--- a/cmd/sb/main.go
+++ b/cmd/sb/main.go
@@ -1,34 +1,38 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// sb - syncbase general-purpose client and management utility.
-// Currently supports syncQL select queries.
-
+//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
package main
import (
- "flag"
+ "regexp"
"v.io/x/lib/cmdline"
+ "v.io/x/ref/cmd/sb/commands"
_ "v.io/x/ref/runtime/factories/generic"
)
func main() {
+ cmdline.HideGlobalFlagsExcept(
+ regexp.MustCompile("^service$"),
+ regexp.MustCompile("^db$"),
+ regexp.MustCompile("^create-if-absent$"),
+ regexp.MustCompile("^format$"),
+ regexp.MustCompile("^csv-delimiter$"))
cmdline.Main(cmdSb)
}
-var cmdSb = &cmdline.Command{
- Name: "sb",
- Short: "sb - Vanadium syncbase client and management utility",
- Long: `
-Syncbase general-purpose client and management utility.
-Currently supports starting a syncQL shell.
-`,
- Children: []*cmdline.Command{cmdSbShell},
-}
+var cmdSb *cmdline.Command
-var (
- // TODO(ivanpi): Decide on convention for local syncbase service name.
- flagSbService = flag.String("service", "/:8101/syncbase", "Location of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
-)
+func init() {
+ cmdSb = &cmdline.Command{
+ Name: "sb",
+ Short: "Vanadium syncbase client and management utility",
+ Long: `
+Syncbase general-purpose client and management utility.
+Supports starting a syncQL shell and executing the commands at top level.
+ `,
+ Children: append([]*cmdline.Command{cmdSbShell}, commands.Commands...),
+ }
+}
diff --git a/cmd/sb/shell.go b/cmd/sb/shell.go
index afa412a..869a4b4 100644
--- a/cmd/sb/shell.go
+++ b/cmd/sb/shell.go
@@ -1,126 +1,39 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Syncbase client shell. Currently supports syncQL select queries.
-
package main
import (
"fmt"
"io"
"os"
- "strconv"
"strings"
isatty "github.com/mattn/go-isatty"
"v.io/v23/context"
- wire "v.io/v23/services/syncbase"
"v.io/v23/syncbase"
- pubutil "v.io/v23/syncbase/util"
"v.io/x/lib/cmdline"
- "v.io/x/ref/cmd/sb/internal/demodb"
+ "v.io/x/ref/cmd/sb/commands"
"v.io/x/ref/cmd/sb/internal/reader"
- "v.io/x/ref/cmd/sb/internal/writer"
- "v.io/x/ref/lib/v23cmd"
)
var cmdSbShell = &cmdline.Command{
- Runner: v23cmd.RunnerFunc(runSbShell),
+ Runner: commands.SbRunner(runSbShell),
Name: "sh",
Short: "Start a syncQL shell",
Long: `
Connect to a database on the Syncbase service and start a syncQL shell.
`,
- ArgsName: "<app_blessing> <db_name>",
- ArgsLong: `
-<app_blessing> and <db_name> specify the database to execute queries against.
-The database must exist unless -create-missing is specified.
-`,
}
-type formatFlag string
-
-func (f *formatFlag) Set(s string) error {
- for _, v := range []string{"table", "csv", "json"} {
- if s == v {
- *f = formatFlag(s)
- return nil
- }
- }
- return fmt.Errorf("unsupported -format %q", s)
-}
-
-func (f *formatFlag) String() string {
- return string(*f)
-}
-
-func (f formatFlag) NewWriter(w io.Writer) writer.FormattingWriter {
- switch f {
- case "table":
- return writer.NewTableWriter(w)
- case "csv":
- return writer.NewCSVWriter(w, flagCSVDelimiter)
- case "json":
- return writer.NewJSONWriter(w)
- default:
- panic("unexpected format:" + f)
- }
- return nil
-}
-
-var (
- flagFormat formatFlag = "table"
- flagCSVDelimiter string
- flagCreateIfNotExists bool
- flagMakeDemoCollections bool
-)
-
-func init() {
- cmdSbShell.Flags.Var(&flagFormat, "format", "Output format. 'table': human-readable table; 'csv': comma-separated values, use -csv-delimiter to control the delimiter; 'json': JSON objects.")
- cmdSbShell.Flags.StringVar(&flagCSVDelimiter, "csv-delimiter", ",", "Delimiter to use when printing data as CSV (e.g. \"\t\", \",\")")
- cmdSbShell.Flags.BoolVar(&flagCreateIfNotExists, "create-missing", false, "Create the database if it does not exist.")
- cmdSbShell.Flags.BoolVar(&flagMakeDemoCollections, "make-demo", false, "(Re)create demo collections in the database.")
-}
-
-func validateFlags() error {
- if flagFormat != "table" && flagFormat != "csv" && flagFormat != "json" {
- return fmt.Errorf("Unsupported -format %q. Must be one of 'table', 'csv', or 'json'.", flagFormat)
- }
- if len(flagCSVDelimiter) == 0 {
- return fmt.Errorf("-csv-delimiter cannot be empty.")
- }
- return nil
-}
-
-// Starts a syncQL shell against the specified database.
-// Runs in interactive or batch mode depending on stdin.
-func runSbShell(ctx *context.T, env *cmdline.Env, args []string) error {
- // TODO(ivanpi): Add 'use' statement, default to no database selected.
- if len(args) != 2 {
- return env.UsageErrorf("exactly two arguments expected")
- }
- // TODO(ivanpi): Derive blessing from context, perhaps?
- blessing, name := args[0], args[1]
- if err := validateFlags(); err != nil {
- return env.UsageErrorf("%v", err)
- }
-
- sbs := syncbase.NewService(*flagSbService)
- d, err := openDB(ctx, sbs, wire.Id{Blessing: blessing, Name: name}, flagCreateIfNotExists)
- if err != nil {
- return err
- }
-
- if flagMakeDemoCollections {
- if err := makeDemoDB(ctx, env.Stdout, d); err != nil {
- return err
- }
- }
-
- var input *reader.T
+// Runs the shell.
+// Takes commands as input, executes them, and prints out output.
+func runSbShell(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+ // Test if input is interactive and get reader.
// TODO(ivanpi): This is hacky, it would be better for lib/cmdline to support IsTerminal.
+ var input *reader.T
stdinFile, ok := env.Stdin.(*os.File)
isTerminal := ok && isatty.IsTerminal(stdinFile.Fd())
if isTerminal {
@@ -130,66 +43,30 @@
}
defer input.Close()
-stmtLoop:
+ // Read-exec loop.
for true {
- if q, err := input.GetQuery(); err != nil {
- if err == io.EOF {
- if isTerminal {
- // ctrl-d
- fmt.Println()
- }
- break
- } else {
- // ctrl-c
- break
+ // Read command.
+ query, err := input.GetQueryWithTerminator('\n')
+ if err != nil {
+ if err == io.EOF && isTerminal {
+ // ctrl-d
+ fmt.Println()
}
- } else {
- var err error
- tq := strings.Fields(q)
- if len(tq) > 0 {
- // TODO(caprita): Command handling should be
- // done similarly to the cmdline library, to
- // enable automatic help and error message
- // generation.
- switch strings.ToLower(tq[0]) {
- case "exit", "quit":
- break stmtLoop
- case "dump":
- err = dumpDB(ctx, env.Stdout, d)
- case "make-demo":
- // TODO(jkline): add an "Are you sure prompt" to give the user a 2nd chance.
- err = makeDemoDB(ctx, env.Stdout, d)
- case "select", "delete":
- err = queryExec(ctx, env.Stdout, d, q)
- case "destroy":
- if len(tq) == 3 {
- switch tq[1] {
- case "db":
- err = destroyDB(ctx, d, tq[2])
- case "collection":
- err = destroyCollection(ctx, d, tq[2])
- default:
- err = fmt.Errorf("unknown type: %q", tq[1])
- }
- } else if len(tq) == 4 {
- switch tq[1] {
- case "syncgroup":
- err = destroySyncgroup(ctx, d, wire.Id{Name: tq[2], Blessing: tq[3]})
- default:
- err = fmt.Errorf("unknown type: %q", tq[1])
- }
- } else {
- err = fmt.Errorf("destroy requires specifying type ('db', 'collection', or 'syncgroup') and name of object ('syncgroup' expects the syncgroup name and blessing)")
- }
- default:
- err = fmt.Errorf("unknown statement: '%s'; expected one of: 'select', 'make-demo', 'destroy', 'dump', 'exit', 'quit'", strings.ToLower(tq[0]))
+ break
+ }
+
+ // Exec command.
+ fields := strings.Fields(query)
+ if len(fields) > 0 {
+ switch cmdName := fields[0]; cmdName {
+ case "exit", "quit":
+ return nil
+ case "help":
+ if err := help(fields[1:]); err != nil {
+ return err
}
- }
- if err != nil {
- if isTerminal {
- fmt.Fprintln(env.Stderr, "Error:", err)
- } else {
- // If running non-interactively, errors stop execution.
+ default:
+ if err := runCommand(ctx, env, db, fields, isTerminal); err != nil {
return err
}
}
@@ -199,145 +76,49 @@
return nil
}
-func destroyDB(ctx *context.T, d syncbase.Database, encDbId string) error {
- // For extra safety, we still require the user to explicitly specify the
- // encoded database id instead of blindly destroying the current database.
- // TODO(ivanpi): Maybe switch to something more user-friendly, e.g. derive
- // blessing from context.
- if pubutil.EncodeId(d.Id()) != encDbId {
- return fmt.Errorf("can only destroy current database %v", d.Id())
- }
- return d.Destroy(ctx)
-}
-
-func destroyCollection(ctx *context.T, d syncbase.Database, encCxId string) error {
- // For extra safety, we still require the user to explicitly specify the
- // encoded collection id instead of assuming the blessing from context.
- // TODO(ivanpi): Maybe switch to something more user-friendly.
- cxId, err := pubutil.DecodeId(encCxId)
- if err != nil {
- return fmt.Errorf("failed to decode collection id %q: %v", encCxId, err)
- }
- c := d.CollectionForId(cxId)
- if exists, err := c.Exists(ctx); err != nil {
- return err
- } else if !exists {
- return fmt.Errorf("couldn't find collection %v", c.Id())
- }
- return c.Destroy(ctx)
-}
-
-func destroySyncgroup(ctx *context.T, d syncbase.Database, sgId wire.Id) error {
- return d.SyncgroupForId(sgId).Destroy(ctx)
-}
-
-func openDB(ctx *context.T, sbs syncbase.Service, id wire.Id, createIfNotExists bool) (syncbase.Database, error) {
- d := sbs.DatabaseForId(id, nil)
- if exists, err := d.Exists(ctx); err != nil {
- return nil, fmt.Errorf("failed checking for db %q: %v", d.FullName(), err)
- } else if !exists {
- if !createIfNotExists {
- return nil, fmt.Errorf("db %q does not exist", d.FullName())
- }
- if err := d.Create(ctx, nil); err != nil {
- return nil, err
- }
- }
- return d, nil
-}
-
-func mergeErrors(errs []error) error {
- if len(errs) == 0 {
- return nil
- }
- err := errs[0]
- for _, e := range errs[1:] {
- err = fmt.Errorf("%v\n%v", err, e)
- }
- return err
-}
-
-func dumpCollections(ctx *context.T, w io.Writer, d syncbase.Database) error {
- collections, err := d.ListCollections(ctx)
- if err != nil {
- return fmt.Errorf("failed listing collections: %v", err)
- }
- var errs []error
- for _, collection := range collections {
- fmt.Fprintf(w, "collection: %v\n", collection)
- // TODO(ivanpi): Queries currently support only the default user blessing.
- if err := queryExec(ctx, w, d, fmt.Sprintf("select k, v from %s", collection.Name)); err != nil {
- errs = append(errs, fmt.Errorf("> %v: %v", collection, err))
- }
- }
- if len(errs) > 0 {
- return fmt.Errorf("failed dumping %d of %d collections:\n%v", len(errs), len(collections), mergeErrors(errs))
- }
- return nil
-}
-
-func dumpSyncgroups(ctx *context.T, w io.Writer, d syncbase.Database) error {
- sgIds, err := d.ListSyncgroups(ctx)
- if err != nil {
- return fmt.Errorf("failed listing syncgroups: %v", err)
- }
- var errs []error
- for _, sgId := range sgIds {
- fmt.Fprintf(w, "syncgroup: %+v\n", sgId)
- if spec, version, err := d.SyncgroupForId(sgId).GetSpec(ctx); err != nil {
- errs = append(errs, err)
+func runCommand(ctx *context.T, env *cmdline.Env, db syncbase.Database,
+ fields []string, isTerminal bool) error {
+ commands.SetCtx(ctx)
+ commands.SetDB(db)
+ if err := cmdline.ParseAndRun(cmdSb, env, fields); err != nil {
+ if isTerminal {
+ fmt.Fprintln(env.Stderr, "Error:", err)
} else {
- fmt.Fprintf(w, "%+v (version: \"%s\")\n", spec, version)
+ // If running non-interactively, errors halt execution.
+ return err
}
}
- if len(errs) > 0 {
- return fmt.Errorf("failed dumping %d of %d syncgroups:\n%v", len(errs), len(sgIds), mergeErrors(errs))
- }
return nil
}
-func dumpDB(ctx *context.T, w io.Writer, d syncbase.Database) error {
- var errors []error
- if err := dumpCollections(ctx, w, d); err != nil {
- errors = append(errors, fmt.Errorf("failed dumping collections: %v", err))
- }
- if err := dumpSyncgroups(ctx, w, d); err != nil {
- errors = append(errors, fmt.Errorf("failed dumping syncgroups: %v", err))
- }
- return mergeErrors(errors)
-}
+func help(args []string) error {
+ switch len(args) {
+ case 0:
+ fmt.Println("Commands:")
+ for _, cmd := range commands.Commands {
+ fmt.Printf("\t%s\t%s\n", cmd.Name, cmd.Short)
+ }
+ fmt.Println("\thelp\tPrint a list of all commands")
+ fmt.Println("\texit\tEnd session (aliased to quit)")
+ return nil
-func makeDemoDB(ctx *context.T, w io.Writer, d syncbase.Database) error {
- if err := demodb.PopulateDemoDB(ctx, d); err == nil {
- fmt.Fprintln(w, "Demo collections created and populated.")
- } else {
- return fmt.Errorf("failed making demo collections: %v", err)
- }
- return nil
-}
+ case 1:
+ cmdName := args[0]
+ if cmdName == "help" {
+ fmt.Println("Print a list of all commands, or useful information about a single command.")
+ fmt.Println()
+ fmt.Println("Usage:")
+ fmt.Println("\thelp [command_name]")
+ } else {
+ cmd, err := commands.GetCommand(cmdName)
+ if err != nil {
+ return err
+ }
+ commands.PrintUsage(cmd)
+ }
+ return nil
-// Split an error message into an offset and the remaining (i.e., rhs of offset) message.
-// The convention for syncql is "<module><optional-rpc>[offset]<remaining-message>".
-func splitError(err error) (int64, string) {
- errMsg := err.Error()
- idx1 := strings.Index(errMsg, "[")
- idx2 := strings.Index(errMsg, "]")
- if idx1 == -1 || idx2 == -1 {
- return 0, errMsg
+ default:
+ return fmt.Errorf("too many arguments")
}
- offsetString := errMsg[idx1+1 : idx2]
- offset, err := strconv.ParseInt(offsetString, 10, 64)
- if err != nil {
- return 0, errMsg
- }
- return offset, errMsg[idx2+1:]
-}
-
-func queryExec(ctx *context.T, w io.Writer, d syncbase.Database, q string) error {
- columnNames, rs, err := d.Exec(ctx, q)
- if err != nil {
- off, msg := splitError(err)
- return fmt.Errorf("\n%s\n%s^\n%d: %s", q, strings.Repeat(" ", int(off)), off+1, msg)
- }
- return flagFormat.NewWriter(w).Write(columnNames, rs)
}