Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
601948f
feat: implement :serverlist and :help interactive commands
dlevy-msft-sql Feb 5, 2026
8fd1d8f
feat: implement :serverlist and :help interactive commands
dlevy-msft-sql Feb 5, 2026
1af7232
Merge branch 'main' into serverlist-command
dlevy-msft-sql Feb 5, 2026
5be11cb
fix: MSSQLSERVER dual-entry bug, README batch script, and test DB dep…
dlevy-msft-sql Apr 17, 2026
e915b54
fix: ListLocalServers returns error instead of writing to os.Stderr
dlevy-msft-sql Apr 20, 2026
b5053d1
docs: document parseInstances behavior change for missing InstanceName
dlevy-msft-sql Apr 24, 2026
99d6d87
Merge origin/main into serverlist-command
dlevy-msft-sql Sep 4, 2026
3b63775
test: make server list tests deterministic
dlevy-msft-sql Sep 4, 2026
473d86e
docs: correct server list error handling
dlevy-msft-sql Sep 4, 2026
5ae47e9
fix: return server list write errors
dlevy-msft-sql Sep 4, 2026
67e52f0
fix: use platform line endings for server lists
dlevy-msft-sql Sep 4, 2026
ed449a1
fix: shorten server discovery timeout
dlevy-msft-sql Sep 4, 2026
70bb344
fix: handle unavailable browser responses
dlevy-msft-sql Sep 4, 2026
adf44d6
fix: route server list errors to stderr
dlevy-msft-sql Sep 4, 2026
3253d9d
fix: retain unterminated browser response
dlevy-msft-sql Sep 4, 2026
6e2ef50
fix: normalize server list line endings
dlevy-msft-sql Sep 4, 2026
a0165f5
fix: validate server list output writes
dlevy-msft-sql Sep 4, 2026
a9ad46e
docs: correct interactive command help
dlevy-msft-sql Sep 4, 2026
137ad3f
fix: honor server list error redirection
dlevy-msft-sql Sep 4, 2026
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,37 @@ switches are most important to you to have implemented next in the new sqlcmd.
- `:Connect` now has an optional `-G` parameter to select one of the authentication methods for Azure SQL Database - `SqlAuthentication`, `ActiveDirectoryDefault`, `ActiveDirectoryIntegrated`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, `ActiveDirectoryPassword`. If `-G` is not provided, either Integrated security or SQL Authentication will be used, dependent on the presence of a `-U` username parameter.
- The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces.
- Sqlcmd can now print results using a vertical format. Use the new `--vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable.
- `:help` displays a list of available sqlcmd commands.
Comment thread
dlevy-msft-sql marked this conversation as resolved.
- `:serverlist` lists local SQL Server instances discovered via the SQL Server Browser service (UDP port 1434). The command queries the SQL Browser service and displays the server name and instance name for each discovered instance. If no instances are found or the Browser service cannot be reached, no output is produced. Other post-connect errors are printed to stderr.
- Sqlcmd defaults to a horizontal output format (space separated, no borders). To use the new ASCII table format, use the new `--ascii` command line option or set `SQLCMDFORMAT` to `ascii` (`-v SQLCMDFORMAT=ascii`). Note that when using the ASCII table format, individual column widths are determined by the content, but the `SQLCMDCOLWIDTH` variable and the `-w` parameter are still used to control the maximum screen width, determining when columns wrap into separate table segments. The following variables are ignored: `SQLCMDMAXFIXEDTYPEWIDTH`, `SQLCMDMAXVARTYPEWIDTH`, and `SQLCMDHEADERS`.

```
1> :serverlist
MYSERVER\SQL2019
MYSERVER\SQL2022
```

#### Using :serverlist in batch scripts

When automating server discovery, capture output and errors separately:

```batch
@echo off
REM Discover local SQL Server instances and connect to the first one
sqlcmd -Q ":serverlist" 2> errors.log > servers.txt
if exist errors.log for %%I in (errors.log) do if %%~zI gtr 0 (
type errors.log
exit /b 1
)
for /f "tokens=1" %%s in (servers.txt) do (
echo Connecting to %%s...
sqlcmd -S %%s -Q "SELECT @@SERVERNAME"
goto :done
)
echo No SQL Server instances found
:done
```

```
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid
2> go
Expand Down
85 changes: 5 additions & 80 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
package sqlcmd

