Skip to content

Commit fef1a31

Browse files
authored
implement -r (#56)
* implement -r * update readme * update to go-mssqldb with a fix * remove server name from test
1 parent f479c2a commit fef1a31

File tree

10 files changed

+88
-18
lines changed

10 files changed

+88
-18
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ We will be implementing command line switches and behaviors over time. Several s
1515
- The `SQLCMDPASSWORD` environment variable
1616
- The `:CONNECT` command
1717
- When prompted, the user can type the password to complete a connection (pending [#50](https://github.com/microsoft/go-sqlcmd/issues/50))
18-
18+
- `-r` requires a 0 or 1 argument
1919
- `-R` switch will be removed. The go runtime does not provide access to user locale information, and it's not readily available through syscall on all supported platforms.
2020
- `-I` switch will be removed. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts.
2121
- `-N` now takes a string value that can be one of `true`, `false`, or `disable` to specify the encryption choice. (`default` is the same as omitting the parameter)

cmd/sqlcmd/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type SQLCmdArguments struct {
4848
ErrorSeverityLevel uint8 `short:"V" help:"Controls the severity level that is used to set the ERRORLEVEL variable on exit."`
4949
ErrorLevel int `short:"m" help:"Controls which error messages are sent to stdout. Messages that have severity level greater than or equal to this level are sent."`
5050
Format string `short:"F" help:"Specifies the formatting for results." default:"horiz" enum:"horiz,horizontal,vert,vertical"`
51+
ErrorsToStderr int `short:"r" help:"Redirects the error message output to the screen (stderr). A value of 0 means messages with severity >= 11 will b redirected. A value of 1 means all error message output including PRINT is redirected." enum:"-1,0,1" default:"-1"`
5152
}
5253

5354
// Validate accounts for settings not described by Kong attributes
@@ -220,6 +221,20 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
220221
if err != nil {
221222
return 1, err
222223
}
224+
} else {
225+
var stderrSeverity uint8 = 11
226+
if args.ErrorsToStderr == 1 {
227+
stderrSeverity = 0
228+
}
229+
if args.ErrorsToStderr >= 0 {
230+
s.PrintError = func(msg string, severity uint8) bool {
231+
if severity >= stderrSeverity {
232+
_, _ = os.Stderr.Write([]byte(msg))
233+
return true
234+
}
235+
return false
236+
}
237+
}
223238
}
224239
once := false
225240
if args.InitialQuery != "" {

cmd/sqlcmd/main_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
7373
{[]string{"-F", "vert"}, func(args SQLCmdArguments) bool {
7474
return args.Format == "vert"
7575
}},
76+
{[]string{"-r", "1"}, func(args SQLCmdArguments) bool {
77+
return args.ErrorsToStderr == 1
78+
}},
7679
}
7780

7881
for _, test := range commands {
@@ -100,6 +103,7 @@ func TestInvalidCommandLine(t *testing.T) {
100103
// the test prefix is a kong artifact https://github.com/alecthomas/kong/issues/221
101104
{[]string{"-a", "100"}, "test: '-a 100': Packet size has to be a number between 512 and 32767."},
102105
{[]string{"-F", "what"}, "--format must be one of \"horiz\",\"horizontal\",\"vert\",\"vertical\" but got \"what\""},
106+
{[]string{"-r", "5"}, `--errors-to-stderr must be one of "-1","0","1" but got '\x05'`},
103107
}
104108

105109
for _, test := range commands {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ require (
1111
github.com/stretchr/testify v1.7.0
1212
)
1313

14-
replace github.com/denisenkom/go-mssqldb => github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7
14+
replace github.com/denisenkom/go-mssqldb => github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
2727
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2828
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2929
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
30-
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7 h1:4CIaYagSRCGr0/Gh6cfF5cQx3RVE3qrQukZn8iMO6Y8=
31-
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
30+
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469 h1:BuUMqsxB86i1QEBf0q+dkQYfNLVpD1nH1fRJPKvXWSg=
31+
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
3232
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3333
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
3434
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

pkg/sqlcmd/commands.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"regexp"
1010
"sort"
1111
"strings"
12-
"syscall"
1312

1413
"github.com/alecthomas/kong"
1514
)
@@ -188,11 +187,14 @@ func goCommand(s *Sqlcmd, args []string, line uint) error {
188187

189188
// outCommand changes the output writer to use a file
190189
func outCommand(s *Sqlcmd, args []string, line uint) error {
190+
if len(args) == 0 || args[0] == "" {
191+
return InvalidCommandError("OUT", line)
192+
}
191193
switch {
192194
case strings.EqualFold(args[0], "stdout"):
193-
s.SetOutput(nil)
195+
s.SetOutput(os.Stdout)
194196
case strings.EqualFold(args[0], "stderr"):
195-
s.SetOutput(os.NewFile(uintptr(syscall.Stderr), "/dev/stderr"))
197+
s.SetOutput(os.Stderr)
196198
default:
197199
o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
198200
if err != nil {
@@ -205,11 +207,14 @@ func outCommand(s *Sqlcmd, args []string, line uint) error {
205207

206208
// errorCommand changes the error writer to use a file
207209
func errorCommand(s *Sqlcmd, args []string, line uint) error {
210+
if len(args) == 0 || args[0] == "" {
211+
return InvalidCommandError("OUT", line)
212+
}
208213
switch {
209214
case strings.EqualFold(args[0], "stderr"):
210-
s.SetError(nil)
215+
s.SetError(os.Stderr)
211216
case strings.EqualFold(args[0], "stdout"):
212-
s.SetError(os.NewFile(uintptr(syscall.Stderr), "/dev/stdout"))
217+
s.SetError(os.Stdout)
213218
default:
214219
o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
215220
if err != nil {

pkg/sqlcmd/commands_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,25 @@ func TestConnectCommand(t *testing.T) {
190190
}
191191
}
192192
}
193+
194+
func TestErrorCommand(t *testing.T) {
195+
s, buf := setupSqlCmdWithMemoryOutput(t)
196+
defer buf.Close()
197+
file, err := os.CreateTemp("", "sqlcmderr")
198+
assert.NoError(t, err, "os.CreateTemp")
199+
defer os.Remove(file.Name())
200+
fileName := file.Name()
201+
_ = file.Close()
202+
err = errorCommand(s, []string{""}, 1)
203+
assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name")
204+
err = errorCommand(s, []string{fileName}, 1)
205+
assert.NoError(t, err, "errorCommand")
206+
// Only some error kinds go to the error output
207+
err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"})
208+
assert.NoError(t, err, "runSqlCmd")
209+
s.SetError(nil)
210+
errText, err := os.ReadFile(file.Name())
211+
if assert.NoError(t, err, "ReadFile") {
212+
assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents")
213+
}
214+
}

pkg/sqlcmd/format.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func (f *sqlCmdFormatterType) AddError(err error) {
208208
if print {
209209
b.WriteString(msg)
210210
b.WriteString(SqlcmdEol)
211-
f.mustWriteOut(fitToScreen(b, f.vars.ScreenWidth()).String())
211+
f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String())
212212
}
213213
}
214214

pkg/sqlcmd/sqlcmd.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ type Sqlcmd struct {
6161
Format Formatter
6262
Query string
6363
Cmd Commands
64+
// PrintError allows the host to redirect errors away from the default output. Returns false if the error is not redirected by the host.
65+
PrintError func(msg string, severity uint8) bool
6466
}
6567

6668
// New creates a new Sqlcmd instance
@@ -73,6 +75,9 @@ func New(l Console, workingDirectory string, vars *Variables) *Sqlcmd {
7375
}
7476
s.batch = NewBatch(s.scanNext, s.Cmd)
7577
mssql.SetContextLogger(s)
78+
s.PrintError = func(msg string, severity uint8) bool {
79+
return false
80+
}
7681
return s
7782
}
7883

@@ -85,7 +90,7 @@ func (s *Sqlcmd) scanNext() (string, error) {
8590
// When processAll is true it executes any remaining batch content when reaching EOF
8691
func (s *Sqlcmd) Run(once bool, processAll bool) error {
8792
setupCloseHandler(s)
88-
stderr, iactive := s.GetError(), s.lineIo != nil
93+
iactive := s.lineIo != nil
8994
var lastError error
9095
for {
9196
var execute bool
@@ -128,7 +133,7 @@ func (s *Sqlcmd) Run(once bool, processAll bool) error {
128133
break
129134
}
130135
if err != nil {
131-
fmt.Fprintln(stderr, err)
136+
_, _ = s.GetOutput().Write([]byte(err.Error() + SqlcmdEol))
132137
lastError = err
133138
}
134139
}
@@ -174,7 +179,7 @@ func (s *Sqlcmd) GetOutput() io.Writer {
174179

175180
// SetOutput sets the io.WriteCloser to use for non-error output
176181
func (s *Sqlcmd) SetOutput(o io.WriteCloser) {
177-
if s.out != nil {
182+
if s.out != nil && s.out != os.Stderr && s.out != os.Stdout {
178183
s.out.Close()
179184
}
180185
s.out = o
@@ -183,14 +188,14 @@ func (s *Sqlcmd) SetOutput(o io.WriteCloser) {
183188
// GetError returns the io.Writer to use for errors
184189
func (s *Sqlcmd) GetError() io.Writer {
185190
if s.err == nil {
186-
return os.Stderr
191+
return s.GetOutput()
187192
}
188193
return s.err
189194
}
190195

191196
// SetError sets the io.WriteCloser to use for errors
192197
func (s *Sqlcmd) SetError(e io.WriteCloser) {
193-
if s.err != nil {
198+
if s.err != nil && s.err != os.Stderr && s.err != os.Stdout {
194199
s.err.Close()
195200
}
196201
s.err = e
@@ -376,9 +381,16 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
376381
msg := retmsg.Message(ctx)
377382
switch m := msg.(type) {
378383
case sqlexp.MsgNotice:
379-
s.Format.AddMessage(m.Message)
384+
if !s.PrintError(m.Message, 10) {
385+
s.Format.AddMessage(m.Message)
386+
}
380387
case sqlexp.MsgError:
381-
s.Format.AddError(m.Error)
388+
switch e := m.Error.(type) {
389+
case mssql.Error:
390+
if !s.PrintError(e.Message, e.Class) {
391+
s.Format.AddError(m.Error)
392+
}
393+
}
382394
qe = s.handleError(&retcode, m.Error)
383395
case sqlexp.MsgRowsAffected:
384396
if m.Count == 1 {

pkg/sqlcmd/sqlcmd_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ func TestSqlCmdExitOnError(t *testing.T) {
246246
err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"})
247247
o := buf.buf.String()
248248
assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error")
249-
assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Only first select should run")
249+
assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'."+SqlcmdEol, o, "Only first select should run")
250250
assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error")
251251

252252
s, buf = setupSqlCmdWithMemoryOutput(t)
@@ -396,6 +396,18 @@ func TestVerticalLayoutWithColumns(t *testing.T) {
396396

397397
}
398398

399+
func TestSqlCmdDefersToPrintError(t *testing.T) {
400+
s, buf := setupSqlCmdWithMemoryOutput(t)
401+
defer buf.Close()
402+
s.PrintError = func(msg string, severity uint8) bool {
403+
return severity > 10
404+
}
405+
err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"})
406+
if assert.NoError(t, err, "runSqlCmd failed") {
407+
assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError")
408+
}
409+
}
410+
399411
// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input
400412
func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error {
401413
t.Helper()

0 commit comments

Comments
 (0)