// Copyright 2019 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cli import ( "context" "fmt" "os" "os/signal" "strings" "syscall" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/fatih/color" "github.com/dolthub/dolt/go/libraries/doltcore/env" "github.com/dolthub/dolt/go/libraries/events" "github.com/dolthub/dolt/go/libraries/utils/argparser" config "github.com/dolthub/dolt/go/libraries/utils/config" "github.com/dolthub/dolt/go/store/nbs" eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1" ) func IsHelp(str string) bool { str = strings.TrimSpace(str) if len(str) == 0 { return false } if str[0] != '-' { return false } str = strings.ToLower(strings.TrimLeft(str, "- ")) return str == "h" || str == "help" } func hasHelpFlag(args []string) bool { for _, arg := range args { if IsHelp(arg) { return true } } return false } // globalArgsSpecifyDB checks if the global arguments contain --use-db or --host flags, // which indicate that the user is manually specifying a database connection // rather than relying on the current directory being a dolt repository. func globalArgsSpecifyDB(cliCtx CliContext) bool { if cliCtx == nil { return false } globalArgs := cliCtx.GlobalArgs() if globalArgs == nil { return false } _, hasUseDb := globalArgs.GetValue("use-db") _, hasHost := globalArgs.GetValue(HostFlag) return hasUseDb || hasHost } // Command is the interface which defines a Dolt cli command type Command interface { // Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command Name() string // Description returns a description of the command Description() string // Exec executes the command Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int // Docs returns the documentation for this command, or nil if it's undocumented Docs() *CommandDocumentation // ArgParser returns the arg parser for this command ArgParser() *argparser.ArgParser } // SignalCommand is an extension of Command that allows commands to install their own signal handlers, rather than use // the global one (which cancels the global context). type SignalCommand interface { Command // InstallsSignalHandlers returns whether this command manages its own signal handlers for interruption / termination. InstallsSignalHandlers() bool } // Queryist is generic interface for executing queries. Commands will be provided a Queryist to perform any work using // SQL. The Queryist can be obtained from the CliContext passed into the Exec method by calling the QueryEngine method. type Queryist interface { Query(ctx *sql.Context, query string) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) QueryWithBindings(ctx *sql.Context, query string, parsed sqlparser.Statement, bindings map[string]sqlparser.Expr, qFlags *sql.QueryFlags) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) } // ShellServerQueryist is used to gather warnings in the sql-shell context when a server is running. // We call an extra "show warnings" query, but want to avoid this in other cases, (i.e. dolt sql -q) type ShellServerQueryist interface { EnableGatherWarnings() } // This type is to store the content of a documented command, elsewhere we can transform this struct into // other structs that are used to generate documentation at the command line and in markdown files. type CommandDocumentationContent struct { ShortDesc string LongDesc string Synopsis []string } // type CommandDocumentation // RepoNotRequiredCommand is an optional interface that commands can implement if the command can be run without // the current directory being a valid Dolt data repository. Any commands not implementing this interface are // assumed to require that they be run from a directory containing a Dolt data repository. type RepoNotRequiredCommand interface { // RequiresRepo should return false if this interface is implemented, and the command does not have the requirement // that it be run from within a data repository directory RequiresRepo() bool } // EventMonitoredCommand is an optional interface that can be overridden in order to generate an event which is sent // to the metrics system when the command is run type EventMonitoredCommand interface { // EventType returns the type of the event to log EventType() eventsapi.ClientEventType } // HiddenCommand is an optional interface that can be overridden so that a command is hidden from the help text type HiddenCommand interface { // Hidden should return true if this command should be hidden from the help text Hidden() bool } // SubCommandHandler is a command implementation which holds subcommands which can be called type SubCommandHandler struct { name string description string // Unspecified ONLY applies when no other command has been given. This is different from how a default command would // function, as a command that doesn't exist for this sub handler will result in an error. Unspecified Command Subcommands []Command hidden bool } // NewSubCommandHandler returns a new SubCommandHandler instance func NewSubCommandHandler(name, description string, subcommands []Command) SubCommandHandler { return SubCommandHandler{name, description, nil, subcommands, false} } // NewHiddenSubCommandHandler returns a new SubCommandHandler instance that is hidden from display func NewHiddenSubCommandHandler(name, description string, subcommands []Command) SubCommandHandler { return SubCommandHandler{name, description, nil, subcommands, true} } // NewSubCommandHandlerWithUnspecified returns a new SubCommandHandler that will invoke the unspecified command ONLY if // no direct command is given. func NewSubCommandHandlerWithUnspecified(name, description string, hidden bool, unspecified Command, subcommands []Command) SubCommandHandler { return SubCommandHandler{name, description, unspecified, subcommands, hidden} } func (hc SubCommandHandler) Name() string { return hc.name } func (hc SubCommandHandler) Description() string { return hc.description } func (hc SubCommandHandler) RequiresRepo() bool { return false } func (hc SubCommandHandler) Docs() *CommandDocumentation { return nil } func (hc SubCommandHandler) ArgParser() *argparser.ArgParser { return nil } func (hc SubCommandHandler) Hidden() bool { return hc.hidden } func (hc SubCommandHandler) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int { if len(args) < 1 && hc.Unspecified == nil { hc.PrintUsage(commandStr) return 1 } var subCommandStr string if len(args) > 0 { subCommandStr = strings.ToLower(strings.TrimSpace(args[0])) } for _, cmd := range hc.Subcommands { if strings.EqualFold(cmd.Name(), subCommandStr) { return hc.handleCommand(ctx, commandStr+" "+subCommandStr, cmd, args[1:], dEnv, cliCtx) } } if hc.Unspecified != nil { return hc.handleCommand(ctx, commandStr, hc.Unspecified, args, dEnv, cliCtx) } if !IsHelp(subCommandStr) { PrintErrln(color.RedString("Unknown Command " + subCommandStr)) return 1 } hc.PrintUsage(commandStr) return 0 } func (hc SubCommandHandler) handleCommand(ctx context.Context, commandStr string, cmd Command, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int { cmdRequiresRepo := true if rnrCmd, ok := cmd.(RepoNotRequiredCommand); ok { cmdRequiresRepo = rnrCmd.RequiresRepo() } // If --use-db or --host is specified, the user is manually specifying a database/server // connection, so we don't require the current directory to be a valid dolt repository. if cmdRequiresRepo && !hasHelpFlag(args) && !globalArgsSpecifyDB(cliCtx) { isValid := CheckEnvIsValid(dEnv) if !isValid { return 2 } } var evt *events.Event if evtCmd, ok := cmd.(EventMonitoredCommand); ok { evt = events.NewEvent(evtCmd.EventType()) ctx = events.NewContextForEvent(ctx, evt) } // Certain commands cannot tolerate a top-level signal handler (which cancels the root context) but need to manage // their own interrupt semantics. if signalCmd, ok := cmd.(SignalCommand); !ok || !signalCmd.InstallsSignalHandlers() { var stop context.CancelFunc ctx, stop = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() } ret := cmd.Exec(ctx, commandStr, args, dEnv, cliCtx) if evt != nil { events.GlobalCollector().CloseEventAndAdd(evt) } return ret } // CheckEnvIsValid validates that a DoltEnv has been initialized properly and no errors occur during load, and prints // error messages to the user if there are issues with the environment or if errors were encountered while loading it. func CheckEnvIsValid(dEnv *env.DoltEnv) bool { if !dEnv.HasDoltDataDir() { PrintErrln(color.RedString("The current directory is not a valid dolt repository.")) PrintErrln("run: dolt init before trying to run this command") return false } else if dEnv.RSLoadErr != nil { PrintErrln(color.RedString("The current directories repository state is invalid")) PrintErrln(dEnv.RSLoadErr.Error()) return false } else if dEnv.DBLoadError != nil { PrintErrln(color.RedString("Failed to load database.")) PrintErrln(dEnv.DBLoadError.Error()) if dEnv.DBLoadError == nbs.ErrUnreadableManifest { PrintErrln("\tyou might need to upgrade your Dolt client") PrintErrln("\tvisit https://github.com/dolthub/dolt/releases/latest/") } return false } return true } const ( userNameRequiredError = `Author identity unknown *** Please tell me who you are. Run dolt config --global --add user.email "you@example.com" dolt config --global --add user.name "Your Name" to set your account's default identity. Omit --global to set the identity only in this repository. fatal: empty ident name not allowed ` ) // CheckUserNameAndEmail returns true if the user name and email are set for this environment, or prints an error and // returns false if not. func CheckUserNameAndEmail(cfg *env.DoltCliConfig) bool { _, err := cfg.GetString(config.UserEmailKey) if err != nil { PrintErr(userNameRequiredError) return false } _, err = cfg.GetString(config.UserNameKey) if err != nil { PrintErr(userNameRequiredError) return false } return true } func (hc SubCommandHandler) GetUsage(commandStr string) string { str := "Valid commands for " + commandStr + " are\n" for _, cmd := range hc.Subcommands { if hiddenCmd, ok := cmd.(HiddenCommand); ok { if hiddenCmd.Hidden() { continue } } str += fmt.Sprintf(" %16s - %s\n", cmd.Name(), cmd.Description()) } return str } func (hc SubCommandHandler) PrintUsage(commandStr string) { usage := hc.GetUsage(commandStr) Println(usage) }