Skip to content

Commit 5e0a81e

Browse files
Add --no-bom flag for ODBC sqlcmd compatibility
By default, -u (unicode output) includes a UTF-16 LE BOM (FF FE) at the start of output files. ODBC sqlcmd does not write a BOM. This adds --no-bom flag to omit the BOM when strict ODBC compatibility is needed. Usage: sqlcmd -u --no-bom -o output.txt Changes: - Add NoBOM field to SQLCmdArguments and Sqlcmd structs - Add --no-bom flag with descriptive help - Conditionally use unicode.IgnoreBOM when flag is set - Update README to document the difference and new flag - Add TestUnicodeOutputNoBOM test
1 parent 56b1fb1 commit 5e0a81e

5 files changed

Lines changed: 38 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ The following switches have different behavior in this version of `sqlcmd` compa
132132
- If both `-N` and `-C` are provided, sqlcmd will use their values for encryption negotiation.
133133
- 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`
134134
- 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>
135-
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
135+
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it. ODBC sqlcmd does not write a BOM; use `--no-bom` with `-u` if you need strict ODBC compatibility.
136136
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
137137
- 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.
138138
- `-i` doesn't handle a comma `,` in a file name correctly unless the file name argument is triple quoted. For example:

cmd/sqlcmd/sqlcmd.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type SQLCmdArguments struct {
6666
ErrorsToStderr *int
6767
Headers int
6868
UnicodeOutputFile bool
69+
NoBOM bool
6970
Version bool
7071
ColumnSeparator string
7172
ScreenWidth *int
@@ -457,6 +458,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
457458
rootCmd.Flags().IntVarP(&args.Headers, "headers", "h", 0, localizer.Sprintf("Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed"))
458459

459460
rootCmd.Flags().BoolVarP(&args.UnicodeOutputFile, "unicode-output-file", "u", false, localizer.Sprintf("Specifies that all output files are encoded with little-endian Unicode"))
461+
rootCmd.Flags().BoolVar(&args.NoBOM, "no-bom", false, localizer.Sprintf("Omit the UTF-16 BOM from Unicode output files. Use with -u for ODBC sqlcmd compatibility"))
460462
rootCmd.Flags().StringVarP(&args.ColumnSeparator, "column-separator", "s", "", localizer.Sprintf("Specifies the column separator character. Sets the %s variable.", localizer.ColSeparatorVar))
461463
rootCmd.Flags().BoolVarP(&args.TrimSpaces, "trim-spaces", "W", false, localizer.Sprintf("Remove trailing spaces from a column"))
462464
_ = rootCmd.Flags().BoolP("multi-subnet-failover", "M", false, localizer.Sprintf("Provided for backward compatibility. Sqlcmd always optimizes detection of the active replica of a SQL Failover Cluster"))
@@ -812,6 +814,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
812814
s.SetupCloseHandler()
813815
defer s.StopCloseHandler()
814816
s.UnicodeOutputFile = args.UnicodeOutputFile
817+
s.NoBOM = args.NoBOM
815818

816819
if args.DisableCmd != nil {
817820
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,32 @@ func TestUnicodeOutput(t *testing.T) {
298298
}
299299
}
300300

301+
func TestUnicodeOutputNoBOM(t *testing.T) {
302+
o, err := os.CreateTemp("", "sqlcmdnobom")
303+
assert.NoError(t, err, "os.CreateTemp")
304+
defer os.Remove(o.Name())
305+
defer o.Close()
306+
args = newArguments()
307+
args.InputFile = []string{"testdata/selectutf8.txt"}
308+
args.OutputFile = o.Name()
309+
args.UnicodeOutputFile = true
310+
args.NoBOM = true
311+
setAzureAuthArgIfNeeded(&args)
312+
vars := sqlcmd.InitializeVariables(args.useEnvVars())
313+
setVars(vars, &args)
314+
315+
exitCode, err := run(vars, &args)
316+
assert.NoError(t, err, "run")
317+
assert.Equal(t, 0, exitCode, "exitCode")
318+
fileBytes, err := os.ReadFile(o.Name())
319+
if assert.NoError(t, err, "os.ReadFile") {
320+
// With --no-bom, the file should NOT start with FF FE (UTF-16 LE BOM)
321+
assert.True(t, len(fileBytes) >= 2, "output file should have content")
322+
hasBOM := len(fileBytes) >= 2 && fileBytes[0] == 0xFF && fileBytes[1] == 0xFE
323+
assert.False(t, hasBOM, "output file should NOT have BOM when --no-bom is used")
324+
}
325+
}
326+
301327
func TestUnicodeInput(t *testing.T) {
302328
testfiles := []string{
303329
filepath.Join(`testdata`, `selectutf8.txt`),

pkg/sqlcmd/commands.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,12 @@ func outCommand(s *Sqlcmd, args []string, line uint) error {
321321
return InvalidFileError(err, args[0])
322322
}
323323
if s.UnicodeOutputFile {
324-
// ODBC sqlcmd doesn't write a BOM but we will.
325-
// Maybe the endian-ness should be configurable.
326-
win16le := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM)
324+
// By default we write a BOM, but --no-bom omits it for ODBC sqlcmd compatibility
325+
bomPolicy := unicode.UseBOM
326+
if s.NoBOM {
327+
bomPolicy = unicode.IgnoreBOM
328+
}
329+
win16le := unicode.UTF16(unicode.LittleEndian, bomPolicy)
327330
encoder := transform.NewWriter(o, win16le.NewEncoder())
328331
s.SetOutput(encoder)
329332
} else {

pkg/sqlcmd/sqlcmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ type Sqlcmd struct {
8484
PrintError func(msg string, severity uint8) bool
8585
// UnicodeOutputFile is true when UTF16 file output is needed
8686
UnicodeOutputFile bool
87+
// NoBOM omits the BOM from UTF-16 output files (ODBC sqlcmd compatibility)
88+
NoBOM bool
8789
// EchoInput tells the GO command to print the batch text before running the query
8890
EchoInput bool
8991
colorizer color.Colorizer

0 commit comments

Comments
 (0)