Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement -r #56

Merged
merged 4 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ We will be implementing command line switches and behaviors over time. Several s
- The `SQLCMDPASSWORD` environment variable
- The `:CONNECT` command
- When prompted, the user can type the password to complete a connection (pending [#50](https://github.com/microsoft/go-sqlcmd/issues/50))

- `-r` requires a 0 or 1 argument
- `-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.
- `-I` switch will be removed. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts.
- `-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)
Expand Down
15 changes: 15 additions & 0 deletions cmd/sqlcmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type SQLCmdArguments struct {
ErrorSeverityLevel uint8 `short:"V" help:"Controls the severity level that is used to set the ERRORLEVEL variable on exit."`
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."`
Format string `short:"F" help:"Specifies the formatting for results." default:"horiz" enum:"horiz,horizontal,vert,vertical"`
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"`
}

// Validate accounts for settings not described by Kong attributes
Expand Down Expand Up @@ -220,6 +221,20 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
if err != nil {
return 1, err
}
} else {
var stderrSeverity uint8 = 11
if args.ErrorsToStderr == 1 {
stderrSeverity = 0
}
if args.ErrorsToStderr >= 0 {
s.PrintError = func(msg string, severity uint8) bool {
if severity >= stderrSeverity {
_, _ = os.Stderr.Write([]byte(msg))
return true
}
return false
}
}
}
once := false
if args.InitialQuery != "" {
Expand Down
4 changes: 4 additions & 0 deletions cmd/sqlcmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-F", "vert"}, func(args SQLCmdArguments) bool {
return args.Format == "vert"
}},
{[]string{"-r", "1"}, func(args SQLCmdArguments) bool {
return args.ErrorsToStderr == 1
}},
}

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

for _, test := range commands {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ require (
github.com/stretchr/testify v1.7.0
)

replace github.com/denisenkom/go-mssqldb => github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7
replace github.com/denisenkom/go-mssqldb => github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7 h1:4CIaYagSRCGr0/Gh6cfF5cQx3RVE3qrQukZn8iMO6Y8=
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220303143659-8896461e4ec7/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469 h1:BuUMqsxB86i1QEBf0q+dkQYfNLVpD1nH1fRJPKvXWSg=
github.com/shueybubbles/go-mssqldb v0.10.1-0.20220317022252-fafb9d92e469/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
15 changes: 10 additions & 5 deletions pkg/sqlcmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"regexp"
"sort"
"strings"
"syscall"

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

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

// errorCommand changes the error writer to use a file
func errorCommand(s *Sqlcmd, args []string, line uint) error {
if len(args) == 0 || args[0] == "" {
return InvalidCommandError("OUT", line)
}
switch {
case strings.EqualFold(args[0], "stderr"):
s.SetError(nil)
s.SetError(os.Stderr)
case strings.EqualFold(args[0], "stdout"):
s.SetError(os.NewFile(uintptr(syscall.Stderr), "/dev/stdout"))
s.SetError(os.Stdout)
default:
o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions pkg/sqlcmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,25 @@ func TestConnectCommand(t *testing.T) {
}
}
}

func TestErrorCommand(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
file, err := os.CreateTemp("", "sqlcmderr")
assert.NoError(t, err, "os.CreateTemp")
defer os.Remove(file.Name())
fileName := file.Name()
_ = file.Close()
err = errorCommand(s, []string{""}, 1)
assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name")
err = errorCommand(s, []string{fileName}, 1)
assert.NoError(t, err, "errorCommand")
// Only some error kinds go to the error output
err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"})
assert.NoError(t, err, "runSqlCmd")
s.SetError(nil)
errText, err := os.ReadFile(file.Name())
if assert.NoError(t, err, "ReadFile") {
assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents")
}
}
2 changes: 1 addition & 1 deletion pkg/sqlcmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (f *sqlCmdFormatterType) AddError(err error) {
if print {
b.WriteString(msg)
b.WriteString(SqlcmdEol)
f.mustWriteOut(fitToScreen(b, f.vars.ScreenWidth()).String())
f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String())
}
}

Expand Down
26 changes: 19 additions & 7 deletions pkg/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type Sqlcmd struct {
Format Formatter
Query string
Cmd Commands
// PrintError allows the host to redirect errors away from the default output. Returns false if the error is not redirected by the host.
PrintError func(msg string, severity uint8) bool
}

// New creates a new Sqlcmd instance
Expand All @@ -73,6 +75,9 @@ func New(l Console, workingDirectory string, vars *Variables) *Sqlcmd {
}
s.batch = NewBatch(s.scanNext, s.Cmd)
mssql.SetContextLogger(s)
s.PrintError = func(msg string, severity uint8) bool {
return false
}
return s
}

Expand All @@ -85,7 +90,7 @@ func (s *Sqlcmd) scanNext() (string, error) {
// When processAll is true it executes any remaining batch content when reaching EOF
func (s *Sqlcmd) Run(once bool, processAll bool) error {
setupCloseHandler(s)
stderr, iactive := s.GetError(), s.lineIo != nil
iactive := s.lineIo != nil
var lastError error
for {
var execute bool
Expand Down Expand Up @@ -128,7 +133,7 @@ func (s *Sqlcmd) Run(once bool, processAll bool) error {
break
}
if err != nil {
fmt.Fprintln(stderr, err)
_, _ = s.GetOutput().Write([]byte(err.Error() + SqlcmdEol))
lastError = err
}
}
Expand Down Expand Up @@ -174,7 +179,7 @@ func (s *Sqlcmd) GetOutput() io.Writer {

// SetOutput sets the io.WriteCloser to use for non-error output
func (s *Sqlcmd) SetOutput(o io.WriteCloser) {
if s.out != nil {
if s.out != nil && s.out != os.Stderr && s.out != os.Stdout {
s.out.Close()
}
s.out = o
Expand All @@ -183,14 +188,14 @@ func (s *Sqlcmd) SetOutput(o io.WriteCloser) {
// GetError returns the io.Writer to use for errors
func (s *Sqlcmd) GetError() io.Writer {
if s.err == nil {
return os.Stderr
return s.GetOutput()
}
return s.err
}

// SetError sets the io.WriteCloser to use for errors
func (s *Sqlcmd) SetError(e io.WriteCloser) {
if s.err != nil {
if s.err != nil && s.err != os.Stderr && s.err != os.Stdout {
s.err.Close()
}
s.err = e
Expand Down Expand Up @@ -376,9 +381,16 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
msg := retmsg.Message(ctx)
switch m := msg.(type) {
case sqlexp.MsgNotice:
s.Format.AddMessage(m.Message)
if !s.PrintError(m.Message, 10) {
s.Format.AddMessage(m.Message)
}
case sqlexp.MsgError:
s.Format.AddError(m.Error)
switch e := m.Error.(type) {
case mssql.Error:
if !s.PrintError(e.Message, e.Class) {
s.Format.AddError(m.Error)
}
}
qe = s.handleError(&retcode, m.Error)
case sqlexp.MsgRowsAffected:
if m.Count == 1 {
Expand Down
14 changes: 13 additions & 1 deletion pkg/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func TestSqlCmdExitOnError(t *testing.T) {
err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"})
o := buf.buf.String()
assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error")
assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Only first select should run")
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")
assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error")

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

}

func TestSqlCmdDefersToPrintError(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.PrintError = func(msg string, severity uint8) bool {
return severity > 10
}
err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"})
if assert.NoError(t, err, "runSqlCmd failed") {
assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError")
}
}

// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input
func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error {
t.Helper()
Expand Down