import (
"context"
"errors"
"fmt"
"net"
"os"
"regexp"
"runtime/trace"
"strconv"
"strings"
"time"

mssql "github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/azuread"
"github.com/microsoft/go-mssqldb/msdsn"
"github.com/microsoft/go-sqlcmd/internal/localizer"
"github.com/microsoft/go-sqlcmd/pkg/console"
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
Expand Down Expand Up @@ -239,10 +235,12 @@ func Execute(version string) {
// emulate -L returning no servers
if args.ListServers != "" {
if args.ListServers != "c" {
fmt.Println()
fmt.Println(localizer.Sprintf("Servers:"))
_, _ = fmt.Fprint(os.Stdout, sqlcmd.SqlcmdEol)
_, _ = fmt.Fprintf(os.Stdout, "%s%s", localizer.Sprintf("Servers:"), sqlcmd.SqlcmdEol)
}
if err := sqlcmd.ListLocalServers(os.Stdout); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v%s", err, sqlcmd.SqlcmdEol)
}
listLocalServers()
os.Exit(0)
}
if len(argss) > 0 {
Expand Down Expand Up @@ -957,76 +955,3 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
s.SetError(nil)
return s.Exitcode, err
}

func listLocalServers() {
bmsg := []byte{byte(msdsn.BrowserAllInstances)}
resp := make([]byte, 16*1024-1)
dialer := &net.Dialer{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
conn, err := dialer.DialContext(ctx, "udp", ":1434")
// silently ignore failures to connect, same as ODBC
if err != nil {
return
}
defer conn.Close()
dl, _ := ctx.Deadline()
_ = conn.SetDeadline(dl)
_, err = conn.Write(bmsg)
if err != nil {
if !errors.Is(err, os.ErrDeadlineExceeded) {
fmt.Println(err)
}
return
}
read, err := conn.Read(resp)
if err != nil {
if !errors.Is(err, os.ErrDeadlineExceeded) {
fmt.Println(err)
}
return
}

data := parseInstances(resp[:read])
instances := make([]string, 0, len(data))
for s := range data {
if s == "MSSQLSERVER" {

instances = append(instances, "(local)", data[s]["ServerName"])
} else {
instances = append(instances, fmt.Sprintf(`%s\%s`, data[s]["ServerName"], s))
}
}
for _, s := range instances {
fmt.Println(" ", s)
}
}

func parseInstances(msg []byte) msdsn.BrowserData {
results := msdsn.BrowserData{}
if len(msg) > 3 && msg[0] == 5 {
out_s := string(msg[3:])
tokens := strings.Split(out_s, ";")
instdict := map[string]string{}
got_name := false
var name string
for _, token := range tokens {
if got_name {
instdict[name] = token
got_name = false
} else {
name = token
if len(name) == 0 {
if len(instdict) == 0 {
break
}
results[strings.ToUpper(instdict["InstanceName"])] = instdict
instdict = map[string]string{}
continue
}
got_name = true
}
}
}
return results
}
75 changes: 75 additions & 0 deletions pkg/sqlcmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ func newCommands() Commands {
action: xmlCommand,
name: "XML",
},
"HELP": {
regex: regexp.MustCompile(`(?im)^[ \t]*:HELP(?:[ \t]+(.*$)|$)`),
action: helpCommand,
name: "HELP",
},
"SERVERLIST": {
regex: regexp.MustCompile(`(?im)^[ \t]*:SERVERLIST(?:[ \t]+(.*$)|$)`),
action: serverlistCommand,
name: "SERVERLIST",
},
}
}

Expand Down Expand Up @@ -596,6 +606,71 @@ func xmlCommand(s *Sqlcmd, args []string, line uint) error {
return nil
}

