replace globalOptions.stdout with termstatus.OutputWriter

This commit is contained in:
Michael Eischer
2025-09-20 23:06:28 +02:00
parent c293736841
commit 76b2cdd4fb
29 changed files with 79 additions and 85 deletions

View File

@@ -161,7 +161,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Ter
})
}
_ = tab.Write(gopts.stdout)
_ = tab.Write(gopts.term.OutputWriter())
printer.S("%d cache dirs in %s", len(dirs), cachedir)
return nil

View File

@@ -23,15 +23,13 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) {
}
func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := CheckOptions{
ReadData: true,
CheckUnused: checkUnused,
}
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term)
return err
})
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := CheckOptions{
ReadData: true,
CheckUnused: checkUnused,
}
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term)
return err
})
return buf.String(), err
}

View File

@@ -200,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term
switch tpe {
case "indexes":
return dumpIndexes(ctx, repo, gopts.stdout, printer)
return dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
case "snapshots":
return debugPrintSnapshots(ctx, repo, gopts.stdout)
return debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
case "packs":
return printPacks(ctx, repo, gopts.stdout, printer)
return printPacks(ctx, repo, gopts.term.OutputWriter(), printer)
case "all":
printer.S("snapshots:")
err := debugPrintSnapshots(ctx, repo, gopts.stdout)
err := debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
if err != nil {
return err
}
printer.S("indexes:")
err = dumpIndexes(ctx, repo, gopts.stdout, printer)
err = dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
if err != nil {
return err
}

View File

@@ -424,7 +424,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
}
if gopts.JSON {
enc := json.NewEncoder(gopts.stdout)
enc := json.NewEncoder(gopts.term.OutputWriter())
c.printChange = func(change *Change) {
err := enc.Encode(change)
if err != nil {
@@ -458,7 +458,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
if gopts.JSON {
err := json.NewEncoder(gopts.stdout).Encode(stats)
err := json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil {
printer.E("JSON encode failed: %v", err)
}

View File

@@ -15,13 +15,11 @@ import (
)
func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := DiffOptions{
ShowMetadata: false,
}
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term)
})
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term)
})
return buf.String(), err
}

View File

@@ -49,7 +49,7 @@ Exit status is 1 if there was any error.
for _, flag := range flags {
tab.AddRow(flag)
}
return tab.Write(globalOptions.stdout)
return tab.Write(globalOptions.term.OutputWriter())
},
}

View File

@@ -11,12 +11,10 @@ import (
)
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = wantJSON
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runFind(ctx, opts, gopts, []string{pattern}, gopts.term)
})
return runFind(ctx, opts, gopts, []string{pattern}, gopts.term)
})
rtest.OK(t, err)
return buf.Bytes()

View File

@@ -251,7 +251,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
}
if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(gopts.stdout, k)
err = PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil {
return err
}
@@ -274,7 +274,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
}
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("keep %d snapshots:\n", len(keep))
if err := PrintSnapshots(gopts.stdout, keep, reasons, opts.Compact); err != nil {
if err := PrintSnapshots(gopts.term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
return err
}
printer.P("\n")
@@ -283,7 +283,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("remove %d snapshots:\n", len(remove))
if err := PrintSnapshots(gopts.stdout, remove, nil, opts.Compact); err != nil {
if err := PrintSnapshots(gopts.term.OutputWriter(), remove, nil, opts.Compact); err != nil {
return err
}
printer.P("\n")
@@ -328,7 +328,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
}
if gopts.JSON && len(jsonGroups) > 0 {
err = printJSONForget(gopts.stdout, jsonGroups)
err = printJSONForget(gopts.term.OutputWriter(), jsonGroups)
if err != nil {
return err
}

View File

@@ -84,7 +84,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
defer func() { err = outFile.Close() }()
outWriter = outFile
} else {
outWriter = gopts.stdout
outWriter = gopts.term.OutputWriter()
}
err = generate(outWriter)

View File

@@ -9,10 +9,8 @@ import (
)
func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runGenerate(opts, gopts, []string{}, gopts.term)
})
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runGenerate(opts, gopts, []string{}, gopts.term)
})
return buf.Bytes(), err
}

