-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
clisqlshell: hide the libedit dep behind a go interface
In #86457 and related work we will want to offer two editors side-by-side in a transition period, so folk can compare or fall back on something known in case they are not happy with the new stuff. To enable this transition period, this commit hides the editor behind a go interface. This also makes the shell code overall easier to read and understand. Release note: None
- Loading branch information
Showing
7 changed files
with
327 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright 2022 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
package clisqlshell | ||
|
||
import "os" | ||
|
||
// editor is the interface between the shell and a line editor. | ||
type editor interface { | ||
init(win, wout, werr *os.File, sqlS sqlShell, maxHistEntries int, histFile string) (cleanupFn func(), err error) | ||
errInterrupted() error | ||
getOutputStream() *os.File | ||
getLine() (string, error) | ||
addHistory(line string) error | ||
canPrompt() bool | ||
setPrompt(prompt string) | ||
} | ||
|
||
type sqlShell interface { | ||
inCopy() bool | ||
runShowCompletions(sql string, offset int) (rows [][]string, err error) | ||
serverSideParse(sql string) (string, error) | ||
} | ||
|
||
// getEditor instantiates an editor compatible with the current configuration. | ||
func getEditor(useEditor bool, displayPrompt bool) editor { | ||
if !useEditor { | ||
return &bufioReader{displayPrompt: displayPrompt} | ||
} | ||
return &editlineReader{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// Copyright 2022 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
package clisqlshell | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"os" | ||
|
||
"github.com/cockroachdb/errors" | ||
) | ||
|
||
// bufioReader implements the editor interface. | ||
type bufioReader struct { | ||
wout *os.File | ||
displayPrompt bool | ||
prompt string | ||
buf *bufio.Reader | ||
} | ||
|
||
var _ editor = (*bufioReader)(nil) | ||
|
||
func (b *bufioReader) init( | ||
win, wout, werr *os.File, _ sqlShell, maxHistEntries int, histFile string, | ||
) (cleanupFn func(), err error) { | ||
b.wout = wout | ||
b.buf = bufio.NewReader(win) | ||
return func() {}, nil | ||
} | ||
|
||
var errBufioInterrupted = errors.New("never happens") | ||
|
||
func (b *bufioReader) errInterrupted() error { | ||
return errBufioInterrupted | ||
} | ||
|
||
func (b *bufioReader) getOutputStream() *os.File { | ||
return b.wout | ||
} | ||
|
||
func (b *bufioReader) addHistory(line string) error { | ||
return nil | ||
} | ||
|
||
func (b *bufioReader) canPrompt() bool { | ||
return b.displayPrompt | ||
} | ||
|
||
func (b *bufioReader) setPrompt(prompt string) { | ||
if b.displayPrompt { | ||
b.prompt = prompt | ||
} | ||
} | ||
|
||
func (b *bufioReader) getLine() (string, error) { | ||
fmt.Fprint(b.wout, b.prompt) | ||
l, err := b.buf.ReadString('\n') | ||
// bufio.ReadString() differs from readline.Readline in the handling of | ||
// EOF. Readline only returns EOF when there is nothing left to read and | ||
// there is no partial line while bufio.ReadString() returns EOF when the | ||
// end of input has been reached but will return the non-empty partial line | ||
// as well. We work around this by converting the bufioReader behavior to match | ||
// the Readline behavior. | ||
if err == io.EOF && len(l) != 0 { | ||
err = nil | ||
} else if err == nil { | ||
// From the bufio.ReadString docs: ReadString returns err != nil if and | ||
// only if the returned data does not end in delim. To match the behavior | ||
// of readline.Readline, we strip off the trailing delimiter. | ||
l = l[:len(l)-1] | ||
} | ||
return l, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
// Copyright 2022 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
package clisqlshell | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"github.com/cockroachdb/cockroach/pkg/cli/clierror" | ||
"github.com/cockroachdb/errors" | ||
readline "github.com/knz/go-libedit" | ||
) | ||
|
||
// editlineReader implements the editor interface. | ||
type editlineReader struct { | ||
wout *os.File | ||
sql sqlShell | ||
prompt string | ||
ins readline.EditLine | ||
} | ||
|
||
var _ editor = (*editlineReader)(nil) | ||
|
||
func (b *editlineReader) init( | ||
win, wout, werr *os.File, sqlS sqlShell, maxHistEntries int, histFile string, | ||
) (cleanupFn func(), err error) { | ||
cleanupFn = func() {} | ||
|
||
b.ins, err = readline.InitFiles("cockroach", | ||
true /* wideChars */, win, wout, werr) | ||
if errors.Is(err, readline.ErrWidecharNotSupported) { | ||
fmt.Fprintln(werr, "warning: wide character support disabled") | ||
b.ins, err = readline.InitFiles("cockroach", | ||
false, win, wout, werr) | ||
} | ||
if err != nil { | ||
return cleanupFn, err | ||
} | ||
cleanupFn = func() { b.ins.Close() } | ||
b.wout = b.ins.Stdout() | ||
b.sql = sqlS | ||
b.ins.SetCompleter(b) | ||
|
||
// If the user has used bind -v or bind -l in their ~/.editrc, | ||
// this will reset the standard bindings. However we really | ||
// want in this shell that Ctrl+C, tab, Ctrl+Z and Ctrl+R | ||
// always have the same meaning. So reload these bindings | ||
// explicitly no matter what ~/.editrc may have changed. | ||
b.ins.RebindControlKeys() | ||
|
||
if err := b.ins.UseHistory(maxHistEntries, true /*dedup*/); err != nil { | ||
fmt.Fprintf(werr, "warning: cannot enable history: %v\n ", err) | ||
} else if histFile != "" { | ||
err = b.ins.LoadHistory(histFile) | ||
if err != nil { | ||
fmt.Fprintf(werr, "warning: cannot load the command-line history (file corrupted?): %v\n", err) | ||
fmt.Fprintf(werr, "note: the history file will be cleared upon first entry\n") | ||
} | ||
// SetAutoSaveHistory() does two things: | ||
// - it preserves the name of the history file, for use | ||
// by the final SaveHistory() call. | ||
// - it decides whether to save the history to file upon | ||
// every new command. | ||
// We disable the latter, since a history file can grow somewhat | ||
// large and we don't want the excess I/O latency to be interleaved | ||
// in-between every command. | ||
b.ins.SetAutoSaveHistory(histFile, false) | ||
prevCleanup := cleanupFn | ||
cleanupFn = func() { | ||
if err := b.ins.SaveHistory(); err != nil { | ||
fmt.Fprintf(werr, "warning: cannot save command-line history: %v\n", err) | ||
} | ||
prevCleanup() | ||
} | ||
} | ||
|
||
return cleanupFn, nil | ||
} | ||
|
||
func (b *editlineReader) errInterrupted() error { | ||
return readline.ErrInterrupted | ||
} | ||
|
||
func (b *editlineReader) getOutputStream() *os.File { | ||
return b.wout | ||
} | ||
|
||
func (b *editlineReader) addHistory(line string) error { | ||
return b.ins.AddHistory(line) | ||
} | ||
|
||
func (b *editlineReader) canPrompt() bool { | ||
return true | ||
} | ||
|
||
func (b *editlineReader) setPrompt(prompt string) { | ||
b.prompt = prompt | ||
b.ins.SetLeftPrompt(prompt) | ||
} | ||
|
||
func (b *editlineReader) GetCompletions(word string) []string { | ||
if b.sql.inCopy() { | ||
return []string{word + "\t"} | ||
} | ||
sql, offset := b.ins.GetLineInfo() | ||
if !strings.HasSuffix(sql, "??") { | ||
rows, err := b.sql.runShowCompletions(sql, offset) | ||
if err != nil { | ||
clierror.OutputError(b.wout, err, true /*showSeverity*/, false /*verbose*/) | ||
} | ||
|
||
var completions []string | ||
for _, row := range rows { | ||
completions = append(completions, row[0]) | ||
} | ||
|
||
return completions | ||
} | ||
|
||
helpText, err := b.sql.serverSideParse(sql) | ||
if helpText != "" { | ||
// We have a completion suggestion. Use that. | ||
fmt.Fprintf(b.wout, "\nSuggestion:\n%s\n", helpText) | ||
} else if err != nil { | ||
// Some other error. Display it. | ||
fmt.Fprintln(b.wout) | ||
clierror.OutputError(b.wout, err, true /*showSeverity*/, false /*verbose*/) | ||
} | ||
|
||
// After a suggestion or error, redisplay the prompt and current entry. | ||
fmt.Fprint(b.wout, b.prompt, sql) | ||
return nil | ||
} | ||
|
||
func (b *editlineReader) getLine() (string, error) { | ||
l, err := b.ins.GetLine() | ||
if len(l) > 0 && l[len(l)-1] == '\n' { | ||
// Strip the final newline. | ||
l = l[:len(l)-1] | ||
} else { | ||
// There was no newline at the end of the input | ||
// (e.g. Ctrl+C was entered). Force one. | ||
fmt.Fprintln(b.wout) | ||
} | ||
return l, err | ||
} |
Oops, something went wrong.