Skip to content
Closed
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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- `:HELP` command to display available sqlcmd commands
- `-p` flag for printing performance statistics after each batch
- `-j` flag for printing raw error messages without formatting
- `:PERFTRACE` command to redirect timing output to file
- `:SERVERLIST` command to list available SQL Server instances on the network
- Multi-line `EXIT(query)` support in interactive mode - queries with unbalanced parentheses now prompt for continuation

### Fixed
- Statistics format (`-p` flag) now matches ODBC sqlcmd output format
- Panic on empty args slice in command parser

### Changed
- **Breaking for go-sqlcmd users**: `-u` (Unicode output) no longer writes a UTF-16LE BOM (Byte Order Mark) to output files. This change aligns go-sqlcmd with ODBC sqlcmd behavior, which never wrote a BOM. If your workflows depended on the BOM being present, you may need to adjust them.

## Notes on ODBC sqlcmd Compatibility

This release significantly improves compatibility with the original ODBC-based sqlcmd:

| Feature | Previous go-sqlcmd | Now | ODBC sqlcmd |
|---------|-------------------|-----|-------------|
| `-u` output BOM | Wrote BOM | No BOM | No BOM ✓ |
| `-p` statistics format | Different format | Matches | Matches ✓ |
| `-r` without argument | Required argument | Defaults to 0 | Defaults to 0 ✓ |
| `EXIT(query)` multi-line | Not supported | Supported | Supported ✓ |
| `:HELP` command | Not available | Available | Available ✓ |
| `:SERVERLIST` command | Not available | Available | Available ✓ |
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,7 @@ The following switches have different behavior in this version of `sqlcmd` compa
- If both `-N` and `-C` are provided, sqlcmd will use their values for encryption negotiation.
- To provide the value of the host name in the server certificate when using strict encryption, pass the host name with `-F`. Example: `-Ns -F myhost.domain.com`
- 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 Expand Up @@ -175,6 +173,13 @@ program_name sqlcmd
net_transport Named pipe
```

- The new `-p` (`--print-statistics`) flag prints performance statistics after each batch execution, including network packet size, transaction count, and clock time (total, average, and transactions per second).
- The new `-j` (`--raw-errors`) flag prints raw error messages without the standard "Msg #, Level, State, Server, Line" prefix formatting.
- The new `:HELP` interactive command displays a list of all available sqlcmd commands with descriptions.
- The new `:PERFTRACE <filename>|STDERR|STDOUT` interactive command redirects timing output to a file or stream. This is useful when using `-p` to separate statistics from query output.
- The new `:SERVERLIST` interactive command lists local and network SQL Server instances (same as `-L` flag but available during an interactive session).
- `EXIT(query)` now supports multi-line queries in interactive mode. When an unclosed parenthesis is detected, sqlcmd prompts for additional input until the query is complete.

### Azure Active Directory Authentication

`sqlcmd` supports a broader range of AAD authentication models (over the original ODBC based `sqlcmd`), based on the [azidentity package](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity). The implementation relies on an AAD Connector in the [driver](https://github.com/microsoft/go-mssqldb).
Expand Down
90 changes: 11 additions & 79 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 @@ -82,6 +78,8 @@ type SQLCmdArguments struct {
ChangePassword string
ChangePasswordAndExit string
TraceFile string
PrintStatistics bool
RawErrors bool
// Keep Help at the end of the list
Help bool
}
Expand Down Expand Up @@ -236,7 +234,11 @@ func Execute(version string) {
fmt.Println()
fmt.Println(localizer.Sprintf("Servers:"))
}
listLocalServers()
instances, _ := sqlcmd.ListServers(0)
servers := sqlcmd.FormatServerList(instances)
for _, s := range servers {
fmt.Println(" ", s)
}
os.Exit(0)
}
if len(argss) > 0 {
Expand Down Expand Up @@ -479,6 +481,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
rootCmd.Flags().StringVarP(&args.ChangePasswordAndExit, "change-password-exit", "Z", "", localizer.Sprintf("New password and exit"))
rootCmd.Flags().BoolVarP(&args.PrintStatistics, "print-statistics", "p", false, localizer.Sprintf("Print performance statistics for each batch"))
rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Print raw error messages without additional formatting"))
}

func setScriptVariable(v string) string {
Expand Down Expand Up @@ -817,6 +821,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
}
s.EchoInput = args.EchoInput
s.PrintStatistics = args.PrintStatistics
if args.BatchTerminator != "GO" {
err = s.Cmd.SetBatchTerminator(args.BatchTerminator)
if err != nil {
Expand All @@ -828,7 +833,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
}

s.Connect = &connectConfig
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
s.Format = sqlcmd.NewSQLCmdDefaultFormatterWithOptions(args.TrimSpaces, args.getControlCharacterBehavior(), args.RawErrors)
if args.OutputFile != "" {
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
if err != nil {
Expand Down Expand Up @@ -911,76 +916,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
}
Binary file modified cmd/sqlcmd/testdata/unicodeout.txt
Binary file not shown.
Binary file modified cmd/sqlcmd/testdata/unicodeout_linux.txt
Binary file not shown.
Loading
Loading