Skip to content

Commit db48592

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 56b1fb1 commit db48592

File tree

6 files changed

+985
-722
lines changed

6 files changed

+985
-722
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,41 @@ switches are most important to you to have implemented next in the new sqlcmd.
154154
- `: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.
155155
- 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.
156156
- 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.
157+
- `:help` displays a list of available sqlcmd commands.
158+
- `: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.
159+
160+
```
161+
1> :serverlist
162+
MYSERVER\SQL2019
163+
MYSERVER\SQL2022
164+
```
165+
166+
#### Using :serverlist in batch scripts
167+
168+
When automating server discovery, you can capture the output and check for errors:
169+
170+
```batch
171+
@echo off
172+
REM Discover local SQL Server instances and connect to the first one
173+
sqlcmd -Q ":serverlist" 2>nul > servers.txt
174+
if %errorlevel% neq 0 (
175+
echo Error discovering servers
176+
exit /b 1
177+
)
178+
for /f "tokens=1" %%s in (servers.txt) do (
179+
echo Connecting to %%s...
180+
sqlcmd -S %%s -Q "SELECT @@SERVERNAME"
181+
goto :done
182+
)
183+
echo No SQL Server instances found
184+
:done
185+
```
186+
187+
To capture stderr separately (for error logging):
188+
```batch
189+
sqlcmd -Q ":serverlist" 2>errors.log > servers.txt
190+
if exist errors.log if not "%%~z errors.log"=="0" type errors.log
191+
```
157192

158193
```
159194
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 {
@@ -911,76 +907,3 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
911907
s.SetError(nil)
912908
return s.Exitcode, err
913909
}
914-
915-
func listLocalServers() {
916-
bmsg := []byte{byte(msdsn.BrowserAllInstances)}
917-
resp := make([]byte, 16*1024-1)
918-
dialer := &net.Dialer{}
919-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
920-
defer cancel()
921-
conn, err := dialer.DialContext(ctx, "udp", ":1434")
922-
// silently ignore failures to connect, same as ODBC
923-
if err != nil {
924-
return
925-
}
926-
defer conn.Close()
927-
dl, _ := ctx.Deadline()
928-
_ = conn.SetDeadline(dl)
929-
_, err = conn.Write(bmsg)
930-
if err != nil {
931-
if !errors.Is(err, os.ErrDeadlineExceeded) {
932-
fmt.Println(err)
933-
}
934-
return
935-
}
936-
read, err := conn.Read(resp)
937-
if err != nil {
938-
if !errors.Is(err, os.ErrDeadlineExceeded) {
939-
fmt.Println(err)
940-
}
941-
return
942-
}
943-
944-
data := parseInstances(resp[:read])
945-
instances := make([]string, 0, len(data))
946-
for s := range data {
947-
if s == "MSSQLSERVER" {
948-
949-
instances = append(instances, "(local)", data[s]["ServerName"])
950-
} else {
951-
instances = append(instances, fmt.Sprintf(`%s\%s`, data[s]["ServerName"], s))
952-
}
953-
}
954-
for _, s := range instances {
955-
fmt.Println(" ", s)
956-
}
957-
}
958-
959-
func parseInstances(msg []byte) msdsn.BrowserData {
960-
results := msdsn.BrowserData{}
961-
if len(msg) > 3 && msg[0] == 5 {
962-
out_s := string(msg[3:])
963-
tokens := strings.Split(out_s, ";")
964-
instdict := map[string]string{}
965-
got_name := false
966-
var name string
967-
for _, token := range tokens {
968-
if got_name {
969-
instdict[name] = token
970-
got_name = false
971-
} else {
972-
name = token
973-
if len(name) == 0 {
974-
if len(instdict) == 0 {
975-
break
976-
}
977-
results[strings.ToUpper(instdict["InstanceName"])] = instdict
978-
instdict = map[string]string{}
979-
continue
980-
}
981-
got_name = true
982-
}
983-
}
984-
}
985-
return results
986-
}

0 commit comments

Comments
 (0)