Skip to content

Commit 6437d73

Browse files
authored
various fixes (#7)
* use type for context key * remove unused field * rename and comment exit helpers * remove unnecessary import parentheses * use `test.NoError` * fix exit, add run test * fix example * add arg input validation tests * add more config validations * add exit tests * add validation tests
1 parent fa58295 commit 6437d73

21 files changed

+228
-49
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
coverage.out

argument.go

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func newArgument(name, description string, options ...option.Option[*Argument])
2929
return nil, errors.Wrapf(err, "building argument %q", name)
3030
}
3131

32+
if err := argument.validateConfig(); err != nil {
33+
return nil, errors.Wrapf(err, "invalid argument %q", name)
34+
}
35+
3236
return argument, nil
3337
}
3438

argument_config_validation.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cli
2+
3+
import (
4+
"strings"
5+
6+
"github.com/bobg/errors"
7+
)
8+
9+
func (a *Argument) validateConfig() error {
10+
if len(strings.Fields(a.name)) > 1 {
11+
return errors.Errorf("argument name %q must be a single token", a.name)
12+
}
13+
14+
if a.name == "" {
15+
return errors.New("argument name cannot be empty")
16+
}
17+
18+
return nil
19+
}

argument_config_validation_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/broothie/test"
7+
)
8+
9+
func TestArgument_validateConfig(t *testing.T) {
10+
t.Run("empty name", func(t *testing.T) {
11+
arg := &Argument{name: ""}
12+
err := arg.validateConfig()
13+
test.ErrorMessageIs(t, err, "argument name cannot be empty")
14+
})
15+
16+
t.Run("multiple tokens", func(t *testing.T) {
17+
arg := &Argument{name: "invalid argument name"}
18+
err := arg.validateConfig()
19+
test.ErrorMessageIs(t, err, `argument name "invalid argument name" must be a single token`)
20+
})
21+
22+
t.Run("valid name", func(t *testing.T) {
23+
arg := &Argument{name: "valid-arg"}
24+
err := arg.validateConfig()
25+
test.NoError(t, err)
26+
})
27+
}

argument_input_validation_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/broothie/test"
7+
)
8+
9+
func TestArgument_validateInput(t *testing.T) {
10+
arg, err := newArgument("test-arg", "Test arg.")
11+
test.MustNoError(t, err)
12+
13+
test.ErrorMessageIs(t, arg.validateInput(), `argument "test-arg": argument missing value`)
14+
15+
arg.value = "something"
16+
test.NoError(t, arg.validateInput())
17+
}

cli_test.go

+14-14
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func Test_git(t *testing.T) {
4242
called()
4343

4444
gitDir, err := FlagValue[string](ctx, "git-dir")
45-
test.Nil(t, err)
45+
test.NoError(t, err)
4646
test.Equal(t, "/path/to/something", gitDir)
4747

4848
return nil
@@ -57,7 +57,7 @@ func Test_git(t *testing.T) {
5757
called()
5858

5959
gitDir, err := FlagValue[string](ctx, "git-dir")
60-
test.Nil(t, err)
60+
test.NoError(t, err)
6161
test.Equal(t, "/path/to/something", gitDir)
6262

6363
return nil
@@ -82,7 +82,7 @@ func Test_git(t *testing.T) {
8282
called()
8383

8484
message, err := FlagValue[string](ctx, "message")
85-
test.Nil(t, err)
85+
test.NoError(t, err)
8686
test.Equal(t, "a commit message", message)
8787

8888
return nil
@@ -97,11 +97,11 @@ func Test_git(t *testing.T) {
9797
called()
9898

9999
isAll, err := FlagValue[bool](ctx, "all")
100-
test.Nil(t, err)
100+
test.NoError(t, err)
101101
test.False(t, isAll)
102102

103103
message, err := FlagValue[string](ctx, "message")
104-
test.Nil(t, err)
104+
test.NoError(t, err)
105105
test.Equal(t, "a commit message", message)
106106

107107
return nil
@@ -116,11 +116,11 @@ func Test_git(t *testing.T) {
116116
called()
117117

118118
isAll, err := FlagValue[bool](ctx, "all")
119-
test.Nil(t, err)
119+
test.NoError(t, err)
120120
test.False(t, isAll)
121121

122122
message, err := FlagValue[string](ctx, "message")
123-
test.Nil(t, err)
123+
test.NoError(t, err)
124124
test.Equal(t, "a commit message", message)
125125

126126
return nil
@@ -135,11 +135,11 @@ func Test_git(t *testing.T) {
135135
called()
136136

137137
isAll, err := FlagValue[bool](ctx, "all")
138-
test.Nil(t, err)
138+
test.NoError(t, err)
139139
test.True(t, isAll)
140140

141141
message, err := FlagValue[string](ctx, "message")
142-
test.Nil(t, err)
142+
test.NoError(t, err)
143143
test.Equal(t, "a commit message", message)
144144

145145
return nil
@@ -154,7 +154,7 @@ func Test_git(t *testing.T) {
154154
called()
155155

156156
branch, err := ArgValue[string](ctx, "branch")
157-
test.Nil(t, err)
157+
test.NoError(t, err)
158158
test.Equal(t, "some-branch", branch)
159159

160160
return nil
@@ -169,11 +169,11 @@ func Test_git(t *testing.T) {
169169
called()
170170

171171
branch, err := ArgValue[string](ctx, "branch")
172-
test.Nil(t, err)
172+
test.NoError(t, err)
173173
test.Equal(t, "some-branch", branch)
174174

175175
isNewBranch, err := FlagValue[bool](ctx, "new-branch")
176-
test.Nil(t, err)
176+
test.NoError(t, err)
177177
test.True(t, isNewBranch)
178178

179179
return nil
@@ -188,7 +188,7 @@ func Test_git(t *testing.T) {
188188
called()
189189

190190
globalGitignore, err := FlagValue[string](ctx, "global-gitignore")
191-
test.Nil(t, err)
191+
test.NoError(t, err)
192192
test.Equal(t, globalGitignore, "path/to/some/.gitignore")
193193
return nil
194194
}
@@ -228,7 +228,7 @@ func Test_git(t *testing.T) {
228228
SetHandler(lo.IfF(testCase.gitHandler != nil, func() Handler { return testCase.gitHandler(t) }).Else(nil)),
229229
)
230230

231-
test.MustNoError(t, err)
231+
test.NoError(t, err)
232232
test.Nil(t, command.Run(context.TODO(), testCase.rawArgs))
233233
})
234234
}

command_config_validation.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package cli
22

3-
import (
4-
"github.com/bobg/errors"
5-
)
3+
import "github.com/bobg/errors"
64

75
func (c *Command) validateConfig() error {
86
validations := []func() error{

command_test.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package cli
22

3-
import "os"
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
)
48

59
func ExampleNewCommand() {
610
command, _ := NewCommand("server", "An http server.",
@@ -38,3 +42,22 @@ func ExampleNewCommand() {
3842
// --port -p Port to run server on. (type: int, default: $PORT, "3000")
3943
// --auth-required Whether to require authentication. (type: bool, default: "true")
4044
}
45+
46+
func ExampleRun() {
47+
oldArgs := os.Args
48+
os.Args = []string{"echo", "hello"}
49+
defer func() { os.Args = oldArgs }()
50+
51+
Run("echo", "Echo the arguments.",
52+
AddArg("arg", "The argument to echo."),
53+
SetHandler(func(ctx context.Context) error {
54+
arg, _ := ArgValue[string](ctx, "arg")
55+
56+
fmt.Println(arg)
57+
return nil
58+
}),
59+
)
60+
61+
// Output:
62+
// hello
63+
}

context.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ func ArgValue[T any](ctx context.Context, name string) (T, error) {
6262
return arg.defaultValue.(T), nil
6363
}
6464

65-
var commandContextKey struct{}
65+
type commandContextKeyType struct{}
66+
67+
var commandContextKey = commandContextKeyType{}
6668

6769
func (c *Command) onContext(parent context.Context) context.Context {
6870
return context.WithValue(parent, commandContextKey, c)

error.go renamed to exit.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ import (
88
"github.com/bobg/errors"
99
)
1010

11+
// ExitError is an error that causes the program to exit with a given status code.
1112
type ExitError struct {
1213
Code int
1314
}
1415

16+
// Error implements the error interface.
1517
func (e ExitError) Error() string {
1618
return fmt.Sprintf("exit status %d", e.Code)
1719
}
1820

19-
func ExitCode(code int) ExitError {
20-
return ExitError{Code: code}
21+
// ExitCode returns an ExitError with the given code.
22+
func ExitCode(code int) *ExitError {
23+
return &ExitError{Code: code}
2124
}
2225

26+
// ExitWithError exits the program with an error.
2327
func ExitWithError(err error) {
2428
fmt.Println(err)
2529

2630
if exitErr := new(ExitError); errors.As(err, &exitErr) {
2731
os.Exit(exitErr.Code)
2832
} else if exitErr := new(exec.ExitError); errors.As(err, &exitErr) {
2933
os.Exit(exitErr.ExitCode())
30-
} else {
34+
} else if err != nil {
3135
os.Exit(1)
3236
}
3337
}

exit_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"testing"
7+
8+
"github.com/broothie/test"
9+
)
10+
11+
func TestExitError_Error(t *testing.T) {
12+
err := &ExitError{Code: 2}
13+
test.Equal(t, "exit status 2", err.Error())
14+
}
15+
16+
func TestExitCode(t *testing.T) {
17+
err := ExitCode(3)
18+
test.Equal(t, 3, err.Code)
19+
}
20+
21+
func TestExitWithError(t *testing.T) {
22+
if os.Getenv("TEST_EXIT") == "1" {
23+
ExitWithError(ExitCode(4))
24+
return
25+
}
26+
27+
// Test ExitError
28+
cmd := exec.Command(os.Args[0], "-test.run=TestExitWithError")
29+
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
30+
err := cmd.Run()
31+
32+
if exitErr, ok := err.(*exec.ExitError); ok {
33+
test.Equal(t, 4, exitErr.ExitCode())
34+
} else {
35+
t.Errorf("expected ExitError, got %v", err)
36+
}
37+
}

flag.go

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func newFlag(name, description string, options ...option.Option[*Flag]) (*Flag,
3939
return nil, errors.Wrapf(err, "building flag %q", name)
4040
}
4141

42+
if err := flag.validateConfig(); err != nil {
43+
return nil, errors.Wrapf(err, "invalid flag %q", name)
44+
}
45+
4246
return flag, nil
4347
}
4448

flag_config_validation.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cli
2+
3+
import (
4+
"strings"
5+
6+
"github.com/bobg/errors"
7+
)
8+
9+
func (f *Flag) validateConfig() error {
10+
if len(strings.Fields(f.name)) > 1 {
11+
return errors.Errorf("flag name %q must be a single token", f.name)
12+
}
13+
14+
if f.name == "" {
15+
return errors.New("flag name cannot be empty")
16+
}
17+
18+
return nil
19+
}

flag_config_validation_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/broothie/test"
7+
)
8+
9+
func TestFlag_validateConfig(t *testing.T) {
10+
t.Run("empty name", func(t *testing.T) {
11+
flag := &Flag{name: ""}
12+
err := flag.validateConfig()
13+
test.ErrorMessageIs(t, err, "flag name cannot be empty")
14+
})
15+
16+
t.Run("multiple tokens", func(t *testing.T) {
17+
flag := &Flag{name: "invalid flag name"}
18+
err := flag.validateConfig()
19+
test.ErrorMessageIs(t, err, `flag name "invalid flag name" must be a single token`)
20+
})
21+
22+
t.Run("valid name", func(t *testing.T) {
23+
flag := &Flag{name: "valid-flag"}
24+
err := flag.validateConfig()
25+
test.NoError(t, err)
26+
})
27+
}

flag_options.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package cli
22

3-
import (
4-
"github.com/broothie/option"
5-
)
3+
import "github.com/broothie/option"
64

75
// AddFlagAlias adds an alias to the flag.
86
func AddFlagAlias(alias string) option.Func[*Flag] {

flag_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestCommand_flagsUpToRoot(t *testing.T) {
1616
),
1717
)
1818

19-
test.Nil(t, err)
19+
test.NoError(t, err)
2020

2121
flags := command.subCommands[0].flagsUpToRoot()
2222
flagNames := lo.Map(flags, func(flag *Flag, _ int) string { return flag.name })

0 commit comments

Comments
 (0)