Merge pull request #7940 from dolthub/macneale4/slash-cmds

Add the ability to run some dolt commands directly from the dolt sql shell.
This commit is contained in:
Neil Macneale IV
2024-06-04 13:05:31 -07:00
committed by GitHub
16 changed files with 455 additions and 112 deletions

View File

@@ -44,17 +44,21 @@ func NewCliContext(args *argparser.ArgParseResults, config *env.DoltCliConfig, l
return nil, errhand.VerboseErrorFromError(errors.New("Invariant violated. args, config, and latebind must be non nil."))
}
return LateBindCliContext{globalArgs: args, config: config, bind: latebind}, nil
return LateBindCliContext{globalArgs: args, config: config, activeContext: &QueryistContext{}, bind: latebind}, nil
}
type QueryistContext struct {
sqlCtx *sql.Context
qryist *Queryist
}
// LateBindCliContext is a struct that implements CliContext. Its primary purpose is to wrap the global arguments and
// provide an implementation of the QueryEngine function. This instance is stateful to ensure that the Queryist is only
// created once.
type LateBindCliContext struct {
globalArgs *argparser.ArgParseResults
queryist Queryist
sqlCtx *sql.Context
config *env.DoltCliConfig
globalArgs *argparser.ArgParseResults
config *env.DoltCliConfig
activeContext *QueryistContext
bind LateBindQueryist
}
@@ -68,8 +72,8 @@ func (lbc LateBindCliContext) GlobalArgs() *argparser.ArgParseResults {
// LateBindQueryist is made, and caches the result. Note that if this is called twice, the closer function returns will
// be nil, callers should check if is nil.
func (lbc LateBindCliContext) QueryEngine(ctx context.Context) (Queryist, *sql.Context, func(), error) {
if lbc.queryist != nil {
return lbc.queryist, lbc.sqlCtx, nil, nil
if lbc.activeContext != nil && lbc.activeContext.qryist != nil && lbc.activeContext.sqlCtx != nil {
return *lbc.activeContext.qryist, lbc.activeContext.sqlCtx, nil, nil
}
qryist, sqlCtx, closer, err := lbc.bind(ctx)
@@ -77,8 +81,8 @@ func (lbc LateBindCliContext) QueryEngine(ctx context.Context) (Queryist, *sql.C
return nil, nil, nil, err
}
lbc.queryist = qryist
lbc.sqlCtx = sqlCtx
lbc.activeContext.qryist = &qryist
lbc.activeContext.sqlCtx = sqlCtx
return qryist, sqlCtx, closer, nil
}

View File

@@ -102,8 +102,10 @@ func generateAddSql(apr *argparser.ArgParseResults) string {
// Exec executes the command
func (cmd AddCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateAddArgParser()
helpPr, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, addDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, helpPr)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, addDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -105,8 +105,10 @@ func (cmd BranchCmd) EventType() eventsapi.ClientEventType {
// Exec executes the command
func (cmd BranchCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, branchDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, branchDocs)
if terminate {
return status
}
queryEngine, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -86,12 +86,14 @@ func (cmd CheckoutCmd) EventType() eventsapi.ClientEventType {
// Exec executes the command
func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateCheckoutArgParser()
helpPrt, usagePrt := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, checkoutDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, helpPrt)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, checkoutDocs)
if terminate {
return status
}
queryEngine, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
if closeFunc != nil {
defer closeFunc()
@@ -110,7 +112,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
// won't be as nice.
branchOrTrack := apr.Contains(cli.CheckoutCreateBranch) || apr.Contains(cli.CreateResetBranch) || apr.Contains(cli.TrackFlag)
if (branchOrTrack && apr.NArg() > 1) || (!branchOrTrack && apr.NArg() == 0) {
usagePrt()
usage()
return 1
}
@@ -122,7 +124,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
branchName, _ = apr.GetValue(cli.CreateResetBranch)
} else if apr.Contains(cli.TrackFlag) {
if apr.NArg() > 0 {
usagePrt()
usage()
return 1
}
remoteAndBranchName, _ := apr.GetValue(cli.TrackFlag)
@@ -133,7 +135,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
sqlQuery, err := generateCheckoutSql(args)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
rows, err := GetRowsForSql(queryEngine, sqlCtx, sqlQuery)
@@ -141,45 +143,50 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
if err != nil {
// In fringe cases the server can't start because the default branch doesn't exist, `dolt checkout <existing branch>`
// offers an escape hatch.
if !branchOrTrack && strings.Contains(err.Error(), "cannot resolve default branch head for database") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
if dEnv != nil {
// TODO - This error handling is not great.
if !branchOrTrack && strings.Contains(err.Error(), "cannot resolve default branch head for database") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
return 0
}
return 0
}
return HandleVErrAndExitCode(handleErrors(branchName, err), usagePrt)
return HandleVErrAndExitCode(handleErrors(branchName, err), usage)
}
if len(rows) != 1 {
return HandleVErrAndExitCode(errhand.BuildDError("expected 1 row response from %s, got %d", sqlQuery, len(rows)).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("expected 1 row response from %s, got %d", sqlQuery, len(rows)).Build(), usage)
}
if len(rows[0]) < 2 {
return HandleVErrAndExitCode(errhand.BuildDError("no 'message' field in response from %s", sqlQuery).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("no 'message' field in response from %s", sqlQuery).Build(), usage)
}
var message string
if message, ok = rows[0][1].(string); !ok {
return HandleVErrAndExitCode(errhand.BuildDError("expected string value for 'message' field in response from %s ", sqlQuery).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("expected string value for 'message' field in response from %s ", sqlQuery).Build(), usage)
}
if message != "" {
cli.Println(message)
}
if strings.Contains(message, "Switched to branch") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
// This command doesn't modify `dEnv` which could break tests that call multiple commands in sequence.
// We must reload it so that it includes changes to the repo state.
err = dEnv.ReloadRepoState()
if err != nil {
return 1
if dEnv != nil {
if strings.Contains(message, "Switched to branch") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
// This command doesn't modify `dEnv` which could break tests that call multiple commands in sequence.
// We must reload it so that it includes changes to the repo state.
err = dEnv.ReloadRepoState()
if err != nil {
return 1
}
}
}

View File

@@ -104,11 +104,13 @@ func (cmd CommitCmd) Exec(ctx context.Context, commandStr string, args []string,
// (e.g. because --skip-empty was specified as an argument).
func performCommit(ctx context.Context, commandStr string, args []string, cliCtx cli.CliContext, temporaryDEnv *env.DoltEnv) (int, bool) {
ap := cli.CreateCommitArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, commitDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, commitDocs)
if terminate {
return status, false
}
if err := cli.VerifyCommitArgs(apr); err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), help), false
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage), false
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)

