From 14fb827dc2bbd2474c751a6100f95cb380e3e0b7 Mon Sep 17 00:00:00 2001 From: David Levy Date: Mon, 26 Jan 2026 16:38:42 -0600 Subject: [PATCH] fix: prevent nil pointer panic when piping input to sqlcmd (fixes #607) When stdin is redirected (piped input) with ODBC-compat flags like -G, the Console was not being initialized, causing a nil pointer dereference in scanNext() when calling s.lineIo.Readline(). The fix ensures isConsoleInitializationRequired() returns true when stdin is redirected and no input file or query is specified, so that console.NewConsole() is called to handle the piped input. Changes: - Add condition in isConsoleInitializationRequired() for piped stdin - Add test case using os.Pipe() to verify piped input detection - Add README section documenting how to pipe input to sqlcmd --- README.md | 18 ++++++++++++ cmd/sqlcmd/sqlcmd.go | 4 +++ cmd/sqlcmd/stdin_console_test.go | 47 ++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe26e192..e4a1e35d 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,24 @@ sqlcmd 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. +### Piping input to sqlcmd + +You can pipe SQL commands directly to `sqlcmd` from the command line. This is useful for scripting and automation: + +**PowerShell:** +```powershell +"SELECT @@version" | sqlcmd -S myserver -d mydb -G +"SELECT name FROM sys.databases" | sqlcmd -S myserver.database.windows.net -d mydb -G +``` + +**Bash:** +```bash +echo "SELECT @@version" | sqlcmd -S myserver -d mydb -G +cat myscript.sql | sqlcmd -S myserver -d mydb -G +``` + +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. + ## Sqlcmd 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][]. diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index ea655b47..7d69b24b 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -775,6 +775,10 @@ func isConsoleInitializationRequired(connect *sqlcmd.ConnectSettings, args *SQLC } else if iactive { // Interactive mode also requires console needsConsole = true + } else if isStdinRedirected && args.InputFile == nil && args.Query == "" && len(args.ChangePasswordAndExit) == 0 { + // Stdin is redirected (piped input) and no input file or query specified + // We need a console to read from the redirected stdin (fixes #607) + needsConsole = true } return needsConsole, iactive diff --git a/cmd/sqlcmd/stdin_console_test.go b/cmd/sqlcmd/stdin_console_test.go index 10ba6cd7..a5d4c189 100644 --- a/cmd/sqlcmd/stdin_console_test.go +++ b/cmd/sqlcmd/stdin_console_test.go @@ -64,8 +64,8 @@ func TestIsConsoleInitializationRequiredWithRedirectedStdin(t *testing.T) { // Now test with no authentication (no password required) connectConfig = sqlcmd.ConnectSettings{} needsConsole, isInteractive = isConsoleInitializationRequired(&connectConfig, args) - // Should not need console and not be interactive - assert.False(t, needsConsole, "Console should not be needed with redirected stdin and no password") + // Should need console (for reading redirected stdin) but not be interactive (fixes #607) + assert.True(t, needsConsole, "Console should be needed with redirected stdin to read piped input") assert.False(t, isInteractive, "Should not be interactive mode with redirected stdin") // Test with direct terminal input (simulated by restoring original stdin) @@ -78,3 +78,46 @@ func TestIsConsoleInitializationRequiredWithRedirectedStdin(t *testing.T) { assert.Equal(t, args.InputFile == nil && args.Query == "" && len(args.ChangePasswordAndExit) == 0, isInteractive, "Interactive mode should be true with terminal stdin and no input files or queries") } + +// TestPipedInputRequiresConsole tests that piped stdin input correctly requires +// console initialization to prevent nil pointer dereference (fixes #607) +func TestPipedInputRequiresConsole(t *testing.T) { + // Save original stdin + originalStdin := os.Stdin + defer func() { os.Stdin = originalStdin }() + + // Create a pipe to simulate piped input like: echo "select 1" | sqlcmd + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + defer r.Close() + defer w.Close() + + // Replace stdin with our pipe reader + os.Stdin = r + + // Write some SQL to the pipe (simulating: echo "select 1" | sqlcmd) + go func() { + _, _ = w.WriteString("SELECT @@SERVERNAME\nGO\n") + w.Close() + }() + + // Test with no authentication required (simulates -G flag with Azure AD) + connectConfig := sqlcmd.ConnectSettings{} + args := &SQLCmdArguments{} // No InputFile, no Query - relies on stdin + + needsConsole, isInteractive := isConsoleInitializationRequired(&connectConfig, args) + + // With piped input, we should need a console to read from stdin + // but should not be in interactive mode + assert.True(t, needsConsole, "Console should be required for piped stdin input to avoid nil pointer dereference") + assert.False(t, isInteractive, "Piped input should not be considered interactive mode") + + // Test that ChangePasswordAndExit bypasses the piped input console requirement + // since no stdin reading is needed for password change operations + args.ChangePasswordAndExit = "newpassword" + needsConsole, isInteractive = isConsoleInitializationRequired(&connectConfig, args) + assert.False(t, needsConsole, "Console should not be required when ChangePasswordAndExit is set") + assert.False(t, isInteractive, "Should not be interactive mode with ChangePasswordAndExit") +}