Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ func (s *shellActionsImpl) ReadPasswordErr() (string, error) {
}

func (s *shellActionsImpl) ReadMultiLinesFunc(f func(string) (keepReading bool)) string {
lines, _ := s.readMultiLinesFunc(f)
lines, _ := s.readMultiLinesFunc(func(line, _ string) (keepReading bool) {
return f(line)
})
return lines
}

Expand Down
6 changes: 6 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ type Context struct {
// RawArgs is unprocessed command arguments.
RawArgs []string

// Statements holds the complete statements parsed from an uninterpreted
// command by the configured statement scanner, in order. It is empty for
// non-statement input (e.g., slash commands, DELIMITER lines, empty input) and
// for interpreted (non-uninterpreted) shells.
Statements []string

// Cmd is the currently executing command. This is empty for NotFound and Interrupt.
Cmd Cmd

Expand Down
125 changes: 86 additions & 39 deletions ishell.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type Shell struct {
historyFile string
autoHelp bool
rawArgs []string
statements []string
progressBar ProgressBar
pager string
pagerArgs []string
Expand Down Expand Up @@ -361,45 +362,12 @@ func (s *Shell) readLine() (line string, err error) {

func (s *Shell) readUninterpreted() (string, error) {
s.rawArgs = nil
s.statements = nil
var lines string
var err error

if s.lineTerminator != "" {
firstLine := true
lines, err = s.readMultiLinesFunc(func(line string) (keepReading bool) {
if firstLine {
firstLine = false
if matches := delimiterRegex.FindStringSubmatch(line); len(matches) == 2 {
s.lineTerminator = matches[1]
return false
}
if strings.HasPrefix(line, "--") {
return false
}
for _, keyword := range s.quitKeywords {
if strings.TrimSpace(line) == keyword {
return false
}
}
}

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
})

lines, err = s.readUninterpretedCommand()
if err != nil {
return "", err
}
Expand All @@ -408,7 +376,7 @@ func (s *Shell) readUninterpreted() (string, error) {
heredoc := false

// heredoc multiline
lines, err = s.readMultiLinesFunc(func(line string) (keepReading bool) {
lines, err = s.readMultiLinesFunc(func(line, _ string) (keepReading bool) {
if !heredoc {
if strings.Contains(line, "<<") {
s := strings.SplitN(line, "<<", 2)
Expand Down Expand Up @@ -437,13 +405,87 @@ func (s *Shell) readUninterpreted() (string, error) {
return lines, nil
}

// readUninterpretedCommand reads one complete command from a delimiter-based
// uninterpreted shell using a single streaming scan. It reads the first line,
// handles the line-level cases that produce no statements (empty input,
// DELIMITER, leading "--", quit keywords, backslash commands), and otherwise
// feeds the input through one StreamScanner to collect the command's
// statements. The statements are stashed in s.statements for delivery on the
// Context; the returned string is the raw command text (lines joined by "\n").
func (s *Shell) readUninterpretedCommand() (string, error) {
firstLine, err := s.readLine()
if err != nil {
return firstLine, err
}

trimmed := strings.TrimSpace(firstLine)

if trimmed == "" {
return firstLine, nil
}

if matches := delimiterRegex.FindStringSubmatch(firstLine); len(matches) == 2 {
s.lineTerminator = matches[1]
return firstLine, nil
}

if strings.HasPrefix(firstLine, "--") {
return firstLine, nil
}

for _, keyword := range s.quitKeywords {
if trimmed == keyword {
return firstLine, nil
}
}

for _, sc := range s.backSlashCmds {
if strings.HasPrefix(trimmed, sc) {
return firstLine, nil
}
}

// Scan the input (possibly multi-line) into statements with a single streaming pass.
// The reader blocks for continuation lines while the scanner is mid-statement, and
// stops at a special terminator (\g/\G).
lr := newUninterpretedReader(firstLine, s.readLine, s.reader.setMultiMode, s.specialTerminators)
scanner := NewStreamScannerWithDelimiter(lr, s.lineTerminator)
// DELIMITER is handled at the line level above; disable in-stream detection
// so a short complete statement does not block on the lookahead read.
scanner.IgnoreDelimiterStatements()
var statements []string
for scanner.Scan() {
if t := strings.TrimSpace(scanner.Text()); t != "" {
statements = append(statements, t)
}
// The command is complete once the scanner is at a clean boundary with
// no further buffered input; stop before triggering a blocking read.
if !scanner.InsideQuote() && !scanner.InsideBlockComment() && !scanner.HasBufferedToken() {
break
}
}
// Revert to the primary prompt for the next command.
s.reader.setMultiMode(false)
if lr.aborted {
// EOF (e.g. Ctrl-D) arrived at the continuation prompt before the command
// was terminated. Discard the partial statement and surface EOF so the
// shell handles it as end-of-input rather than executing the fragment.
s.statements = nil
return "", io.EOF
}
// DELIMITER is handled at the line level above; the scanner's delimiter is
// unchanged here, so there is nothing to sync back.
s.statements = statements
return lr.Raw(), scanner.Err()
}

func (s *Shell) read() ([]string, error) {
s.rawArgs = nil
eof := ""
heredoc := false

// heredoc multiline
lines, err := s.readMultiLinesFunc(func(line string) (keepReading bool) {
lines, err := s.readMultiLinesFunc(func(line, _ string) (keepReading bool) {
if !heredoc {
if strings.Contains(line, "<<") {
s := strings.SplitN(line, "<<", 2)
Expand Down Expand Up @@ -482,7 +524,11 @@ func (s *Shell) read() ([]string, error) {
return args, err
}

func (s *Shell) readMultiLinesFunc(f func(string) (keepReading bool)) (string, error) {
// readMultiLinesFunc accumulates physical lines until the predicate returns
// false. The predicate receives the latest line and the cumulative buffer
// (lines joined by "\n"); callers that only need the line can ignore the
// second argument.
func (s *Shell) readMultiLinesFunc(f func(line, accumulated string) (keepReading bool)) (string, error) {
var lines bytes.Buffer
currentLine := 0
var err error
Expand All @@ -494,7 +540,7 @@ func (s *Shell) readMultiLinesFunc(f func(string) (keepReading bool)) (string, e
var line string
line, err = s.readLine()
fmt.Fprint(&lines, line)
if !f(line) || err != nil {
if !f(line, lines.String()) || err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repeatedly calling lines.String() to get the accumulated string does not seem performant to me. Also, looking at how it's integrated with Dolt, it seems like everything is getting scanned twice, first via the iShell and then again with the sql scanner. And if it's multiple lines, then the first line gets scanned n times, second line n-1 times, and so on? Ideally, the input should just get scanned once.

break
}
fmt.Fprintln(&lines)
Expand Down Expand Up @@ -825,6 +871,7 @@ func newContext(s *Shell, cmd *Cmd, args []string) *Context {
progressBar: copyShellProgressBar(s),
Args: args,
RawArgs: s.rawArgs,
Statements: s.statements,
Cmd: *cmd,
contextValues: func() contextValues {
values := contextValues{}
Expand Down
Loading