Skip to content
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The `sqlcmd` project aims to be a complete port of the original ODBC sqlcmd to t
- There are new posix-style versions of each flag, such as `--input-file` for `-i`. `sqlcmd -?` will print those parameter names. Those new names do not preserve backward compatibility with ODBC `sqlcmd`. For example, to specify multiple input file names using `--input-file`, the file names must be comma-delimited, not space-delimited.

The following switches have different behavior in this version of `sqlcmd` compared to the original ODBC based `sqlcmd`.
- `-R` switch is ignored. The go runtime does not provide access to user locale information, and it's not readily available through syscall on all supported platforms.
- `-R` switch enables regional formatting for numeric, currency, and date/time values based on the user's locale. Formatting includes locale-specific thousand separators for numbers, and locale-specific date/time formats. On Windows, the user's default locale is detected from system settings. On Linux/macOS, the locale is detected from environment variables (`LC_ALL`, `LC_MESSAGES`, `LANG`).
- `-I` switch is ignored; quoted identifiers are always set on. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts.
- `-N` now takes an optional string value that can be one of `s[trict]`,`t[rue]`,`m[andatory]`, `yes`,`1`, `o[ptional]`,`no`, `0`, `f[alse]`, or `disable` to specify the encryption choice.
- If `-N` is passed but no value is provided, `true` is used.
Expand All @@ -133,6 +133,7 @@ The following switches have different behavior in this version of `sqlcmd` compa
- 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.
- `-f` Specifies the code page for input and output files. Format: `codepage | i:codepage[,o:codepage] | o:codepage[,i:codepage]`. Use `65001` for UTF-8. Supported codepages include Unicode (65001, 1200, 1201), Windows (874, 1250-1258), OEM/DOS (437, 850, etc.), ISO-8859 (28591-28606), CJK (932, 936, 949, 950), and EBCDIC (37, 1047, 1140). Use `--list-codepages` to see all supported code pages.
- 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:
Expand Down
33 changes: 31 additions & 2 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ type SQLCmdArguments struct {
ChangePassword string
ChangePasswordAndExit string
TraceFile string
CodePage string
ListCodePages bool
UseRegionalSettings bool
// Keep Help at the end of the list
Help bool
}
Expand Down Expand Up @@ -171,6 +174,10 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
case a.CodePage != "":
if _, parseErr := sqlcmd.ParseCodePage(a.CodePage); parseErr != nil {
err = localizer.Errorf(`'-f %s': %v`, a.CodePage, parseErr)
}
}
}
if err != nil {
Expand Down Expand Up @@ -239,6 +246,17 @@ func Execute(version string) {
listLocalServers()
os.Exit(0)
}
// List supported codepages
if args.ListCodePages {
fmt.Println(localizer.Sprintf("Supported Code Pages:"))
fmt.Println()
fmt.Printf("%-8s %-20s %s\n", "Code", "Name", "Description")
fmt.Printf("%-8s %-20s %s\n", "----", "----", "-----------")
for _, cp := range sqlcmd.SupportedCodePages() {
fmt.Printf("%-8d %-20s %s\n", cp.CodePage, cp.Name, cp.Description)
}
os.Exit(0)
}
if len(argss) > 0 {
fmt.Printf("%s'%s': Unknown command. Enter '--help' for command help.", sqlcmdErrorPrefix, argss[0])
os.Exit(1)
Expand Down Expand Up @@ -472,13 +490,15 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
rootCmd.Flags().StringVarP(&args.ListServers, listServers, "L", "", localizer.Sprintf("%s List servers. Pass %s to omit 'Servers:' output.", "-L[c]", "c"))
rootCmd.Flags().BoolVarP(&args.DedicatedAdminConnection, "dedicated-admin-connection", "A", false, localizer.Sprintf("Dedicated administrator connection"))
_ = rootCmd.Flags().BoolP("enable-quoted-identifiers", "I", true, localizer.Sprintf("Provided for backward compatibility. Quoted identifiers are always enabled"))
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
rootCmd.Flags().BoolVarP(&args.UseRegionalSettings, "client-regional-setting", "R", false, localizer.Sprintf("Use client regional settings for currency, date, and time formatting"))
_ = rootCmd.Flags().IntP(removeControlCharacters, "k", 0, localizer.Sprintf("%s Remove control characters from output. Pass 1 to substitute a space per character, 2 for a space per consecutive characters", "-k [1|2]"))
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
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().StringVarP(&args.CodePage, "code-page", "f", "", localizer.Sprintf("Specifies the code page for input/output. Use 65001 for UTF-8. Format: codepage | i:codepage[,o:codepage] | o:codepage[,i:codepage]"))
rootCmd.Flags().BoolVar(&args.ListCodePages, "list-codepages", false, localizer.Sprintf("List supported code pages and exit"))
}

func setScriptVariable(v string) string {
Expand Down Expand Up @@ -813,6 +833,15 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
defer s.StopCloseHandler()
s.UnicodeOutputFile = args.UnicodeOutputFile

// Parse and apply codepage settings
if args.CodePage != "" {
codePageSettings, err := sqlcmd.ParseCodePage(args.CodePage)
if err != nil {
return 1, localizer.Errorf("Invalid code page: %v", err)
}
s.CodePage = codePageSettings
}

if args.DisableCmd != nil {
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
}
Expand All @@ -828,7 +857,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
}

s.Connect = &connectConfig
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
s.Format = sqlcmd.NewSQLCmdDefaultFormatterWithRegional(args.TrimSpaces, args.getControlCharacterBehavior(), args.UseRegionalSettings)
if args.OutputFile != "" {
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,29 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
}},
// Codepage flag tests
{[]string{"-f", "65001"}, func(args SQLCmdArguments) bool {
return args.CodePage == "65001"
}},
{[]string{"-f", "i:1252,o:65001"}, func(args SQLCmdArguments) bool {
return args.CodePage == "i:1252,o:65001"
}},
{[]string{"-f", "o:65001,i:1252"}, func(args SQLCmdArguments) bool {
return args.CodePage == "o:65001,i:1252"
}},
{[]string{"--code-page", "1252"}, func(args SQLCmdArguments) bool {
return args.CodePage == "1252"
}},
{[]string{"--list-codepages"}, func(args SQLCmdArguments) bool {
return args.ListCodePages
}},
// Regional settings flag test
{[]string{"-R"}, func(args SQLCmdArguments) bool {
return args.UseRegionalSettings
}},
{[]string{"--client-regional-setting"}, func(args SQLCmdArguments) bool {
return args.UseRegionalSettings
}},
}

for _, test := range commands {
Expand Down Expand Up @@ -178,6 +201,11 @@ func TestInvalidCommandLine(t *testing.T) {
{[]string{"-N", "optional", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "disable", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "strict", "-F", "myserver.domain.com", "-J", "/path/to/cert.pem"}, "The -F and the -J options are mutually exclusive."},
// Codepage validation tests
{[]string{"-f", "invalid"}, `'-f invalid': invalid codepage: invalid`},
{[]string{"-f", "99999"}, `'-f 99999': unsupported codepage 99999`},
{[]string{"-f", "i:invalid"}, `'-f i:invalid': invalid input codepage: i:invalid`},
{[]string{"-f", "x:1252"}, `'-f x:1252': invalid codepage: x:1252`},
}

for _, test := range commands {
Expand Down
Loading
Loading