feat: Allow Kong to exit with semantic exit codes (#507)
* feat: Allow Kong to exit with semantic exit codes At Block, we've instrumented a number of commandline tools and set SLOs on some tools' reliability. To do that effectively, we had to partition usage errors from reliability issues. We looked at [prior art](https://github.com/square/exit?tab=readme-ov-file#reserved-codes-and-prior-art) and, taking inspiration from HTTP, defined [a set of semantic exit codes](https://github.com/square/exit?tab=readme-ov-file#about) in ranges: 80-99 for user errors, 100-119 for system errors. We've been wrapping errors in `exit.Error` at whatever level of the stack can tell which class an error is and unwrapping them at exit (`os.Exit(exit.FromError(err))`). This adds support for semantic exit codes to Kong, to `FatalIfErrorf`, which is used internally by `kong.Parse` and often used in Kong applications. * feat: Exit 80 (Usage Error) when usage is syntactically or semantically invalid * refactor: Always exit 80 (Usage Error) on a `ParseError` but don't wrap errors from hooks in `ParseError`
This commit is contained in:
@@ -10,3 +10,6 @@ type ParseError struct {
|
|||||||
|
|
||||||
// Unwrap returns the original cause of the error.
|
// Unwrap returns the original cause of the error.
|
||||||
func (p *ParseError) Unwrap() error { return p.error }
|
func (p *ParseError) Unwrap() error { return p.error }
|
||||||
|
|
||||||
|
// ExitCode returns the status that Kong should exit with if it fails with a ParseError.
|
||||||
|
func (p *ParseError) ExitCode() int { return exitUsageError }
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package kong
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const (
|
||||||
|
exitOk = 0
|
||||||
|
exitNotOk = 1
|
||||||
|
|
||||||
|
// Semantic exit codes from https://github.com/square/exit?tab=readme-ov-file#about
|
||||||
|
exitUsageError = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExitCoder is an interface that may be implemented by an error value to
|
||||||
|
// provide an integer exit code. The method ExitCode should return an integer
|
||||||
|
// that is intended to be used as the exit code for the application.
|
||||||
|
type ExitCoder interface {
|
||||||
|
ExitCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// exitCodeFromError returns the exit code for the given error.
|
||||||
|
// If err implements the exitCoder interface, the ExitCode method is called.
|
||||||
|
// Otherwise, exitCodeFromError returns 0 if err is nil, and 1 if it is not.
|
||||||
|
func exitCodeFromError(err error) int {
|
||||||
|
var e ExitCoder
|
||||||
|
if errors.As(err, &e) {
|
||||||
|
return e.ExitCode()
|
||||||
|
} else if err == nil {
|
||||||
|
return exitOk
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitNotOk
|
||||||
|
}
|
||||||
+9
-3
@@ -786,10 +786,11 @@ func TestUsageOnError(t *testing.T) {
|
|||||||
Flag string `help:"A required flag." required`
|
Flag string `help:"A required flag." required`
|
||||||
}
|
}
|
||||||
w := &strings.Builder{}
|
w := &strings.Builder{}
|
||||||
|
exitCode := -1
|
||||||
p := mustNew(t, &cli,
|
p := mustNew(t, &cli,
|
||||||
kong.Writers(w, w),
|
kong.Writers(w, w),
|
||||||
kong.Description("Some description."),
|
kong.Description("Some description."),
|
||||||
kong.Exit(func(int) {}),
|
kong.Exit(func(code int) { exitCode = code }),
|
||||||
kong.UsageOnError(),
|
kong.UsageOnError(),
|
||||||
)
|
)
|
||||||
_, err := p.Parse([]string{})
|
_, err := p.Parse([]string{})
|
||||||
@@ -806,6 +807,7 @@ Flags:
|
|||||||
test: error: missing flags: --flag=STRING
|
test: error: missing flags: --flag=STRING
|
||||||
`
|
`
|
||||||
assert.Equal(t, expected, w.String())
|
assert.Equal(t, expected, w.String())
|
||||||
|
assert.Equal(t, 80, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShortUsageOnError(t *testing.T) {
|
func TestShortUsageOnError(t *testing.T) {
|
||||||
@@ -813,10 +815,11 @@ func TestShortUsageOnError(t *testing.T) {
|
|||||||
Flag string `help:"A required flag." required`
|
Flag string `help:"A required flag." required`
|
||||||
}
|
}
|
||||||
w := &strings.Builder{}
|
w := &strings.Builder{}
|
||||||
|
exitCode := -1
|
||||||
p := mustNew(t, &cli,
|
p := mustNew(t, &cli,
|
||||||
kong.Writers(w, w),
|
kong.Writers(w, w),
|
||||||
kong.Description("Some description."),
|
kong.Description("Some description."),
|
||||||
kong.Exit(func(int) {}),
|
kong.Exit(func(code int) { exitCode = code }),
|
||||||
kong.ShortUsageOnError(),
|
kong.ShortUsageOnError(),
|
||||||
)
|
)
|
||||||
_, err := p.Parse([]string{})
|
_, err := p.Parse([]string{})
|
||||||
@@ -829,6 +832,7 @@ Run "test --help" for more information.
|
|||||||
test: error: missing flags: --flag=STRING
|
test: error: missing flags: --flag=STRING
|
||||||
`
|
`
|
||||||
assert.Equal(t, expected, w.String())
|
assert.Equal(t, expected, w.String())
|
||||||
|
assert.Equal(t, 80, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomShortUsageOnError(t *testing.T) {
|
func TestCustomShortUsageOnError(t *testing.T) {
|
||||||
@@ -840,10 +844,11 @@ func TestCustomShortUsageOnError(t *testing.T) {
|
|||||||
fmt.Fprintln(ctx.Stdout, "🤷 wish I could help")
|
fmt.Fprintln(ctx.Stdout, "🤷 wish I could help")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
exitCode := -1
|
||||||
p := mustNew(t, &cli,
|
p := mustNew(t, &cli,
|
||||||
kong.Writers(w, w),
|
kong.Writers(w, w),
|
||||||
kong.Description("Some description."),
|
kong.Description("Some description."),
|
||||||
kong.Exit(func(int) {}),
|
kong.Exit(func(code int) { exitCode = code }),
|
||||||
kong.ShortHelp(shortHelp),
|
kong.ShortHelp(shortHelp),
|
||||||
kong.ShortUsageOnError(),
|
kong.ShortUsageOnError(),
|
||||||
)
|
)
|
||||||
@@ -856,4 +861,5 @@ func TestCustomShortUsageOnError(t *testing.T) {
|
|||||||
test: error: missing flags: --flag=STRING
|
test: error: missing flags: --flag=STRING
|
||||||
`
|
`
|
||||||
assert.Equal(t, expected, w.String())
|
assert.Equal(t, expected, w.String())
|
||||||
|
assert.Equal(t, 80, exitCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,35 +311,35 @@ func (k *Kong) extraFlags() []*Flag {
|
|||||||
// invalid one, which will report a normal error).
|
// invalid one, which will report a normal error).
|
||||||
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
|
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
|
||||||
ctx, err = Trace(k, args)
|
ctx, err = Trace(k, args)
|
||||||
if err != nil {
|
if err != nil { // Trace is not expected to return an err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ctx.Error != nil {
|
if ctx.Error != nil {
|
||||||
return nil, &ParseError{error: ctx.Error, Context: ctx}
|
return nil, &ParseError{error: ctx.Error, Context: ctx}
|
||||||
}
|
}
|
||||||
if err = k.applyHook(ctx, "BeforeReset"); err != nil {
|
if err = k.applyHook(ctx, "BeforeReset"); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = ctx.Reset(); err != nil {
|
if err = ctx.Reset(); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, &ParseError{error: err, Context: ctx}
|
||||||
}
|
}
|
||||||
if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
|
if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = ctx.Resolve(); err != nil {
|
if err = ctx.Resolve(); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, &ParseError{error: err, Context: ctx}
|
||||||
}
|
}
|
||||||
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
|
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err = ctx.Apply(); err != nil {
|
if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = ctx.Validate(); err != nil {
|
if err = ctx.Validate(); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, &ParseError{error: err, Context: ctx}
|
||||||
}
|
}
|
||||||
if err = k.applyHook(ctx, "AfterApply"); err != nil {
|
if err = k.applyHook(ctx, "AfterApply"); err != nil {
|
||||||
return nil, &ParseError{error: err, Context: ctx}
|
return nil, err
|
||||||
}
|
}
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
@@ -428,13 +428,15 @@ func (k *Kong) Errorf(format string, args ...any) *Kong {
|
|||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status.
|
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1.
|
||||||
func (k *Kong) Fatalf(format string, args ...any) {
|
func (k *Kong) Fatalf(format string, args ...any) {
|
||||||
k.Errorf(format, args...)
|
k.Errorf(format, args...)
|
||||||
k.Exit(1)
|
k.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FatalIfErrorf terminates with an error message if err != nil.
|
// FatalIfErrorf terminates with an error message if err != nil.
|
||||||
|
// If the error implements the ExitCoder interface, the ExitCode() method is called and
|
||||||
|
// the application exits with that status. Otherwise, the application exits with status 1.
|
||||||
func (k *Kong) FatalIfErrorf(err error, args ...any) {
|
func (k *Kong) FatalIfErrorf(err error, args ...any) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
@@ -455,7 +457,8 @@ func (k *Kong) FatalIfErrorf(err error, args ...any) {
|
|||||||
fmt.Fprintln(k.Stdout)
|
fmt.Fprintln(k.Stdout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
k.Fatalf("%s", msg)
|
k.Errorf("%s", msg)
|
||||||
|
k.Exit(exitCodeFromError(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig from path using the loader configured via Configuration(loader).
|
// LoadConfig from path using the loader configured via Configuration(loader).
|
||||||
|
|||||||
Reference in New Issue
Block a user