-
Notifications
You must be signed in to change notification settings - Fork 4
Add opt-in IsComplete callback to UninterpretedConfig #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
9a5c1e6
061915e
65ae5c0
4bf4a1c
8cd80da
950b93b
a91cae3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ type Shell struct { | |
| specialTerminators []string | ||
| backSlashCmds []string | ||
| quitKeywords []string | ||
| isComplete func(accumulated, delimiter string) bool | ||
| contextValues | ||
| Actions | ||
| } | ||
|
|
@@ -80,6 +81,10 @@ type UninterpretedConfig struct { | |
| BackSlashCmds []string | ||
| // Quit keywords to exit the shell if discovered | ||
| QuitKeywords []string | ||
| // IsComplete, when set, replaces the LineTerminator suffix check: it is | ||
| // called with the cumulative buffer and current delimiter and returns true | ||
| // when accumulation should stop. | ||
| IsComplete func(accumulated, delimiter string) bool | ||
| } | ||
|
|
||
| // New creates a new shell with default settings. Uses standard output and default prompt ">> ". | ||
|
|
@@ -109,6 +114,7 @@ func NewUninterpreted(conf *UninterpretedConfig) *Shell { | |
| shell.specialTerminators = conf.SpecialTerminators | ||
| shell.backSlashCmds = conf.BackSlashCmds | ||
| shell.quitKeywords = conf.QuitKeywords | ||
| shell.isComplete = conf.IsComplete | ||
|
|
||
| return shell | ||
| } | ||
|
|
@@ -366,7 +372,7 @@ func (s *Shell) readUninterpreted() (string, error) { | |
|
|
||
| if s.lineTerminator != "" { | ||
| firstLine := true | ||
| lines, err = s.readMultiLinesFunc(func(line string) (keepReading bool) { | ||
| lines, err = s.readMultiLinesAccumFunc(func(line, accumulated string) (keepReading bool) { | ||
| if firstLine { | ||
| firstLine = false | ||
| if matches := delimiterRegex.FindStringSubmatch(line); len(matches) == 2 { | ||
|
|
@@ -382,22 +388,7 @@ func (s *Shell) readUninterpreted() (string, error) { | |
| } | ||
| } | ||
| } | ||
|
|
||
| if strings.HasSuffix(strings.TrimSpace(line), s.lineTerminator) { | ||
| return false | ||
| } | ||
| for _, sc := range s.specialTerminators { | ||
| if strings.HasSuffix(strings.TrimSpace(line), sc) { | ||
| return false | ||
| } | ||
| } | ||
| for _, sc := range s.backSlashCmds { | ||
| if strings.HasPrefix(strings.TrimSpace(line), sc) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| return !s.lineIsComplete(line, accumulated) | ||
| }) | ||
|
|
||
| if err != nil { | ||
|
|
@@ -437,6 +428,27 @@ func (s *Shell) readUninterpreted() (string, error) { | |
| return lines, nil | ||
| } | ||
|
|
||
| // lineIsComplete reports whether reading should stop: special terminators and | ||
| // backslash commands always end input, otherwise it defers to IsComplete or the | ||
| // line-terminator suffix check. | ||
| func (s *Shell) lineIsComplete(line, accumulated string) bool { | ||
| trimmed := strings.TrimSpace(line) | ||
| for _, sc := range s.specialTerminators { | ||
| if strings.HasSuffix(trimmed, sc) { | ||
| return true | ||
| } | ||
| } | ||
| for _, sc := range s.backSlashCmds { | ||
| if strings.HasPrefix(trimmed, sc) { | ||
| return true | ||
| } | ||
| } | ||
| if s.isComplete != nil { | ||
| return s.isComplete(accumulated, s.lineTerminator) | ||
| } | ||
| return strings.HasSuffix(trimmed, s.lineTerminator) | ||
| } | ||
|
|
||
| func (s *Shell) read() ([]string, error) { | ||
| s.rawArgs = nil | ||
| eof := "" | ||
|
|
@@ -483,18 +495,38 @@ func (s *Shell) read() ([]string, error) { | |
| } | ||
|
|
||
| func (s *Shell) readMultiLinesFunc(f func(string) (keepReading bool)) (string, error) { | ||
| return s.readMultiLinesAccumFunc(func(line, _ string) bool { | ||
| return f(line) | ||
| }) | ||
| } | ||
|
|
||
| // readMultiLinesAccumFunc accumulates physical lines until the predicate | ||
| // returns false. The predicate receives the latest line and the cumulative | ||
| // buffer (lines joined by "\n"). | ||
| func (s *Shell) readMultiLinesAccumFunc(f func(line, accumulated string) (keepReading bool)) (string, error) { | ||
| return readMultiLinesAccumFromReader(s.readLine, s.reader.setMultiMode, f) | ||
| } | ||
|
|
||
| // readMultiLinesAccumFromReader is the inner accumulation loop, parameterized | ||
| // on the line reader and multi-mode toggle so tests can drive it without a real | ||
| // readline instance. | ||
| func readMultiLinesAccumFromReader( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this need to be its own function when it's never directly called and only ever called from
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @angelamayxie I added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That works -- it doesn't need to be unit tested here since it gets tested anyways in the Dolt BATS tests. |
||
| readLine func() (string, error), | ||
| setMulti func(bool), | ||
| f func(line, accumulated string) (keepReading bool), | ||
| ) (string, error) { | ||
| var lines bytes.Buffer | ||
| currentLine := 0 | ||
| var err error | ||
| for { | ||
| if currentLine == 1 { | ||
| // from second line, enable next line prompt. | ||
| s.reader.setMultiMode(true) | ||
| setMulti(true) | ||
| } | ||
| var line string | ||
| line, err = s.readLine() | ||
| line, err = readLine() | ||
| fmt.Fprint(&lines, line) | ||
| if !f(line) || err != nil { | ||
| if !f(line, lines.String()) || err != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. repeatedly calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there some way we can avoid repeatedly calling
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry about that @angelamayxie I completely scanned over and missed your original comment and focused on the twice scanning rather than the also inefficient lines.String() 🤕 I'll get this fixed |
||
| break | ||
| } | ||
| fmt.Fprintln(&lines) | ||
|
|
@@ -503,7 +535,7 @@ func (s *Shell) readMultiLinesFunc(f func(string) (keepReading bool)) (string, e | |
| if currentLine > 0 { | ||
| // if more than one line is read | ||
| // revert to standard prompt. | ||
| s.reader.setMultiMode(false) | ||
| setMulti(false) | ||
| } | ||
| return lines.String(), err | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package ishell | ||
|
|
||
| import ( | ||
| "io" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // linesReader returns a function that yields the supplied lines one at a | ||
| // time, then returns io.EOF on subsequent calls. | ||
| func linesReader(lines []string) func() (string, error) { | ||
| i := 0 | ||
| return func() (string, error) { | ||
| if i >= len(lines) { | ||
| return "", io.EOF | ||
| } | ||
| l := lines[i] | ||
| i++ | ||
| return l, nil | ||
| } | ||
| } | ||
|
|
||
| func TestReadMultiLinesAccumFromReaderTerminator(t *testing.T) { | ||
| // Predicate stops accumulation once the buffer contains "GO", regardless of | ||
| // per-line semicolon terminators. | ||
| read := linesReader([]string{"select 1;", "select 2;", "GO"}) | ||
| multiCalls := []bool{} | ||
| pred := func(line, accumulated string) bool { | ||
| return !strings.Contains(accumulated, "GO") | ||
| } | ||
| out, err := readMultiLinesAccumFromReader(read, func(b bool) { multiCalls = append(multiCalls, b) }, pred) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "select 1;\nselect 2;\nGO", out) | ||
| // setMulti(true) once at currentLine==1, setMulti(false) once at end. | ||
| assert.Equal(t, []bool{true, false}, multiCalls) | ||
| } | ||
|
|
||
| func TestReadMultiLinesAccumFromReaderSingleLine(t *testing.T) { | ||
| // First line ends accumulation; setMulti is never called. | ||
| read := linesReader([]string{"select 1;"}) | ||
| multiCalls := []bool{} | ||
| pred := func(line, accumulated string) bool { return false } | ||
| out, err := readMultiLinesAccumFromReader(read, func(b bool) { multiCalls = append(multiCalls, b) }, pred) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "select 1;", out) | ||
| assert.Empty(t, multiCalls) | ||
| } | ||
|
|
||
| func TestReadMultiLinesAccumFromReaderEmpty(t *testing.T) { | ||
| // First line is empty and predicate returns false: shell never enters | ||
| // multi-mode; output is empty. This mirrors the empty-Enter path. | ||
| read := linesReader([]string{""}) | ||
| multiCalls := []bool{} | ||
| pred := func(line, accumulated string) bool { return false } | ||
| out, err := readMultiLinesAccumFromReader(read, func(b bool) { multiCalls = append(multiCalls, b) }, pred) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "", out) | ||
| assert.Empty(t, multiCalls) | ||
| } | ||
|
|
||
| func TestReadMultiLinesAccumFromReaderEOFMidStream(t *testing.T) { | ||
| // EOF arriving after the predicate has chosen to keep reading surfaces | ||
| // the EOF error and returns the buffer collected so far. The trailing | ||
| // "\n" is the inter-line separator written by fmt.Fprintln before the | ||
| // next readLine call returned EOF. | ||
| read := linesReader([]string{"select 1;"}) | ||
| pred := func(line, accumulated string) bool { return true } | ||
| out, err := readMultiLinesAccumFromReader(read, func(b bool) {}, pred) | ||
| assert.Equal(t, io.EOF, err) | ||
| assert.Equal(t, "select 1;\n", out) | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think a new function is necessary. the original
readMultiLinesFuncand calls to it can just be modified.