consolidate checks whether stdin/stdout is terminal

This commit is contained in:
Michael Eischer
2025-09-07 14:19:29 +02:00
parent 93ccc548c8
commit 0b0dd07f15
8 changed files with 54 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/restic/restic/internal/dump" "github.com/restic/restic/internal/dump"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/terminal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -199,7 +200,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
} }
func checkStdoutArchive() error { func checkStdoutArchive() error {
if stdoutIsTerminal() { if terminal.StdoutIsTerminal() {
return fmt.Errorf("stdout is the terminal, please redirect output") return fmt.Errorf("stdout is the terminal, please redirect output")
} }
return nil return nil

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/terminal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -71,7 +72,7 @@ func writeManpages(root *cobra.Command, dir string) error {
} }
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) {
if stdoutIsTerminal() { if terminal.StdoutIsTerminal() {
Verbosef("writing %s completion file to %v\n", shell, filename) Verbosef("writing %s completion file to %v\n", shell, filename)
} }
var outWriter io.Writer var outWriter io.Writer

View File

@@ -10,6 +10,7 @@ import (
"github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/filter"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/restorer"
"github.com/restic/restic/internal/terminal"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
restoreui "github.com/restic/restic/internal/ui/restore" restoreui "github.com/restic/restic/internal/ui/restore"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
@@ -260,7 +261,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
} }
var count int var count int
t0 := time.Now() t0 := time.Now()
bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && stdoutIsTerminal(), 0, "files verified", term) bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && terminal.StdoutIsTerminal(), 0, "files verified", term)
count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar) count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar)
if err != nil { if err != nil {
return err return err

View File

@@ -199,28 +199,6 @@ func collectBackends() *location.Registry {
return backends return backends
} }
func stdinIsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
}
func stdoutIsTerminal() bool {
// mintty on windows can use pipes which behave like a posix terminal,
// but which are not a terminal handle
return term.IsTerminal(int(os.Stdout.Fd())) || stdoutCanUpdateStatus()
}
func stdoutCanUpdateStatus() bool {
return terminal.CanUpdateStatus(os.Stdout.Fd())
}
func stdoutTerminalWidth() int {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
return 0
}
return w
}
// ClearLine creates a platform dependent string to clear the current // ClearLine creates a platform dependent string to clear the current
// line, so it can be overwritten. // line, so it can be overwritten.
// //
@@ -232,7 +210,7 @@ func clearLine(w int) string {
// ANSI sequences are not supported on Windows cmd shell. // ANSI sequences are not supported on Windows cmd shell.
if w <= 0 { if w <= 0 {
if w = stdoutTerminalWidth(); w <= 0 { if w = terminal.StdoutTerminalWidth(); w <= 0 {
return "" return ""
} }
} }
@@ -399,11 +377,11 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin
err error err error
) )
if stdinIsTerminal() { if terminal.StdinIsTerminal() {
password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt) password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt)
} else { } else {
password, err = readPassword(os.Stdin) password, err = readPassword(os.Stdin)
if stdoutIsTerminal() { if terminal.StdoutIsTerminal() {
Verbosef("reading repository password from stdin\n") Verbosef("reading repository password from stdin\n")
} }
} }
@@ -427,7 +405,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt
if err != nil { if err != nil {
return "", err return "", err
} }
if stdinIsTerminal() { if terminal.StdinIsTerminal() {
pw2, err := ReadPassword(ctx, gopts, prompt2) pw2, err := ReadPassword(ctx, gopts, prompt2)
if err != nil { if err != nil {
return "", err return "", err
@@ -490,7 +468,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
} }
passwordTriesLeft := 1 passwordTriesLeft := 1
if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
passwordTriesLeft = 3 passwordTriesLeft = 3
} }
@@ -520,7 +498,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
return nil, errors.Fatalf("%s", err) return nil, errors.Fatalf("%s", err)
} }
if stdoutIsTerminal() && !opts.JSON { if terminal.StdoutIsTerminal() && !opts.JSON {
id := s.Config().ID id := s.Config().ID
if len(id) > 8 { if len(id) > 8 {
id = id[:8] id = id[:8]
@@ -544,7 +522,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
return s, nil return s, nil
} }
if c.Created && !opts.JSON && stdoutIsTerminal() { if c.Created && !opts.JSON && terminal.StdoutIsTerminal() {
Verbosef("created new cache in %v\n", c.Base) Verbosef("created new cache in %v\n", c.Base)
} }
@@ -563,7 +541,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
// cleanup old cache dirs if instructed to do so // cleanup old cache dirs if instructed to do so
if opts.CleanupCache { if opts.CleanupCache {
if stdoutIsTerminal() && !opts.JSON { if terminal.StdoutIsTerminal() && !opts.JSON {
Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base)
} }
for _, item := range oldCacheDirs { for _, item := range oldCacheDirs {
@@ -574,7 +552,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
} }
} }
} else { } else {
if stdoutIsTerminal() { if terminal.StdoutIsTerminal() {
Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n", Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n",
len(oldCacheDirs), c.Base) len(oldCacheDirs), c.Base)
} }

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/restic/restic/internal/terminal"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/progress"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
@@ -23,7 +24,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration {
fps = 60 fps = 60
} }
interval = time.Duration(float64(time.Second) / fps) interval = time.Duration(float64(time.Second) / fps)
} else if !json && !stdoutCanUpdateStatus() || !show { } else if !json && !terminal.StdoutCanUpdateStatus() || !show {
interval = 0 interval = 0
} }
return interval return interval
@@ -68,9 +69,9 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter
func printProgress(status string, final bool) { func printProgress(status string, final bool) {
canUpdateStatus := stdoutCanUpdateStatus() canUpdateStatus := terminal.StdoutCanUpdateStatus()
w := stdoutTerminalWidth() w := terminal.StdoutTerminalWidth()
if w > 0 { if w > 0 {
if w < 3 { if w < 3 {
status = termstatus.Truncate(status, w) status = termstatus.Truncate(status, w)
@@ -103,11 +104,11 @@ func printProgress(status string, final bool) {
} }
func newIndexProgress(quiet bool, json bool) *progress.Counter { func newIndexProgress(quiet bool, json bool) *progress.Counter {
return newProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded") return newProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded")
} }
func newIndexTerminalProgress(quiet bool, json bool, term *termstatus.Terminal) *progress.Counter { func newIndexTerminalProgress(quiet bool, json bool, term *termstatus.Terminal) *progress.Counter {
return newTerminalProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded", term) return newTerminalProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded", term)
} }
type terminalProgressPrinter struct { type terminalProgressPrinter struct {

View File

@@ -0,0 +1,29 @@
package terminal
import (
"os"
"golang.org/x/term"
)
func StdinIsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
}
func StdoutIsTerminal() bool {
// mintty on windows can use pipes which behave like a posix terminal,
// but which are not a terminal handle
return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus()
}
func StdoutCanUpdateStatus() bool {
return CanUpdateStatus(os.Stdout.Fd())
}
func StdoutTerminalWidth() int {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
return 0
}
return w
}

View File

@@ -13,7 +13,7 @@ const (
PosixControlClearLine = "\x1b[2K" PosixControlClearLine = "\x1b[2K"
) )
// posixClearCurrentLine removes all characters from the current line and resets the // PosixClearCurrentLine removes all characters from the current line and resets the
// cursor position to the first column. // cursor position to the first column.
func PosixClearCurrentLine(wr io.Writer, _ uintptr) { func PosixClearCurrentLine(wr io.Writer, _ uintptr) {
// clear current line // clear current line
@@ -24,7 +24,7 @@ func PosixClearCurrentLine(wr io.Writer, _ uintptr) {
} }
} }
// posixMoveCursorUp moves the cursor to the line n lines above the current one. // PosixMoveCursorUp moves the cursor to the line n lines above the current one.
func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) { func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) {
data := []byte(PosixControlMoveCursorHome) data := []byte(PosixControlMoveCursorHome)
data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...) data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...)

View File

@@ -10,13 +10,13 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// clearCurrentLine removes all characters from the current line and resets the // ClearCurrentLine removes all characters from the current line and resets the
// cursor position to the first column. // cursor position to the first column.
func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) { func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) {
return PosixClearCurrentLine return PosixClearCurrentLine
} }
// moveCursorUp moves the cursor to the line n lines above the current one. // MoveCursorUp moves the cursor to the line n lines above the current one.
func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) {
return PosixMoveCursorUp return PosixMoveCursorUp
} }