Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions cobracli/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
package cobracli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"maps"
"os"
"slices"
"sort"
"strings"

werror "github.com/palantir/witchcraft-go-error"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -205,3 +213,69 @@ func FlagErrorsUsageErrorConfigurer(command *cobra.Command) {
return fmt.Errorf("%s\n%s", err.Error(), strings.TrimSuffix(c.UsageString(), "\n"))
})
}

// PrintInfoLevelErrorAndParamsWithDebugTransformer provides a cobra error handler that prints the error message and
// any werror-attached safe/unsafe params, one per line. Param values are formatted using their JSON-marshalled
// representation; if a value cannot be marshalled, the marshalling error message is printed in its place.
//
// If the debugVar pointer is non-nil and resolves to true, the debugErrTransform function is called to render the
// error instead, and params are not printed separately. Commonly, debugErrTransform is paired with
// errorstringer.StackWithInterleavedMessages to print a stacktrace.
//
// Output is written to errWriter. If errWriter is nil, os.Stderr is used. Note that this handler does not write to
// the cobra command's configured error stream (command.ErrOrStderr()): callers that want to redirect output through
// the command must pass command.ErrOrStderr() (or an equivalent writer) explicitly. Output is buffered and flushed
// in a single write so partial output is not emitted on a write failure.
func PrintInfoLevelErrorAndParamsWithDebugTransformer(errWriter io.Writer, debugVar *bool, debugErrTransform func(error) string) func(command *cobra.Command, err error) {
if errWriter == nil {
errWriter = os.Stderr
}
return func(_ *cobra.Command, err error) {
if err == nil || err.Error() == "" {
return
}

var buf bytes.Buffer
// always flush buffer no matter what return path from function
defer func() {
_, _ = buf.WriteTo(errWriter)
}()

if debugVar != nil && *debugVar && debugErrTransform != nil {
// writes to bytes.Buffer always return nil error
_, _ = fmt.Fprintln(&buf, "Error:", debugErrTransform(err))
return
}
// writes to bytes.Buffer always return nil error
_, _ = fmt.Fprintln(&buf, "Error:", err.Error())
safe, unsafe := werror.ParamsFromError(err)
if len(safe) == 0 && len(unsafe) == 0 {
return
}
var keys []string
keys = slices.AppendSeq(keys, maps.Keys(safe))
keys = slices.AppendSeq(keys, maps.Keys(unsafe))
sort.Strings(keys)
// writes to bytes.Buffer always return nil error
_, _ = fmt.Fprintln(&buf, "Error params:")
for _, key := range keys {
if val, ok := safe[key]; ok {
// writes to bytes.Buffer always return nil error
_, _ = fmt.Fprintln(&buf, formattedParamLine(key, val))
}
if val, ok := unsafe[key]; ok {
// writes to bytes.Buffer always return nil error
_, _ = fmt.Fprintln(&buf, formattedParamLine(key, val))
}
}
}
}

func formattedParamLine(key string, val any) string {
marshalled, err := json.Marshal(val)
formattedVal := string(marshalled)
if err != nil {
formattedVal = fmt.Sprintf("error json marshalling parameter value: %s", err.Error())
}
return fmt.Sprintf(" %s: %s", key, formattedVal)
}
65 changes: 58 additions & 7 deletions cobracli/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package cobracli_test

