Skip to content

Commit 34ab426

Browse files
Automatically suppress line prompts when stdin is redirected (#582)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: shueybubbles <[email protected]>
1 parent 6513013 commit 34ab426

File tree

7 files changed

+218
-17
lines changed

7 files changed

+218
-17
lines changed

cmd/sqlcmd/sqlcmd.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,24 @@ func setConnect(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments, vars *sq
715715
}
716716

717717
func isConsoleInitializationRequired(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments) bool {
718+
// Password input always requires console initialization
719+
if connect.RequiresPassword() {
720+
return true
721+
}
722+
723+
// Check if stdin is from a terminal or a redirection
724+
file, err := os.Stdin.Stat()
725+
if err == nil {
726+
// If stdin is not a character device, it's coming from a pipe or redirect
727+
if (file.Mode() & os.ModeCharDevice) == 0 {
728+
// Non-interactive: stdin is redirected
729+
return false
730+
}
731+
}
732+
733+
// If we get here, stdin is from a terminal or we couldn't determine
718734
iactive := args.InputFile == nil && args.Query == "" && len(args.ChangePasswordAndExit) == 0
719-
return iactive || connect.RequiresPassword()
735+
return iactive
720736
}
721737

722738
func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {

cmd/sqlcmd/stdin_console_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package sqlcmd
5+
6+
import (
7+
"os"
8+
"testing"
9+
10+
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestIsConsoleInitializationRequiredWithRedirectedStdin(t *testing.T) {
15+
// Create a temp file to simulate redirected stdin
16+
tempFile, err := os.CreateTemp("", "stdin-test-*.txt")
17+
if err != nil {
18+
t.Fatalf("Failed to create temp file: %v", err)
19+
}
20+
defer os.Remove(tempFile.Name())
21+
defer tempFile.Close()
22+
23+
// Write some data to it
24+
_, err = tempFile.WriteString("SELECT 1;\nGO\n")
25+
if err != nil {
26+
t.Fatalf("Failed to write to temp file: %v", err)
27+
}
28+
29+
// Remember the original stdin
30+
originalStdin := os.Stdin
31+
defer func() { os.Stdin = originalStdin }()
32+
33+
// Test with a file redirection
34+
stdinFile, err := os.Open(tempFile.Name())
35+
if err != nil {
36+
t.Fatalf("Failed to open temp file: %v", err)
37+
}
38+
defer stdinFile.Close()
39+
40+
// Replace stdin with our redirected file
41+
os.Stdin = stdinFile
42+
43+
// Set up a connect settings instance for SQL authentication
44+
connectConfig := sqlcmd.ConnectSettings{
45+
UserName: "testuser", // This will trigger SQL authentication, requiring a password
46+
}
47+
48+
// Test regular args
49+
args := &SQLCmdArguments{}
50+
51+
// Print file stat mode for debugging
52+
fileStat, _ := os.Stdin.Stat()
53+
t.Logf("File mode: %v", fileStat.Mode())
54+
t.Logf("Is character device: %v", (fileStat.Mode()&os.ModeCharDevice) != 0)
55+
t.Logf("Connection config: %+v", connectConfig)
56+
t.Logf("RequiresPassword() returns: %v", connectConfig.RequiresPassword())
57+
58+
// Test with SQL authentication that requires a password
59+
res := isConsoleInitializationRequired(&connectConfig, args)
60+
// Should be true since password is required, even with redirected stdin
61+
assert.True(t, res, "Console initialization should be required when SQL authentication is used")
62+
63+
// Now test with no authentication (no password required)
64+
connectConfig = sqlcmd.ConnectSettings{}
65+
res = isConsoleInitializationRequired(&connectConfig, args)
66+
// Should be false since stdin is redirected and no password is required
67+
assert.False(t, res, "Console initialization should not be required with redirected stdin and no password")
68+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ require (
2020
github.com/spf13/pflag v1.0.5
2121
github.com/spf13/viper v1.14.0
2222
github.com/stretchr/testify v1.10.0
23-
golang.org/x/sys v0.32.0
23+
golang.org/x/sys v0.33.0
2424
golang.org/x/text v0.24.0
2525
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
2626
gopkg.in/yaml.v2 v2.4.0
@@ -85,6 +85,7 @@ require (
8585
golang.org/x/mod v0.17.0 // indirect
8686
golang.org/x/net v0.39.0 // indirect
8787
golang.org/x/sync v0.13.0 // indirect
88+
golang.org/x/term v0.32.0 // indirect
8889
google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197 // indirect
8990
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect
9091
google.golang.org/grpc v1.71.1 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
519519
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
520520
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
521521
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
522-
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
523-
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
522+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
523+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
524524
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
525+
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
526+
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
525527
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
526528
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
527529
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

pkg/console/console.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,83 @@
44
package console
55

66
import (
7+
"bufio"
78
"os"
89

910
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
1011
"github.com/peterh/liner"
1112
)
1213

1314
type console struct {
14-
impl *liner.State
15-
historyFile string
16-
prompt string
15+
impl *liner.State
16+
historyFile string
17+
prompt string
18+
stdinRedirected bool
19+
stdinReader *bufio.Reader
1720
}
1821

1922
// NewConsole creates a sqlcmdConsole implementation that provides these features:
2023
// - Storage of input history to a local file. History can be scrolled through using the up and down arrow keys.
2124
// - Simple tab key completion of SQL keywords
2225
func NewConsole(historyFile string) sqlcmd.Console {
2326
c := &console{
24-
impl: liner.NewLiner(),
25-
historyFile: historyFile,
27+
impl: liner.NewLiner(),
28+
historyFile: historyFile,
29+
stdinRedirected: isStdinRedirected(),
2630
}
27-
c.impl.SetCtrlCAborts(true)
28-
c.impl.SetCompleter(CompleteLine)
29-
if c.historyFile != "" {
30-
if f, err := os.Open(historyFile); err == nil {
31-
_, _ = c.impl.ReadHistory(f)
32-
f.Close()
31+
32+
if c.stdinRedirected {
33+
c.stdinReader = bufio.NewReader(os.Stdin)
34+
} else {
35+
c.impl.SetCtrlCAborts(true)
36+
c.impl.SetCompleter(CompleteLine)
37+
if c.historyFile != "" {
38+
if f, err := os.Open(historyFile); err == nil {
39+
_, _ = c.impl.ReadHistory(f)
40+
f.Close()
41+
}
3342
}
3443
}
3544
return c
3645
}
3746

3847
// Close writes out the history data to disk and closes the console buffers
3948
func (c *console) Close() {
40-
if c.historyFile != "" {
49+
if !c.stdinRedirected && c.historyFile != "" {
4150
if f, err := os.Create(c.historyFile); err == nil {
4251
_, _ = c.impl.WriteHistory(f)
4352
f.Close()
4453
}
4554
}
46-
c.impl.Close()
55+
56+
if !c.stdinRedirected {
57+
c.impl.Close()
58+
}
4759
}
4860

4961
// Readline displays the current prompt and returns a line of text entered by the user.
5062
// It appends the returned line to the history buffer.
5163
// If the user presses Ctrl-C the error returned is sqlcmd.ErrCtrlC
64+
// If stdin is redirected, it reads directly from stdin without displaying prompts
5265
func (c *console) Readline() (string, error) {
66+
// Handle redirected stdin without displaying prompts
67+
if c.stdinRedirected {
68+
line, err := c.stdinReader.ReadString('\n')
69+
if err != nil {
70+
return "", err
71+
}
72+
// Trim the trailing newline
73+
if len(line) > 0 && line[len(line)-1] == '\n' {
74+
line = line[:len(line)-1]
75+
// Also trim carriage return if present
76+
if len(line) > 0 && line[len(line)-1] == '\r' {
77+
line = line[:len(line)-1]
78+
}
79+
}
80+
return line, nil
81+
}
82+
83+
// Interactive terminal mode with prompts
5384
s, err := c.impl.Prompt(c.prompt)
5485
if err == liner.ErrPromptAborted {
5586
return "", sqlcmd.ErrCtrlC
@@ -61,6 +92,8 @@ func (c *console) Readline() (string, error) {
6192
// ReadPassword displays the given prompt and returns the password entered by the user.
6293
// If the user presses Ctrl-C the error returned is sqlcmd.ErrCtrlC
6394
func (c *console) ReadPassword(prompt string) ([]byte, error) {
95+
// Even when stdin is redirected, we need to use the prompt for passwords
96+
// since they should not be read from the redirected input
6497
b, err := c.impl.PasswordPrompt(prompt)
6598
if err == liner.ErrPromptAborted {
6699
return []byte{}, sqlcmd.ErrCtrlC

pkg/console/console_redirect.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package console
5+
6+
import (
7+
"os"
8+
"golang.org/x/term"
9+
)
10+
11+
// isStdinRedirected checks if stdin is coming from a pipe or redirection
12+
func isStdinRedirected() bool {
13+
stat, err := os.Stdin.Stat()
14+
if err != nil {
15+
// If we can't determine, assume it's not redirected
16+
return false
17+
}
18+
19+
// If it's not a character device, it's coming from a pipe or redirection
20+
if (stat.Mode() & os.ModeCharDevice) == 0 {
21+
return true
22+
}
23+
24+
// Double-check using term.IsTerminal
25+
fd := int(os.Stdin.Fd())
26+
return !term.IsTerminal(fd)
27+
}

pkg/console/console_redirect_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package console
5+
6+
import (
7+
"io"
8+
"os"
9+
"testing"
10+
)
11+
12+
func TestStdinRedirectionDetection(t *testing.T) {
13+
// Save original stdin
14+
origStdin := os.Stdin
15+
defer func() { os.Stdin = origStdin }()
16+
17+
// Create a pipe
18+
r, w, err := os.Pipe()
19+
if err != nil {
20+
t.Fatalf("Couldn't create pipe: %v", err)
21+
}
22+
defer r.Close()
23+
defer w.Close()
24+
25+
// Replace stdin with our pipe
26+
os.Stdin = r
27+
28+
// Test if stdin is properly detected as redirected
29+
if !isStdinRedirected() {
30+
t.Errorf("Pipe input should be detected as redirected")
31+
}
32+
33+
// Write some test input
34+
go func() {
35+
_, _ = io.WriteString(w, "test input\n")
36+
w.Close()
37+
}()
38+
39+
// Create console with redirected stdin
40+
console := NewConsole("")
41+
42+
// Test readline
43+
line, err := console.Readline()
44+
if err != nil {
45+
t.Fatalf("Failed to read from redirected stdin: %v", err)
46+
}
47+
48+
if line != "test input" {
49+
t.Errorf("Expected 'test input', got '%s'", line)
50+
}
51+
52+
// Clean up
53+
console.Close()
54+
}

0 commit comments

Comments
 (0)