feat: allow for stdout to be buffered on each command (#2335)

* feat: add preRun func to version to restore stdout

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: add test to capture version in output

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* change stdout buffering to log to be opt-in per command

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips
2023-11-17 14:14:13 -05:00
committed by GitHub
co-authored by Alex Goodman
parent 1c787f436f
commit ba80e490c2
11 changed files with 62 additions and 75 deletions
+11 -5
View File
@@ -8,13 +8,19 @@ import (
"github.com/anchore/syft/internal/log"
)
// capture replaces the provided *os.File and redirects output to the provided writer. The return value is a function,
// which is used to stop the current capturing of output and restore the original file.
const defaultStdoutLogBufferSize = 1024
// CaptureStdoutToTraceLog replaces stdout and redirects output to the log as trace lines. The return value is a
// function, which is used to stop the current capturing of output and restore the original file.
// Example:
//
// restore := capture(&os.Stderr, writer)
// // here, stderr will be captured and redirected to the provided writer
// restore() // block until the output has all been sent to the writer and restore the original stderr
// restore := CaptureStdoutToTraceLog()
// // here, stdout will be captured and redirected to the provided writer
// restore() // block until the output has all been sent to the writer and restore the original stdout
func CaptureStdoutToTraceLog() (close func()) {
return capture(&os.Stdout, newLogWriter(), defaultStdoutLogBufferSize)
}
func capture(target **os.File, writer io.Writer, bufSize int) (close func()) {
original := *target
+1 -1
View File
@@ -25,7 +25,7 @@ func (l *logWriter) Write(data []byte) (n int, err error) {
s, err := l.r.ReadString('\n')
s = strings.TrimRight(s, "\n")
for s != "" {
log.Trace(s)
log.Trace("[unexpected stdout] " + s)
n += len(s)
if err != nil {
break
+2 -2
View File
@@ -23,14 +23,14 @@ func Test_logWriter(t *testing.T) {
_, _ = w.Write([]byte("a\nvalue"))
expected := []any{"a", "value"}
expected := []any{"[unexpected stdout] a", "[unexpected stdout] value"}
require.Equal(t, expected, bl.values)
bl.values = nil
_, _ = w.Write([]byte("some"))
_, _ = w.Write([]byte("thing"))
expected = []any{"some", "thing"}
expected = []any{"[unexpected stdout] some", "[unexpected stdout] thing"}
require.Equal(t, expected, bl.values)
}
@@ -1,63 +0,0 @@
package ui
import (
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/anchore/clio"
)
const defaultStdoutLogBufferSize = 1024
// StdoutLoggingApplication wraps the provided app in a clio.Application, which captures non-report data
// written to os.Stdout and instead logs it to the internal logger. It also modifies the rootCmd help function
// to restore os.Stdout in order for Cobra to properly print help to stdout
func StdoutLoggingApplication(app clio.Application, rootCmd *cobra.Command) clio.Application {
return &stdoutLoggingApplication{
delegate: app,
rootCmd: rootCmd,
}
}
// stdoutLoggingApplication is a clio.Application, which captures data written to os.Stdout prior to the report
// being output and instead sends non-report output to the internal logger
type stdoutLoggingApplication struct {
delegate clio.Application
rootCmd *cobra.Command
}
func (s *stdoutLoggingApplication) ID() clio.Identification {
return s.delegate.ID()
}
func (s *stdoutLoggingApplication) AddFlags(flags *pflag.FlagSet, cfgs ...any) {
s.delegate.AddFlags(flags, cfgs...)
}
func (s *stdoutLoggingApplication) SetupCommand(cmd *cobra.Command, cfgs ...any) *cobra.Command {
return s.delegate.SetupCommand(cmd, cfgs...)
}
func (s *stdoutLoggingApplication) SetupRootCommand(cmd *cobra.Command, cfgs ...any) *cobra.Command {
return s.delegate.SetupRootCommand(cmd, cfgs...)
}
func (s *stdoutLoggingApplication) Run() {
// capture everything written to stdout that is not report output
restoreStdout := capture(&os.Stdout, newLogWriter(), defaultStdoutLogBufferSize)
defer restoreStdout()
// need to restore stdout for cobra to properly output help text to the user on stdout
baseHelpFunc := s.rootCmd.HelpFunc()
defer s.rootCmd.SetHelpFunc(baseHelpFunc)
s.rootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
restoreStdout()
baseHelpFunc(command, strings)
})
s.delegate.Run()
}
var _ clio.Application = (*stdoutLoggingApplication)(nil)