import (
"bytes"
"context"
"regexp"
"testing"

"github.com/palantir/pkg/cobracli"
werror "github.com/palantir/witchcraft-go-error"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -44,7 +46,7 @@ func TestExecuteWithParams(t *testing.T) {
{
"version command prints version",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("hello-error")
return errors.New("hello-error")
},
nil,
[]string{"version"},
Expand All @@ -55,7 +57,7 @@ func TestExecuteWithParams(t *testing.T) {
{
"version command not present if not requested",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("hello-error")
return errors.New("hello-error")
},
nil,
[]string{"version"},
Expand All @@ -67,7 +69,7 @@ func TestExecuteWithParams(t *testing.T) {
{
"version flag prints version",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("hello-error")
return errors.New("hello-error")
},
nil,
[]string{"--version"},
Expand All @@ -78,7 +80,7 @@ func TestExecuteWithParams(t *testing.T) {
{
"version flag exists if version is set on root command",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("hello-error")
return errors.New("hello-error")
},
func(cmd *cobra.Command) {
cmd.Version = "1.0.0"
Expand All @@ -91,7 +93,7 @@ func TestExecuteWithParams(t *testing.T) {
{
"version flag not present if not requested",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("hello-error")
return errors.New("hello-error")
},
nil,
[]string{"--version"},
Expand All @@ -108,7 +110,7 @@ Flags:
{
"standard fail",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("custom failure")
return errors.New("custom failure")
},
nil,
nil,
Expand All @@ -119,7 +121,7 @@ Flags:
{
"standard fail with package debug variable prints stack trace",
func(cmd *cobra.Command, args []string) error {
return errors.Errorf("custom failure")
return errors.New("custom failure")
},
nil,
[]string{
Expand Down Expand Up @@ -161,3 +163,52 @@ $`),
}()
}
}

func TestPrintInfoLevelErrorAndParamsWithDebugTransformer(t *testing.T) {
t.Run("nil error produces no output", func(t *testing.T) {
var out bytes.Buffer
cobracli.PrintInfoLevelErrorAndParamsWithDebugTransformer(&out, nil, nil)(&cobra.Command{}, nil)
require.Equal(t, "", out.String())
})

t.Run("error without params prints only error message", func(t *testing.T) {
var out bytes.Buffer
cobracli.PrintInfoLevelErrorAndParamsWithDebugTransformer(&out, nil, nil)(&cobra.Command{}, errors.New("🥳"))
require.Equal(t, "Error: 🥳\n", out.String())
})

t.Run("error with params prints error and params", func(t *testing.T) {
var out bytes.Buffer
type jsonInvalidMapKey struct{}
cobracli.PrintInfoLevelErrorAndParamsWithDebugTransformer(&out, nil, nil)(&cobra.Command{}, werror.ErrorWithContextParams(
context.Background(),
"🥳",
werror.SafeParam("foo", "bar"),
werror.UnsafeParam("baz", 7),
werror.UnsafeParam("qux", map[jsonInvalidMapKey]string{{}: ""}),
))
require.Equal(t, `Error: 🥳
Error params:
baz: 7
foo: "bar"
qux: error json marshalling parameter value: json: unsupported type: map[cobracli_test.jsonInvalidMapKey]string
`, out.String())
})

t.Run("debug mode uses debug transformer", func(t *testing.T) {
var out bytes.Buffer
cobracli.PrintInfoLevelErrorAndParamsWithDebugTransformer(&out, ptr(true), func(err error) string {
return "✅"
})(&cobra.Command{}, werror.ErrorWithContextParams(
context.Background(),
"🥳",
werror.SafeParam("foo", "bar"),
werror.UnsafeParam("baz", "qux"),
))
require.Equal(t, "Error: ✅\n", out.String())
})
}

func ptr[T any](v T) *T {
return &v
}
2 changes: 2 additions & 0 deletions cobracli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.0
require (
github.com/nmiyake/pkg/errorstringer v1.0.0
github.com/palantir/pkg v1.1.0
github.com/palantir/witchcraft-go-error v1.42.0
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
Expand All @@ -13,6 +14,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/palantir/witchcraft-go-params v1.38.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions cobracli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ github.com/nmiyake/pkg/errorstringer v1.0.0 h1:i8VFMHpy2orAs8Lsoi7wOdQ6ipxOUe9GR
github.com/nmiyake/pkg/errorstringer v1.0.0/go.mod h1:M7rsuKy+fiW7j812cNTScqf7kixe7k/ETY/+cbaqzRw=
github.com/palantir/pkg v1.1.0 h1:0EhrSUP8oeeh3MUvk7V/UU7WmsN1UiJNTvNj0sN9Cpo=
github.com/palantir/pkg v1.1.0/go.mod h1:KC9srP/9ssWRxBxFCIqhUGC4Jt7OJkWRz0Iqehup1/c=
github.com/palantir/witchcraft-go-error v1.42.0 h1:uQlWLAn6MSvj4JkOJeh9rAzPbkflOw1s7O1utVy6giw=
github.com/palantir/witchcraft-go-error v1.42.0/go.mod h1:VGIRpvW3a6tzWMyIM1vwPglDdz5I3Gy5NGxkSYlgSf0=
github.com/palantir/witchcraft-go-params v1.38.0 h1:PPyVF+GARkFjk6ID+8PuEQjkYtv+wuyG57uersVxvD4=
github.com/palantir/witchcraft-go-params v1.38.0/go.mod h1:gZinqdF9iMUIHgQMVGtVpQ9GIZuvG9s48oT/+nUm6vg=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading