Skip to content

various fixes #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 26, 2025
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage.out
4 changes: 4 additions & 0 deletions argument.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
return nil, errors.Wrapf(err, "building argument %q", name)
}

if err := argument.validateConfig(); err != nil {
return nil, errors.Wrapf(err, "invalid argument %q", name)
}

Check warning on line 34 in argument.go

View check run for this annotation

Codecov / codecov/patch

argument.go#L33-L34

Added lines #L33 - L34 were not covered by tests

return argument, nil
}

Expand Down
19 changes: 19 additions & 0 deletions argument_config_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cli

import (
"strings"

"github.com/bobg/errors"
)

func (a *Argument) validateConfig() error {
if len(strings.Fields(a.name)) > 1 {
return errors.Errorf("argument name %q must be a single token", a.name)
}

if a.name == "" {
return errors.New("argument name cannot be empty")
}

return nil
}
27 changes: 27 additions & 0 deletions argument_config_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cli

import (
"testing"

"github.com/broothie/test"
)

func TestArgument_validateConfig(t *testing.T) {
t.Run("empty name", func(t *testing.T) {
arg := &Argument{name: ""}
err := arg.validateConfig()
test.ErrorMessageIs(t, err, "argument name cannot be empty")
})

t.Run("multiple tokens", func(t *testing.T) {
arg := &Argument{name: "invalid argument name"}
err := arg.validateConfig()
test.ErrorMessageIs(t, err, `argument name "invalid argument name" must be a single token`)
})

t.Run("valid name", func(t *testing.T) {
arg := &Argument{name: "valid-arg"}
err := arg.validateConfig()
test.NoError(t, err)
})
}
17 changes: 17 additions & 0 deletions argument_input_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cli

import (
"testing"

"github.com/broothie/test"
)

