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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ sqlcmd

If no current context exists, `sqlcmd` (with no connection parameters) reverts to the original ODBC `sqlcmd` behavior of creating an interactive session to the default local instance on port 1433 using trusted authentication, otherwise it will create an interactive session to the current context.

### Interactive Mode Commands

In interactive mode, `sqlcmd` supports several special commands. The `EXIT` command can execute a query and use its result as the exit code:

```
1> EXIT(SELECT 100)
```

For complex queries, `EXIT(query)` can span multiple lines. When parentheses are unbalanced, `sqlcmd` prompts for continuation:

```
1> EXIT(SELECT 1
-> + 2
-> + 3)
```

The query result (6 in this example) becomes the process exit code.

### Piping input to sqlcmd

You can pipe SQL commands directly to `sqlcmd` from the command line. This is useful for scripting and automation:
Expand Down Expand Up @@ -163,7 +181,6 @@ The following switches have different behavior in this version of `sqlcmd` compa
- 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>
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
- 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.
- `-i` doesn't handle a comma `,` in a file name correctly unless the file name argument is triple quoted. For example:
`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`
- 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`
Expand Down
25 changes: 25 additions & 0 deletions pkg/sqlcmd/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package sqlcmd

import "strings"

const minCapIncrease = 512

// lineend is the slice to use when appending a line.
Expand Down Expand Up @@ -177,6 +179,29 @@ parse:
return command, args, err
}

func (b *Batch) nextQuery(query string) (*Command, []string, error) {
lines := strings.Split(query, "\n")
line := 0
read := b.read
defer func() {
b.read = read
}()
b.read = func() (string, error) {
value := strings.TrimSuffix(lines[line], "\r")
line++
return value, nil
}

b.Reset(nil)
for range lines {
command, args, err := b.Next()
if command != nil || err != nil {
return command, args, err
}
}
return nil, nil, nil
}

// append appends r to b.Buffer separated by sep when b.Buffer is not already empty.
//
// Dynamically grows b.Buf as necessary to accommodate r and the separator.
Expand Down
13 changes: 13 additions & 0 deletions pkg/sqlcmd/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ func TestBatchNextErrOnInvalidVariable(t *testing.T) {
}
}

func TestBatchNextQueryFindsCommandAfterNewlines(t *testing.T) {
b := NewBatch(nil, newCommands())

command, args, err := b.nextQuery("SELECT 2;\r\n\r\n:EXIT(SELECT 200)")

assert.NoError(t, err)
if assert.NotNil(t, command) {
assert.Equal(t, "EXIT", command.name)
}
assert.Equal(t, []string{"(SELECT 200)"}, args)
assert.Equal(t, "SELECT 2;"+SqlcmdEol, b.String())
}

func TestReadString(t *testing.T) {
tests := []struct {
// input string
Expand Down
131 changes: 130 additions & 1 deletion pkg/sqlcmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,114 @@ func (c Commands) SetBatchTerminator(terminator string) error {
return nil
}

// exitParenDepth returns the parenthesis depth of an EXIT command argument, or -1 if it over-closes.
// It tracks quotes to avoid counting parentheses inside string literals.
// It handles SQL Server's quote escaping: '' inside single-quoted strings, "" inside double-quoted strings, and ]] inside bracket identifiers.
// It also ignores parentheses inside SQL comments (-- single-line and /* multi-line */).
func exitParenDepth(s string) int {
depth := 0
var quote rune
inLineComment := false
inBlockComment := false
runes := []rune(s)
for i := 0; i < len(runes); i++ {
c := runes[i]

// Handle line comment state
if inLineComment {
// Line comment ends at newline
if c == '\n' {
inLineComment = false
}
continue
}

// Handle block comment state
if inBlockComment {
// Check for end of block comment
if c == '*' && i+1 < len(runes) && runes[i+1] == '/' {
inBlockComment = false
i++ // skip the '/'
}
continue
}

switch {
case quote != 0:
// Inside a quoted string
if c == quote {
// Check for escaped quote ('' or ]])
if i+1 < len(runes) && runes[i+1] == quote {
i++ // skip the escaped quote
} else {
quote = 0
}
}
case c == '-' && i+1 < len(runes) && runes[i+1] == '-':
// Start of single-line comment
inLineComment = true
i++ // skip the second '-'
case c == '/' && i+1 < len(runes) && runes[i+1] == '*':
// Start of block comment
inBlockComment = true
i++ // skip the '*'
case c == '\'' || c == '"':
quote = c
case c == '[':
quote = ']' // SQL Server bracket quoting
case c == '(':
depth++
case c == ')':
depth--
if depth < 0 {
return -1
}
}
}
return depth
}

func isExitParenBalanced(s string) bool {
return exitParenDepth(s) == 0
}

// readExitContinuation reads additional lines from the console until the EXIT
// parentheses are balanced. This enables multi-line EXIT(query) in interactive mode.
func readExitContinuation(s *Sqlcmd, params string, commandLine uint) (string, error) {
var builder strings.Builder
builder.WriteString(params)

// Save original prompt and restore it when done (if batch is initialized)
if s.batch != nil {
originalPrompt := s.Prompt()
defer s.lineIo.SetPrompt(originalPrompt)
}

for {
depth := exitParenDepth(builder.String())
if depth < 0 {
return "", InvalidCommandError("EXIT", commandLine)
}
if depth == 0 {
return builder.String(), nil
}

// Show continuation prompt
s.lineIo.SetPrompt(" -> ")
line, err := s.lineIo.Readline()
if err != nil {
return "", err
}
builder.WriteString(SqlcmdEol)
builder.WriteString(line)
}
}

// exitCommand has 3 modes.
// With no (), it just exits without running any query
// With () it runs whatever batch is in the buffer then exits
// With any text between () it runs the text as a query then exits
// In interactive mode, if parentheses are unbalanced, it prompts for continuation lines.
func exitCommand(s *Sqlcmd, args []string, line uint) error {
if len(args) == 0 {
return ErrExitRequested
Expand All @@ -220,9 +324,34 @@ func exitCommand(s *Sqlcmd, args []string, line uint) error {
if params == "" {
return ErrExitRequested
}
if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") {

// Check if we have an opening paren
if !strings.HasPrefix(params, "(") {
return InvalidCommandError("EXIT", line)
}

depth := exitParenDepth(params)
if depth < 0 {
return InvalidCommandError("EXIT", line)
}

// If parentheses are unbalanced, try to read continuation lines (interactive mode only)
if depth > 0 {
if s.lineIo == nil {
// Not in interactive mode, can't read more lines
return InvalidCommandError("EXIT", line)
}
var err error
params, err = readExitContinuation(s, params, line)
if err != nil {
return err
}
}
Comment thread
dlevy-msft-sql marked this conversation as resolved.

if !strings.HasSuffix(params, ")") {
return InvalidCommandError("EXIT", line)
}

// First we save the current batch
query1 := s.batch.String()
if len(query1) > 0 {
Expand Down
Loading