Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
# Task
!Taskfile.yml

# mise
!mise.toml

# Git
!.gitattributes
!.gitignore
Expand Down
127 changes: 0 additions & 127 deletions Taskfile.yml

This file was deleted.

Binary file modified docs/img/cancel.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/namedargs.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/quickstart.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/subcommands.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 17 additions & 19 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -35,53 +35,51 @@ 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
}

// Default returns the default value for the flag, as a 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
Expand All @@ -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
}
Expand All @@ -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"
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
10 changes: 4 additions & 6 deletions internal/flag/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion internal/flag/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
100 changes: 100 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -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",
]
Loading