func TestArgument_validateInput(t *testing.T) {
arg, err := newArgument("test-arg", "Test arg.")
test.MustNoError(t, err)

test.ErrorMessageIs(t, arg.validateInput(), `argument "test-arg": argument missing value`)

arg.value = "something"
test.NoError(t, arg.validateInput())
}
28 changes: 14 additions & 14 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Test_git(t *testing.T) {
called()

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

return nil
Expand All @@ -57,7 +57,7 @@ func Test_git(t *testing.T) {
called()

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

return nil
Expand All @@ -82,7 +82,7 @@ func Test_git(t *testing.T) {
called()

message, err := FlagValue[string](ctx, "message")
test.Nil(t, err)
test.NoError(t, err)
test.Equal(t, "a commit message", message)

return nil
Expand All @@ -97,11 +97,11 @@ func Test_git(t *testing.T) {
called()

isAll, err := FlagValue[bool](ctx, "all")
test.Nil(t, err)
test.NoError(t, err)
test.False(t, isAll)

message, err := FlagValue[string](ctx, "message")
test.Nil(t, err)
test.NoError(t, err)
test.Equal(t, "a commit message", message)

return nil
Expand All @@ -116,11 +116,11 @@ func Test_git(t *testing.T) {
called()

isAll, err := FlagValue[bool](ctx, "all")
test.Nil(t, err)
test.NoError(t, err)
test.False(t, isAll)

message, err := FlagValue[string](ctx, "message")
test.Nil(t, err)
test.NoError(t, err)
test.Equal(t, "a commit message", message)

return nil
Expand All @@ -135,11 +135,11 @@ func Test_git(t *testing.T) {
called()

isAll, err := FlagValue[bool](ctx, "all")
test.Nil(t, err)
test.NoError(t, err)
test.True(t, isAll)

message, err := FlagValue[string](ctx, "message")
test.Nil(t, err)
test.NoError(t, err)
test.Equal(t, "a commit message", message)

return nil
Expand All @@ -154,7 +154,7 @@ func Test_git(t *testing.T) {
called()

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

return nil
Expand All @@ -169,11 +169,11 @@ func Test_git(t *testing.T) {
called()

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

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

return nil
Expand All @@ -188,7 +188,7 @@ func Test_git(t *testing.T) {
called()

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

test.MustNoError(t, err)
test.NoError(t, err)
test.Nil(t, command.Run(context.TODO(), testCase.rawArgs))
})
}
Expand Down
4 changes: 1 addition & 3 deletions command_config_validation.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cli

import (
"github.com/bobg/errors"
)
import "github.com/bobg/errors"

func (c *Command) validateConfig() error {
validations := []func() error{
Expand Down
25 changes: 24 additions & 1 deletion command_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cli

import "os"
import (
"context"
"fmt"
"os"
)

func ExampleNewCommand() {
command, _ := NewCommand("server", "An http server.",
Expand Down Expand Up @@ -38,3 +42,22 @@ func ExampleNewCommand() {
// --port -p Port to run server on. (type: int, default: $PORT, "3000")
// --auth-required Whether to require authentication. (type: bool, default: "true")
}

func ExampleRun() {
oldArgs := os.Args
os.Args = []string{"echo", "hello"}
defer func() { os.Args = oldArgs }()

Run("echo", "Echo the arguments.",
AddArg("arg", "The argument to echo."),
SetHandler(func(ctx context.Context) error {
arg, _ := ArgValue[string](ctx, "arg")

fmt.Println(arg)
return nil
}),
)

// Output:
// hello
}
4 changes: 3 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ func ArgValue[T any](ctx context.Context, name string) (T, error) {
return arg.defaultValue.(T), nil
}

var commandContextKey struct{}
type commandContextKeyType struct{}

var commandContextKey = commandContextKeyType{}

func (c *Command) onContext(parent context.Context) context.Context {
return context.WithValue(parent, commandContextKey, c)
Expand Down
10 changes: 7 additions & 3 deletions error.go → exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,30 @@
"github.com/bobg/errors"
)

// ExitError is an error that causes the program to exit with a given status code.
type ExitError struct {
Code int
}

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

func ExitCode(code int) ExitError {
return ExitError{Code: code}
// ExitCode returns an ExitError with the given code.
func ExitCode(code int) *ExitError {
return &ExitError{Code: code}
}

// ExitWithError exits the program with an error.
func ExitWithError(err error) {
fmt.Println(err)

if exitErr := new(ExitError); errors.As(err, &exitErr) {
os.Exit(exitErr.Code)
} else if exitErr := new(exec.ExitError); errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode())
} else {
} else if err != nil {

Check warning on line 34 in exit.go

View check run for this annotation

Codecov / codecov/patch

exit.go#L34

Added line #L34 was not covered by tests
os.Exit(1)
}
}
37 changes: 37 additions & 0 deletions exit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cli

import (
"os"
"os/exec"
"testing"

"github.com/broothie/test"
)

func TestExitError_Error(t *testing.T) {
err := &ExitError{Code: 2}
test.Equal(t, "exit status 2", err.Error())
}

func TestExitCode(t *testing.T) {
err := ExitCode(3)
test.Equal(t, 3, err.Code)
}

func TestExitWithError(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
ExitWithError(ExitCode(4))
return
}

// Test ExitError
cmd := exec.Command(os.Args[0], "-test.run=TestExitWithError")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err := cmd.Run()

if exitErr, ok := err.(*exec.ExitError); ok {
test.Equal(t, 4, exitErr.ExitCode())
} else {
t.Errorf("expected ExitError, got %v", err)
}
}
4 changes: 4 additions & 0 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
return nil, errors.Wrapf(err, "building flag %q", name)
}

if err := flag.validateConfig(); err != nil {
return nil, errors.Wrapf(err, "invalid flag %q", name)
}

Check warning on line 44 in flag.go

View check run for this annotation

Codecov / codecov/patch

flag.go#L43-L44

Added lines #L43 - L44 were not covered by tests

return flag, nil
}

Expand Down
19 changes: 19 additions & 0 deletions flag_config_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cli

import (
"strings"

"github.com/bobg/errors"
)

func (f *Flag) validateConfig() error {
if len(strings.Fields(f.name)) > 1 {
return errors.Errorf("flag name %q must be a single token", f.name)
}

if f.name == "" {
return errors.New("flag name cannot be empty")
}

return nil
}
27 changes: 27 additions & 0 deletions flag_config_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cli

import (
"testing"

"github.com/broothie/test"
)

func TestFlag_validateConfig(t *testing.T) {
t.Run("empty name", func(t *testing.T) {
flag := &Flag{name: ""}
err := flag.validateConfig()
test.ErrorMessageIs(t, err, "flag name cannot be empty")
})

t.Run("multiple tokens", func(t *testing.T) {
flag := &Flag{name: "invalid flag name"}
err := flag.validateConfig()
test.ErrorMessageIs(t, err, `flag name "invalid flag name" must be a single token`)
})

t.Run("valid name", func(t *testing.T) {
flag := &Flag{name: "valid-flag"}
err := flag.validateConfig()
test.NoError(t, err)
})
}
4 changes: 1 addition & 3 deletions flag_options.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cli

import (
"github.com/broothie/option"
)
import "github.com/broothie/option"

// AddFlagAlias adds an alias to the flag.
func AddFlagAlias(alias string) option.Func[*Flag] {
Expand Down
2 changes: 1 addition & 1 deletion flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestCommand_flagsUpToRoot(t *testing.T) {
),
)

test.Nil(t, err)
test.NoError(t, err)

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