Skip to content

Commit 170f439

Browse files
feat: add -f codepage flag for input/output encoding
- Add -f/--code-page flag with ODBC-compatible format parsing - Support 50+ codepages: Unicode, Windows, OEM/DOS, ISO-8859, CJK, EBCDIC, Macintosh - Apply input codepage in IncludeFile() for :r command - Apply output codepage in outCommand() for :OUT file writes - Add --list-codepages flag to display all supported codepages - Add comprehensive unit tests for parsing and encoding lookup
1 parent 758fca9 commit 170f439

7 files changed

Lines changed: 644 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ type SQLCmdArguments struct {
8282
ChangePassword string
8383
ChangePasswordAndExit string
8484
TraceFile string
85+
CodePage string
86+
ListCodePages bool
8587
// Keep Help at the end of the list
8688
Help bool
8789
}
@@ -171,6 +173,10 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
171173
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
172174
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
173175
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
176+
case a.CodePage != "":
177+
if _, parseErr := sqlcmd.ParseCodePage(a.CodePage); parseErr != nil {
178+
err = localizer.Errorf(`'-f %s': %v`, a.CodePage, parseErr)
179+
}
174180
}
175181
}
176182
if err != nil {
@@ -239,6 +245,17 @@ func Execute(version string) {
239245
listLocalServers()
240246
os.Exit(0)
241247
}
248+
// List supported codepages
249+
if args.ListCodePages {
250+
fmt.Println(localizer.Sprintf("Supported Code Pages:"))
251+
fmt.Println()
252+
fmt.Printf("%-8s %-20s %s\n", "Code", "Name", "Description")
253+
fmt.Printf("%-8s %-20s %s\n", "----", "----", "-----------")
254+
for _, cp := range sqlcmd.SupportedCodePages() {
255+
fmt.Printf("%-8d %-20s %s\n", cp.CodePage, cp.Name, cp.Description)
256+
}
257+
os.Exit(0)
258+
}
242259
if len(argss) > 0 {
243260
fmt.Printf("%s'%s': Unknown command. Enter '--help' for command help.", sqlcmdErrorPrefix, argss[0])
244261
os.Exit(1)
@@ -479,6 +496,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
479496
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
480497
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
481498
rootCmd.Flags().StringVarP(&args.ChangePasswordAndExit, "change-password-exit", "Z", "", localizer.Sprintf("New password and exit"))
499+
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]"))
500+
rootCmd.Flags().BoolVar(&args.ListCodePages, "list-codepages", false, localizer.Sprintf("List supported code pages and exit"))
482501
}
483502

484503
func setScriptVariable(v string) string {
@@ -813,6 +832,15 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
813832
defer s.StopCloseHandler()
814833
s.UnicodeOutputFile = args.UnicodeOutputFile
815834

835+
// Parse and apply codepage settings
836+
if args.CodePage != "" {
837+
codePageSettings, err := sqlcmd.ParseCodePage(args.CodePage)
838+
if err != nil {
839+
return 1, localizer.Errorf("Invalid code page: %v", err)
840+
}
841+
s.CodePage = codePageSettings
842+
}
843+
816844
if args.DisableCmd != nil {
817845
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
818846
}

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ 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+
}},
126142
}
127143

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

183204
for _, test := range commands {

0 commit comments

Comments
 (0)