Skip to content

Commit 3dea15d

Browse files
pieternclaude
andauthored
Make all prompts write to stderr instead of stdout (#4301)
## Changes All prompt functions now consistently write to stderr, removing the need to check if stdout is a TTY for prompt support. Depends on #4298. ## Why This simplifies the logic and follows Unix conventions: keep stdout clean for data/piping, use stderr for interactive UI elements. Now prompts work even when stdout is redirected (e.g., `databricks cmd | tee output.txt`). --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent cf8b93f commit 3dea15d

File tree

4 files changed

+20
-8
lines changed

4 files changed

+20
-8
lines changed

libs/cmdio/capabilities.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ func (c Capabilities) SupportsInteractive() bool {
3737
}
3838

3939
// SupportsPrompt returns true if terminal supports user prompting.
40+
// Prompts write to stderr and read from stdin, so we only need those to be TTYs.
4041
func (c Capabilities) SupportsPrompt() bool {
41-
return c.SupportsInteractive() && c.stdinIsTTY && c.stdoutIsTTY && !c.isGitBash
42+
return c.SupportsInteractive() && c.stdinIsTTY && !c.isGitBash
4243
}
4344

4445
// SupportsColor returns true if the given writer supports colored output.

libs/cmdio/capabilities_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ func TestCapabilities_SupportsPrompt(t *testing.T) {
8484
expected: false,
8585
},
8686
{
87-
name: "stdout not TTY",
87+
name: "stdout not TTY (prompts write to stderr, so this is OK)",
8888
caps: Capabilities{
8989
stdinIsTTY: true,
9090
stdoutIsTTY: false,
9191
stderrIsTTY: true,
9292
color: true,
9393
isGitBash: false,
9494
},
95-
expected: false,
95+
expected: true,
9696
},
9797
{
9898
name: "stderr not TTY",

libs/cmdio/compat.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,6 @@ func AskSelect(ctx context.Context, question string, choices []string) (string,
115115
return "", err
116116
}
117117

118-
// Note: by default this prompt uses os.Stdin and os.Stdout.
119-
// This is contrary to the rest of the original progress logger
120-
// functions that write to stderr.
121118
prompt := promptui.Select{
122119
Label: last,
123120
Items: choices,
@@ -126,6 +123,8 @@ func AskSelect(ctx context.Context, question string, choices []string) (string,
126123
Label: "{{.}}: ",
127124
Selected: last + ": {{.}}",
128125
},
126+
Stdin: io.NopCloser(c.in),
127+
Stdout: nopWriteCloser{c.err},
129128
}
130129

131130
_, ans, err := prompt.Run()

libs/cmdio/io.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import (
1616
// cmdIO is the private instance, that is not supposed to be accessed
1717
// outside of `cmdio` package. Use the public package-level functions
1818
// to access the inner state.
19+
//
20+
// Stream Architecture:
21+
// - in: stdin for user input (prompts, confirmations)
22+
// - out: stdout for data output (JSON, tables, command results)
23+
// - err: stderr for interactive UI (prompts, spinners, logs, diagnostics)
24+
//
25+
// This separation enables piping stdout while maintaining interactivity:
26+
//
27+
// databricks clusters list --output json | jq # User sees prompts, jq gets JSON
1928
type cmdIO struct {
2029
capabilities Capabilities
2130
outputFormat flags.Output
@@ -75,7 +84,8 @@ func (c *cmdIO) Select(items []Tuple, label string) (id string, err error) {
7584
Active: `{{.Name | bold}} ({{.Id|faint}})`,
7685
Inactive: `{{.Name}}`,
7786
},
78-
Stdin: io.NopCloser(c.in),
87+
Stdin: io.NopCloser(c.in),
88+
Stdout: nopWriteCloser{c.err},
7989
}).Run()
8090
if err != nil {
8191
return id, err
@@ -110,6 +120,8 @@ func (c *cmdIO) Secret(label string) (value string, err error) {
110120
Label: label,
111121
Mask: '*',
112122
HideEntered: true,
123+
Stdin: io.NopCloser(c.in),
124+
Stdout: nopWriteCloser{c.err},
113125
})
114126

115127
return prompt.Run()
@@ -132,7 +144,7 @@ func Prompt(ctx context.Context) *promptui.Prompt {
132144
c := fromContext(ctx)
133145
return &promptui.Prompt{
134146
Stdin: io.NopCloser(c.in),
135-
Stdout: nopWriteCloser{c.out},
147+
Stdout: nopWriteCloser{c.err},
136148
}
137149
}
138150

0 commit comments

Comments
 (0)