diff --git a/.gitignore b/.gitignore index 077ec2a..355901a 100755 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ # Task !Taskfile.yml +# mise +!mise.toml + # Git !.gitattributes !.gitignore diff --git a/Taskfile.yml b/Taskfile.yml deleted file mode 100644 index 967d4a9..0000000 --- a/Taskfile.yml +++ /dev/null @@ -1,127 +0,0 @@ -# https://taskfile.dev - -version: "3" - -vars: - COV_DATA: coverage.out - -tasks: - default: - desc: List all available tasks - silent: true - cmds: - - task --list - - tidy: - desc: Tidy dependencies in go.mod and go.sum - sources: - - "**/*.go" - - go.mod - - go.sum - cmds: - - go mod tidy - - fmt: - desc: Run go fmt on all source files - preconditions: - - sh: command -v golangci-lint - msg: golangci-lint not installed, see https://golangci-lint.run/usage/install/#local-installation - sources: - - "**/*.go" - - .golangci.yml - - "**/*.md" - cmds: - - golangci-lint fmt ./... - - test: - desc: Run the test suite - sources: - - "**/*.go" - - "**/testdata/**/*" - - go.mod - - go.sum - env: - # -race needs CGO (https://go.dev/doc/articles/race_detector#Requirements) - CGO_ENABLED: 1 - cmds: - - go test -race ./... {{.CLI_ARGS}} - - bench: - desc: Run all project benchmarks - sources: - - "**/*.go" - cmds: - - go test ./... -run None -benchmem -bench . {{.CLI_ARGS}} - - lint: - desc: Run the linters and auto-fix if possible - sources: - - "**/*.go" - - .golangci.yml - deps: - - fmt - preconditions: - - sh: command -v golangci-lint - msg: golangci-lint not installed, see https://golangci-lint.run/usage/install/#local-installation - - - sh: command -v nilaway - msg: nilaway not installed, see https://github.com/uber-go/nilaway - - - sh: command -v typos - msg: requires typos-cli, run `brew install typos-cli` - cmds: - - golangci-lint run --fix - - typos - - nilaway ./... - - docs: - desc: Render the pkg docs locally - cmds: - - pkgsite -open - preconditions: - - sh: command -v pkgsite - msg: pkgsite not installed, run go install golang.org/x/pkgsite/cmd/pkgsite@latest - - demo: - desc: Render the demo gifs - sources: - - ./docs/src/*.tape - - "**/*.go" - preconditions: - - sh: command -v vhs - msg: vhs not installed, see https://github.com/charmbracelet/vhs - - - sh: command -v freeze - msg: freeze not installed, see https://github.com/charmbracelet/freeze - cmds: - - for file in ./docs/src/*.tape; do vhs "$file"; done - - freeze ./examples/cover/main.go --config ./docs/src/freeze.json --output ./docs/img/demo.png --show-line-numbers - - cov: - desc: Calculate test coverage and render the html - cmds: - - go test -race -cover -covermode atomic -coverprofile {{.COV_DATA}} ./... - - go tool cover -html {{.COV_DATA}} - - check: - desc: Run tests and linting in one - cmds: - - task: test - - task: lint - - sloc: - desc: Print lines of code - cmds: - - fd . -e go | xargs wc -l | sort -nr | head - - clean: - desc: Remove build artifacts and other clutter - cmds: - - go clean ./... - - rm -rf *.out - - update: - desc: Updates dependencies in go.mod and go.sum - cmds: - - go get -u ./... - - go mod tidy diff --git a/docs/img/cancel.gif b/docs/img/cancel.gif index 47941ca..0fba604 100644 Binary files a/docs/img/cancel.gif and b/docs/img/cancel.gif differ diff --git a/docs/img/namedargs.gif b/docs/img/namedargs.gif index cbc47e7..2c9531d 100644 Binary files a/docs/img/namedargs.gif and b/docs/img/namedargs.gif differ diff --git a/docs/img/quickstart.gif b/docs/img/quickstart.gif index f04ce60..8f1a305 100644 Binary files a/docs/img/quickstart.gif and b/docs/img/quickstart.gif differ diff --git a/docs/img/subcommands.gif b/docs/img/subcommands.gif index 18bb6cb..35b3cd4 100644 Binary files a/docs/img/subcommands.gif and b/docs/img/subcommands.gif differ diff --git a/internal/flag/flag.go b/internal/flag/flag.go index d5d8a28..93f9aa8 100644 --- a/internal/flag/flag.go +++ b/internal/flag/flag.go @@ -20,7 +20,7 @@ import ( "go.followtheprocess.codes/cli/internal/parse" ) -var _ Value = Flag[string]{} // This will fail if we violate our Value interface +var _ Value = &Flag[string]{} // This will fail if we violate our Value interface // Flag represents a single command line flag. type Flag[T flag.Flaggable] struct { @@ -35,45 +35,43 @@ type Flag[T flag.Flaggable] struct { // // The name should be as it appears on the command line, e.g. "force" for a --force flag. An optional // shorthand can be created by setting short to a single letter value, e.g. "f" to also create a -f version of "force". -func New[T flag.Flaggable](p *T, name string, short rune, usage string, config Config[T]) (Flag[T], error) { +func New[T flag.Flaggable](p *T, name string, short rune, usage string, config Config[T]) (*Flag[T], error) { if err := validateFlagName(name); err != nil { - return Flag[T]{}, fmt.Errorf("invalid flag name %q: %w", name, err) + return nil, fmt.Errorf("invalid flag name %q: %w", name, err) } if err := validateFlagShort(short); err != nil { - return Flag[T]{}, fmt.Errorf("invalid shorthand for flag %q: %w", name, err) + return nil, fmt.Errorf("invalid shorthand for flag %q: %w", name, err) } if p == nil { - p = new(T) + return nil, fmt.Errorf("flag %q: target pointer must not be nil", name) } *p = config.DefaultValue - flag := Flag[T]{ + return &Flag[T]{ value: p, name: name, usage: usage, short: short, envVar: config.EnvVar, - } - - return flag, nil + }, nil } // Name returns the name of the [Flag]. -func (f Flag[T]) Name() string { +func (f *Flag[T]) Name() string { return f.name } // Short returns the shorthand registered for the flag (e.g. -d for --delete), or // NoShortHand if the flag should be long only. -func (f Flag[T]) Short() rune { +func (f *Flag[T]) Short() rune { return f.short } // Usage returns the usage line for the flag. -func (f Flag[T]) Usage() string { +func (f *Flag[T]) Usage() string { return f.usage } @@ -81,7 +79,7 @@ func (f Flag[T]) Usage() string { // // If the flag's default is unset (i.e. the zero value for its type), // an empty string is returned. -func (f Flag[T]) Default() string { +func (f *Flag[T]) Default() string { // Special case a --help flag, because if we didn't, when you call --help // it would show up with a default of true because you've passed it // so it's value is true here @@ -94,13 +92,13 @@ func (f Flag[T]) Default() string { // EnvVar returns the name of the environment variable associated with this flag, // or an empty string if none was configured. -func (f Flag[T]) EnvVar() string { +func (f *Flag[T]) EnvVar() string { return f.envVar } // IsSlice reports whether the flag holds a slice value that accumulates repeated // calls to Set. Returns false for []byte and net.IP, which are parsed atomically. -func (f Flag[T]) IsSlice() bool { +func (f *Flag[T]) IsSlice() bool { if f.value == nil { return false } @@ -118,7 +116,7 @@ func (f Flag[T]) IsSlice() bool { // NoArgValue returns a string representation of value the flag should hold // when it is given no arguments on the command line. For example a boolean flag // --delete, when passed without arguments implies --delete true. -func (f Flag[T]) NoArgValue() string { +func (f *Flag[T]) NoArgValue() string { switch f.Type() { case format.TypeBool: // Boolean flags imply passing true, "--force" vs "--force true" @@ -135,7 +133,7 @@ func (f Flag[T]) NoArgValue() string { // part of [Value], allowing a flag to print itself. // //nolint:cyclop // No other way of doing this realistically -func (f Flag[T]) String() string { +func (f *Flag[T]) String() string { if f.value == nil { return format.Nil } @@ -217,7 +215,7 @@ func (f Flag[T]) String() string { } // Type returns a string representation of the type of the Flag. -func (f Flag[T]) Type() string { //nolint:cyclop // No other way of doing this realistically +func (f *Flag[T]) Type() string { //nolint:cyclop // No other way of doing this realistically if f.value == nil { return format.Nil } @@ -295,7 +293,7 @@ func (f Flag[T]) Type() string { //nolint:cyclop // No other way of doing this r // Set sets a [Flag] value based on string input, i.e. parsing from the command line. // //nolint:gocognit,maintidx // No other way of doing this realistically -func (f Flag[T]) Set(str string) error { +func (f *Flag[T]) Set(str string) error { if f.value == nil { return fmt.Errorf("cannot set value %s, flag.value was nil", str) } diff --git a/internal/flag/flag_test.go b/internal/flag/flag_test.go index a4b5362..40ee3d4 100644 --- a/internal/flag/flag_test.go +++ b/internal/flag/flag_test.go @@ -1059,14 +1059,12 @@ func TestFlagValidation(t *testing.T) { func TestFlagNilSafety(t *testing.T) { t.Run("with new", func(t *testing.T) { - // Should be impossible to make a nil pointer dereference when using .New + // Passing a nil target is an error var bang *bool - flag, err := flag.New(bang, "bang", 'b', "Nil go bang?", flag.Config[bool]{}) - test.Ok(t, err) - - test.Equal(t, flag.String(), "false") - test.Equal(t, flag.Type(), "bool") + f, err := flag.New(bang, "bang", 'b', "Nil go bang?", flag.Config[bool]{}) + test.Err(t, err) + test.Equal(t, f, nil) }) t.Run("composite literal", func(t *testing.T) { diff --git a/internal/flag/set.go b/internal/flag/set.go index 95f68b8..bffdb0d 100644 --- a/internal/flag/set.go +++ b/internal/flag/set.go @@ -31,11 +31,15 @@ func NewSet() *Set { } // AddToSet adds a flag to the given Set. -func AddToSet[T flag.Flaggable](set *Set, f Flag[T]) error { +func AddToSet[T flag.Flaggable](set *Set, f *Flag[T]) error { if set == nil { return errors.New("cannot add flag to a nil set") } + if f == nil { + return errors.New("cannot add nil flag to a set") + } + name := f.Name() short := f.Short() diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..f22f801 --- /dev/null +++ b/mise.toml @@ -0,0 +1,100 @@ +[tools] +go = "1.26" +golangci-lint = "latest" +typos = "latest" +vhs = "latest" +"aqua:charmbracelet/freeze" = "latest" +fd = "latest" +"go:go.uber.org/nilaway/cmd/nilaway" = "latest" +"go:golang.org/x/pkgsite/cmd/pkgsite" = "latest" + +[vars] +COV_DATA = "coverage.out" + +[tasks.tidy] +description = "Tidy dependencies in go.mod and go.sum" +run = "go mod tidy" +sources = ["**/*.go", "go.mod", "go.sum"] + +[tasks.fmt] +description = "Run go fmt on all source files" +run = "golangci-lint fmt ./..." +sources = ["**/*.go", ".golangci.yml", "**/*.md"] + +[tasks.test] +description = "Run the test suite" +usage = ''' +arg "[args]..." help="Extra args to pass to go test" +''' +# -race needs CGO (https://go.dev/doc/articles/race_detector#Requirements) +env = { CGO_ENABLED = "1" } +run = "go test -race ./... {{arg(name='args', default='')}}" +sources = ["**/*.go", "**/testdata/**/*", "go.mod", "go.sum"] + +[tasks.bench] +description = "Run all project benchmarks" +usage = ''' +arg "[args]..." help="Extra args to pass to go test" +''' +run = "go test ./... -run None -benchmem -bench . {{arg(name='args', default='')}}" +sources = ["**/*.go"] + +[tasks.lint] +description = "Run the linters and auto-fix if possible" +depends = ["fmt"] +run = [ + "golangci-lint run --fix", + "typos", + "nilaway ./...", +] +sources = ["**/*.go", ".golangci.yml"] + +[tasks.docs] +description = "Render the pkg docs locally" +raw = true +run = "pkgsite -open" + +[tasks.demo] +description = "Render the demo gifs in parallel" +run = [ + 'for file in ./docs/src/*.tape; do vhs "$file" & done; wait', + "freeze ./examples/cover/main.go --config ./docs/src/freeze.json --output ./docs/img/demo.png --show-line-numbers", +] +sources = ["./docs/src/*.tape", "**/*.go"] +outputs = ["./docs/img/*.gif", "./docs/img/demo.png"] + +[tasks.cov] +description = "Calculate test coverage (pass --open to view as HTML in a browser)" +usage = ''' +flag "--open" help="Open the coverage report in a browser" +''' +run = ''' +go test -race -cover -covermode atomic -coverprofile {{vars.COV_DATA}} ./... +if [ "${usage_open:-false}" = "true" ]; then + go tool cover -html {{vars.COV_DATA}} +fi +''' +sources = ["**/*.go", "**/testdata/**/*", "go.mod", "go.sum"] +outputs = ["{{vars.COV_DATA}}"] + +[tasks.check] +description = "Run tests and linting in one" +depends = ["test", "lint"] + +[tasks.sloc] +description = "Print lines of code" +run = "fd . -e go | xargs wc -l | sort -nr | head" + +[tasks.clean] +description = "Remove build artifacts and other clutter" +run = [ + "go clean ./...", + "rm -rf *.out", +] + +[tasks.update] +description = "Updates dependencies in go.mod and go.sum" +run = [ + "go get -u ./...", + "go mod tidy", +]