View File

@@ -132,7 +132,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
ID: s.Config().ID,
Repository: location.StripPassword(gopts.backends, gopts.Repo),
}
return json.NewEncoder(gopts.stdout).Encode(status)
return json.NewEncoder(gopts.term.OutputWriter()).Encode(status)
}
return nil

View File

@@ -16,10 +16,8 @@ import (
)
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term)
})
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)

View File

@@ -95,7 +95,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
}
if gopts.JSON {
return json.NewEncoder(gopts.stdout).Encode(keys)
return json.NewEncoder(gopts.term.OutputWriter()).Encode(keys)
}
tab := table.New()
@@ -108,5 +108,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
tab.AddRow(key)
}
return tab.Write(gopts.stdout)
return tab.Write(gopts.term.OutputWriter())
}

View File

@@ -11,10 +11,8 @@ import (
)
func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runList(ctx, gopts, []string{tpe}, gopts.term)
})
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runList(ctx, gopts, []string{tpe}, gopts.term)
})
rtest.OK(t, err)
return parseIDsFromReader(t, buf)

View File

@@ -382,11 +382,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
if gopts.JSON {
printer = &jsonLsPrinter{
enc: json.NewEncoder(gopts.stdout),
enc: json.NewEncoder(gopts.term.OutputWriter()),
}
} else if opts.Ncdu {
printer = &ncduLsPrinter{
out: gopts.stdout,
out: gopts.term.OutputWriter(),
}
} else {
printer = &textLsPrinter{

View File

@@ -13,11 +13,9 @@ import (
)
func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.Quiet = true
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runLs(context.TODO(), opts, gopts, args, gopts.term)
})
return runLs(context.TODO(), opts, gopts, args, gopts.term)
})
rtest.OK(t, err)
return buf.Bytes()

View File

@@ -89,7 +89,7 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) {
}
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true
opts := ForgetOptions{
DryRun: true,
@@ -98,9 +98,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
pruneOpts := PruneOptions{
MaxUnused: "5%",
}
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args)
})
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args)
})
rtest.OK(t, err)

View File

@@ -17,7 +17,7 @@ import (
func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) {
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.stdout = io.Discard
gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
}))
}
@@ -129,7 +129,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
return &appendOnlyBackend{r}, nil
}
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.stdout = io.Discard
gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
})

View File

@@ -103,7 +103,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
}
if gopts.JSON {
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped)
err := printSnapshotGroupJSON(gopts.term.OutputWriter(), snapshotGroups, grouped)
if err != nil {
printer.E("error printing snapshots: %v", err)
}
@@ -116,12 +116,12 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
}
if grouped {
err := PrintSnapshotGroupHeader(gopts.stdout, k)
err := PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil {
return err
}
}
err := PrintSnapshots(gopts.stdout, list, nil, opts.Compact)
err := PrintSnapshots(gopts.term.OutputWriter(), list, nil, opts.Compact)
if err != nil {
return err
}

View File

@@ -10,13 +10,11 @@ import (
)
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true
opts := SnapshotOptions{}
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runSnapshots(ctx, opts, gopts, []string{}, gopts.term)
})
return runSnapshots(ctx, opts, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)

View File

