Skip to content

Commit 36accd3

Browse files
Copilotshueybubbles
andcommitted
Implement stdin redirection detection in console.go instead of using command line flag
Co-authored-by: shueybubbles <[email protected]>
1 parent 2c454b5 commit 36accd3

8 files changed

Lines changed: 167 additions & 24 deletions

File tree

cmd/sqlcmd/sqlcmd.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ type SQLCmdArguments struct {
7878
EnableColumnEncryption bool
7979
ChangePassword string
8080
ChangePasswordAndExit string
81-
DisablePrompts bool
8281
// Keep Help at the end of the list
8382
Help bool
8483
}
@@ -439,7 +438,6 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
439438
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
440439
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
441440
rootCmd.Flags().StringVarP(&args.ChangePasswordAndExit, "change-password-exit", "Z", "", localizer.Sprintf("New password and exit"))
442-
rootCmd.Flags().BoolVar(&args.DisablePrompts, "disable-prompts", false, localizer.Sprintf("Disables line prompts when running as a subprocess"))
443441
}
444442

445443
func setScriptVariable(v string) string {
@@ -756,7 +754,6 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
756754
s.SetupCloseHandler()
757755
defer s.StopCloseHandler()
758756
s.UnicodeOutputFile = args.UnicodeOutputFile
759-
s.DisablePrompts = args.DisablePrompts
760757

761758
if args.DisableCmd != nil {
762759
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())

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
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+
}

pkg/console/console_unix.go

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

pkg/console/console_windows.go

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

pkg/sqlcmd/sqlcmd.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,6 @@ type Sqlcmd struct {
8686
UnicodeOutputFile bool
8787
// EchoInput tells the GO command to print the batch text before running the query
8888
EchoInput bool
89-
// DisablePrompts suppresses printing of line prompts like "1>" when running in non-interactive mode
90-
DisablePrompts bool
9189
colorizer color.Colorizer
9290
termchan chan os.Signal
9391
}
@@ -351,9 +349,7 @@ func (s *Sqlcmd) IncludeFile(path string, processAll bool) error {
351349
ln = append(ln, line...)
352350
}
353351
if err == nil && echoFileLines {
354-
if !s.DisablePrompts {
355-
_, _ = s.GetOutput().Write([]byte(s.Prompt()))
356-
}
352+
_, _ = s.GetOutput().Write([]byte(s.Prompt()))
357353
_, _ = s.GetOutput().Write(ln)
358354
_, _ = s.GetOutput().Write([]byte(SqlcmdEol))
359355
}

0 commit comments

Comments
 (0)