Skip to content

Commit 601948f

Browse files
feat: implement :serverlist and :help interactive commands
- :serverlist queries SQL Browser service (UDP 1434) to discover instances - :help displays available sqlcmd commands - Refactored server listing logic to pkg/sqlcmd/serverlist.go for reuse by both -L flag and :serverlist command
1 parent 460b0c9 commit 601948f

6 files changed

Lines changed: 985 additions & 722 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,41 @@ switches are most important to you to have implemented next in the new sqlcmd.
172172
- `: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.
173173
- 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.
174174
- 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.
175+
- `:help` displays a list of available sqlcmd commands.
176+
- `: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 is not running, no output is produced. Non-timeout errors are printed to stderr.
177+
178+
```
179+
1> :serverlist
180+
MYSERVER\SQL2019
181+
MYSERVER\SQL2022
182+
```
183+
184+
#### Using :serverlist in batch scripts
185+
186+
When automating server discovery, you can capture the output and check for errors:
187+
188+
```batch
189+
@echo off
190+
REM Discover local SQL Server instances and connect to the first one
191+
sqlcmd -Q ":serverlist" 2>nul > servers.txt
192+
if %errorlevel% neq 0 (
193+
echo Error discovering servers
194+
exit /b 1
195+
)
196+
for /f "tokens=1" %%s in (servers.txt) do (
197+
echo Connecting to %%s...
198+
sqlcmd -S %%s -Q "SELECT @@SERVERNAME"
199+
goto :done
200+
)
201+
echo No SQL Server instances found
202+
:done
203+
```
204+
205+
To capture stderr separately (for error logging):
206+
```batch
207+
sqlcmd -Q ":serverlist" 2>errors.log > servers.txt
208+
if exist errors.log if not "%%~z errors.log"=="0" type errors.log
209+
```
175210

176211
```
177212
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid

cmd/sqlcmd/sqlcmd.go

Lines changed: 1 addition & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,16 @@
55
package sqlcmd
66

77
import (
8-
"context"
98
"errors"
109
"fmt"
11-
"net"
1210
"os"
1311
"regexp"
1412
"runtime/trace"
1513
"strconv"
1614
"strings"
17-
"time"
1815

1916
mssql "github.com/microsoft/go-mssqldb"
2017
"github.com/microsoft/go-mssqldb/azuread"
21-
"github.com/microsoft/go-mssqldb/msdsn"
2218
"github.com/microsoft/go-sqlcmd/internal/localizer"
2319
"github.com/microsoft/go-sqlcmd/pkg/console"
2420
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
@@ -236,7 +232,7 @@ func Execute(version string) {
236232
fmt.Println()
237233
fmt.Println(localizer.Sprintf("Servers:"))
238234
}
239-
listLocalServers()
235+
sqlcmd.ListLocalServers(os.Stdout)
240236
os.Exit(0)
241237
}
242238
if len(argss) > 0 {
@@ -915,76 +911,3 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
915911
s.SetError(nil)
916912
return s.Exitcode, err
917913
}
918-
919-
func listLocalServers() {
920-
bmsg := []byte{byte(msdsn.BrowserAllInstances)}
921-
resp := make([]byte, 16*1024-1)
922-
dialer := &net.Dialer{}
923-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
924-
defer cancel()
925-
conn, err := dialer.DialContext(ctx, "udp", ":1434")
926-
// silently ignore failures to connect, same as ODBC
927-
if err != nil {
928-
return
929-
}
930-
defer conn.Close()
931-
dl, _ := ctx.Deadline()
932-
_ = conn.SetDeadline(dl)
933-
_, err = conn.Write(bmsg)
934-
if err != nil {
935-
if !errors.Is(err, os.ErrDeadlineExceeded) {
936-
fmt.Println(err)
937-
}
938-
return
939-
}
940-
read, err := conn.Read(resp)
941-
if err != nil {
942-
if !errors.Is(err, os.ErrDeadlineExceeded) {
943-
fmt.Println(err)
944-
}
945-
return
946-
}
947-
948-
data := parseInstances(resp[:read])
949-
instances := make([]string, 0, len(data))
950-
for s := range data {
951-
if s == "MSSQLSERVER" {
952-
953-
instances = append(instances, "(local)", data[s]["ServerName"])
954-
} else {
955-
instances = append(instances, fmt.Sprintf(`%s\%s`, data[s]["ServerName"], s))
956-
}
957-
}
958-
for _, s := range instances {
959-
fmt.Println(" ", s)
960-
}
961-
}
962-
963-
func parseInstances(msg []byte) msdsn.BrowserData {
964-
results := msdsn.BrowserData{}
965-
if len(msg) > 3 && msg[0] == 5 {
966-
out_s := string(msg[3:])
967-
tokens := strings.Split(out_s, ";")
968-
instdict := map[string]string{}
969-
got_name := false
970-
var name string
971-
for _, token := range tokens {
972-
if got_name {
973-
instdict[name] = token
974-
got_name = false
975-
} else {
976-
name = token
977-
if len(name) == 0 {
978-
if len(instdict) == 0 {
979-
break
980-
}
981-
results[strings.ToUpper(instdict["InstanceName"])] = instdict
982-
instdict = map[string]string{}
983-
continue
984-
}
985-
got_name = true
986-
}
987-
}
988-
}
989-
return results
990-
}

0 commit comments

Comments
 (0)