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)
 }