Skip to content

Commit 724059a

Browse files
Implement multi-line EXIT(query) support
In interactive mode, EXIT(query) can now span multiple lines when parentheses are unbalanced. The shell prompts for continuation lines until parentheses balance, matching ODBC sqlcmd behavior. Changes: - Add isExitParenBalanced() to check paren balance respecting quotes - Add readExitContinuation() to prompt for additional lines - Update exitCommand() to call continuation when needed - Add TestIsExitParenBalanced tests - Remove limitation note from README
1 parent 56b1fb1 commit 724059a

3 files changed

Lines changed: 94 additions & 2 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ The following switches have different behavior in this version of `sqlcmd` compa
134134
- More information about client/server encryption negotiation can be found at <https://docs.microsoft.com/openspecs/windows_protocols/ms-tds/60f56408-0188-4cd5-8b90-25c6f2423868>
135135
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
136136
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
137-
- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The ODBC sqlcmd allows the query run by `EXIT(query)` to span multiple lines.
138137
- `-i` doesn't handle a comma `,` in a file name correctly unless the file name argument is triple quoted. For example:
139138
`sqlcmd -i """select,100.sql"""` will try to open a file named `sql,100.sql` while `sqlcmd -i "select,100.sql"` will try to open two files `select` and `100.sql`
140139
- If using a single `-i` flag to pass multiple file names, there must be a space after the `-i`. Example: `-i file1.sql file2.sql`

pkg/sqlcmd/commands.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,55 @@ func (c Commands) SetBatchTerminator(terminator string) error {
208208
return nil
209209
}
210210

211+
// isExitParenBalanced checks if the parentheses in an EXIT command argument are balanced.
212+
// It tracks quotes to avoid counting parens inside string literals.
213+
func isExitParenBalanced(s string) bool {
214+
depth := 0
215+
var quote rune
216+
for _, c := range s {
217+
switch {
218+
case quote != 0:
219+
// Inside a quoted string
220+
if c == quote {
221+
quote = 0
222+
}
223+
case c == '\'' || c == '"':
224+
quote = c
225+
case c == '[':
226+
quote = ']' // SQL Server bracket quoting
227+
case c == '(':
228+
depth++
229+
case c == ')':
230+
depth--
231+
}
232+
}
233+
return depth == 0
234+
}
235+
236+
// readExitContinuation reads additional lines from the console until the EXIT
237+
// parentheses are balanced. This enables multi-line EXIT(query) in interactive mode.
238+
func readExitContinuation(s *Sqlcmd, params string) (string, error) {
239+
var builder strings.Builder
240+
builder.WriteString(params)
241+
242+
for !isExitParenBalanced(builder.String()) {
243+
// Show continuation prompt
244+
s.lineIo.SetPrompt(" -> ")
245+
line, err := s.lineIo.Readline()
246+
if err != nil {
247+
return "", err
248+
}
249+
builder.WriteString(SqlcmdEol)
250+
builder.WriteString(line)
251+
}
252+
return builder.String(), nil
253+
}
254+
211255
// exitCommand has 3 modes.
212256
// With no (), it just exits without running any query
213257
// With () it runs whatever batch is in the buffer then exits
214258
// With any text between () it runs the text as a query then exits
259+
// In interactive mode, if parentheses are unbalanced, it prompts for continuation lines.
215260
func exitCommand(s *Sqlcmd, args []string, line uint) error {
216261
if len(args) == 0 {
217262
return ErrExitRequested
@@ -220,9 +265,29 @@ func exitCommand(s *Sqlcmd, args []string, line uint) error {
220265
if params == "" {
221266
return ErrExitRequested
222267
}
223-
if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") {
268+
269+
// Check if we have an opening paren
270+
if !strings.HasPrefix(params, "(") {
224271
return InvalidCommandError("EXIT", line)
225272
}
273+
274+
// If parentheses are unbalanced, try to read continuation lines (interactive mode only)
275+
if !isExitParenBalanced(params) {
276+
if s.lineIo == nil {
277+
// Not in interactive mode, can't read more lines
278+
return InvalidCommandError("EXIT", line)
279+
}
280+
var err error
281+
params, err = readExitContinuation(s, params)
282+
if err != nil {
283+
return err
284+
}
285+
}
286+
287+
if !strings.HasSuffix(params, ")") {
288+
return InvalidCommandError("EXIT", line)
289+
}
290+
226291
// First we save the current batch
227292
query1 := s.batch.String()
228293
if len(query1) > 0 {

pkg/sqlcmd/commands_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,31 @@ func TestExitCommandAppendsParameterToCurrentBatch(t *testing.T) {
458458
}
459459

460460
}
461+
func TestIsExitParenBalanced(t *testing.T) {
462+
tests := []struct {
463+
input string
464+
balanced bool
465+
}{
466+
{"()", true},
467+
{"(select 1)", true},
468+
{"(select 1", false},
469+
{"(select (1 + 2))", true},
470+
{"(select ')')", true}, // paren inside string
471+
{"(select \"(\")", true}, // paren inside double-quoted string
472+
{"(select [col)])", true}, // paren inside bracket-quoted identifier
473+
{"(select 1) extra", true}, // balanced even with trailing text
474+
{"((nested))", true},
475+
{"((nested)", false},
476+
{"", true}, // empty string is balanced
477+
{"no parens", true}, // no parens is balanced
478+
{"(", false},
479+
{")", false}, // depth goes -1, not balanced
480+
{"(test))", false}, // depth goes -1 at end
481+
}
482+
for _, test := range tests {
483+
t.Run(test.input, func(t *testing.T) {
484+
result := isExitParenBalanced(test.input)
485+
assert.Equal(t, test.balanced, result, "isExitParenBalanced(%q)", test.input)
486+
})
487+
}
488+
}

0 commit comments

Comments
 (0)