// helpCommand displays the list of available sqlcmd commands
func helpCommand(s *Sqlcmd, args []string, line uint) error {
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
return InvalidCommandError("HELP", line)
}
helpText := `:!! [<command>]
- Executes a command in the operating system shell.
:connect server[\instance] [-l timeout] [-U user [-P password]] [-D database] [-G authentication-method]
- Connects to a SQL Server instance.
:ed
- Edits the current or last executed statement cache.
:error <dest>
- Redirects error output to a file, stderr, or stdout.
:exit
- Quits sqlcmd immediately.
:exit()
- Execute statement cache; quit with no return value.
:exit(<query>)
- Execute the specified query; returns numeric result.
go [<n>]
- Executes the statement cache (n times).
:help
- Shows this list of commands.
:list
- Prints the content of the statement cache.
:listvar
- Lists the set sqlcmd scripting variables.
:on error [exit|ignore]
- Action for batch or sqlcmd command errors.
:out <filename>|stderr|stdout
- Redirects query output to a file, stderr, or stdout.
:quit
- Quits sqlcmd immediately.
:r <filename>
- Append file contents to the statement cache.
:reset
- Discards the statement cache.
:serverlist
- Lists local SQL Server instances.
:setvar <variable>
- Removes a sqlcmd scripting variable.
:setvar <variable> <value>
- Sets a sqlcmd scripting variable.
:xml [on|off]
- Sets XML output mode.
`
_, err := s.GetOutput().Write([]byte(helpText))
return err
}

// serverlistCommand lists locally available SQL Server instances
func serverlistCommand(s *Sqlcmd, args []string, line uint) error {
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
return InvalidCommandError("SERVERLIST", line)
}
if err := ListLocalServers(s.GetOutput()); err != nil {
errorOutput := s.err
if errorOutput == nil {
errorOutput = os.Stderr
}
s.WriteError(errorOutput, err)
}
return nil
}

func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) {
var b *strings.Builder
end := len(arg)
Expand Down
31 changes: 31 additions & 0 deletions pkg/sqlcmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func TestCommandParsing(t *testing.T) {
{`:XML ON `, "XML", []string{`ON `}},
{`:RESET`, "RESET", []string{""}},
{`RESET`, "RESET", []string{""}},
{`:HELP`, "HELP", []string{""}},
{`:help`, "HELP", []string{""}},
{`:SERVERLIST`, "SERVERLIST", []string{""}},
{`:serverlist`, "SERVERLIST", []string{""}},
}

for _, test := range commands {
Expand Down Expand Up @@ -464,3 +468,30 @@ func TestExitCommandAppendsParameterToCurrentBatch(t *testing.T) {
}

}

func TestHelpCommand(t *testing.T) {
v := InitializeVariables(false)
s := New(nil, "", v)
buf := &memoryBuffer{buf: new(bytes.Buffer)}
s.SetOutput(buf)
defer func() { _ = buf.Close() }()

err := helpCommand(s, []string{""}, 1)
assert.NoError(t, err, "helpCommand should not error")

output := buf.buf.String()
// Verify key commands are listed
assert.Contains(t, output, ":connect", "help should list :connect")
assert.Contains(t, output, ":exit", "help should list :exit")
assert.Contains(t, output, ":help", "help should list :help")
assert.Contains(t, output, ":setvar", "help should list :setvar")
assert.Contains(t, output, ":listvar", "help should list :listvar")
assert.Contains(t, output, ":out", "help should list :out")
assert.Contains(t, output, ":error", "help should list :error")
assert.Contains(t, output, ":r", "help should list :r")
assert.Contains(t, output, ":serverlist", "help should list :serverlist")
assert.Contains(t, output, "go [<n>]", "help should list go")
assert.Contains(t, output, `:connect server[\instance] [-l timeout] [-U user [-P password]] [-D database] [-G authentication-method]`)
assert.Contains(t, output, ":setvar <variable>")
assert.NotContains(t, output, ":setvar {variable}")
}
Loading