Simple, intuitive CLI framework for Go pkg.go.dev/go.followtheprocess.codes/cli
go cli
0

Configure Feed

Select the types of activity you want to include in your feed.

Add tangled workflows and update linter config

Tom Fleet (Jun 13, 2026, 7:28 PM +0100) 5b62ac2e a3f18a0a

+203 -141
+3
.gitignore
··· 33 33 !.github/**/*.yaml 34 34 !.github/renovate.json 35 35 36 + # Tangled 37 + !.tangled/**/*.yml 38 + 36 39 # Docs 37 40 !docs/**/*.png 38 41 !docs/**/*.gif
+51 -112
.golangci.yml
··· 11 11 extra-rules: true 12 12 13 13 golines: 14 - max-len: 140 15 - shorten-comments: true 16 - reformat-tags: true 14 + max-len: 120 17 15 18 16 linters: 19 17 default: all 20 18 disable: 21 - - cyclop # I prefer cognitive complexity 22 - - decorder # Don't care about this 23 - - dupl # Basically every table driven test ever triggers this 24 - - dupword # Messes with test cases more often than not 25 - - err113 # Opaque errors are fine 26 - - exhaustruct # No 27 - - forbidigo # Nothing to forbid 28 - - funlen # Bad metric for complexity 29 - - gocyclo # I prefer cognitive complexity 30 - - godox # "todo" and "fixme" comments are allowed 31 - - goheader # No need 32 - - gomodguard # Deprecated in v2.12.0, replaced by gomodguard_v2 which is enabled by default 33 - - gosmopolitan # No need 34 - - grouper # Imports take care of themselves, rest is common sense 35 - - ireturn # Returning an interface is sometimes a good abstraction 36 - - lll # Auto formatters do this and what they can't do I don't care about 37 - - nlreturn # Similar to wsl 38 - - noinlineerr # Inline errors are fine 39 - - nonamedreturns # Used sparingly in short functions these are fine 40 - - paralleltest # This makes go test -v ./... much more difficult to read 41 - - thelper # Lots of false positives in here due to the way I do table tests 42 - - unparam # gopls is better and more subtle 43 - - varnamelen # Lots of false positives of things that are fine 44 - - wrapcheck # Not every error must be wrapped 45 - - wsl # Deprecated 19 + - cyclop # gocognit is the single complexity linter, cognitive beats cyclomatic 20 + - decorder # Don't care about this 21 + - err113 # Not everything needs this 22 + - exhaustruct # This is just a bad idea 23 + - forbidigo # Nothing to forbid 24 + - funlen # Bad metric for complexity 25 + - gocyclo # Prefer cognitive complexity over naive metrics 26 + - godox # "todo" and "fixme" comments are allowed 27 + - gomodguard # Deprecated 28 + - ireturn # The Arg and Flag interfaces are intentionally opaque 29 + - lll # Auto formatters do this and what they can't do I don't care about 30 + - maintidx # Prefer cognitive 31 + - mnd # Annoying and too sensitive 32 + - nestif # Nesting depth is the dominant factor in gocognit's score already 33 + - noinlineerr # Inline errors are fine 34 + - nonamedreturns # Named returns are often helpful documentation, it's naked returns that are the issue 35 + - paralleltest # I've never had Go tests take longer than a few seconds, it's fine 36 + - unparam # gopls is better and more subtle 37 + - varnamelen # Lots of false positives of things that are fine 38 + - wrapcheck # Not every error must be wrapped 39 + - wsl # Deprecated 46 40 47 41 exclusions: 48 42 presets: 49 43 # See https://golangci-lint.run/usage/false-positives/#exclusion-presets 50 - - std-error-handling 51 44 - common-false-positives 45 + - std-error-handling 52 46 rules: 53 47 - path: _test\.go 54 48 linters: 55 - - prealloc # These kinds of optimisations will make no difference to test code 56 - - gosec # Tests don't need security stuff 57 - - gochecknoglobals # e.g. global test flags 58 - - goconst # Table driven tests naturally repeat string literals 59 - - maintidx # Flags table driven tests 49 + - dupl # Common false positives with table driven tests 50 + - dupword # Messes with test cases more often than not 51 + - gochecknoglobals # Flags and env vars 52 + - goconst # Sometimes repetition is okay in tests 53 + - gosec # Tests don't need security stuff 54 + - gosmopolitan # Testing unicode flags etc. 55 + - thelper # Lots of false positives because of how I've done CLI tests 56 + - prealloc # These kinds of optimisations will make no difference to test code 60 57 61 58 settings: 62 - cyclop: 63 - max-complexity: 20 64 - 65 59 depguard: 66 60 rules: 67 61 main: ··· 73 67 desc: use math/rand/v2 instead 74 68 75 69 errcheck: 76 - check-type-assertions: true 77 70 check-blank: true 71 + check-type-assertions: true 78 72 79 73 exhaustive: 80 74 check: 81 - - switch 82 75 - map 76 + - switch 83 77 default-signifies-exhaustive: true 84 - 85 - staticcheck: 86 - checks: 87 - - all 88 78 89 79 gosec: 90 80 excludes: 91 - - G104 # Errors not checked, handled by errcheck 81 + - G104 # Errors not checked, handled by errcheck 92 82 93 83 govet: 94 84 enable-all: true 95 85 96 86 nakedret: 97 - max-func-lines: 0 # Disallow any naked returns 87 + max-func-lines: 0 # Disallow any naked returns 98 88 99 89 nolintlint: 100 90 allow-unused: false 101 91 require-explanation: true 102 92 require-specific: true 103 93 94 + revive: 95 + # every other revive rule overlaps one or more of the other 96 + # linters. These are the ones nothing else covers. 97 + enable-all-rules: false 98 + rules: 99 + - name: context-as-argument # ctx must be the first parameter 100 + - name: deep-exit # os.Exit / log.Fatal outside main and init 101 + - name: modifies-parameter # don't mutate value parameters 102 + - name: modifies-value-receiver # value receiver mutations are lost 103 + - name: unexported-return # exported func returning an unexported type 104 + 105 + staticcheck: 106 + checks: 107 + - all 108 + 104 109 usetesting: 105 110 context-background: true 106 111 context-todo: true 107 112 os-chdir: true 113 + os-create-temp: true 108 114 os-mkdir-temp: true 109 115 os-setenv: true 110 - os-create-temp: true 111 116 os-temp-dir: true 112 - 113 - revive: 114 - max-open-files: 256 115 - enable-all-rules: true 116 - rules: 117 - - name: add-constant 118 - disabled: true # goconst does this 119 - 120 - - name: argument-limit 121 - arguments: 122 - - 5 123 - 124 - - name: cognitive-complexity 125 - disabled: true # gocognit does this 126 - 127 - - name: comment-spacings 128 - arguments: 129 - - "nolint:" 130 - 131 - - name: cyclomatic 132 - disabled: true # cyclop does this 133 - 134 - - name: enforce-switch-style 135 - disabled: true # exhaustive handles this 136 - 137 - - name: exported 138 - arguments: 139 - - checkPrivateReceivers 140 - - checkPublicInterface 141 - 142 - - name: function-length 143 - disabled: true # Bad proxy for complexity 144 - 145 - - name: function-result-limit 146 - arguments: 147 - - 3 148 - 149 - - name: import-shadowing 150 - disabled: true # predeclared does this 151 - 152 - - name: line-length-limit 153 - disabled: true # gofmt/golines handles this well enough 154 - 155 - - name: max-public-structs 156 - disabled: true # This is a dumb rule 157 - 158 - - name: redefines-builtin-id 159 - disabled: true # predeclared does this 160 - 161 - - name: unhandled-error 162 - disabled: true # errcheck handles this 163 - 164 - - name: flag-parameter 165 - disabled: true # As far as I can work out this just doesn't like bools 166 - 167 - - name: unused-parameter 168 - disabled: true # The gopls unused analyzer covers this better 169 - 170 - - name: unused-receiver 171 - disabled: true # As above 172 - 173 - - name: var-naming 174 - disabled: true 175 - 176 - - name: package-naming 177 - disabled: true
+23 -4
command_test.go
··· 420 420 name: "with named arguments", 421 421 options: []cli.Option{ 422 422 cli.OverrideArgs([]string{"--help"}), 423 - cli.Arg(new(string), "src", "The file to copy"), // This one is required 424 - cli.Arg(new(string), "dest", "Destination to copy to", cli.ArgDefault("default.txt")), // This one is optional 423 + cli.Arg( 424 + new(string), 425 + "src", 426 + "The file to copy", 427 + ), // This one is required 428 + cli.Arg( 429 + new(string), 430 + "dest", 431 + "Destination to copy to", 432 + cli.ArgDefault("default.txt"), 433 + ), // This one is optional 425 434 cli.Arg(new(int), "other", "Something else", cli.ArgDefault(0)), 426 435 cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), 427 436 }, ··· 431 440 name: "with verbosity count", 432 441 options: []cli.Option{ 433 442 cli.OverrideArgs([]string{"--help"}), 434 - cli.Arg(new(string), "src", "The file to copy"), // This one is required 435 - cli.Arg(new(string), "dest", "Destination to copy to", cli.ArgDefault("destination.txt")), // This one is optional 443 + cli.Arg( 444 + new(string), 445 + "src", 446 + "The file to copy", 447 + ), // This one is required 448 + cli.Arg( 449 + new(string), 450 + "dest", 451 + "Destination to copy to", 452 + cli.ArgDefault("destination.txt"), 453 + ), // This one is optional 436 454 cli.Flag(new(flag.Count), "verbosity", 'v', "Increase the verbosity level"), 437 455 cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), 438 456 }, ··· 945 963 cli.OverrideArgs(tt.args), 946 964 cli.Run(func(ctx context.Context, cmd *cli.Command) error { 947 965 fmt.Fprintf(cmd.Stdout(), "force: %v\n", force) 966 + 948 967 return nil 949 968 }), 950 969 )
+2 -2
go.mod
··· 16 16 require ( 17 17 go.followtheprocess.codes/diff v0.2.0 // indirect 18 18 go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect 19 - golang.org/x/sys v0.45.0 // indirect 20 - golang.org/x/term v0.43.0 // indirect 19 + golang.org/x/sys v0.46.0 // indirect 20 + golang.org/x/term v0.44.0 // indirect 21 21 )
+4 -4
go.sum
··· 8 8 go.followtheprocess.codes/test v1.4.0/go.mod h1:/Lq3YrwTqU/tb1wbO+Kt7Gs1I3qzFu/o/CUykOavoVA= 9 9 go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= 10 10 go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= 11 - golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= 12 - golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 13 - golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= 14 - golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= 11 + golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= 12 + golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 13 + golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= 14 + golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= 15 15 golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= 16 16 golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
+2 -8
mise.toml
··· 22 22 23 23 [tasks.test] 24 24 description = "Run the test suite" 25 - usage = ''' 26 - arg "[args]..." help="Extra args to pass to go test" 27 - ''' 28 25 # -race needs CGO (https://go.dev/doc/articles/race_detector#Requirements) 29 26 env = { CGO_ENABLED = "1" } 30 - run = "go test -race ./... {{arg(name='args', default='')}}" 27 + run = "go test -race ./..." 31 28 sources = ["**/*.go", "**/testdata/**/*", "go.mod", "go.sum"] 32 29 33 30 [tasks.bench] 34 31 description = "Run all project benchmarks" 35 - usage = ''' 36 - arg "[args]..." help="Extra args to pass to go test" 37 - ''' 38 - run = "go test ./... -run None -benchmem -bench . {{arg(name='args', default='')}}" 32 + run = "go test ./... -run None -benchmem -bench ." 39 33 sources = ["**/*.go"] 40 34 41 35 [tasks.lint]
+2
option.go
··· 461 461 //nolint:unused // Satisfies the unexported ArgOption.apply method, staticcheck can't see across the interface. 462 462 func (o argDefaultOpt[T]) apply(cfg *internalarg.Config[T]) error { 463 463 cfg.DefaultValue = &o.value 464 + 464 465 return nil 465 466 } 466 467 ··· 514 515 //nolint:unused // Satisfies the unexported FlagOption.apply method, staticcheck can't see across the interface. 515 516 func (o flagDefaultOpt[T]) apply(cfg *internalflag.Config[T]) error { 516 517 cfg.DefaultValue = o.value 518 + 517 519 return nil 518 520 } 519 521
+25
.tangled/workflows/CI.yml
··· 1 + when: 2 + - event: [push] 3 + branch: [main] 4 + - event: [pull_request] 5 + branch: [main] 6 + 7 + engine: nixery 8 + 9 + dependencies: 10 + nixpkgs: 11 + - go 12 + - golangci-lint 13 + - gcc # Needed for CGO_ENABLED=1 (go test -race) 14 + 15 + steps: 16 + - name: Test 17 + environment: 18 + CGO_ENABLED: "1" 19 + command: go test -race ./... 20 + 21 + - name: Format 22 + command: golangci-lint fmt ./... --diff-colored 23 + 24 + - name: Lint 25 + command: golangci-lint run ./...
+15 -2
examples/subcommands/cli.go
··· 88 88 cli.Example("Do it for a specific duration", "demo do something --duration 1m30s"), 89 89 cli.Version("do version"), 90 90 cli.Arg(&thing, "thing", "Thing to do"), 91 - cli.Flag(&options.count, "count", 'c', "Number of times to do the thing", cli.FlagDefault(1), cli.Env[int]("DEMO_COUNT")), 91 + cli.Flag( 92 + &options.count, 93 + "count", 94 + 'c', 95 + "Number of times to do the thing", 96 + cli.FlagDefault(1), 97 + cli.Env[int]("DEMO_COUNT"), 98 + ), 92 99 cli.Flag(&options.fast, "fast", 'f', "Do the thing quickly"), 93 100 cli.Flag(&options.verbosity, "verbosity", 'v', "Increase the verbosity level"), 94 - cli.Flag(&options.duration, "duration", 'd', "Do the thing for a specific duration", cli.FlagDefault(1*time.Second)), 101 + cli.Flag( 102 + &options.duration, 103 + "duration", 104 + 'd', 105 + "Do the thing for a specific duration", 106 + cli.FlagDefault(1*time.Second), 107 + ), 95 108 cli.Run(func(ctx context.Context, cmd *cli.Command) error { 96 109 if options.fast { 97 110 fmt.Fprintf(
+1
internal/flag/flag.go
··· 752 752 return len(*parse.Cast[[]string](f.value)) == 0 753 753 case kind.Time: 754 754 var zero time.Time 755 + 755 756 return parse.Cast[time.Time](f.value).Equal(zero) 756 757 case kind.Duration: 757 758 return *parse.Cast[time.Duration](f.value) == 0
+63 -9
internal/flag/set_test.go
··· 714 714 name: "valid count long", 715 715 newSet: func(t *testing.T) *flag.Set { 716 716 set := flag.NewSet() 717 - f, err := flag.New(new(publicflag.Count), "count", 'c', "Count something", flag.Config[publicflag.Count]{}) 717 + f, err := flag.New( 718 + new(publicflag.Count), 719 + "count", 720 + 'c', 721 + "Count something", 722 + flag.Config[publicflag.Count]{}, 723 + ) 718 724 test.Ok(t, err) 719 725 720 726 err = flag.AddToSet(set, f) ··· 736 742 name: "valid count short", 737 743 newSet: func(t *testing.T) *flag.Set { 738 744 set := flag.NewSet() 739 - f, err := flag.New(new(publicflag.Count), "count", 'c', "Count something", flag.Config[publicflag.Count]{}) 745 + f, err := flag.New( 746 + new(publicflag.Count), 747 + "count", 748 + 'c', 749 + "Count something", 750 + flag.Config[publicflag.Count]{}, 751 + ) 740 752 test.Ok(t, err) 741 753 742 754 err = flag.AddToSet(set, f) ··· 758 770 name: "valid count super short", 759 771 newSet: func(t *testing.T) *flag.Set { 760 772 set := flag.NewSet() 761 - f, err := flag.New(new(publicflag.Count), "count", 'c', "Count something", flag.Config[publicflag.Count]{}) 773 + f, err := flag.New( 774 + new(publicflag.Count), 775 + "count", 776 + 'c', 777 + "Count something", 778 + flag.Config[publicflag.Count]{}, 779 + ) 762 780 test.Ok(t, err) 763 781 764 782 err = flag.AddToSet(set, f) ··· 972 990 973 991 var val publicflag.Count 974 992 975 - f, err := flag.New(&val, "verbosity", 'v', "Increase verbosity", flag.Config[publicflag.Count]{EnvVar: "MYTOOL_VERBOSITY"}) 993 + f, err := flag.New( 994 + &val, 995 + "verbosity", 996 + 'v', 997 + "Increase verbosity", 998 + flag.Config[publicflag.Count]{EnvVar: "MYTOOL_VERBOSITY"}, 999 + ) 976 1000 test.Ok(t, err) 977 1001 978 1002 set := flag.NewSet() ··· 1089 1113 1090 1114 var val time.Duration 1091 1115 1092 - f, err := flag.New(&val, "timeout", 't', "Request timeout", flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}) 1116 + f, err := flag.New( 1117 + &val, 1118 + "timeout", 1119 + 't', 1120 + "Request timeout", 1121 + flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}, 1122 + ) 1093 1123 test.Ok(t, err) 1094 1124 1095 1125 set := flag.NewSet() ··· 1113 1143 1114 1144 var val time.Duration 1115 1145 1116 - f, err := flag.New(&val, "timeout", 't', "Request timeout", flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}) 1146 + f, err := flag.New( 1147 + &val, 1148 + "timeout", 1149 + 't', 1150 + "Request timeout", 1151 + flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}, 1152 + ) 1117 1153 test.Ok(t, err) 1118 1154 1119 1155 set := flag.NewSet() ··· 1137 1173 1138 1174 var val time.Duration 1139 1175 1140 - f, err := flag.New(&val, "timeout", 't', "Request timeout", flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}) 1176 + f, err := flag.New( 1177 + &val, 1178 + "timeout", 1179 + 't', 1180 + "Request timeout", 1181 + flag.Config[time.Duration]{EnvVar: "MYTOOL_TIMEOUT"}, 1182 + ) 1141 1183 test.Ok(t, err) 1142 1184 1143 1185 set := flag.NewSet() ··· 2289 2331 number, err := flag.New(new(int), "number", 'n', "Number of times", flag.Config[int]{}) 2290 2332 test.Ok(t, err) 2291 2333 2292 - duration, err := flag.New(new(time.Duration), "duration", 'D', "The time to do something for", flag.Config[time.Duration]{}) 2334 + duration, err := flag.New( 2335 + new(time.Duration), 2336 + "duration", 2337 + 'D', 2338 + "The time to do something for", 2339 + flag.Config[time.Duration]{}, 2340 + ) 2293 2341 test.Ok(t, err) 2294 2342 2295 2343 test.Ok(t, flag.AddToSet(set, verbose)) ··· 2381 2429 number, err := flag.New(new(int), "number", 'n', "Number of times", flag.Config[int]{}) 2382 2430 test.Ok(t, err) 2383 2431 2384 - duration, err := flag.New(new(time.Duration), "duration", 'D', "The time to do something for", flag.Config[time.Duration]{}) 2432 + duration, err := flag.New( 2433 + new(time.Duration), 2434 + "duration", 2435 + 'D', 2436 + "The time to do something for", 2437 + flag.Config[time.Duration]{}, 2438 + ) 2385 2439 test.Ok(t, err) 2386 2440 2387 2441 test.Ok(t, flag.AddToSet(set, verbose))
+12
internal/parse/parse_test.go
··· 23 23 24 24 reference := func(str string) (int, error) { 25 25 val, err := strconv.ParseInt(str, base10, 0) 26 + 26 27 return int(val), err 27 28 } 28 29 ··· 40 41 41 42 reference := func(str string) (int8, error) { 42 43 val, err := strconv.ParseInt(str, base10, bits8) 44 + 43 45 return int8(val), err 44 46 } 45 47 ··· 57 59 58 60 reference := func(str string) (int16, error) { 59 61 val, err := strconv.ParseInt(str, base10, bits16) 62 + 60 63 return int16(val), err 61 64 } 62 65 ··· 74 77 75 78 reference := func(str string) (int32, error) { 76 79 val, err := strconv.ParseInt(str, base10, bits32) 80 + 77 81 return int32(val), err 78 82 } 79 83 ··· 91 95 92 96 reference := func(str string) (int64, error) { 93 97 val, err := strconv.ParseInt(str, base10, bits64) 98 + 94 99 return val, err 95 100 } 96 101 ··· 108 113 109 114 reference := func(str string) (uint, error) { 110 115 val, err := strconv.ParseUint(str, base10, 0) 116 + 111 117 return uint(val), err 112 118 } 113 119 ··· 125 131 126 132 reference := func(str string) (uint8, error) { 127 133 val, err := strconv.ParseUint(str, base10, bits8) 134 + 128 135 return uint8(val), err 129 136 } 130 137 ··· 142 149 143 150 reference := func(str string) (uint16, error) { 144 151 val, err := strconv.ParseUint(str, base10, bits16) 152 + 145 153 return uint16(val), err 146 154 } 147 155 ··· 159 167 160 168 reference := func(str string) (uint32, error) { 161 169 val, err := strconv.ParseUint(str, base10, bits32) 170 + 162 171 return uint32(val), err 163 172 } 164 173 ··· 176 185 177 186 reference := func(str string) (uint64, error) { 178 187 val, err := strconv.ParseUint(str, base10, bits64) 188 + 179 189 return val, err 180 190 } 181 191 ··· 193 203 194 204 reference := func(str string) (float32, error) { 195 205 val, err := strconv.ParseFloat(str, bits32) 206 + 196 207 return float32(val), err 197 208 } 198 209 ··· 210 221 211 222 reference := func(str string) (float64, error) { 212 223 val, err := strconv.ParseFloat(str, bits64) 224 + 213 225 return float64(val), err 214 226 } 215 227