Skip to content

Commit 88a565d

Browse files
authored
Error handling (#12)
* Replace osExit with configurable error handlign * fix fallback to default value * update callback signature and add comments * improve coverage * improve coverage * update readme * update comment
1 parent 00e848f commit 88a565d

File tree

4 files changed

+100
-41
lines changed

4 files changed

+100
-41
lines changed

Readme.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ straightforward API, making configuration predictable and easy to manage.
2525

2626
## Features
2727

28-
- **Type-safe**: Generics-based implementation, no reflection
29-
- **Container-optimized**: Unified API for both environment variables and cmd flags
30-
- **Comprehensive support**: Built-in parsing for all widely used types
31-
- **Flexible**: Supports configurable slices, binary decoders,
32-
and JSON values
33-
- **Extensible**: Easily add custom parsers
34-
- **Minimalistic**: Clean, straightforward API
35-
- **Lightweight**: Zero external dependencies
28+
- **Type-safe** – Generics-based design, no runtime reflection
29+
- **Container-optimized** – Unified API for env vars and CLI flags
30+
- **Flexible** – Handles primitives, slices, JSON, and binary formats
31+
- **Extensible** – Custom parsers can be added easily
32+
- **Error handling** – Built-in handlers with custom callback support
33+
- **Secure & lightweight** – No external dependencies, suitable for security-sensitive
34+
applications
3635

3736
## Usage
3837

binding.go

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ flag > environment variable > default value. Both environment variables and flag
77
88
Flag parsing is handled by the standard library's flag package via the CommandLine flag set.
99
Like the flag package, errors encountered during environment variable parsing will cause
10-
the program to exit with status code 2.
10+
the program to exit with status code 2 by default, but the error handler can be predefined.
1111
1212
# Example usage:
1313
@@ -31,7 +31,6 @@ import (
3131
"encoding/base64"
3232
"encoding/json"
3333
"flag"
34-
"fmt"
3534
"net"
3635
"net/url"
3736
"os"
@@ -387,26 +386,18 @@ func handleVar[T any](b binding, ptr *T, parser func(string) (T, error)) {
387386
if envVal := os.Getenv(b.envName); envVal != "" {
388387
v, err := parser(envVal)
389388
if err != nil {
390-
fmt.Fprintf(
391-
flag.CommandLine.Output(),
392-
"Unable to parse env-variable %s as type %T\n",
393-
b.envName,
394-
*ptr,
395-
)
396-
397-
// os.Exit(2) replicates the default error handling behavior of flag.CommandLine
398-
if !isTestEnv {
399-
os.Exit(2)
400-
}
389+
handleError(err, ptr, envVal, b.envName, "")
390+
} else {
391+
*ptr = v
401392
}
402-
*ptr = v
403393
}
404394

405395
if b.flagName != "" {
406396
flag.Func(b.flagName, b.flagUsage, func(s string) error {
407397
parsed, err := parser(s)
408398
if err != nil {
409-
return err
399+
handleError(err, ptr, s, "", b.flagName)
400+
return nil
410401
}
411402

412403
*ptr = parsed
@@ -420,18 +411,10 @@ func handleSlice[T any](b binding, ptr *[]T, parser func(string) (T, error)) {
420411
for _, v := range strings.Split(envVal, b.sliceSep) {
421412
parsed, err := parser(v)
422413
if err != nil {
423-
fmt.Fprintf(
424-
flag.CommandLine.Output(),
425-
"Unable to parse env-variable %s as type %T\n",
426-
b.envName,
427-
*ptr,
428-
)
429-
430-
// os.Exit(2) replicates the default error handling behavior of flag.CommandLine
431-
if !isTestEnv {
432-
os.Exit(2)
433-
}
414+
handleError(err, ptr, envVal, b.envName, "")
415+
continue
434416
}
417+
435418
*ptr = append(*ptr, parsed)
436419
}
437420
}
@@ -441,7 +424,8 @@ func handleSlice[T any](b binding, ptr *[]T, parser func(string) (T, error)) {
441424
for _, v := range strings.Split(s, b.sliceSep) {
442425
parsed, err := parser(v)
443426
if err != nil {
444-
return err
427+
handleError(err, ptr, s, "", b.flagName)
428+
continue
445429
}
446430

447431
*ptr = append(*ptr, parsed)
@@ -451,5 +435,3 @@ func handleSlice[T any](b binding, ptr *[]T, parser func(string) (T, error)) {
451435
})
452436
}
453437
}
454-
455-
var isTestEnv bool

binding_test.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
func TestBind(t *testing.T) {
15-
isTestEnv = true
15+
ErrorHandlerFunc = OnErrorLogAndContinue
1616

1717
type tc struct {
1818
name string
@@ -464,7 +464,7 @@ func TestBind(t *testing.T) {
464464

465465
Var(&target).WithDefault(80).Bind("PORT", "port")
466466

467-
return toSlice(func() { checkVal(t, uint(0), target) })
467+
return toSlice(func() { checkVal(t, uint(80), target) })
468468
},
469469
},
470470
{
@@ -489,7 +489,7 @@ func TestBind(t *testing.T) {
489489

490490
Var(&target).BindEnv("PORTS")
491491

492-
return toSlice(func() { checkSlice(t, []int{0, 0}, target) })
492+
return toSlice(func() { checkSlice(t, []int{}, target) })
493493
},
494494
},
495495
{
@@ -527,7 +527,7 @@ func TestBind(t *testing.T) {
527527
}
528528
VarFunc(&target, parser).WithDefault(10).Bind("MY_FORMAT", "my-format")
529529

530-
return toSlice(func() { checkVal(t, 0, target) })
530+
return toSlice(func() { checkVal(t, 10, target) })
531531
},
532532
},
533533
{
@@ -577,6 +577,41 @@ func TestBind(t *testing.T) {
577577
}
578578
}
579579

580+
func TestErrroHandling(t *testing.T) {
581+
t.Run("Err ignore", func(t *testing.T) {
582+
ErrorHandlerFunc = OnErrorIgnore
583+
584+
reset()
585+
var target int
586+
587+
os.Setenv("ENV_ERR", "one")
588+
BindVar(&target, "ENV_ERR", "")
589+
Parse()
590+
})
591+
592+
t.Run("Err exit", func(t *testing.T) {
593+
var exitStatus int
594+
595+
oldFunc := osExitFunc
596+
osExitFunc = func(code int) {
597+
exitStatus = code
598+
}
599+
defer func() { osExitFunc = oldFunc }()
600+
601+
ErrorHandlerFunc = OnErrorExit
602+
603+
reset()
604+
var target int
605+
606+
os.Setenv("ENV_ERR", "one")
607+
BindVar(&target, "ENV_ERR", "")
608+
Parse()
609+
610+
checkVal(t, 2, exitStatus)
611+
})
612+
613+
}
614+
580615
func checkVal[A comparable](t *testing.T, want A, got A) {
581616
t.Helper()
582617

err.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package enflag
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// ErrorHandlerFunc is a function called after a value parser returns an error.
10+
// See predefined options: OnErrorExit, OnErrorIgnore, and OnErrorLogAndContinue.
11+
// It can also be replaced with a custom handler.
12+
var ErrorHandlerFunc = OnErrorExit
13+
14+
// OnErrorExit prints the error and exits with status code 2.
15+
var OnErrorExit = func(err error, rawVal string, target any, envName string, flagName string) {
16+
OnErrorLogAndContinue(err, rawVal, target, envName, flagName)
17+
osExitFunc(2)
18+
}
19+
20+
// OnErrorIgnore silently ignores the error.
21+
// If a default value is specified, it will be used.
22+
var OnErrorIgnore = func(err error, rawVal string, target any, envName string, flagName string) {}
23+
24+
// OnErrorLogAndContinue prints the error message but continues execution.
25+
// If a default value is specified, it will be used.
26+
var OnErrorLogAndContinue = func(err error, rawVal string, target any, envName string, flagName string) {
27+
_, _ = err, rawVal
28+
29+
var msg string
30+
if envName != "" {
31+
msg = fmt.Sprintf("unable to parse env-variable %q as type %T\n", envName, target)
32+
} else if flagName != "" {
33+
msg = fmt.Sprintf("unable to parse flag %q as type %T\n", flagName, target)
34+
}
35+
36+
flag.CommandLine.Output().Write([]byte(msg))
37+
}
38+
39+
func handleError[T any](err error, target *T, rawVal, envName string, flagName string) {
40+
ErrorHandlerFunc(err, rawVal, *target, envName, flagName)
41+
}
42+
43+
var osExitFunc = os.Exit

0 commit comments

Comments
 (0)