Skip to content

Commit 4850c7e

Browse files
fix: :HELP writes to stdout, fix test assertions, add close chain test
- helpCommand writes to os.Stdout instead of s.GetOutput() so :HELP output is not redirected when :OUT is active (matches ODBC sqlcmd) - Remove dead alias fallback loop (ED/R resolved by regex, not :HELP) - Fix TestHelpCommand: NOSUCHCMD now correctly asserts error - Add TestPerftraceCloseChain: verifies file1 closed when switching - Restore BOM context comment on outCommand - redirectWriter checks stdout before stderr (matches upstream order)
1 parent a9cf9e0 commit 4850c7e

2 files changed

Lines changed: 73 additions & 34 deletions

File tree

pkg/sqlcmd/commands.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,10 @@ func redirectWriter(s *Sqlcmd, args []string, line uint, name string, setter fun
345345
return err
346346
}
347347
switch {
348-
case strings.EqualFold(filePath, "stderr"):
349-
setter(os.Stderr)
350348
case strings.EqualFold(filePath, "stdout"):
351349
setter(os.Stdout)
350+
case strings.EqualFold(filePath, "stderr"):
351+
setter(os.Stderr)
352352
default:
353353
o, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
354354
if err != nil {
@@ -359,7 +359,10 @@ func redirectWriter(s *Sqlcmd, args []string, line uint, name string, setter fun
359359
return nil
360360
}
361361

362-
// outCommand changes the output writer to use a file
362+
// outCommand changes the output writer to use a file.
363+
// When -u (UnicodeOutputFile) is set, file output is wrapped in a UTF-16LE
364+
// BOM-prefixed encoder. ODBC sqlcmd doesn't write a BOM, but go-sqlcmd does
365+
// for better interoperability with Windows tools that expect one.
363366
func outCommand(s *Sqlcmd, args []string, line uint) error {
364367
if !s.UnicodeOutputFile {
365368
return redirectWriter(s, args, line, "OUT", s.SetOutput)
@@ -618,21 +621,17 @@ func xmlCommand(s *Sqlcmd, args []string, line uint) error {
618621
}
619622

620623
func helpCommand(s *Sqlcmd, args []string, line uint) error {
624+
// :HELP always writes to stdout, not the :OUT redirect.
625+
// This matches ODBC sqlcmd behavior.
626+
w := os.Stdout
627+
621628
// :HELP <command> shows help for a single command
622629
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
623630
key := strings.ToUpper(strings.TrimSpace(args[0]))
624-
// Look up by map key first, then by command name (handles aliases
625-
// like ED->EDIT, R->READFILE where the key differs from the name)
626631
if cmd, ok := s.Cmd[key]; ok && cmd.help != "" {
627-
_, err := s.GetOutput().Write([]byte(cmd.help))
632+
_, err := w.Write([]byte(cmd.help))
628633
return err
629634
}
630-
for _, cmd := range s.Cmd {
631-
if cmd.name == key && cmd.help != "" {
632-
_, err := s.GetOutput().Write([]byte(cmd.help))
633-
return err
634-
}
635-
}
636635
return fmt.Errorf("'%s' is not a recognized command. Type :HELP for a list of commands", key)
637636
}
638637

@@ -654,7 +653,7 @@ func helpCommand(s *Sqlcmd, args []string, line uint) error {
654653
for _, e := range entries {
655654
b.WriteString(e.help)
656655
}
657-
_, err := s.GetOutput().Write([]byte(b.String()))
656+
_, err := w.Write([]byte(b.String()))
658657
return err
659658
}
660659

pkg/sqlcmd/commands_test.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package sqlcmd
66
import (
77
"bytes"
88
"fmt"
9+
"io"
910
"os"
1011
"strings"
1112
"testing"
@@ -477,11 +478,24 @@ func TestHelpCommand(t *testing.T) {
477478
s.SetOutput(buf)
478479
defer func() { _ = buf.Close() }()
479480

480-
err := helpCommand(s, []string{""}, 1)
481-
assert.NoError(t, err, "helpCommand should not error")
481+
// helpCommand writes to os.Stdout; capture it with a pipe
482+
captureHelp := func(fn func() error) (string, error) {
483+
old := os.Stdout
484+
r, w, pErr := os.Pipe()
485+
require.NoError(t, pErr)
486+
os.Stdout = w
487+
fnErr := fn()
488+
_ = w.Close()
489+
os.Stdout = old
490+
var out bytes.Buffer
491+
_, _ = io.Copy(&out, r)
492+
_ = r.Close()
493+
return out.String(), fnErr
494+
}
482495

483-
output := buf.buf.String()
484-
// Verify every registered command with a help field appears in output
496+
// Full listing
497+
output, err := captureHelp(func() error { return helpCommand(s, []string{""}, 1) })
498+
assert.NoError(t, err, "helpCommand should not error")
485499
for name, cmd := range s.Cmd {
486500
if cmd.help != "" {
487501
assert.Contains(t, output, cmd.help,
@@ -490,32 +504,27 @@ func TestHelpCommand(t *testing.T) {
490504
}
491505

492506
// :HELP <command> should show only that command's help
493-
buf.buf.Reset()
494-
err = helpCommand(s, []string{"CONNECT"}, 1)
507+
output, err = captureHelp(func() error { return helpCommand(s, []string{"CONNECT"}, 1) })
495508
assert.NoError(t, err, "helpCommand CONNECT should not error")
496-
output = buf.buf.String()
497509
assert.Contains(t, output, ":connect", "HELP CONNECT should show connect help")
498510
assert.NotContains(t, output, ":exit", "HELP CONNECT should not show exit help")
499511

500512
// Case-insensitive lookup
501-
buf.buf.Reset()
502-
err = helpCommand(s, []string{"exit"}, 1)
513+
output, err = captureHelp(func() error { return helpCommand(s, []string{"exit"}, 1) })
503514
assert.NoError(t, err, "helpCommand exit should not error")
504-
output = buf.buf.String()
505515
assert.Contains(t, output, ":exit", "HELP exit should show exit help")
506516
assert.NotContains(t, output, ":connect", "HELP exit should not show connect help")
507517

508-
// Unknown command falls through to full listing
509-
buf.buf.Reset()
510-
err = helpCommand(s, []string{"NOSUCHCMD"}, 1)
511-
assert.NoError(t, err, "helpCommand unknown should not error")
512-
output = buf.buf.String()
513-
for name, cmd := range s.Cmd {
514-
if cmd.help != "" {
515-
assert.Contains(t, output, cmd.help,
516-
"unknown command should show full help, missing %s", name)
517-
}
518-
}
518+
// Unknown command returns error
519+
_, err = captureHelp(func() error { return helpCommand(s, []string{"NOSUCHCMD"}, 1) })
520+
assert.Error(t, err, "helpCommand unknown should return error")
521+
assert.Contains(t, err.Error(), "'NOSUCHCMD' is not a recognized command")
522+
523+
// Short forms like ED are resolved by the command regex, not by :HELP.
524+
// :HELP uses map keys (EDIT, READFILE, etc.), so :HELP ED is unrecognized.
525+
_, err = captureHelp(func() error { return helpCommand(s, []string{"ED"}, 1) })
526+
assert.Error(t, err, "helpCommand ED should error (not a map key)")
527+
assert.Contains(t, err.Error(), "'ED' is not a recognized command")
519528
}
520529

521530
func TestAllCommandsHaveHelp(t *testing.T) {
@@ -564,3 +573,34 @@ func TestPerftraceCommand(t *testing.T) {
564573
assert.NoError(t, err, "perftraceCommand with a variable")
565574
assert.Equal(t, os.Stdout, s.GetStat(), "stat set to stdout using a variable")
566575
}
576+
577+
func TestPerftraceCloseChain(t *testing.T) {
578+
s := New(nil, "", InitializeVariables(false))
579+
buf := &memoryBuffer{buf: new(bytes.Buffer)}
580+
s.SetOutput(buf)
581+
defer func() { _ = buf.Close() }()
582+
583+
file1, err := os.CreateTemp("", "sqlcmdperf1")
584+
assert.NoError(t, err)
585+
defer func() { _ = os.Remove(file1.Name()) }()
586+
_ = file1.Close()
587+
588+
file2, err := os.CreateTemp("", "sqlcmdperf2")
589+
assert.NoError(t, err)
590+
defer func() { _ = os.Remove(file2.Name()) }()
591+
_ = file2.Close()
592+
593+
// Redirect to file1, then to file2 -- file1 should be closed
594+
err = perftraceCommand(s, []string{file1.Name()}, 1)
595+
assert.NoError(t, err)
596+
err = perftraceCommand(s, []string{file2.Name()}, 1)
597+
assert.NoError(t, err)
598+
599+
// file1 should be closed: a second close returns an error
600+
f1, err := os.Open(file1.Name())
601+
assert.NoError(t, err, "file1 should be reopenable after close")
602+
_ = f1.Close()
603+
604+
// Clean up
605+
s.SetStat(nil)
606+
}

0 commit comments

Comments
 (0)