Skip to content

Commit 9c9383d

Browse files
feat: implement -R (regional settings) and -f (codepage) flags
-R: Locale-aware formatting for numbers, dates, times - Detects locale from Windows LCID or Unix LC_* environment variables - Applies regional thousand separators and date/time formats -f: Input/output codepage control - Format: codepage | i:codepage[,o:codepage] | o:codepage[,i:codepage] - Use 65001 for UTF-8 - --list-codepages shows all supported encodings
1 parent 56b1fb1 commit 9c9383d

File tree

14 files changed

+1576
-31
lines changed

14 files changed

+1576
-31
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ The `sqlcmd` project aims to be a complete port of the original ODBC sqlcmd to t
122122
- 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.
123123

124124
The following switches have different behavior in this version of `sqlcmd` compared to the original ODBC based `sqlcmd`.
125-
- `-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.
125+
- `-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`).
126126
- `-I` switch is ignored; quoted identifiers are always set on. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts.
127127
- `-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.
128128
- If `-N` is passed but no value is provided, `true` is used.
@@ -133,6 +133,7 @@ The following switches have different behavior in this version of `sqlcmd` compa
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>
135135
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
136+
- `-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.
136137
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
137138
- 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.
138139
- `-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: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ type SQLCmdArguments struct {
8282
ChangePassword string
8383
ChangePasswordAndExit string
8484
TraceFile string
85+
CodePage string
86+
ListCodePages bool
87+
UseRegionalSettings bool
8588
// Keep Help at the end of the list
8689
Help bool
8790
}
@@ -171,6 +174,10 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
171174
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
172175
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
173176
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
177+
case a.CodePage != "":
178+
if _, parseErr := sqlcmd.ParseCodePage(a.CodePage); parseErr != nil {
179+
err = localizer.Errorf(`'-f %s': %v`, a.CodePage, parseErr)
180+
}
174181
}
175182
}
176183
if err != nil {
@@ -239,6 +246,17 @@ func Execute(version string) {
239246
listLocalServers()
240247
os.Exit(0)
241248
}
249+
// List supported codepages
250+
if args.ListCodePages {
251+
fmt.Println(localizer.Sprintf("Supported Code Pages:"))
252+
fmt.Println()
253+
fmt.Printf("%-8s %-20s %s\n", "Code", "Name", "Description")
254+
fmt.Printf("%-8s %-20s %s\n", "----", "----", "-----------")
255+
for _, cp := range sqlcmd.SupportedCodePages() {
256+
fmt.Printf("%-8d %-20s %s\n", cp.CodePage, cp.Name, cp.Description)
257+
}
258+
os.Exit(0)
259+
}
242260
if len(argss) > 0 {
243261
fmt.Printf("%s'%s': Unknown command. Enter '--help' for command help.", sqlcmdErrorPrefix, argss[0])
244262
os.Exit(1)
@@ -472,13 +490,15 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
472490
rootCmd.Flags().StringVarP(&args.ListServers, listServers, "L", "", localizer.Sprintf("%s List servers. Pass %s to omit 'Servers:' output.", "-L[c]", "c"))
473491
rootCmd.Flags().BoolVarP(&args.DedicatedAdminConnection, "dedicated-admin-connection", "A", false, localizer.Sprintf("Dedicated administrator connection"))
474492
_ = rootCmd.Flags().BoolP("enable-quoted-identifiers", "I", true, localizer.Sprintf("Provided for backward compatibility. Quoted identifiers are always enabled"))
475-
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
493+
rootCmd.Flags().BoolVarP(&args.UseRegionalSettings, "client-regional-setting", "R", false, localizer.Sprintf("Use client regional settings for currency, date, and time formatting"))
476494
_ = 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]"))
477495
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
478496
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
479497
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
480498
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
481499
rootCmd.Flags().StringVarP(&args.ChangePasswordAndExit, "change-password-exit", "Z", "", localizer.Sprintf("New password and exit"))
500+
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]"))
501+
rootCmd.Flags().BoolVar(&args.ListCodePages, "list-codepages", false, localizer.Sprintf("List supported code pages and exit"))
482502
}
483503

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

836+
// Parse and apply codepage settings
837+
if args.CodePage != "" {
838+
codePageSettings, err := sqlcmd.ParseCodePage(args.CodePage)
839+
if err != nil {
840+
return 1, localizer.Errorf("Invalid code page: %v", err)
841+
}
842+
s.CodePage = codePageSettings
843+
}
844+
816845
if args.DisableCmd != nil {
817846
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
818847
}
@@ -828,7 +857,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
828857
}
829858

830859
s.Connect = &connectConfig
831-
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
860+
s.Format = sqlcmd.NewSQLCmdDefaultFormatterWithRegional(args.TrimSpaces, args.getControlCharacterBehavior(), args.UseRegionalSettings)
832861
if args.OutputFile != "" {
833862
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
834863
if err != nil {

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,29 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
123123
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
124124
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
125125
}},
126+
// Codepage flag tests
127+
{[]string{"-f", "65001"}, func(args SQLCmdArguments) bool {
128+
return args.CodePage == "65001"
129+
}},
130+
{[]string{"-f", "i:1252,o:65001"}, func(args SQLCmdArguments) bool {
131+
return args.CodePage == "i:1252,o:65001"
132+
}},
133+
{[]string{"-f", "o:65001,i:1252"}, func(args SQLCmdArguments) bool {
134+
return args.CodePage == "o:65001,i:1252"
135+
}},
136+
{[]string{"--code-page", "1252"}, func(args SQLCmdArguments) bool {
137+
return args.CodePage == "1252"
138+
}},
139+
{[]string{"--list-codepages"}, func(args SQLCmdArguments) bool {
140+
return args.ListCodePages
141+
}},
142+
// Regional settings flag test
143+
{[]string{"-R"}, func(args SQLCmdArguments) bool {
144+
return args.UseRegionalSettings
145+
}},
146+
{[]string{"--client-regional-setting"}, func(args SQLCmdArguments) bool {
147+
return args.UseRegionalSettings
148+
}},
126149
}
127150

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

183211
for _, test := range commands {

0 commit comments

Comments
 (0)