Files
seaweedfs/weed/command/shell.go
Chris Lu 7f3908297c fix(weed/shell): suppress prompt when piped (#8990)
* fix(weed/shell): suppress prompt when stdin or stdout is not a TTY

When piping weed shell output (e.g. `echo "s3.user.list" | weed shell | jq`),
the "> " prompt was written to stdout, breaking JSON parsers.

`liner.TerminalSupported()` only checks platform support, not whether
stdin/stdout are actual TTYs. Add explicit checks using `term.IsTerminal()`
so the shell falls back to the non-interactive scanner path when piped.

Fixes #8962

* fix(weed/shell): suppress informational logs unless -verbose is set

Suppress glog info messages and connection status logs on stderr by
default. Add -verbose flag to opt in to the previous noisy behavior.
This keeps piped output clean (e.g. `echo "s3.user.list" | weed shell | jq`).

* fix(weed/shell): defer liner init until after TTY check

Move liner.NewLiner() and related setup (history, completion, interrupt
handler) inside the interactive block so the terminal is not put into
raw mode when stdout is redirected. Previously, liner would set raw mode
unconditionally at startup, leaving the terminal broken when falling
back to the scanner path.

Addresses review feedback from gemini-code-assist.

* refactor(weed/shell): consolidate verbose logging into single block

Group all verbose stderr output within one conditional block instead of
scattering three separate if-verbose checks around the filer logic.

Addresses review feedback from gemini-code-assist.

* fix(weed/shell): clean up global liner state and suppress logtostderr

- Set line=nil after Close() to prevent stale state if RunShell is
  called again (e.g. in tests)
- Add nil check in OnInterrupt handler for non-interactive sessions
- Also set logtostderr=false when not verbose, in case it was enabled

Addresses review feedback from gemini-code-assist.

* refactor(weed/shell): make liner state local to eliminate data race

Replace the package-level `line` variable with a local variable in
RunShell, passing it explicitly to setCompletionHandler, loadHistory,
and saveHistory. This eliminates a data race between the OnInterrupt
goroutine and the defer that previously set the global to nil.

Addresses review feedback from gemini-code-assist.

* rename(weed/shell): rename -verbose flag to -debug

Avoid conflict with -verbose flags already used by individual shell
commands (e.g. ec.encode, volume.fix.replication, volume.check.disk).
2026-04-08 13:07:15 -07:00

76 lines
2.1 KiB
Go

package command
import (
"fmt"
"os"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/shell"
"github.com/seaweedfs/seaweedfs/weed/util"
)
var (
shellOptions shell.ShellOptions
shellInitialFiler *string
shellCluster *string
shellDebug *bool
)
func init() {
cmdShell.Run = runShell // break init cycle
shellOptions.Masters = cmdShell.Flag.String("master", "", "comma-separated master servers, e.g. localhost:9333")
shellOptions.FilerGroup = cmdShell.Flag.String("filerGroup", "", "filerGroup for the filers")
shellInitialFiler = cmdShell.Flag.String("filer", "", "filer host and port for initial connection, e.g. localhost:8888")
shellCluster = cmdShell.Flag.String("cluster", "", "cluster defined in shell.toml")
shellDebug = cmdShell.Flag.Bool("debug", false, "print informational logs to stderr")
}
var cmdShell = &Command{
UsageLine: "shell",
Short: "run interactive administrative commands",
Long: `run interactive administrative commands.
Generate shell.toml via "weed scaffold -config=shell"
`,
}
func runShell(command *Command, args []string) bool {
util.LoadSecurityConfiguration()
shellOptions.GrpcDialOption = security.LoadClientTLS(util.GetViper(), "grpc.client")
shellOptions.Directory = "/"
util.LoadConfiguration("shell", false)
viper := util.GetViper()
cluster := viper.GetString("cluster.default")
if *shellCluster != "" {
cluster = *shellCluster
}
if *shellOptions.Masters == "" {
if cluster == "" {
*shellOptions.Masters = "localhost:9333"
} else {
*shellOptions.Masters = viper.GetString("cluster." + cluster + ".master")
}
}
filerAddress := *shellInitialFiler
if filerAddress == "" && cluster != "" {
filerAddress = viper.GetString("cluster." + cluster + ".filer")
}
shellOptions.FilerAddress = pb.ServerAddress(filerAddress)
shellOptions.Debug = *shellDebug
if shellOptions.Debug {
fmt.Fprintf(os.Stderr, "master: %s filer: %s\n", *shellOptions.Masters, shellOptions.FilerAddress)
}
shell.RunShell(shellOptions)
return true
}