Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,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 @@ -151,6 +151,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 @@ -817,6 +837,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 @@ -832,7 +861,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