View File

@@ -185,10 +185,12 @@ func (cmd DiffCmd) RequiresRepo() bool {
}
// Exec executes the command
func (cmd DiffCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
func (cmd DiffCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, diffDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, diffDocs)
if terminate {
return status
}
verr := cmd.validateArgs(apr)
if verr != nil {

View File

@@ -99,8 +99,10 @@ func (cmd LogCmd) Exec(ctx context.Context, commandStr string, args []string, dE
func (cmd LogCmd) logWithLoggerFunc(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, logDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, logDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -94,8 +94,10 @@ func (cmd MergeCmd) RequiresRepo() bool {
func (cmd MergeCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateMergeArgParser()
ap.SupportsFlag(cli.NoJsonMergeFlag, "", "Do not attempt to automatically resolve multiple changes to the same JSON value, report a conflict instead.")
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, mergeDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, mergeDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -89,10 +89,12 @@ func (cmd ResetCmd) RequiresRepo() bool {
}
// Exec executes the command
func (cmd ResetCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
func (cmd ResetCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateResetArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, resetDocContent, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, resetDocContent)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -21,6 +21,7 @@ import (
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
@@ -96,7 +97,7 @@ const (
welcomeMsg = `# Welcome to the DoltSQL shell.
# Statements must be terminated with ';'.
# "exit" or "quit" (or Ctrl-D) to exit.`
# "exit" or "quit" (or Ctrl-D) to exit. "/help;" for help.`
)
// TODO: get rid of me, use a real integration point to define system variables
@@ -260,7 +261,7 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE
}
if isTty {
err := execShell(sqlCtx, queryist, format)
err := execShell(sqlCtx, queryist, format, cliCtx)
if err != nil {
return sqlHandleVErrAndExitCode(queryist, errhand.VerboseErrorFromError(err), usage)
}
@@ -685,7 +686,7 @@ func buildBatchSqlErr(stmtStartLine int, query string, err error) error {
// execShell starts a SQL shell. Returns when the user exits the shell. The Root of the sqlEngine may
// be updated by any queries which were processed.
func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat) error {
func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat, cliCtx cli.CliContext) error {
_ = iohelp.WriteLine(cli.CliOut, welcomeMsg)
historyFile := filepath.Join(".sqlhistory") // history file written to working dir
@@ -750,65 +751,85 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
return
}
closureFormat := format
// TODO: there's a bug in the readline library when editing multi-line history entries.
// Longer term we need to switch to a new readline library, like in this bug:
// https://github.com/cockroachdb/cockroach/issues/15460
// For now, we store all history entries as single-line strings to avoid the issue.
singleLine := strings.ReplaceAll(query, "\n", " ")
if err := shell.AddHistory(singleLine); err != nil {
// TODO: handle better, like by turning off history writing for the rest of the session
shell.Println(color.RedString(err.Error()))
}
query = strings.TrimSuffix(query, shell.LineTerminator())
// TODO: it would be better to build this into the statement parser rather than special case it here
for _, terminator := range verticalOutputLineTerminators {
if strings.HasSuffix(query, terminator) {
closureFormat = engine.FormatVertical
}
query = strings.TrimSuffix(query, terminator)
}
cont := true
var nextPrompt string
var multiPrompt string
var sqlSch sql.Schema
var rowIter sql.RowIter
func() {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
if sqlSch, rowIter, err = processQuery(sqlCtx, query, qryist); err != nil {
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
switch closureFormat {
case engine.FormatTabular, engine.FormatVertical:
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter)
default:
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter)
}
re := regexp.MustCompile(`\s*/(.*)`)
matches := re.FindStringSubmatch(query)
// If the query starts with a slash, it's a shell command. We don't want to print the query in that case.
if len(matches) > 1 {
func() {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
slashCmd := matches[1]
err := handleSlashCommand(sqlCtx, slashCmd, cliCtx)
if err != nil {
shell.Println(color.RedString(err.Error()))
}
nextPrompt, multiPrompt = postCommandUpdate(sqlCtx, qryist)
}()
} else {
closureFormat := format
// TODO: there's a bug in the readline library when editing multi-line history entries.
// Longer term we need to switch to a new readline library, like in this bug:
// https://github.com/cockroachdb/cockroach/issues/15460
// For now, we store all history entries as single-line strings to avoid the issue.
singleLine := strings.ReplaceAll(query, "\n", " ")
if err := shell.AddHistory(singleLine); err != nil {
// TODO: handle better, like by turning off history writing for the rest of the session
shell.Println(color.RedString(err.Error()))
}
db, branch, ok := getDBBranchFromSession(sqlCtx, qryist)
if ok {
sqlCtx.SetCurrentDatabase(db)
query = strings.TrimSuffix(query, shell.LineTerminator())
// TODO: it would be better to build this into the statement parser rather than special case it here
for _, terminator := range verticalOutputLineTerminators {
if strings.HasSuffix(query, terminator) {
closureFormat = engine.FormatVertical
}
query = strings.TrimSuffix(query, terminator)
}
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
nextPrompt, multiPrompt = formattedPrompts(db, branch, dirty)
}()
var sqlSch sql.Schema
var rowIter sql.RowIter
cont = func() bool {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
if sqlSch, rowIter, err = processQuery(sqlCtx, query, qryist); err != nil {
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
switch closureFormat {
case engine.FormatTabular, engine.FormatVertical:
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter)
default:
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter)
}
if err != nil {
shell.Println(color.RedString(err.Error()))
}
}
nextPrompt, multiPrompt = postCommandUpdate(sqlCtx, qryist)
return true
}()
}
if !cont {
return
}
shell.SetPrompt(nextPrompt)
shell.SetMultiPrompt(multiPrompt)
@@ -820,6 +841,20 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
return nil
}
// postCommandUpdate is a helper function that is run after the shell has completed a command. It updates the the database
// if needed, and generates new prompts for the shell (based on the branch and if the workspace is dirty).
func postCommandUpdate(sqlCtx *sql.Context, qryist cli.Queryist) (string, string) {
db, branch, ok := getDBBranchFromSession(sqlCtx, qryist)
if ok {
sqlCtx.SetCurrentDatabase(db)
}
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
return formattedPrompts(db, branch, dirty)
}
// formattedPrompts returns the prompt and multiline prompt for the current session. If the db is empty, the prompt will
// be "> ", otherwise it will be "db> ". If the branch is empty, the multiline prompt will be "-> ", left padded for
// alignment with the prompt.
@@ -892,7 +927,7 @@ func getDBBranchFromSession(sqlCtx *sql.Context, qryist cli.Queryist) (db string
// isDirty returns true if the workspace is dirty, false otherwise. This function _assumes_ you are on a database
// with a branch. If you are not, you will get an error.
func isDirty(sqlCtx *sql.Context, qryist cli.Queryist) (bool, error) {
_, resp, err := qryist.Query(sqlCtx, "select count(table_name) > 0 as dirty from dolt_Status")
_, resp, err := qryist.Query(sqlCtx, "select count(table_name) > 0 as dirty from dolt_status")
if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))

View File

@@ -0,0 +1,181 @@
// Copyright 2024 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 commands
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
)
var slashCmds = []cli.Command{
StatusCmd{},
DiffCmd{},
LogCmd{},
AddCmd{},
CommitCmd{},
CheckoutCmd{},
ResetCmd{},
BranchCmd{},
MergeCmd{},
SlashHelp{},
}
// parseSlashCmd parses a command line string into a slice of strings, splitting on spaces, but allowing spaces within
// double quotes. For example, the string `foo "bar baz"` would be parsed into the slice `[]string{"foo", "bar baz"}`.
// This is quick and dirty for slash command prototype, and doesn't try and handle all the crazy edge cases that come
// up with supporting many types of quotes. Also, pretty sure a dangling quote will break it. But it's a start.
func parseSlashCmd(cmd string) []string {
// TODO: determine if we can get rid of the ";" as the terminator for cli commands.
cmd = strings.TrimSuffix(cmd, ";")
cmd = strings.TrimRight(cmd, " \t\n\r\v\f")
cmd = strings.TrimLeft(cmd, " \t\n\r\v\f")
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|\S+`)
cmdWords := r.FindAllString(cmd, -1)
for i := range cmdWords {
if cmdWords[i][0] == '"' {
cmdWords[i] = cmdWords[i][1 : len(cmdWords[i])-1]
cmdWords[i] = strings.ReplaceAll(cmdWords[i], `\"`, `"`)
}
}
if len(cmdWords) == 0 {
return []string{}
}
return cmdWords
}
func handleSlashCommand(sqlCtx *sql.Context, fullCmd string, cliCtx cli.CliContext) error {
cliCmd := parseSlashCmd(fullCmd)
if len(cliCmd) == 0 {
return fmt.Errorf("Empty command. Use `/help;` for help.")
}
subCmd := cliCmd[0]
subCmdArgs := cliCmd[1:]
status := 1
subCmdInst, ok := findSlashCmd(subCmd)
if ok {
status = subCmdInst.Exec(sqlCtx, subCmd, subCmdArgs, nil, cliCtx)
} else {
return fmt.Errorf("Unknown command: %s. Use `/help;` for a list of command.", subCmd)
}
if status != 0 {
return fmt.Errorf("error executing command: %s", cliCmd)
}
return nil
}
type SlashHelp struct{}
func (s SlashHelp) Name() string {
return "help"
}
func (s SlashHelp) Description() string {
return "What you see right now."
}
func (s SlashHelp) Docs() *cli.CommandDocumentation {
return &cli.CommandDocumentation{
CommandStr: "/help",
ShortDesc: "What you see right now.",
LongDesc: "It would seem that you are crying out for help. Please join us on Discord (https://discord.gg/gqr7K4VNKe)!",
Synopsis: []string{},
ArgParser: s.ArgParser(),
}
}
func (s SlashHelp) Exec(ctx context.Context, _ string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
if args != nil && len(args) > 0 {
subCmd := args[0]
subCmdInst, ok := findSlashCmd(subCmd)
if ok {
foo, _ := cli.HelpAndUsagePrinters(subCmdInst.Docs())
foo()
} else {
cli.Println(fmt.Sprintf("Unknown command: %s", subCmd))
}
return 0
}
qryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if closeFunc != nil {
defer closeFunc()
}
if err != nil {
cli.Println(fmt.Sprintf("error getting query engine: %s", err))
return 1
}
prompt := generateHelpPrompt(sqlCtx, qryist)
cli.Println("Dolt SQL Shell Help")
cli.Printf("Default behavior is to interpret SQL statements. (e.g. '%sselect * from my_table;')\n", prompt)
cli.Printf("Dolt CLI commands can be invoked with a leading '/'. (e.g. '%s/status;')\n", prompt)
cli.Println("All statements are terminated with a ';'.")
cli.Println("\nAvailable commands:")
for _, cmdInst := range slashCmds {
cli.Println(fmt.Sprintf(" %10s - %s", cmdInst.Name(), cmdInst.Description()))
}
cli.Printf("\nFor more information on a specific command, type '/help <command>;' (e.g. '%s/help status;')\n", prompt)
moreWords := `
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
Still need assistance? Talk directly to Dolt developers on Discord! https://discord.gg/gqr7K4VNKe
Found a bug? Want additional features? Please let us know! https://github.com/dolthub/dolt/issues
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-`
cli.Println(moreWords)
return 0
}
func generateHelpPrompt(sqlCtx *sql.Context, qryist cli.Queryist) string {
db, branch, _ := getDBBranchFromSession(sqlCtx, qryist)
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
prompt, _ := formattedPrompts(db, branch, dirty)
return prompt
}
func (s SlashHelp) ArgParser() *argparser.ArgParser {
return &argparser.ArgParser{}
}
func findSlashCmd(cmd string) (cli.Command, bool) {
for _, cmdInst := range slashCmds {
if cmdInst.Name() == cmd {
return cmdInst, true
}
}
return nil, false
}

View File

@@ -100,12 +100,13 @@ func (cmd StatusCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_STATUS
}
// Exec executes the command
func (cmd StatusCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
// parse arguments
ap := cmd.ArgParser()
help, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, statusDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, statusDocs)
if terminate {
return status
}
showIgnoredTables := apr.Contains(cli.ShowIgnoredFlag)
// configure SQL engine
@@ -656,7 +657,9 @@ and have %v and %v different commits each, respectively.
}
func handleStatusVErr(err error) int {
cli.PrintErrln(errhand.VerboseErrorFromError(err).Verbose())
if err != argparser.ErrHelp {
cli.PrintErrln(errhand.VerboseErrorFromError(err).Verbose())
}
return 1
}

View File

@@ -774,6 +774,23 @@ func getHashOf(queryist cli.Queryist, sqlCtx *sql.Context, ref string) (string,
return rows[0][0].(string), nil
}
func ParseArgsAndPrintHelp(
ap *argparser.ArgParser,
commandStr string,
args []string,
docs cli.CommandDocumentationContent) (apr *argparser.ArgParseResults, usage cli.UsagePrinter, terminate bool, exitStatus int) {
helpPrt, usagePrt := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, docs, ap))
var err error
apr, err = cli.ParseArgs(ap, args, helpPrt)
if err != nil {
if err == argparser.ErrHelp {
return nil, usagePrt, true, 0
}
return nil, usagePrt, true, HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
}
return apr, usagePrt, false, 0
}
func HandleVErrAndExitCode(verr errhand.VerboseError, usage cli.UsagePrinter) int {
if verr != nil {
if msg := verr.Verbose(); strings.TrimSpace(msg) != "" {

View File

@@ -72,7 +72,8 @@ func Start() *Pager {
// -S ... Chop (truncate) long lines rather than wrapping.
// -R ... Output "raw" control characters.
// -X ... Don't use termcap init/deinit strings.
cmd = exec.Command(lessPath, "-FSRX")
// -d ... Don't complain about dumb terminals.
cmd = exec.Command(lessPath, "-FSRXd")
}
stdin, stdout, err := os.Pipe()

View File

@@ -0,0 +1,69 @@
#!/usr/bin/expect
set timeout 5
set env(NO_COLOR) 1
proc expect_with_defaults {pattern action} {
expect {
-re $pattern {
# puts "Matched pattern: $pattern"
eval $action
}
timeout {
puts "<<Timeout>>";
exit 1
}
eof {
puts "<<End of File reached>>";
exit 1
}
failed {
puts "<<Failed>>";
exit 1
}
}
}
proc expect_with_defaults_2 {patternA patternB action} {
expect {
-re $patternA {
# puts "Matched pattern: $patternA"
exp_continue
}
-re $patternB {
# puts "Matched pattern: $patternB"
eval $action
}
timeout {
puts "<<Timeout>>";
exit 1
}
eof {
puts "<<End of File reached>>";
exit 1
}
failed {
puts "<<Failed>>";
exit 1
}
}
}
spawn dolt sql
expect_with_defaults {dolt-repo-[0-9]+/main\*> } { send "/commit -A -m \"sql-shell-slash-cmds commit\";\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main> } { send "/log -n 1;\r"; }
expect_with_defaults_2 {sql-shell-slash-cmds commit} {dolt-repo-[0-9]+/main> } { send "/status;\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main> } { send "/reset HEAD~1;\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main\*> } { send "/diff;\r"; }
expect_with_defaults_2 {diff --dolt a/tbl b/tbl} {dolt-repo-[0-9]+/main\*> } {send "quit\r";}
expect eof
exit

View File

@@ -67,6 +67,18 @@ teardown() {
[[ "$output" =~ "+---------------------" ]] || false
}
# bats test_tags=no_lambda
@test "sql-shell: sql shell executes slash commands" {
skiponwindows "Need to install expect and make this script work on windows."
if [ "$SQL_ENGINE" = "remote-engine" ]; then
skip "Current test setup results in remote calls having a clean branch, where this expect script expects dirty."
fi
run $BATS_TEST_DIRNAME/sql-shell-slash-cmds.expect
echo "$output"
[ "$status" -eq 0 ]
}
# bats test_tags=no_lambda
@test "sql-shell: sql shell prompt updates" {
skiponwindows "Need to install expect and make this script work on windows."