Skip to content

Commit cbf535e

Browse files
Redirect sqlcmd errors to err stream (#143)
By default sqlcmd uses stdout as the default stream for output and errors. As a result when the stderr is redirected externally to file, the sqlcmd errors are missed. This commit logs all the sqlcmd errors to the stderr of OS in case default output stream is stdout and stderr for sqlcmd is not set. Resolves #105
1 parent a1f7b21 commit cbf535e

File tree

5 files changed

+86
-8
lines changed

5 files changed

+86
-8
lines changed

cmd/sqlcmd/main.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package main
55

66
import (
7+
"errors"
78
"fmt"
89
"os"
910

@@ -259,7 +260,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
259260
if args.ErrorsToStderr >= 0 {
260261
s.PrintError = func(msg string, severity uint8) bool {
261262
if severity >= stderrSeverity {
262-
_, _ = os.Stderr.Write([]byte(msg + sqlcmd.SqlcmdEol))
263+
s.WriteError(os.Stderr, errors.New(msg+sqlcmd.SqlcmdEol))
263264
return true
264265
}
265266
return false
@@ -285,7 +286,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
285286
} else {
286287
for f := range args.InputFile {
287288
if err = s.IncludeFile(args.InputFile[f], true); err != nil {
288-
_, _ = os.Stderr.Write([]byte(err.Error() + sqlcmd.SqlcmdEol))
289+
s.WriteError(s.GetError(), err)
289290
s.Exitcode = 1
290291
break
291292
}

pkg/sqlcmd/commands.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ func (c Commands) matchCommand(line string) (*Command, []string) {
116116
}
117117

118118
func warnDisabled(s *Sqlcmd, args []string, line uint) error {
119-
_, _ = s.GetError().Write([]byte(ErrCommandsDisabled.Error() + SqlcmdEol))
119+
s.WriteError(s.GetError(), ErrCommandsDisabled)
120120
return nil
121121
}
122122

123123
func errorDisabled(s *Sqlcmd, args []string, line uint) error {
124-
_, _ = s.GetError().Write([]byte(ErrCommandsDisabled.Error() + SqlcmdEol))
124+
s.WriteError(s.GetError(), ErrCommandsDisabled)
125125
s.Exitcode = 1
126126
return ErrExitRequested
127127
}
@@ -433,7 +433,7 @@ func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (str
433433
if failOnUnresolved {
434434
return "", UndefinedVariable(varName)
435435
}
436-
_, _ = s.GetError().Write([]byte(UndefinedVariable(varName).Error() + SqlcmdEol))
436+
s.WriteError(s.GetError(), UndefinedVariable(varName))
437437
if b != nil {
438438
b.WriteString(string(arg[i : vl+1]))
439439
}

pkg/sqlcmd/sqlcmd.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (s *Sqlcmd) Run(once bool, processAll bool) error {
136136
args = make([]string, 0)
137137
once = true
138138
} else {
139-
_, _ = s.GetOutput().Write([]byte(err.Error() + SqlcmdEol))
139+
s.WriteError(s.GetOutput(), err)
140140
}
141141
}
142142
if cmd != nil {
@@ -146,7 +146,7 @@ func (s *Sqlcmd) Run(once bool, processAll bool) error {
146146
break
147147
}
148148
if err != nil {
149-
_, _ = s.GetOutput().Write([]byte(err.Error() + SqlcmdEol))
149+
s.WriteError(s.GetOutput(), err)
150150
lastError = err
151151
}
152152
}
@@ -209,6 +209,19 @@ func (s *Sqlcmd) SetError(e io.WriteCloser) {
209209
s.err = e
210210
}
211211

212+
// WriteError writes the error on specified stream
213+
func (s *Sqlcmd) WriteError(stream io.Writer, err error) {
214+
if strings.HasPrefix(err.Error(), ErrorPrefix) {
215+
if s.GetError() != os.Stdout {
216+
_, _ = s.GetError().Write([]byte(err.Error() + SqlcmdEol))
217+
} else {
218+
_, _ = os.Stderr.Write([]byte(err.Error() + SqlcmdEol))
219+
}
220+
} else {
221+
_, _ = stream.Write([]byte(err.Error() + SqlcmdEol))
222+
}
223+
}
224+
212225
// ConnectDb opens a connection to the database with the given modifications to the connection
213226
// nopw == true means don't prompt for a password if the auth type requires it
214227
// if connect is nil, ConnectDb uses the current connection. If non-nil and the connection succeeds,
@@ -364,7 +377,7 @@ func setupCloseHandler(s *Sqlcmd) {
364377
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
365378
go func() {
366379
<-c
367-
_, _ = s.GetOutput().Write([]byte(ErrCtrlC.Error() + SqlcmdEol))
380+
s.WriteError(s.GetOutput(), ErrCtrlC)
368381
os.Exit(0)
369382
}()
370383
}

pkg/sqlcmd/sqlcmd_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,44 @@ func TestQueryServerPropertyReturnsColumnName(t *testing.T) {
462462
}
463463
}
464464

465+
func TestSqlCmdOutputAndError(t *testing.T) {
466+
s, outfile, errfile := setupSqlcmdWithFileErrorOutput(t)
467+
defer os.Remove(outfile.Name())
468+
defer os.Remove(errfile.Name())
469+
s.Query = "select $(X"
470+
err := s.Run(true, false)
471+
if assert.NoError(t, err, "s.Run(once = true)") {
472+
bytes, err := os.ReadFile(errfile.Name())
473+
if assert.NoError(t, err, "os.ReadFile") {
474+
assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution")
475+
}
476+
}
477+
s.Query = "select '1'"
478+
err = s.Run(true, false)
479+
if assert.NoError(t, err, "s.Run(once = true)") {
480+
bytes, err := os.ReadFile(outfile.Name())
481+
if assert.NoError(t, err, "os.ReadFile") {
482+
assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for query execution")
483+
}
484+
}
485+
486+
s, outfile, errfile = setupSqlcmdWithFileErrorOutput(t)
487+
defer os.Remove(outfile.Name())
488+
defer os.Remove(errfile.Name())
489+
dataPath := "testdata" + string(os.PathSeparator)
490+
err = s.IncludeFile(dataPath+"testerrorredirection.sql", false)
491+
if assert.NoError(t, err, "IncludeFile testerrorredirection.sql false") {
492+
bytes, err := os.ReadFile(outfile.Name())
493+
if assert.NoError(t, err, "os.ReadFile outfile") {
494+
assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for sql file execution in outfile")
495+
}
496+
bytes, err = os.ReadFile(errfile.Name())
497+
if assert.NoError(t, err, "os.ReadFile errfile") {
498+
assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile")
499+
}
500+
}
501+
}
502+
465503
// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input
466504
func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error {
467505
t.Helper()
@@ -509,6 +547,28 @@ func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) {
509547
return s, file
510548
}
511549

550+
func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File) {
551+
t.Helper()
552+
v := InitializeVariables(true)
553+
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
554+
s := New(nil, "", v)
555+
s.Connect = newConnect(t)
556+
s.Format = NewSQLCmdDefaultFormatter(true)
557+
outfile, err := os.CreateTemp("", "sqlcmdout")
558+
assert.NoError(t, err, "os.CreateTemp")
559+
errfile, err := os.CreateTemp("", "sqlcmderr")
560+
assert.NoError(t, err, "os.CreateTemp")
561+
s.SetOutput(outfile)
562+
s.SetError(errfile)
563+
err = s.ConnectDb(nil, true)
564+
if err != nil {
565+
os.Remove(outfile.Name())
566+
os.Remove(errfile.Name())
567+
}
568+
assert.NoError(t, err, "s.ConnectDB")
569+
return s, outfile, errfile
570+
}
571+
512572
// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set
513573
func canTestAzureAuth() bool {
514574
server := os.Getenv(SQLCMDSERVER)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
select '1'
2+
go
3+
select $(var
4+
go

0 commit comments

Comments
 (0)