Add viper (for env variable) support to CLI (#2251)

* Add viper (for env variable) support to CLI

* Move the "featurebase cli" sub-command to its own "fbsql" command

I don't know if this is the final name, but putting it here as a
placeholder for now.

* Handle single `--command` flags.

This also adds a printer interface so we can opt NOT to print setup
information in non-interactive mode.

* Add support for multiple `--command` flags in the same call

* Add support for multiple `--file` flags

* Move members related to Config into a separate struct

* Make sure non-interactive mode can connect to a database

* comment fix

* support control-C on readline

* Prevent connection message from printing in non-interactive mode

* Return errors (instead of printing them) in non-interactive mode
This commit is contained in:
Travis Turner 2023-02-16 20:41:39 -06:00 committed by GitHub
parent 4172976e6a
commit 393721c0ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 414 additions and 135 deletions

View file

@ -144,7 +144,7 @@ authclustertests: vendor
CLUSTERTESTS_FB_ARGS=$(AUTH_ARGS) $(DOCKER_COMPOSE) -f internal/clustertests/docker-compose.yml down
# Install FeatureBase and IDK
install: install-featurebase install-idk
install: install-featurebase install-idk install-fbsql
install-featurebase:
$(GO) install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) ./cmd/featurebase
@ -152,6 +152,9 @@ install-featurebase:
install-idk:
$(MAKE) -C ./idk install
install-fbsql:
$(GO) install ./cmd/fbsql
# Build the lattice assets
build-lattice:
docker build -t lattice:build ./lattice

View file

