Skip to content

Commit c61c1a7

Browse files
authored
Fix: space delimited args for -i and -v (#440)
1 parent 6be32c9 commit c61c1a7

File tree

3 files changed

+81
-5
lines changed

3 files changed

+81
-5
lines changed

README.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ The `sqlcmd` project aims to be a complete port of the original ODBC sqlcmd to t
121121

122122
### Changes in behavior from the ODBC based sqlcmd
123123

124+
- `/` is not accepted as a flag specifier, only `-`
125+
- 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.
126+
124127
The following switches have different behavior in this version of `sqlcmd` compared to the original ODBC based `sqlcmd`.
125128
- `-r` requires a 0 or 1 argument
126129
- `-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.
@@ -133,19 +136,19 @@ The following switches have different behavior in this version of `sqlcmd` compa
133136
- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it.
134137
- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types.
135138
- 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.
136-
- `-i` now requires multiple arguments for the switch to be separated by `,`.
137-
- `-v` requires multiple variable setters to be comma-separated. eg: `-v var1=v1,var2=v2 -v "var3=v 3"`
139+
- `-i` doesn't handle a comma `,` in a file name correctly unless the file name argument is triple quoted. For example:
140+
`sqlcmd -i """select,100.sql"""` will try to open a file named `sql,100.sql` while `sqlcmd -i "select,100.sql"` will try to open two files `select` and `100.sql`
141+
- If using a single `-i` flag to pass multiple file names, there must be a space after the `-i`. Example: `-i file1.sql file2.sql`
138142
- `-M` switch is ignored. Sqlcmd always enables multi-subnet failover.
139143

144+
140145
### Switches not available in the new sqlcmd (go-sqlcmd) yet
141146

142147
There are a few switches yet to be implemented in the new `sqlcmd` (go-sqlcmd) compared
143148
to the original ODBC based `sqlcmd`, discussion [#293](https://github.com/microsoft/go-sqlcmd/discussions/292)
144149
lists these switches. Please provide feedback in the discussion on which
145150
switches are most important to you to have implemented next in the new sqlcmd.
146151

147-
Also, the XML Output command `:XML [On]|[Off]` is not implemented yet
148-
in the new sqlcmd (go-sqlcmd).
149152

150153
### Miscellaneous enhancements
151154

cmd/sqlcmd/sqlcmd.go

+34
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,45 @@ func Execute(version string) {
218218
fmt.Println()
219219
})
220220
})
221+
rootCmd.SetArgs(convertOsArgs(os.Args[1:]))
221222
if err := rootCmd.Execute(); err != nil {
222223
os.Exit(1)
223224
}
224225
}
225226

227+
// We need to rewrite the arguments to add -i and -v in front of each space-delimited value to be Cobra-friendly.
228+
func convertOsArgs(args []string) (cargs []string) {
229+
flag := ""
230+
first := true
231+
for _, a := range args {
232+
if flag != "" {
233+
// If the user has a file named "-i" the only way they can pass it on the command line
234+
// is with triple quotes: sqlcmd -i """-i""" which will convince the flags parser to
235+
// inject `"-i"` into the string slice. Same for any file with a comma in its name.
236+
if isFlag(a) {
237+
flag = ""
238+
} else if !first {
239+
cargs = append(cargs, flag)
240+
}
241+
first = false
242+
}
243+
if isListFlag(a) {
244+
flag = a
245+
first = true
246+
}
247+
cargs = append(cargs, a)
248+
}
249+
return
250+
}
251+
252+
func isFlag(arg string) bool {
253+
return len(arg) == 2 && arg[0] == '-'
254+
}
255+
256+
func isListFlag(arg string) bool {
257+
return arg == "-v" || arg == "-i"
258+
}
259+
226260
func formatDescription(description string, maxWidth, indentWidth int) string {
227261
var lines []string
228262
words := strings.Fields(description)

cmd/sqlcmd/sqlcmd_test.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
3131
{[]string{}, func(args SQLCmdArguments) bool {
3232
return args.Server == "" && !args.UseTrustedConnection && args.UserName == "" && args.ScreenWidth == nil && args.ErrorsToStderr == -1 && args.EncryptConnection == "default"
3333
}},
34+
{[]string{"-v", "a=b", "x=y", "-E"}, func(args SQLCmdArguments) bool {
35+
return len(args.Variables) == 2 && args.Variables["a"] == "b" && args.Variables["x"] == "y" && args.UseTrustedConnection
36+
}},
3437
{[]string{"-c", "MYGO", "-C", "-E", "-i", "file1", "-o", "outfile", "-i", "file2"}, func(args SQLCmdArguments) bool {
3538
return args.BatchTerminator == "MYGO" && args.TrustServerCertificate && len(args.InputFile) == 2 && strings.HasSuffix(args.OutputFile, "outfile")
3639
}},
@@ -84,6 +87,15 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
8487
{[]string{"-y", "100", "-Y", "200", "-P", "placeholder"}, func(args SQLCmdArguments) bool {
8588
return *args.FixedTypeWidth == 200 && *args.VariableTypeWidth == 100 && args.Password == "placeholder"
8689
}},
90+
{[]string{"-E", "-v", "a=b", "x=y", "-i", "a.sql", "b.sql", "-v", "f=g", "-i", "c.sql", "-C", "-v", "ab=cd", "ef=hi"}, func(args SQLCmdArguments) bool {
91+
return args.UseTrustedConnection && args.Variables["x"] == "y" && len(args.InputFile) == 3 && args.InputFile[0] == "a.sql" && args.TrustServerCertificate
92+
}},
93+
{[]string{"-i", `comma,text.sql`}, func(args SQLCmdArguments) bool {
94+
return args.InputFile[0] == "comma" && args.InputFile[1] == "text.sql"
95+
}},
96+
{[]string{"-i", `"comma,text.sql"`}, func(args SQLCmdArguments) bool {
97+
return args.InputFile[0] == "comma,text.sql"
98+
}},
8799
}
88100

89101
for _, test := range commands {
@@ -105,7 +117,7 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
105117
cmd.SetOut(new(bytes.Buffer))
106118
cmd.SetErr(new(bytes.Buffer))
107119
setFlags(cmd, arguments)
108-
cmd.SetArgs(test.commandLine)
120+
cmd.SetArgs(convertOsArgs(test.commandLine))
109121
err := cmd.Execute()
110122
msg := ""
111123
if err != nil {
@@ -479,6 +491,33 @@ func TestStartupScript(t *testing.T) {
479491
}
480492
}
481493

494+
func TestConvertOsArgs(t *testing.T) {
495+
type test struct {
496+
name string
497+
in []string
498+
expected []string
499+
}
500+
501+
tests := []test{
502+
{
503+
"Multiple variables/one switch",
504+
[]string{"-E", "-v", "a=b", "x=y", "f=g", "-C"},
505+
[]string{"-E", "-v", "a=b", "-v", "x=y", "-v", "f=g", "-C"},
506+
},
507+
{
508+
"Multiple variables and files/multiple switches",
509+
[]string{"-E", "-v", "a=b", "x=y", "-i", "a.sql", "b.sql", "-v", "f=g", "-i", "c.sql", "-C", "-v", "ab=cd", "ef=hi"},
510+
[]string{"-E", "-v", "a=b", "-v", "x=y", "-i", "a.sql", "-i", "b.sql", "-v", "f=g", "-i", "c.sql", "-C", "-v", "ab=cd", "-v", "ef=hi"},
511+
},
512+
}
513+
for _, c := range tests {
514+
t.Run(c.name, func(t *testing.T) {
515+
actual := convertOsArgs(c.in)
516+
assert.ElementsMatch(t, c.expected, actual, "Incorrect converted args")
517+
})
518+
}
519+
}
520+
482521
// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set
483522
func canTestAzureAuth() bool {
484523
server := os.Getenv(sqlcmd.SQLCMDSERVER)

0 commit comments

Comments
 (0)