@@ -169,7 +169,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args
}
if gopts.JSON {
err = json.NewEncoder(gopts.stdout).Encode(stats)
err = json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil {
return fmt.Errorf("encoding output: %v", err)
}

View File

@@ -43,7 +43,7 @@ Exit status is 1 if there was any error.
GoArch: runtime.GOARCH,
}
err := json.NewEncoder(globalOptions.stdout).Encode(jsonS)
err := json.NewEncoder(globalOptions.term.OutputWriter()).Encode(jsonS)
if err != nil {
printer.E("JSON encode failed: %v\n", err)
return

View File

@@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@@ -74,7 +73,6 @@ type GlobalOptions struct {
limiter.Limits
password string
stdout io.Writer
term ui.Terminal
backends *location.Registry

View File

@@ -212,10 +212,6 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
Quiet: true,
CacheDir: env.cache,
password: rtest.TestPassword,
// stdout and stderr are written to by Warnf etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
stdout: &logOutputter{t},
extended: make(options.Options),
// replace this hook with "nil" if listing a filetype more than once is necessary
@@ -416,18 +412,24 @@ func testFileSize(filename string, size int64) error {
return nil
}
func withCaptureStdout(gopts GlobalOptions, inner func(gopts GlobalOptions) error) (*bytes.Buffer, error) {
func withCaptureStdout(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
gopts.stdout = buf
err := inner(gopts)
err := withTermStatusRaw(os.Stdin, buf, &logOutputter{t: t}, gopts, callback)
return buf, err
}
func withTermStatus(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
// stdout and stderr are written to by printer functions etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
return withTermStatusRaw(os.Stdin, &logOutputter{t: t}, &logOutputter{t: t}, gopts, callback)
}
func withTermStatusRaw(stdin io.ReadCloser, stdout, stderr io.Writer, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
ctx, cancel := context.WithCancel(context.TODO())
var wg sync.WaitGroup
term := termstatus.New(os.Stdin, gopts.stdout, &logOutputter{t: t}, gopts.Quiet)
term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
gopts.term = term
wg.Add(1)
go func() {

View File

@@ -173,13 +173,11 @@ func main() {
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
globalOptions := GlobalOptions{
stdout: os.Stdout,
backends: collectBackends(),
}
func() {
term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet)
defer cancel()
globalOptions.stdout = termstatus.WrapStdout(term)
globalOptions.term = term
ctx := createGlobalContext(os.Stderr)
err = newRootCommand(&globalOptions).ExecuteContext(ctx)

View File

@@ -40,6 +40,10 @@ func (m *MockTerminal) ReadPassword(_ context.Context, _ string) (string, error)
return "password", nil
}
func (m *MockTerminal) OutputWriter() io.Writer {
return nil
}
func (m *MockTerminal) OutputRaw() io.Writer {
return nil
}

View File

@@ -16,9 +16,12 @@ type Terminal interface {
SetStatus(lines []string)
// CanUpdateStatus returns true if the terminal can update the status lines.
CanUpdateStatus() bool
InputRaw() io.ReadCloser
InputIsTerminal() bool
ReadPassword(ctx context.Context, prompt string) (string, error)
OutputWriter() io.Writer
// OutputRaw returns the output writer. Should only be used if there is no
// other option. Must not be used in combination with Print, Error, SetStatus
// or any other method that writes to the terminal.

View File

@@ -30,6 +30,9 @@ type Terminal struct {
outputIsTerminal bool
canUpdateStatus bool
outputWriter io.WriteCloser
outputWriterOnce sync.Once
// will be closed when the goroutine which runs Run() terminates, so it'll
// yield a default value immediately
closed chan struct{}
@@ -73,6 +76,9 @@ func Setup(stdin io.ReadCloser, stdout, stderr io.Writer, quiet bool) (*Terminal
}()
return term, func() {
if term.outputWriter != nil {
_ = term.outputWriter.Close()
}
// shutdown termstatus
cancel()
wg.Wait()
@@ -158,6 +164,13 @@ func (t *Terminal) CanUpdateStatus() bool {
return t.canUpdateStatus
}
func (t *Terminal) OutputWriter() io.Writer {
t.outputWriterOnce.Do(func() {
t.outputWriter = newLineWriter(t.Print)
})
return t.outputWriter
}
// OutputRaw returns the output writer. Should only be used if there is no
// other option. Must not be used in combination with Print, Error, SetStatus
// or any other method that writes to the terminal.

View File

@@ -6,12 +6,6 @@ import (
"sync"
)
// WrapStdout returns line-buffering replacements for os.Stdout.
// On Close, the remaining bytes are written, followed by a line break.
func WrapStdout(term *Terminal) (stdout io.WriteCloser) {
return newLineWriter(term.Print)
}
type lineWriter struct {
m sync.Mutex
buf bytes.Buffer