@ -14,8 +14,8 @@ import (
"github.com/chzyer/readline"
featurebase "github.com/featurebasedb/featurebase/v3"
"github.com/featurebasedb/featurebase/v3/cli/fbcloud"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/pkg/errors"
)
const (
@ -38,23 +38,19 @@ Type "\q" to quit.
`, featurebase.Version)
)
type CLICommand struct {
Host string `json:"host"`
Port string `json:"port"`
HistoryPath string `json:"history-path"`
// Ensure type implments interfaces.
var _ printer = (*Command)(nil)
// Cloud Auth
ClientID string `json:"client-id"`
Region string `json:"region"`
Email string `json:"email"`
Password string `json:"password"`
type Command struct {
host string
port string
splitter *splitter
buffer *buffer
workingDir *workingDir
OrganizationID string `json:"org-id"`
Database string `json:"db"`
organizationID string
database string
databaseID string
databaseName string
@ -69,17 +65,47 @@ type CLICommand struct {
output io.Writer `json:"-"`
writeOptions *writeOptions
Config *Config `json:"config"`
historyPath string
// Commands contains optional commands provided via one or more `-c` (or
// `--command`) flags. If this is non-empty, the cli will run in
// non-interactive mode; i.e. it will quit after the command is complete.
Commands []string `json:"commands"`
// Files contains optional files provided via one or more `-f` (or `--file`)
// flags. If this is non-empty, the cli will run in non-interactive mode;
// i.e. it will quit after the command is complete.
Files []string `json:"files"`
// nonInteractiveMode is set to true when fbsql is running in
// non-ineracative mode. And example of this is when the user has provided a
// `-c` flag in the command line.
nonInteractiveMode bool
// quit gets closed when Run should stop listening for input.
quit chan struct{}
}
func NewCLICommand(logdest logger.Logger) *CLICommand {
return &CLICommand{
Host: defaultHost,
HistoryPath: "",
func NewCommand(logdest logger.Logger) *Command {
return &Command{
Config: &Config{
Host: defaultHost,
Port: "",
OrganizationID: "",
Database: "",
OrganizationID: "",
Database: "",
CloudAuth: CloudAuthConfig{
ClientID: "",
Region: "",
Email: "",
Password: "",
},
HistoryPath: "",
},
splitter: newSplitter(),
buffer: newBuffer(),
@ -96,9 +122,40 @@ func NewCLICommand(logdest logger.Logger) *CLICommand {
}
}
// Run is the main entry-point to the CLI. Currently it handles the interaction
// with a user, as opposed to calling `featurebase cli` in a script.
func (cmd *CLICommand) Run(ctx context.Context) error {
// Run is the main entry-point to the CLI.
func (cmd *Command) Run(ctx context.Context) error {
cmd.setupConfig()
// Check to see if Command needs to run in non-interactive mode.
if len(cmd.Commands) > 0 || len(cmd.Files) > 0 {
cmd.nonInteractiveMode = true
if err := cmd.setupClient(); err != nil {
return errors.Wrap(err, "setting up client")
}
if err := cmd.connectToDatabase(cmd.database); err != nil {
cmd.Errorf(errors.Wrap(err, "connecting to database").Error() + "\n")
}
// Run Commands.
for _, line := range cmd.Commands {
if err := cmd.handleLine(line); err != nil {
cmd.Errorf(err.Error())
return nil
}
}
// Run Files.
for _, fname := range cmd.Files {
if _, err := executeFile(cmd, fname); err != nil {
cmd.Errorf(err.Error())
return nil
}
}
return nil
}
// Print the splash message.
cmd.Printf(splash)
cmd.setupHistory()
@ -106,13 +163,13 @@ func (cmd *CLICommand) Run(ctx context.Context) error {
return errors.Wrap(err, "setting up client")
}
cmd.printConnInfo()
if err := cmd.connectToDatabase(cmd.Database); err != nil {
if err := cmd.connectToDatabase(cmd.database); err != nil {
cmd.Errorf(errors.Wrap(err, "connecting to database").Error() + "\n")
}
rl, err := readline.NewEx(&readline.Config{
Prompt: promptBegin,
HistoryFile: cmd.HistoryPath,
HistoryFile: cmd.historyPath,
HistoryLimit: 100000,
DisableAutoSaveHistory: true,
@ -138,7 +195,11 @@ func (cmd *CLICommand) Run(ctx context.Context) error {
// Read user provided input.
line, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt {
inMidCommand = false
cmd.buffer.reset()
continue
} else if err != nil {
return errors.Wrap(err, "reading line")
}
@ -236,11 +297,27 @@ func (cmd *CLICommand) Run(ctx context.Context) error {
// close is called upon quitting. It should close any remaining open file
// handles used by the CLICommand.
func (cmd *CLICommand) close() error {
func (cmd *Command) close() error {
return cmd.closeOutput()
}
func (cmd *CLICommand) executeAndWriteQuery(qry query) error {
// setupConfig sets up private struct members based on values provided via the
// configuration flags.
func (cmd *Command) setupConfig() {
if cmd.Config == nil {
return
}
cmd.host = cmd.Config.Host
cmd.port = cmd.Config.Port
cmd.organizationID = cmd.Config.OrganizationID
cmd.database = cmd.Config.Database
cmd.historyPath = cmd.Config.HistoryPath
}
func (cmd *Command) executeAndWriteQuery(qry query) error {
queryResponse, err := cmd.executeQuery(qry)
if err != nil {
return errors.Wrap(err, "making query")
@ -253,32 +330,63 @@ func (cmd *CLICommand) executeAndWriteQuery(qry query) error {
return nil
}
func (cmd *CLICommand) executeQuery(qry query) (*featurebase.WireQueryResponse, error) {
return cmd.Queryer.Query(cmd.OrganizationID, cmd.databaseID, qry.Reader())
func (cmd *Command) executeQuery(qry query) (*featurebase.WireQueryResponse, error) {
wqr, err := cmd.Queryer.Query(cmd.organizationID, cmd.databaseID, qry.Reader())
if err != nil {
return nil, errors.Wrap(err, "executing query")
}
// If we're running in non-interactive mode, we need to check the error that
// comes back in the WireQueryResponse. If there's an error, we want to
// return it now (rather than just printing it later) so that we immediately
// stop any further execution of commands.
if cmd.nonInteractiveMode && wqr.Error != "" {
return nil, errors.Errorf(wqr.Error)
}
return wqr, nil
}
// printer is an interface which encapsulates the methods used to print output
// to the various io.Writers.
type printer interface {
Printf(format string, a ...any)
Outputf(format string, a ...any)
Errorf(format string, a ...any)
}
type nopPrinter struct{}
func newNopPrinter() *nopPrinter {
return &nopPrinter{}
}
func (n *nopPrinter) Printf(format string, a ...any) {}
func (n *nopPrinter) Outputf(format string, a ...any) {}
func (n *nopPrinter) Errorf(format string, a ...any) {}
// Printf is a helper method which sends the given payload to stdout.
func (cmd *CLICommand) Printf(format string, a ...any) {
func (cmd *Command) Printf(format string, a ...any) {
out := fmt.Sprintf(format, a...)
cmd.Stdout.Write([]byte(out))
}
// Outputf is a helper method which sends the given payload to output.
func (cmd *CLICommand) Outputf(format string, a ...any) {
func (cmd *Command) Outputf(format string, a ...any) {
out := fmt.Sprintf(format, a...)
cmd.output.Write([]byte(out))
}
// Errorf is a helper method which sends the given payload to stderr.
func (cmd *CLICommand) Errorf(format string, a ...any) {
func (cmd *Command) Errorf(format string, a ...any) {
out := fmt.Sprintf(format, a...)
cmd.Stderr.Write([]byte(out))
}
func (cmd *CLICommand) setupHistory() {
func (cmd *Command) setupHistory() {
// If HistoryPath has already been configured (i.e. with a command flag),
// don't bother setting up the default in the home directory.
if cmd.HistoryPath != "" {
if cmd.historyPath != "" {
return
}
@ -294,20 +402,25 @@ func (cmd *CLICommand) setupHistory() {
historyPath = filepath.Join(historyDir, "cli_history")
}
}
cmd.HistoryPath = historyPath
cmd.historyPath = historyPath
}
// printConnInfo displays the currently set host.
// TODO(tlt): extend this to be the output of the /conninfo meta-command.
func (cmd *CLICommand) printConnInfo() {
cmd.Printf("Host: %s\n", hostPort(cmd.Host, cmd.Port))
func (cmd *Command) printConnInfo() {
cmd.Printf("Host: %s\n", hostPort(cmd.host, cmd.port))
}
func (cmd *CLICommand) connectToDatabase(dbName string) error {
func (cmd *Command) connectToDatabase(dbName string) error {
var p printer = cmd
if cmd.nonInteractiveMode {
p = newNopPrinter()
}
if dbName == "" {
cmd.databaseID = ""
cmd.databaseName = ""
cmd.Printf(cmd.connectionMessage())
p.Printf(cmd.connectionMessage())
return nil
}
@ -327,40 +440,45 @@ func (cmd *CLICommand) connectToDatabase(dbName string) error {
if db[1] == dbName {
cmd.databaseName = dbName
cmd.databaseID = db[0].(string)
cmd.Printf(cmd.connectionMessage())
p.Printf(cmd.connectionMessage())
return nil
}
}
return errors.Errorf("invalid database: %s", dbName)
}
func (cmd *CLICommand) orgMessage() string {
if cmd.OrganizationID == "" {
func (cmd *Command) orgMessage() string {
if cmd.organizationID == "" {
return "You have not set an organization.\n"
}
return fmt.Sprintf("You have set organization \"%s\".\n", cmd.OrganizationID)
return fmt.Sprintf("You have set organization \"%s\".\n", cmd.organizationID)
}
func (cmd *CLICommand) connectionMessage() string {
func (cmd *Command) connectionMessage() string {
if cmd.databaseName == "" {
return "You are not connected to a database.\n"
}
return fmt.Sprintf("You are now connected to database \"%s\" (%s) as user \"???\".\n", cmd.databaseName, cmd.databaseID)
}
func (cmd *CLICommand) setupClient() error {
func (cmd *Command) setupClient() error {
// If the Queryer has already been set (in tests for example), don't bother
// trying to detect it.
if cmd.Queryer != nil {
return nil
}
if strings.TrimSpace(cmd.Host) == "" {
var p printer = cmd
if cmd.nonInteractiveMode {
p = newNopPrinter()
}
if strings.TrimSpace(cmd.host) == "" {
return errors.Errorf("no host provided\n")
}
if !strings.HasPrefix(cmd.Host, "http") {
cmd.Host = "http://" + cmd.Host
if !strings.HasPrefix(cmd.host, "http") {
cmd.host = "http://" + cmd.host
}
typ, err := cmd.detectFBType()
@ -370,39 +488,40 @@ func (cmd *CLICommand) setupClient() error {
switch typ {
case featurebaseTypeOnPremClassic:
cmd.Printf("Detected on-prem, classic deployment.\n")
p.Printf("Detected on-prem, classic deployment.\n")
cmd.Queryer = &standardQueryer{
Host: cmd.Host,
Port: cmd.Port,
Host: cmd.host,
Port: cmd.port,
}
case featurebaseTypeOnPremServerless:
cmd.Printf("Detected on-prem, serverless deployment.\n")
p.Printf("Detected on-prem, serverless deployment.\n")
cmd.Queryer = &serverlessQueryer{
Host: cmd.Host,
Port: cmd.Port,
Host: cmd.host,
Port: cmd.port,
}
case featurebaseTypeCloud:
cmd.Printf("Detected cloud deployment.\n")
cmd.Queryer = &fbcloud.Queryer{
Host: hostPort(cmd.Host, cmd.Port),
p.Printf("Detected cloud deployment.\n")
ClientID: cmd.ClientID,
Region: cmd.Region,
Email: cmd.Email,
Password: cmd.Password,
cmd.Queryer = &fbcloud.Queryer{
Host: hostPort(cmd.host, cmd.port),
ClientID: cmd.Config.CloudAuth.ClientID,
Region: cmd.Config.CloudAuth.Region,
Email: cmd.Config.CloudAuth.Email,
Password: cmd.Config.CloudAuth.Password,
}
case featurebaseTypeUnknown:
cmd.Printf("Could not detect deployment\n")
p.Printf("Could not detect deployment\n")
// cmd.Queryer = &nopQueryer{}
// Instead of using a no-op queryer when the type can't be detected, we
// default to using a cloud queryer.
cmd.Queryer = &fbcloud.Queryer{
Host: hostPort(cmd.Host, cmd.Port),
Host: hostPort(cmd.host, cmd.port),
ClientID: cmd.ClientID,
Region: cmd.Region,
Email: cmd.Email,
Password: cmd.Password,
ClientID: cmd.Config.CloudAuth.ClientID,
Region: cmd.Config.CloudAuth.Region,
Email: cmd.Config.CloudAuth.Email,
Password: cmd.Config.CloudAuth.Password,
}
default:
return errors.Errorf("unknown type: %s", typ)
@ -428,7 +547,7 @@ func hostPort(host, port string) string {
// detectFBType determines if we're talking to standalone FeatureBase
// or FeatureBase Cloud
func (cmd *CLICommand) detectFBType() (featurebaseType, error) {
func (cmd *Command) detectFBType() (featurebaseType, error) {
type trial struct {
port string
health string
@ -440,23 +559,23 @@ func (cmd *CLICommand) detectFBType() (featurebaseType, error) {
trials := []trial{}
var clientTimeout time.Duration
if cmd.Port != "" {
if cmd.port != "" {
clientTimeout = 100 * time.Millisecond
trials = append(trials,
// on-prem, serverless
trial{
port: cmd.Port,
port: cmd.port,
health: "/queryer/health",
typ: featurebaseTypeOnPremServerless,
},
// on-prem, classic
trial{
port: cmd.Port,
port: cmd.port,
health: "/status",
typ: featurebaseTypeOnPremClassic,
},
)
} else if strings.HasPrefix(cmd.Host, "https") {
} else if strings.HasPrefix(cmd.host, "https") {
// https suggesting we might be connecting to a cloud host
clientTimeout = 1 * time.Second
trials = append(trials,
@ -490,11 +609,11 @@ func (cmd *CLICommand) detectFBType() (featurebaseType, error) {
Timeout: clientTimeout,
}
for _, trial := range trials {
url := hostPort(cmd.Host, trial.port) + trial.health
url := hostPort(cmd.host, trial.port) + trial.health
if resp, err := client.Get(url); err != nil {
continue
} else if resp.StatusCode/100 == 2 {
cmd.Port = trial.port
cmd.port = trial.port
return trial.typ, nil
}
}
@ -502,7 +621,7 @@ func (cmd *CLICommand) detectFBType() (featurebaseType, error) {
return featurebaseTypeUnknown, nil
}
func (cmd *CLICommand) closeOutput() error {
func (cmd *Command) closeOutput() error {
if cmd.output == nil {
return nil
}
@ -513,3 +632,62 @@ func (cmd *CLICommand) closeOutput() error {
return nil
}
func (cmd *Command) handleLine(line string) error {
// For single-line command handling, we handle either a meta-command, or
// query parts, but not both. The logic is that any line which begins with
// "\" will be handled as a meta-command, otherwise it will be handled as a
// query.
if len(line) == 0 {
return nil
} else if line[0] == byte('\\') {
return cmd.handleLineAsMetaCommand(line)
} else {
return cmd.handleLineAsQueryParts(line)
}
}
func (cmd *Command) handleLineAsMetaCommand(line string) error {
_, mcs, err := cmd.splitter.split(line)
if err != nil {
return errors.Wrapf(err, "splitting line")
}
for i := range mcs {
_, err := mcs[i].execute(cmd)
if err != nil {
return errors.Wrap(err, "executing meta command")
}
}
return nil
}
func (cmd *Command) handleLineAsQueryParts(line string) error {
qps, mcs, err := cmd.splitter.split(line)
if err != nil {
return errors.Wrapf(err, "splitting line")
} else if len(mcs) > 0 {
return errors.Errorf("--command does not support meta-commands")
}
// Add a termintor part to the end of []queryPart. We do this because the
// command is coming in from the --command flag, it may not end with a
// semi-colon, but we still want to execute it.
if len(qps) > 0 {
if _, ok := qps[len(qps)-1].(*partTerminator); !ok {
qps = append(qps, newPartTerminator())
}
}
for i := range qps {
if qry, err := cmd.buffer.addPart(qps[i]); err != nil {
return errors.Wrap(err, "adding part to buffer")
} else if qry != nil {
if err := cmd.executeAndWriteQuery(qry); err != nil {
return errors.Wrap(err, "executing query")
}
}
}
return nil
}

View file

@ -21,7 +21,7 @@ func TestCLI(t *testing.T) {
capture := newCapture(t)
cli := cli.NewCLICommand(logger.StderrLogger)
cli := cli.NewCommand(logger.StderrLogger)
cli.Stdin = capture
cli.Stdout = capture
cli.Queryer = capture

22
cli/config.go Normal file
View file

@ -0,0 +1,22 @@
package cli
// Config represents the configuration for the command.
type Config struct {
Host string `json:"host"`
Port string `json:"port"`
OrganizationID string `json:"org-id"`
Database string `json:"db"`
// CloudAuth
CloudAuth CloudAuthConfig `json:"cloud-auth"`
HistoryPath string `json:"history-path"`
}
type CloudAuthConfig struct {
ClientID string `json:"client-id"`
Region string `json:"region"`
Email string `json:"email"`
Password string `json:"password"`
}

View file

@ -29,7 +29,7 @@ const (
// metaCommand is the interface for any type responding to a "\" meta-command.
type metaCommand interface {
execute(cmd *CLICommand) (action, error)
execute(cmd *Command) (action, error)
}
// Ensure type implements interface.
@ -71,7 +71,7 @@ func newMetaBang(args []string) *metaBang {
}
}
func (m *metaBang) execute(cmd *CLICommand) (action, error) {
func (m *metaBang) execute(cmd *Command) (action, error) {
if len(m.args) == 0 {
return actionNone, errors.Errorf("meta command '!' requires at least one argument")
}
@ -95,7 +95,7 @@ func newMetaBorder(args []string) *metaBorder {
}
}
func (m *metaBorder) execute(cmd *CLICommand) (action, error) {
func (m *metaBorder) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
// pass
@ -129,7 +129,7 @@ func newMetaChangeDirectory(args []string) *metaChangeDirectory {
}
}
func (m *metaChangeDirectory) execute(cmd *CLICommand) (action, error) {
func (m *metaChangeDirectory) execute(cmd *Command) (action, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'cd' requires exactly one argument")
}
@ -150,7 +150,7 @@ func newMetaConnect(args []string) *metaConnect {
}
}
func (m *metaConnect) execute(cmd *CLICommand) (action, error) {
func (m *metaConnect) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.Printf(cmd.connectionMessage())
@ -177,7 +177,7 @@ func newMetaEcho(args []string) *metaEcho {
}
}
func (m *metaEcho) execute(cmd *CLICommand) (action, error) {
func (m *metaEcho) execute(cmd *Command) (action, error) {
return echo(m.args, cmd.Stdout)
}
@ -218,7 +218,7 @@ func newMetaExpanded(args []string) *metaExpanded {
}
}
func (m *metaExpanded) execute(cmd *CLICommand) (action, error) {
func (m *metaExpanded) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.expanded = !cmd.writeOptions.expanded
@ -257,7 +257,7 @@ func newMetaFile(args []string) *metaFile {
}
}
func (m *metaFile) execute(cmd *CLICommand) (action, error) {
func (m *metaFile) execute(cmd *Command) (action, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'file' requires exactly one argument")
}
@ -292,7 +292,7 @@ func newMetaHelp(args []string) *metaHelp {
}
}
func (m *metaHelp) execute(cmd *CLICommand) (action, error) {
func (m *metaHelp) execute(cmd *Command) (action, error) {
helpText := `General
\q[uit] quit psql
\watch [SEC] execute query every SEC seconds
@ -323,7 +323,7 @@ Formatting
\pset [NAME [VALUE]] set table output option
(border|expanded|tuples_only)
\t [on|off] show only rows
\x [on|off|auto] toggle expanded output
\x [on|off] toggle expanded output
Connection
\c[onnect] [DBNAME] connect to new database
@ -352,14 +352,18 @@ func newMetaInclude(args []string) *metaInclude {
}
}
func (m *metaInclude) execute(cmd *CLICommand) (action, error) {
func (m *metaInclude) execute(cmd *Command) (action, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'include' requires exactly one argument")
}
file, err := os.Open(m.args[0])
return executeFile(cmd, m.args[0])
}
func executeFile(cmd *Command, fileName string) (action, error) {
file, err := os.Open(fileName)
if err != nil {
return actionNone, errors.Wrapf(err, "opening file: %s", m.args[0])
return actionNone, errors.Wrapf(err, "opening file: %s", fileName)
}
defer file.Close()
@ -376,7 +380,7 @@ func (m *metaInclude) execute(cmd *CLICommand) (action, error) {
if err != nil {
return actionNone, errors.Wrapf(err, "splitting lines")
} else if len(mcs) > 0 {
return actionNone, errors.Errorf("include does not support meta-commands")
return actionNone, errors.Errorf("include does not support meta-commands")
}
for i := range qps {
@ -390,7 +394,7 @@ func (m *metaInclude) execute(cmd *CLICommand) (action, error) {
}
}
if err := sc.Err(); err != nil {
return actionNone, errors.Wrapf(err, "scanning file: %s", m.args[0])
return actionNone, errors.Wrapf(err, "scanning file: %s", fileName)
}
return actionReset, nil
@ -405,7 +409,7 @@ func newMetaListDatabases() *metaListDatabases {
return &metaListDatabases{}
}
func (m *metaListDatabases) execute(cmd *CLICommand) (action, error) {
func (m *metaListDatabases) execute(cmd *Command) (action, error) {
qry := []queryPart{
newPartRaw("SHOW DATABASES"),
}
@ -426,7 +430,7 @@ func newMetaListTables() *metaListTables {
return &metaListTables{}
}
func (m *metaListTables) execute(cmd *CLICommand) (action, error) {
func (m *metaListTables) execute(cmd *Command) (action, error) {
qry := []queryPart{
newPartRaw("SHOW TABLES"),
}
@ -451,14 +455,14 @@ func newMetaOrg(args []string) *metaOrg {
}
}
func (m *metaOrg) execute(cmd *CLICommand) (action, error) {
func (m *metaOrg) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.Printf(cmd.orgMessage())
return actionNone, nil
case 1:
cmd.OrganizationID = m.args[0]
cmd.organizationID = m.args[0]
cmd.Printf(cmd.orgMessage())
return actionNone, nil
@ -480,7 +484,7 @@ func newMetaOutput(args []string) *metaOutput {
}
}
func (m *metaOutput) execute(cmd *CLICommand) (action, error) {
func (m *metaOutput) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
// Close cmd.output (if closable).
@ -522,7 +526,7 @@ func newMetaPrint() *metaPrint {
return &metaPrint{}
}
func (m *metaPrint) execute(cmd *CLICommand) (action, error) {
func (m *metaPrint) execute(cmd *Command) (action, error) {
cmd.Printf(cmd.buffer.print() + "\n")
return actionNone, nil
}
@ -540,7 +544,7 @@ func newMetaPSet(args []string) *metaPSet {
}
}
func (m *metaPSet) print(cmd *CLICommand) {
func (m *metaPSet) print(cmd *Command) {
onOff := func(b bool) string {
if b {
return "on"
@ -561,7 +565,7 @@ tuples_only %s
}
func (m *metaPSet) execute(cmd *CLICommand) (action, error) {
func (m *metaPSet) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
m.print(cmd)
@ -598,7 +602,7 @@ func newMetaQEcho(args []string) *metaQEcho {
}
}
func (m *metaQEcho) execute(cmd *CLICommand) (action, error) {
func (m *metaQEcho) execute(cmd *Command) (action, error) {
return echo(m.args, cmd.output)
}
@ -611,7 +615,7 @@ func newMetaQuit() *metaQuit {
return &metaQuit{}
}
func (m *metaQuit) execute(cmd *CLICommand) (action, error) {
func (m *metaQuit) execute(cmd *Command) (action, error) {
return actionQuit, nil
}
@ -624,7 +628,7 @@ func newMetaReset() *metaReset {
return &metaReset{}
}
func (m *metaReset) execute(cmd *CLICommand) (action, error) {
func (m *metaReset) execute(cmd *Command) (action, error) {
cmd.Printf(cmd.buffer.reset())
return actionReset, nil
}
@ -642,7 +646,7 @@ func newMetaSet(args []string) *metaSet {
}
}
func (m *metaSet) execute(cmd *CLICommand) (action, error) {
func (m *metaSet) execute(cmd *Command) (action, error) {
// TODO: set the variable (or clear it, etc)
return actionNone, nil
}
@ -660,7 +664,7 @@ func newMetaTiming(args []string) *metaTiming {
}
}
func (m *metaTiming) execute(cmd *CLICommand) (action, error) {
func (m *metaTiming) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.timing = !cmd.writeOptions.timing
@ -699,7 +703,7 @@ func newMetaTuplesOnly(args []string) *metaTuplesOnly {
}
}
func (m *metaTuplesOnly) execute(cmd *CLICommand) (action, error) {
func (m *metaTuplesOnly) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.tuplesOnly = !cmd.writeOptions.tuplesOnly
@ -738,7 +742,7 @@ func newMetaWarn(args []string) *metaWarn {
}
}
func (m *metaWarn) execute(cmd *CLICommand) (action, error) {
func (m *metaWarn) execute(cmd *Command) (action, error) {
return echo(m.args, cmd.Stderr)
}
@ -755,7 +759,7 @@ func newMetaWatch(args []string) *metaWatch {
}
}
func (m *metaWatch) execute(cmd *CLICommand) (action, error) {
func (m *metaWatch) execute(cmd *Command) (action, error) {
period := 2 * time.Second
qry := cmd.buffer.lastQuery
@ -809,7 +813,7 @@ func newMetaWrite(args []string) *metaWrite {
}
}
func (m *metaWrite) execute(cmd *CLICommand) (action, error) {
func (m *metaWrite) execute(cmd *Command) (action, error) {
switch len(m.args) {
case 0:
cmd.Errorf(`\w: missing required argument` + "\n")

View file

@ -23,7 +23,7 @@ func defaultWriteOptions() *writeOptions {
return &writeOptions{
border: 1,
expanded: false,
timing: true,
timing: false,
tuplesOnly: false,
}
}

View file

@ -50,6 +50,25 @@ func TestWriter(t *testing.T) {
" 3 | Cindy | 28 ",
"",
),
expOut: "",
expErr: "",
},
{
// timing on
format: &writeOptions{
border: 1,
expanded: false,
timing: true,
tuplesOnly: false,
},
expQOut: stringOfLines(
" _id | name | age ",
"-----+-------+-----",
" 1 | Amy | 44 ",
" 2 | Bob | 32 ",
" 3 | Cindy | 28 ",
"",
),
expOut: "Execution time: 0μs\n",
expErr: "",
},

View file

@ -2,34 +2,34 @@
package cmd
import (
"io"
"github.com/featurebasedb/featurebase/v3/cli"
"github.com/featurebasedb/featurebase/v3/ctl"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cliCmd *cli.CLICommand
var cliCmd *cli.Command
// newCLICommand runs the FeatureBase CLI subcommand for ingesting bulk data.
func newCLICommand(logdest logger.Logger) *cobra.Command {
cliCmd = cli.NewCLICommand(logdest)
// NewCLICommand runs the FeatureBase CLI subcommand.
func NewCLICommand(stderr io.Writer) *cobra.Command {
logdest := logger.NewStandardLogger(stderr)
cliCmd = cli.NewCommand(logdest)
cobraCmd := &cobra.Command{
Use: "cli",
Use: "fbsql",
Short: "Query FeatureBase with SQL from the command line",
Long: ``,
RunE: usageErrorWrapper(cliCmd),
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
v := viper.New()
return setAllConfig(v, cmd.Flags(), "FBSQL")
},
SilenceErrors: true,
}
flags := cobraCmd.Flags()
flags.StringVarP(&cliCmd.Host, "host", "", cliCmd.Host, "hostname of FeatureBase.")
flags.StringVarP(&cliCmd.Port, "port", "", cliCmd.Port, "port of FeatureBase.")
flags.StringVar(&cliCmd.HistoryPath, "history-path", cliCmd.HistoryPath, "path for history files.")
flags.StringVar(&cliCmd.OrganizationID, "org-id", cliCmd.OrganizationID, "OrganizationID.")
flags.StringVar(&cliCmd.Database, "db", cliCmd.Database, "Name of the database to connect to.")
flags.StringVar(&cliCmd.ClientID, "client-id", cliCmd.ClientID, "Cognito Client ID for FeatureBase Cloud access.")
flags.StringVar(&cliCmd.Region, "region", cliCmd.Region, "Cloud region for FeatureBase Cloud access (e.g. us-east-2).")
flags.StringVar(&cliCmd.Email, "email", cliCmd.Email, "Email address for FeatureBase Cloud access.")
flags.StringVar(&cliCmd.Password, "password", cliCmd.Password, "Password for FeatureBase Cloud access.")
// Attach flags to the command.
ctl.BuildCLIFlags(cobraCmd, cliCmd)
return cobraCmd
}

13
cmd/fbsql/main.go Normal file
View file

@ -0,0 +1,13 @@
// Copyright 2021 Molecula Corp. All rights reserved.
package main
import (
"os"
"github.com/featurebasedb/featurebase/v3/cmd"
)
func main() {
command := cmd.NewCLICommand(os.Stderr)
command.Execute()
}

View file

@ -63,11 +63,12 @@ at https://docs.featurebase.com/.
` + pilosa.VersionInfo(true) + "\n",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
v := viper.New()
if cmd.Use == "dax" {
switch cmd.Use {
case "dax":
v.Set("future.rename", true) // always use FEATUREBASE env for dax
}
err := setAllConfig(v, cmd.Flags())
if err != nil {
if err := setAllConfig(v, cmd.Flags(), ""); err != nil {
return err
}
@ -104,7 +105,6 @@ at https://docs.featurebase.com/.
rc.AddCommand(newServeCmd(stderr))
rc.AddCommand(newHolderCmd(stderr))
rc.AddCommand(newKeygenCommand(logdest))
rc.AddCommand(newCLICommand(logdest))
rc.AddCommand(newDAXCommand(stderr))
rc.AddCommand(newDataframeCsvLoaderCommand(logdest))
rc.AddCommand(newPreSortCommand(logdest))
@ -124,17 +124,18 @@ at https://docs.featurebase.com/.
// setAllConfig looks for environment variables which are capitalized versions
// of the flag names with dashes replaced by underscores, and prefixed with
// envPrefix plus an underscore.
func setAllConfig(v *viper.Viper, flags *pflag.FlagSet) error { // nolint: unparam
func setAllConfig(v *viper.Viper, flags *pflag.FlagSet, envPrefix string) error { // nolint: unparam
// add cmd line flag def to viper
err := v.BindPFlags(flags)
if err != nil {
return err
}
envPrefix := "PILOSA"
rename := v.GetBool("future.rename")
if rename {
envPrefix = "FEATUREBASE"
if envPrefix == "" {
envPrefix = "PILOSA"
if v.GetBool("future.rename") {
envPrefix = "FEATUREBASE"
}
}
// add env to viper

39
ctl/cli.go Normal file
View file

@ -0,0 +1,39 @@
package ctl
import (
"github.com/featurebasedb/featurebase/v3/cli"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// BuildCLIFlags attaches a set of flags to the command for a cli instance.
func BuildCLIFlags(cmd *cobra.Command, cliCmd *cli.Command) {
flags := cmd.Flags()
// Base struct flags.
flags.StringSliceVarP(&cliCmd.Commands, "command", "c", cliCmd.Commands, "Command to run in non-interactive mode. Provide multiple flags to execute more than one command. All `--command` flags run before all `--file` flags.")
flags.StringSliceVarP(&cliCmd.Files, "file", "f", cliCmd.Files, "File to run in non-interactive mode. Provide multiple flags to execute more than one file. All `--command` flags run before all `--file` flags.")
// Config flags.
flags.AddFlagSet(cliConfigFlagSet(cliCmd.Config))
}
// cliConfigFlagSet returns a pflag.FlagSet for the CLI Config struct.
func cliConfigFlagSet(cfg *cli.Config) *pflag.FlagSet {
flags := pflag.NewFlagSet("cli", pflag.ExitOnError)
flags.StringVarP(&cfg.Host, "host", "", cfg.Host, "hostname of FeatureBase.")
flags.StringVarP(&cfg.Port, "port", "", cfg.Port, "port of FeatureBase.")
flags.StringVar(&cfg.HistoryPath, "history-path", cfg.HistoryPath, "path for history files.")
flags.StringVar(&cfg.OrganizationID, "org-id", cfg.OrganizationID, "OrganizationID.")
flags.StringVar(&cfg.Database, "db", cfg.Database, "Name of the database to connect to.")
flags.StringVar(&cfg.CloudAuth.ClientID, "client-id", cfg.CloudAuth.ClientID, "Cognito Client ID for FeatureBase Cloud access.")
flags.StringVar(&cfg.CloudAuth.Region, "region", cfg.CloudAuth.Region, "Cloud region for FeatureBase Cloud access (e.g. us-east-2).")
flags.StringVar(&cfg.CloudAuth.Email, "email", cfg.CloudAuth.Email, "Email address for FeatureBase Cloud access.")
flags.StringVar(&cfg.CloudAuth.Password, "password", cfg.CloudAuth.Password, "Password for FeatureBase Cloud access.")
flags.String("config", "", "Configuration file to read from.")
return flags
}