···191191192192> [!NOTE]
193193> `cli.Env` requires an explicit type parameter because Go cannot infer it from the string argument alone.
194194-> The compiler enforces that the type matches the flag — `cli.Env[string](...)` on a `bool` flag is a compile error.
194194+> The compiler enforces that the type matches the flag, `cli.Env[string](...)` on a `bool` flag is a compile error.
195195196196When `MYTOOL_FORCE=true` is set in the environment, `--force` is implied. Passing `--force=false` on the command line always wins.
197197
+3-13
mise.toml
···4141[tasks.lint]
4242description = "Run the linters and auto-fix if possible"
4343depends = ["fmt"]
4444-run = [
4545- "golangci-lint run --fix",
4646- "typos",
4747- "nilaway ./...",
4848-]
4444+run = ["golangci-lint run --fix", "typos", "nilaway ./..."]
4945sources = ["**/*.go", ".golangci.yml"]
50465147[tasks.docs]
···86828783[tasks.clean]
8884description = "Remove build artifacts and other clutter"
8989-run = [
9090- "go clean ./...",
9191- "rm -rf *.out",
9292-]
8585+run = ["go clean ./...", "rm -rf *.out"]
93869487[tasks.update]
9588description = "Updates dependencies in go.mod and go.sum"
9696-run = [
9797- "go get -u ./...",
9898- "go mod tidy",
9999-]
8989+run = ["go get -u ./...", "go mod tidy"]
+1-1
internal/flag/flag.go
···682682//
683683// "ish" means that empty slices will return true despite their official zero
684684// value being nil. The primary use is to determine whether a default value is
685685-// worth displaying to the user in the help text — an empty slice is probably
685685+// worth displaying to the user in the help text, an empty slice is probably
686686// not.
687687//
688688//nolint:cyclop // Not much else we can do here
+2-5
internal/flag/set_test.go
···984984 test: func(t *testing.T, set *flag.Set) {
985985 f, exists := set.Get("verbosity")
986986 test.True(t, exists)
987987- // Env var contributes 2, CLI contributes 1 more — total 3
987987+ // Env var contributes 2, CLI contributes 1 more, total 3
988988 test.Equal(t, f.String(), "3")
989989 },
990990 args: []string{"--verbosity"},
···16331633 {
16341634 name: "slice env var splits on every comma (no escape mechanism)",
16351635 newSet: func(t *testing.T) *flag.Set {
16361636- // Any comma in an env var value is interpreted as a separator —
16371637- // there is no way to embed a literal comma in a slice item.
16381638- // Users needing commas should pass values via --flag one,two.
16391636 t.Setenv("MYTOOL_ITEMS", "a,b,c")
1640163716411638 var val []string
···18451842 {
18461843 name: "combined short flags where early flag needs value captures rest",
18471844 newSet: func(t *testing.T) *flag.Set {
18481848- // -fgh — f is non-bool so consumes the rest of the cluster as its value
18451845+ // -fgh. f is non-bool so consumes the rest of the cluster as its value
18491846 // i.e. f gets "gh", and g/h are not parsed as flags.
18501847 set := flag.NewSet()
18511848
+1-1
internal/flag/value.go
···33333434 // IsSlice reports whether the flag holds a slice value that accumulates
3535 // repeated calls to Set (e.g. []string, []int). Note that []byte and net.IP
3636- // are NOT slice flags in this sense — they are parsed atomically.
3636+ // are NOT slice flags in this sense, they are parsed atomically.
3737 IsSlice() bool
38383939 // Set sets the stored value of a flag by parsing the string "str".
+140-25
internal/format/format.go
···44package format
5566import (
77- "fmt"
88- "reflect"
97 "strconv"
1010- "strings"
88+ "unsafe"
1191210 "go.followtheprocess.codes/cli/internal/constraints"
1311)
···1715 floatFmt = 'g'
1816 floatPrecision = -1
1917 slice = "[]"
1818+1919+ // Capacity hints used to pre-size []byte buffers in the slice formatters.
2020+ // The "brackets" pair is the leading '[' and trailing ']'.
2121+ //
2222+ // The per-element hints are good enough default cases to minimise the
2323+ // buffer growing and re-allocating.
2424+ bracketsCap = 2
2525+ intElemHint = 4 // "-12, "
2626+ floatElemHint = 8 // "-1.234, "
2727+ boolElemHint = 7 // "false, "
2828+ stringElemHint = 4 // surrounding quotes plus ", "
2029)
21302231const (
···99108// Slice([]int{1, 2, 3, 4}) // "[1, 2, 3, 4]"
100109// Slice([]string{"one", "two", "three"}) // `["one", "two", "three"]`
101110func Slice[T any](s []T) string {
102102- length := len(s)
103103-104104- if length == 0 {
105105- // If it's empty or nil, avoid doing the work below
106106- // and just return "[]"
111111+ if len(s) == 0 {
107112 return slice
108113 }
109114110110- builder := &strings.Builder{}
111111- builder.WriteByte('[')
115115+ switch v := any(s).(type) {
116116+ case []string:
117117+ return formatStringSlice(v)
118118+ case []bool:
119119+ return formatBoolSlice(v)
120120+ case []int:
121121+ return formatSignedSlice(v)
122122+ case []int8:
123123+ return formatSignedSlice(v)
124124+ case []int16:
125125+ return formatSignedSlice(v)
126126+ case []int32:
127127+ return formatSignedSlice(v)
128128+ case []int64:
129129+ return formatSignedSlice(v)
130130+ case []uint:
131131+ return formatUnsignedSlice(v)
132132+ case []uint16:
133133+ return formatUnsignedSlice(v)
134134+ case []uint32:
135135+ return formatUnsignedSlice(v)
136136+ case []uint64:
137137+ return formatUnsignedSlice(v)
138138+ case []float32:
139139+ return formatFloat32Slice(v)
140140+ case []float64:
141141+ return formatFloat64Slice(v)
142142+ default:
143143+ return slice
144144+ }
145145+}
112146113113- typ := reflect.TypeFor[T]().Kind()
147147+// toString casts b to a string by reinterpreting the bytes.
148148+//
149149+// This is the same trick [strings.Builder.String] uses to avoid the
150150+// allocation of doing `string(b)`. The caveat is b MUST not be mutated
151151+// after passing to this function.
152152+//
153153+// This is fine in our case here as the []byte buffer is created in the function body
154154+// and never escapes.
155155+func toString(b []byte) string {
156156+ return unsafe.String(unsafe.SliceData(b), len(b))
157157+}
114158115115- first := fmt.Sprintf("%v", s[0])
116116- if typ == reflect.String {
117117- first = strconv.Quote(first)
159159+func formatSignedSlice[T constraints.Signed](s []T) string {
160160+ buf := make([]byte, 0, bracketsCap+len(s)*intElemHint)
161161+ buf = append(buf, '[')
162162+ buf = strconv.AppendInt(buf, int64(s[0]), base10)
163163+164164+ for _, e := range s[1:] {
165165+ buf = append(buf, ", "...)
166166+ buf = strconv.AppendInt(buf, int64(e), base10)
118167 }
119168120120- builder.WriteString(first)
169169+ buf = append(buf, ']')
121170122122- for _, element := range s[1:] {
123123- builder.WriteString(", ")
171171+ return toString(buf)
172172+}
124173125125- str := fmt.Sprintf("%v", element)
126126- if typ == reflect.String {
127127- // If it's a string, quote it
128128- str = strconv.Quote(str)
129129- }
174174+func formatUnsignedSlice[T constraints.Unsigned](s []T) string {
175175+ buf := make([]byte, 0, bracketsCap+len(s)*intElemHint)
176176+ buf = append(buf, '[')
177177+ buf = strconv.AppendUint(buf, uint64(s[0]), base10)
130178131131- builder.WriteString(str)
179179+ for _, e := range s[1:] {
180180+ buf = append(buf, ", "...)
181181+ buf = strconv.AppendUint(buf, uint64(e), base10)
132182 }
133183134134- builder.WriteByte(']')
184184+ buf = append(buf, ']')
135185136136- return builder.String()
186186+ return toString(buf)
187187+}
188188+189189+func formatFloat32Slice(s []float32) string {
190190+ buf := make([]byte, 0, bracketsCap+len(s)*floatElemHint)
191191+ buf = append(buf, '[')
192192+ buf = strconv.AppendFloat(buf, float64(s[0]), floatFmt, floatPrecision, bits32)
193193+194194+ for _, e := range s[1:] {
195195+ buf = append(buf, ", "...)
196196+ buf = strconv.AppendFloat(buf, float64(e), floatFmt, floatPrecision, bits32)
197197+ }
198198+199199+ buf = append(buf, ']')
200200+201201+ return toString(buf)
202202+}
203203+204204+func formatFloat64Slice(s []float64) string {
205205+ buf := make([]byte, 0, bracketsCap+len(s)*floatElemHint)
206206+ buf = append(buf, '[')
207207+ buf = strconv.AppendFloat(buf, s[0], floatFmt, floatPrecision, bits64)
208208+209209+ for _, e := range s[1:] {
210210+ buf = append(buf, ", "...)
211211+ buf = strconv.AppendFloat(buf, e, floatFmt, floatPrecision, bits64)
212212+ }
213213+214214+ buf = append(buf, ']')
215215+216216+ return toString(buf)
217217+}
218218+219219+func formatStringSlice(s []string) string {
220220+ capacity := bracketsCap
221221+ for _, e := range s {
222222+ capacity += len(e) + stringElemHint
223223+ }
224224+225225+ buf := make([]byte, 0, capacity)
226226+ buf = append(buf, '[')
227227+ buf = strconv.AppendQuote(buf, s[0])
228228+229229+ for _, e := range s[1:] {
230230+ buf = append(buf, ", "...)
231231+ buf = strconv.AppendQuote(buf, e)
232232+ }
233233+234234+ buf = append(buf, ']')
235235+236236+ return toString(buf)
237237+}
238238+239239+func formatBoolSlice(s []bool) string {
240240+ buf := make([]byte, 0, bracketsCap+len(s)*boolElemHint)
241241+ buf = append(buf, '[')
242242+ buf = strconv.AppendBool(buf, s[0])
243243+244244+ for _, e := range s[1:] {
245245+ buf = append(buf, ", "...)
246246+ buf = strconv.AppendBool(buf, e)
247247+ }
248248+249249+ buf = append(buf, ']')
250250+251251+ return toString(buf)
137252}