Skip to content

Commit e31f42f

Browse files
fix: prevent nil pointer panic when piping input to sqlcmd (fixes #607) (#640)
1 parent 56b1fb1 commit e31f42f

File tree

3 files changed

+67
-2
lines changed

3 files changed

+67
-2
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ sqlcmd
112112

113113
If no current context exists, `sqlcmd` (with no connection parameters) reverts to the original ODBC `sqlcmd` behavior of creating an interactive session to the default local instance on port 1433 using trusted authentication, otherwise it will create an interactive session to the current context.
114114

115+
### Piping input to sqlcmd
116+
117+
You can pipe SQL commands directly to `sqlcmd` from the command line. This is useful for scripting and automation:
118+
119+
**PowerShell:**
120+
```powershell
121+
"SELECT @@version" | sqlcmd -S myserver -d mydb -G
122+
"SELECT name FROM sys.databases" | sqlcmd -S myserver.database.windows.net -d mydb -G
123+
```
124+
125+
**Bash:**
126+
```bash
127+
echo "SELECT @@version" | sqlcmd -S myserver -d mydb -G
128+
cat myscript.sql | sqlcmd -S myserver -d mydb -G
129+
```
130+
131+
Note: When piping input, `GO` batch terminators are optional—`sqlcmd` will automatically execute the batch when the input ends. However, you can still include `GO` statements if you want to execute multiple batches.
132+
115133
## Sqlcmd
116134

117135
The `sqlcmd` project aims to be a complete port of the original ODBC sqlcmd to the `Go` language, utilizing the [go-mssqldb][] driver. For full documentation of the tool and installation instructions, see [go-sqlcmd-utility][].

cmd/sqlcmd/sqlcmd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,10 @@ func isConsoleInitializationRequired(connect *sqlcmd.ConnectSettings, args *SQLC
775775
} else if iactive {
776776
// Interactive mode also requires console
777777
needsConsole = true
778+
} else if isStdinRedirected && args.InputFile == nil && args.Query == "" && len(args.ChangePasswordAndExit) == 0 {
779+
// Stdin is redirected (piped input) and no input file or query specified
780+
// We need a console to read from the redirected stdin (fixes #607)
781+
needsConsole = true
778782
}
779783

780784
return needsConsole, iactive

cmd/sqlcmd/stdin_console_test.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ func TestIsConsoleInitializationRequiredWithRedirectedStdin(t *testing.T) {
6464
// Now test with no authentication (no password required)
6565
connectConfig = sqlcmd.ConnectSettings{}
6666
needsConsole, isInteractive = isConsoleInitializationRequired(&connectConfig, args)
67-
// Should not need console and not be interactive
68-
assert.False(t, needsConsole, "Console should not be needed with redirected stdin and no password")
67+
// Should need console (for reading redirected stdin) but not be interactive (fixes #607)
68+
assert.True(t, needsConsole, "Console should be needed with redirected stdin to read piped input")
6969
assert.False(t, isInteractive, "Should not be interactive mode with redirected stdin")
7070

7171
// Test with direct terminal input (simulated by restoring original stdin)
@@ -78,3 +78,46 @@ func TestIsConsoleInitializationRequiredWithRedirectedStdin(t *testing.T) {
7878
assert.Equal(t, args.InputFile == nil && args.Query == "" && len(args.ChangePasswordAndExit) == 0, isInteractive,
7979
"Interactive mode should be true with terminal stdin and no input files or queries")
8080
}
81+
82+
// TestPipedInputRequiresConsole tests that piped stdin input correctly requires
83+
// console initialization to prevent nil pointer dereference (fixes #607)
84+
func TestPipedInputRequiresConsole(t *testing.T) {
85+
// Save original stdin
86+
originalStdin := os.Stdin
87+
defer func() { os.Stdin = originalStdin }()
88+
89+
// Create a pipe to simulate piped input like: echo "select 1" | sqlcmd
90+
r, w, err := os.Pipe()
91+
if err != nil {
92+
t.Fatalf("Failed to create pipe: %v", err)
93+
}
94+
defer r.Close()
95+
defer w.Close()
96+
97+
// Replace stdin with our pipe reader
98+
os.Stdin = r
99+
100+
// Write some SQL to the pipe (simulating: echo "select 1" | sqlcmd)
101+
go func() {
102+
_, _ = w.WriteString("SELECT @@SERVERNAME\nGO\n")
103+
w.Close()
104+
}()
105+
106+
// Test with no authentication required (simulates -G flag with Azure AD)
107+
connectConfig := sqlcmd.ConnectSettings{}
108+
args := &SQLCmdArguments{} // No InputFile, no Query - relies on stdin
109+
110+
needsConsole, isInteractive := isConsoleInitializationRequired(&connectConfig, args)
111+
112+
// With piped input, we should need a console to read from stdin
113+
// but should not be in interactive mode
114+
assert.True(t, needsConsole, "Console should be required for piped stdin input to avoid nil pointer dereference")
115+
assert.False(t, isInteractive, "Piped input should not be considered interactive mode")
116+
117+
// Test that ChangePasswordAndExit bypasses the piped input console requirement
118+
// since no stdin reading is needed for password change operations
119+
args.ChangePasswordAndExit = "newpassword"
120+
needsConsole, isInteractive = isConsoleInitializationRequired(&connectConfig, args)
121+
assert.False(t, needsConsole, "Console should not be required when ChangePasswordAndExit is set")
122+
assert.False(t, isInteractive, "Should not be interactive mode with ChangePasswordAndExit")
123+
}

0 commit comments

Comments
 (0)