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