diff --git a/internal/arg/arg_test.go b/internal/arg/arg_test.go index 55e508c..c6cf9a8 100644 --- a/internal/arg/arg_test.go +++ b/internal/arg/arg_test.go @@ -8,6 +8,7 @@ import ( "time" "go.followtheprocess.codes/cli/internal/arg" + "go.followtheprocess.codes/cli/internal/format" "go.followtheprocess.codes/cli/internal/parse" "go.followtheprocess.codes/test" ) @@ -341,11 +342,11 @@ func TestArgableTypes(t *testing.T) { boolArg, err := arg.New(&b, "bool", "Set a bool value", arg.Config[bool]{}) test.Ok(t, err) - err = boolArg.Set("true") + err = boolArg.Set(format.True) test.Ok(t, err) test.Equal(t, b, true) test.Equal(t, boolArg.Type(), "bool") - test.Equal(t, boolArg.String(), "true") + test.Equal(t, boolArg.String(), format.True) }) t.Run("bool invalid", func(t *testing.T) { diff --git a/internal/flag/flag.go b/internal/flag/flag.go index f8aa711..2b60962 100644 --- a/internal/flag/flag.go +++ b/internal/flag/flag.go @@ -13,57 +13,10 @@ import ( "time" "unicode" "unicode/utf8" - "unsafe" "go.followtheprocess.codes/cli/flag" - "go.followtheprocess.codes/cli/internal/constraints" -) - -const ( - _ = 4 << iota // Unused - bits8 // 8 bit integer - bits16 // 16 bit integer - bits32 // 32 bit integer - bits64 // 64 bit integer -) - -const ( - typeInt = "int" - typeInt8 = "int8" - typeInt16 = "int16" - typeInt32 = "int32" - typeInt64 = "int64" - typeCount = "count" - typeUint = "uint" - typeUint8 = "uint8" - typeUint16 = "uint16" - typeUint32 = "uint32" - typeUint64 = "uint64" - typeUintptr = "uintptr" - typeFloat32 = "float32" - typeFloat64 = "float64" - typeString = "string" - typeBool = "bool" - typeBytesHex = "bytesHex" - typeTime = "time" - typeDuration = "duration" - typeIP = "ip" - typeIntSlice = "[]int" - typeInt8Slice = "[]int8" - typeInt16Slice = "[]int16" - typeInt32Slice = "[]int32" - typeInt64Slice = "[]int64" - typeUintSlice = "[]uint" - typeUint16Slice = "[]uint16" - typeUint32Slice = "[]uint32" - typeUint64Slice = "[]uint64" - typeFloat32Slice = "[]float32" - typeFloat64Slice = "[]float64" - typeStringSlice = "[]string" -) - -const ( - boolTrue = "true" + "go.followtheprocess.codes/cli/internal/format" + "go.followtheprocess.codes/cli/internal/parse" ) var _ Value = Flag[string]{} // This will fail if we violate our Value interface @@ -139,10 +92,10 @@ func (f Flag[T]) Usage() string { // --delete, when passed without arguments implies --delete true. func (f Flag[T]) NoArgValue() string { switch f.Type() { - case typeBool: + case format.TypeBool: // Boolean flags imply passing true, "--force" vs "--force true" - return boolTrue - case typeCount: + return format.True + case format.TypeCount: // Count flags imply passing 1, "--count --count" or "-cc" should inc by 2 return "1" default: @@ -161,33 +114,33 @@ func (f Flag[T]) String() string { switch typ := any(*f.value).(type) { case int: - return formatInt(typ) + return format.Int(typ) case int8: - return formatInt(typ) + return format.Int(typ) case int16: - return formatInt(typ) + return format.Int(typ) case int32: - return formatInt(typ) + return format.Int(typ) case int64: - return formatInt(typ) + return format.Int(typ) case flag.Count: - return formatUint(typ) + return format.Uint(typ) case uint: - return formatUint(typ) + return format.Uint(typ) case uint8: - return formatUint(typ) + return format.Uint(typ) case uint16: - return formatUint(typ) + return format.Uint(typ) case uint32: - return formatUint(typ) + return format.Uint(typ) case uint64: - return formatUint(typ) + return format.Uint(typ) case uintptr: - return formatUint(typ) + return format.Uint(typ) case float32: - return formatFloat[float32](bits32)(typ) + return format.Float32(typ) case float64: - return formatFloat[float64](bits64)(typ) + return format.Float64(typ) case string: return typ case bool: @@ -201,29 +154,29 @@ func (f Flag[T]) String() string { case net.IP: return typ.String() case []int: - return formatSlice(typ) + return format.Slice(typ) case []int8: - return formatSlice(typ) + return format.Slice(typ) case []int16: - return formatSlice(typ) + return format.Slice(typ) case []int32: - return formatSlice(typ) + return format.Slice(typ) case []int64: - return formatSlice(typ) + return format.Slice(typ) case []uint: - return formatSlice(typ) + return format.Slice(typ) case []uint16: - return formatSlice(typ) + return format.Slice(typ) case []uint32: - return formatSlice(typ) + return format.Slice(typ) case []uint64: - return formatSlice(typ) + return format.Slice(typ) case []float32: - return formatSlice(typ) + return format.Slice(typ) case []float64: - return formatSlice(typ) + return format.Slice(typ) case []string: - return formatStringSlice(typ) + return format.Slice(typ) default: return fmt.Sprintf("Flag.String: unsupported flag type: %T", typ) } @@ -237,69 +190,69 @@ func (f Flag[T]) Type() string { //nolint:cyclop // No other way of doing this r switch typ := any(*f.value).(type) { case int: - return typeInt + return format.TypeInt case int8: - return typeInt8 + return format.TypeInt8 case int16: - return typeInt16 + return format.TypeInt16 case int32: - return typeInt32 + return format.TypeInt32 case int64: - return typeInt64 + return format.TypeInt64 case flag.Count: - return typeCount + return format.TypeCount case uint: - return typeUint + return format.TypeUint case uint8: - return typeUint8 + return format.TypeUint8 case uint16: - return typeUint16 + return format.TypeUint16 case uint32: - return typeUint32 + return format.TypeUint32 case uint64: - return typeUint64 + return format.TypeUint64 case uintptr: - return typeUintptr + return format.TypeUintptr case float32: - return typeFloat32 + return format.TypeFloat32 case float64: - return typeFloat64 + return format.TypeFloat64 case string: - return typeString + return format.TypeString case bool: - return typeBool + return format.TypeBool case []byte: - return typeBytesHex + return format.TypeBytesHex case time.Time: - return typeTime + return format.TypeTime case time.Duration: - return typeDuration + return format.TypeDuration case net.IP: - return typeIP + return format.TypeIP case []int: - return typeIntSlice + return format.TypeIntSlice case []int8: - return typeInt8Slice + return format.TypeInt8Slice case []int16: - return typeInt16Slice + return format.TypeInt16Slice case []int32: - return typeInt32Slice + return format.TypeInt32Slice case []int64: - return typeInt64Slice + return format.TypeInt64Slice case []uint: - return typeUintSlice + return format.TypeUintSlice case []uint16: - return typeUint16Slice + return format.TypeUint16Slice case []uint32: - return typeUint32Slice + return format.TypeUint32Slice case []uint64: - return typeUint64Slice + return format.TypeUint64Slice case []float32: - return typeFloat32Slice + return format.TypeFloat32Slice case []float64: - return typeFloat64Slice + return format.TypeFloat64Slice case []string: - return typeStringSlice + return format.TypeStringSlice default: return fmt.Sprintf("%T", typ) } @@ -313,48 +266,48 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa switch typ := any(*f.value).(type) { case int: - val, err := parseInt[int](0)(str) + val, err := parse.Int(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case int8: - val, err := parseInt[int8](bits8)(str) + val, err := parse.Int8(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case int16: - val, err := parseInt[int16](bits16)(str) + val, err := parse.Int16(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case int32: - val, err := parseInt[int32](bits32)(str) + val, err := parse.Int32(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case int64: - val, err := parseInt[int64](bits64)(str) + val, err := parse.Int64(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case flag.Count: @@ -370,135 +323,135 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa // Add the count and store it back, we still parse the given str rather // than just +1 every time as this allows people to do e.g. --verbosity=3 // as well as -vvv - val, err := parseUint[uint](0)(str) + val, err := parse.Uint(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } newValue := current + flag.Count(val) - *f.value = *cast[T](&newValue) + *f.value = *parse.Cast[T](&newValue) return nil case uint: - val, err := parseUint[uint](0)(str) + val, err := parse.Uint(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case uint8: - val, err := parseUint[uint8](bits8)(str) + val, err := parse.Uint8(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case uint16: - val, err := parseUint[uint16](bits16)(str) + val, err := parse.Uint16(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case uint32: - val, err := parseUint[uint32](bits32)(str) + val, err := parse.Uint32(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case uint64: - val, err := parseUint[uint64](bits64)(str) + val, err := parse.Uint64(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case uintptr: - val, err := parseUint[uint64](bits64)(str) + val, err := parse.Uint64(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case float32: - val, err := parseFloat[float32](bits32)(str) + val, err := parse.Float32(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case float64: - val, err := parseFloat[float64](bits64)(str) + val, err := parse.Float64(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case string: val := str - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case bool: val, err := strconv.ParseBool(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case []byte: val, err := hex.DecodeString(strings.TrimSpace(str)) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case time.Time: val, err := time.Parse(time.RFC3339, str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case time.Duration: val, err := time.ParseDuration(str) if err != nil { - return errParse(f.name, str, typ, err) + return parse.Error(parse.KindFlag, f.name, str, typ, err) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case net.IP: val := net.ParseIP(str) if val == nil { - return errParse(f.name, str, typ, errors.New("invalid IP address")) + return parse.Error(parse.KindFlag, f.name, str, typ, errors.New("invalid IP address")) } - *f.value = *cast[T](&val) + *f.value = *parse.Cast[T](&val) return nil case []int: @@ -509,13 +462,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa } // Append the given value to the slice - newValue, err := parseInt[int](0)(str) + newValue, err := parse.Int(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []int8: @@ -524,13 +477,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseInt[int8](bits8)(str) + newValue, err := parse.Int8(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []int16: @@ -539,13 +492,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseInt[int16](bits16)(str) + newValue, err := parse.Int16(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []int32: @@ -554,13 +507,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseInt[int32](bits32)(str) + newValue, err := parse.Int32(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []int64: @@ -569,13 +522,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseInt[int64](bits64)(str) + newValue, err := parse.Int64(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil @@ -586,13 +539,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa } // Append the given value to the slice - newValue, err := parseUint[uint](0)(str) + newValue, err := parse.Uint(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []uint16: @@ -601,13 +554,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseUint[uint16](bits16)(str) + newValue, err := parse.Uint16(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []uint32: @@ -616,13 +569,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseUint[uint32](bits32)(str) + newValue, err := parse.Uint32(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []uint64: @@ -631,13 +584,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseUint[uint64](bits64)(str) + newValue, err := parse.Uint64(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []float32: @@ -646,13 +599,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseFloat[float32](bits32)(str) + newValue, err := parse.Float32(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []float64: @@ -661,13 +614,13 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa return errBadType(*f.value) } - newValue, err := parseFloat[float64](bits64)(str) + newValue, err := parse.Float64(str) if err != nil { - return errParseSlice(f.name, str, typ, err) + return parse.ErrorSlice(parse.KindFlag, f.name, str, typ, err) } slice = append(slice, newValue) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil case []string: @@ -678,7 +631,7 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa // No parsing to do because a string is... well, a string slice = append(slice, str) - *f.value = *cast[T](&slice) + *f.value = *parse.Cast[T](&slice) return nil default: @@ -686,30 +639,6 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa } } -// cast converts a *T1 to a *T2, we use it here when we know (via generics and compile time checks) -// that e.g. the Flag.value is a string, but we can't directly do Flag.value = "value" because -// we can't assign a string to a generic 'T', but we *know* that the value *is* a string because when -// instantiating a Flag[T], you have to provide (or compiler has to infer) Flag[string]. -// -// # Safety -// -// This function uses [unsafe.Pointer] underneath to reassign the types but we know this is safe to do -// based on the compile time checks provided by generics. Further, it fits the following valid pattern -// specified in the docs for [unsafe.Pointer]. -// -// Conversion of a *T1 to Pointer to *T2 -// -// Provided that T2 is no larger than T1 and that the two share an equivalent -// memory layout, this conversion allows reinterpreting data of one type as -// data of another type. -// -// This describes our use case as we're converting a *T to e.g a *string but *only* when we know -// that a Flag[T] is actually Flag[string], so the memory layout and size is guaranteed by the -// compiler to be equivalent. -func cast[T2, T1 any](v *T1) *T2 { - return (*T2)(unsafe.Pointer(v)) -} - // validateFlagName ensures a flag name is valid, returning an error if it's not. // // Flags names must be all lower case ASCII letters, a hyphen separator is allowed e.g. "set-default" @@ -774,122 +703,12 @@ func validateFlagShort(short rune) error { return nil } -// errParse is a helper to quickly return a consistent error in the face of flag -// value parsing errors. -func errParse[T flag.Flaggable](name, str string, typ T, err error) error { - return fmt.Errorf( - "flag %q received invalid value %q (expected %T), detail: %w", - name, - str, - typ, - err, - ) -} - -// errParseSlice is like errParse but for []T flags where the error message -// needs to be a bit more specific. -func errParseSlice[T flag.Flaggable](name, str string, typ T, err error) error { - return fmt.Errorf( - "flag %q (type %T) cannot append element %q: %w", - name, - typ, - str, - err, - ) -} - // errBadType makes a consistent error in the face of a bad type // assertion. func errBadType[T flag.Flaggable](value T) error { return fmt.Errorf("bad value %v, could not cast to %T", value, value) } -// parseInt is a generic helper to parse all signed integers, given a bit size. -// -// It returns the parsed value or an error. -func parseInt[T constraints.Signed](bits int) func(str string) (T, error) { - return func(str string) (T, error) { - val, err := strconv.ParseInt(str, 0, bits) - if err != nil { - return 0, err - } - - return T(val), nil - } -} - -// parseUint is a generic helper to parse all signed integers, given a bit size. -// -// It returns the parsed value or an error. -func parseUint[T constraints.Unsigned](bits int) func(str string) (T, error) { - return func(str string) (T, error) { - val, err := strconv.ParseUint(str, 0, bits) - if err != nil { - return 0, err - } - - return T(val), nil - } -} - -// parseFloat is a generic helper to parse floating point numbers, given a bit size. -// -// It returns the parsed value or an error. -func parseFloat[T ~float32 | ~float64](bits int) func(str string) (T, error) { - return func(str string) (T, error) { - val, err := strconv.ParseFloat(str, bits) - if err != nil { - return 0, err - } - - return T(val), nil - } -} - -// formatInt is a generic helper to return a string representation of any signed integer. -func formatInt[T constraints.Signed](in T) string { - return strconv.FormatInt(int64(in), 10) -} - -// formatUint is a generic helper to return a string representation of any unsigned integer. -func formatUint[T constraints.Unsigned](in T) string { - return strconv.FormatUint(uint64(in), 10) -} - -// formatFloat is a generic helper to return a string representation of any floating point digit. -func formatFloat[T ~float32 | ~float64](bits int) func(T) string { - return func(in T) string { - return strconv.FormatFloat(float64(in), 'g', -1, bits) - } -} - -// formatSlice is a generic helper to return a string representation of a slice. -func formatSlice[T any](slice []T) string { - return fmt.Sprintf("%v", slice) -} - -// formatStringSlice is a specialisation of formatSlice because for string -// slices we want to quote the individual strings, which is not an available -// option using one of the format verbs. -func formatStringSlice(slice []string) string { - length := len(slice) - s := &strings.Builder{} - s.WriteByte('[') - - for index, elem := range slice { - s.WriteString(strconv.Quote(elem)) - // Write commas and a space on every element other than the last one - if index < length-1 { - s.WriteByte(',') - s.WriteByte(' ') - } - } - - s.WriteByte(']') - - return s.String() -} - // isZeroIsh reports whether value is the zero value (ish) for it's type. // // "ish" means that empty slices will return true from isZeroIsh despite their official diff --git a/internal/flag/flag_test.go b/internal/flag/flag_test.go index dea7640..f8e21cf 100644 --- a/internal/flag/flag_test.go +++ b/internal/flag/flag_test.go @@ -2,6 +2,7 @@ package flag_test import ( "bytes" + "errors" "net" "slices" "testing" @@ -9,6 +10,8 @@ import ( publicflag "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/cli/internal/flag" + "go.followtheprocess.codes/cli/internal/format" + "go.followtheprocess.codes/cli/internal/parse" "go.followtheprocess.codes/test" ) @@ -37,11 +40,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "int" received invalid value "word" (expected int), detail: strconv.ParseInt: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int8 valid", func(t *testing.T) { @@ -65,11 +64,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "int" received invalid value "word" (expected int8), detail: strconv.ParseInt: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int16 valid", func(t *testing.T) { @@ -93,11 +88,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "int" received invalid value "word" (expected int16), detail: strconv.ParseInt: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int32 valid", func(t *testing.T) { @@ -121,11 +112,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "int" received invalid value "word" (expected int32), detail: strconv.ParseInt: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int64 valid", func(t *testing.T) { @@ -149,11 +136,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "int" received invalid value "word" (expected int64), detail: strconv.ParseInt: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("count valid", func(t *testing.T) { @@ -192,11 +175,7 @@ func TestFlaggableTypes(t *testing.T) { err = countFlag.Set("a word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "count" received invalid value "a word" (expected flag.Count), detail: strconv.ParseUint: parsing "a word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint valid", func(t *testing.T) { @@ -220,11 +199,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uint" received invalid value "word" (expected uint), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint8 valid", func(t *testing.T) { @@ -248,11 +223,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uint" received invalid value "word" (expected uint8), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint16 valid", func(t *testing.T) { @@ -276,11 +247,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uint" received invalid value "word" (expected uint16), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint32 valid", func(t *testing.T) { @@ -304,11 +271,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uint" received invalid value "word" (expected uint32), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint64 valid", func(t *testing.T) { @@ -332,11 +295,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uint" received invalid value "word" (expected uint64), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uintptr valid", func(t *testing.T) { @@ -360,11 +319,7 @@ func TestFlaggableTypes(t *testing.T) { err = intFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "uintptr" received invalid value "word" (expected uintptr), detail: strconv.ParseUint: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("float32 valid", func(t *testing.T) { @@ -388,11 +343,7 @@ func TestFlaggableTypes(t *testing.T) { err = floatFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "float" received invalid value "word" (expected float32), detail: strconv.ParseFloat: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("float64 valid", func(t *testing.T) { @@ -416,11 +367,7 @@ func TestFlaggableTypes(t *testing.T) { err = floatFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "float" received invalid value "word" (expected float64), detail: strconv.ParseFloat: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("bool valid", func(t *testing.T) { @@ -429,11 +376,11 @@ func TestFlaggableTypes(t *testing.T) { boolFlag, err := flag.New(&b, "bool", 'b', false, "Set a bool value") test.Ok(t, err) - err = boolFlag.Set("true") + err = boolFlag.Set(format.True) test.Ok(t, err) test.Equal(t, b, true) test.Equal(t, boolFlag.Type(), "bool") - test.Equal(t, boolFlag.String(), "true") + test.Equal(t, boolFlag.String(), format.True) }) t.Run("bool invalid", func(t *testing.T) { @@ -444,11 +391,7 @@ func TestFlaggableTypes(t *testing.T) { err = boolFlag.Set("word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "bool" received invalid value "word" (expected bool), detail: strconv.ParseBool: parsing "word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) // No invalid case as all command line args are strings anyway so no real way of @@ -487,11 +430,7 @@ func TestFlaggableTypes(t *testing.T) { err = byteFlag.Set("0xF") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "byte" received invalid value "0xF" (expected []uint8), detail: encoding/hex: invalid byte: U+0078 'x'`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("time.Time valid", func(t *testing.T) { @@ -518,11 +457,7 @@ func TestFlaggableTypes(t *testing.T) { err = timeFlag.Set("not a time") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "time" received invalid value "not a time" (expected time.Time), detail: parsing time "not a time" as "2006-01-02T15:04:05Z07:00": cannot parse "not a time" as "2006"`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("time.Duration valid", func(t *testing.T) { @@ -561,11 +496,7 @@ func TestFlaggableTypes(t *testing.T) { err = durationFlag.Set("not a duration") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "duration" received invalid value "not a duration" (expected time.Duration), detail: time: invalid duration "not a duration"`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("ip valid", func(t *testing.T) { @@ -589,11 +520,7 @@ func TestFlaggableTypes(t *testing.T) { err = ipFlag.Set("not an ip") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "ip" received invalid value "not an ip" (expected net.IP), detail: invalid IP address`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int slice valid", func(t *testing.T) { @@ -614,7 +541,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []int{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]int") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("int slice invalid", func(t *testing.T) { @@ -625,11 +552,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("a word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []int) cannot append element "a word": strconv.ParseInt: parsing "a word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int8 slice valid", func(t *testing.T) { @@ -650,7 +573,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []int8{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]int8") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("int8 slice invalid", func(t *testing.T) { @@ -661,11 +584,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("cheese") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []int8) cannot append element "cheese": strconv.ParseInt: parsing "cheese": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int16 slice valid", func(t *testing.T) { @@ -686,7 +605,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []int16{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]int16") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("int16 slice invalid", func(t *testing.T) { @@ -697,11 +616,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []int16) cannot append element "balls": strconv.ParseInt: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int32 slice valid", func(t *testing.T) { @@ -722,7 +637,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []int32{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]int32") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("int32 slice invalid", func(t *testing.T) { @@ -733,11 +648,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []int32) cannot append element "balls": strconv.ParseInt: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("int64 slice valid", func(t *testing.T) { @@ -758,7 +669,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []int64{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]int64") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("int64 slice invalid", func(t *testing.T) { @@ -769,11 +680,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []int64) cannot append element "balls": strconv.ParseInt: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint slice valid", func(t *testing.T) { @@ -794,7 +701,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []uint{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]uint") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("uint slice invalid", func(t *testing.T) { @@ -805,11 +712,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("a word") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []uint) cannot append element "a word": strconv.ParseUint: parsing "a word": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint16 slice valid", func(t *testing.T) { @@ -830,7 +733,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []uint16{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]uint16") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("uint16 slice invalid", func(t *testing.T) { @@ -841,11 +744,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []uint16) cannot append element "balls": strconv.ParseUint: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint32 slice valid", func(t *testing.T) { @@ -866,7 +765,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []uint32{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]uint32") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("uint32 slice invalid", func(t *testing.T) { @@ -877,11 +776,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []uint32) cannot append element "balls": strconv.ParseUint: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("uint64 slice valid", func(t *testing.T) { @@ -902,7 +797,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []uint64{1, 2}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]uint64") - test.Equal(t, sliceFlag.String(), "[1 2]") + test.Equal(t, sliceFlag.String(), "[1, 2]") }) t.Run("uint64 slice invalid", func(t *testing.T) { @@ -913,11 +808,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []uint64) cannot append element "balls": strconv.ParseUint: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("float32 slice valid", func(t *testing.T) { @@ -938,7 +829,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []float32{3.14159, 2.7128}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]float32") - test.Equal(t, sliceFlag.String(), "[3.14159 2.7128]") + test.Equal(t, sliceFlag.String(), "[3.14159, 2.7128]") }) t.Run("float32 slice invalid", func(t *testing.T) { @@ -949,11 +840,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []float32) cannot append element "balls": strconv.ParseFloat: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("float64 slice valid", func(t *testing.T) { @@ -974,7 +861,7 @@ func TestFlaggableTypes(t *testing.T) { test.EqualFunc(t, slice, []float64{3.14159, 2.7128}, slices.Equal) test.Equal(t, sliceFlag.Type(), "[]float64") - test.Equal(t, sliceFlag.String(), "[3.14159 2.7128]") + test.Equal(t, sliceFlag.String(), "[3.14159, 2.7128]") }) t.Run("float64 slice invalid", func(t *testing.T) { @@ -985,11 +872,7 @@ func TestFlaggableTypes(t *testing.T) { err = sliceFlag.Set("balls") test.Err(t, err) - test.Equal( - t, - err.Error(), - `flag "slice" (type []float64) cannot append element "balls": strconv.ParseFloat: parsing "balls": invalid syntax`, - ) + test.True(t, errors.Is(err, parse.Err)) }) t.Run("string slice valid", func(t *testing.T) { @@ -1148,7 +1031,7 @@ func TestFlagNilSafety(t *testing.T) { test.Equal(t, flag.String(), "") test.Equal(t, flag.Type(), "") - err := flag.Set("true") + err := flag.Set(format.True) test.Err(t, err) test.Equal(t, err.Error(), "cannot set value true, flag.value was nil") }) diff --git a/internal/flag/set.go b/internal/flag/set.go index c1b431f..0c2dadb 100644 --- a/internal/flag/set.go +++ b/internal/flag/set.go @@ -8,6 +8,7 @@ import ( "strings" "go.followtheprocess.codes/cli/flag" + "go.followtheprocess.codes/cli/internal/format" "go.followtheprocess.codes/cli/internal/style" "go.followtheprocess.codes/hue/tabwriter" ) @@ -101,12 +102,12 @@ func (s *Set) Help() (value, ok bool) { return false, false } // Is it a bool flag? - if flag.Type() != typeBool { + if flag.Type() != format.TypeBool { return false, false } // It is there, we can infer from the string value what it's set to // avoid unnecessary type conversions - return flag.String() == boolTrue, true + return flag.String() == format.True, true } // Version returns whether the [Set] has a boolean flag named "version" and what the value @@ -118,12 +119,12 @@ func (s *Set) Version() (value, ok bool) { return false, false } // Is it a bool flag? - if flag.Type() != typeBool { + if flag.Type() != format.TypeBool { return false, false } // It is there, we can infer from the string value what it's set to // avoid unnecessary type conversions - return flag.String() == boolTrue, true + return flag.String() == format.True, true } // Args returns a slice of all the non-flag arguments, including any diff --git a/internal/flag/set_test.go b/internal/flag/set_test.go index 4d491a1..aaa68f1 100644 --- a/internal/flag/set_test.go +++ b/internal/flag/set_test.go @@ -8,6 +8,7 @@ import ( publicflag "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/cli/internal/flag" + "go.followtheprocess.codes/cli/internal/format" "go.followtheprocess.codes/snapshot" "go.followtheprocess.codes/test" ) @@ -280,7 +281,7 @@ func TestParse(t *testing.T) { test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) test.EqualFunc(t, set.Args(), nil, slices.Equal) }, @@ -304,7 +305,7 @@ func TestParse(t *testing.T) { test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) test.EqualFunc(t, set.Args(), []string{"some", "subcommand", "extra", "args"}, slices.Equal) test.EqualFunc(t, set.ExtraArgs(), []string{"extra", "args"}, slices.Equal) @@ -330,14 +331,14 @@ func TestParse(t *testing.T) { test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) // Get by short flag, exists = set.GetShort('d') test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) test.EqualFunc(t, set.Args(), nil, slices.Equal) }, @@ -392,14 +393,14 @@ func TestParse(t *testing.T) { test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) // Get by short flag, exists = set.Get("delete") test.True(t, exists) test.Equal(t, flag.Type(), "bool") - test.Equal(t, flag.String(), "true") + test.Equal(t, flag.String(), format.True) test.EqualFunc(t, set.Args(), []string{"some", "arg"}, slices.Equal) }, @@ -525,7 +526,7 @@ func TestParse(t *testing.T) { }, args: []string{"--number", "-8"}, // Trying to set a uint flag to negative number wantErr: true, - errMsg: `flag "number" received invalid value "-8" (expected uint), detail: strconv.ParseUint: parsing "-8": invalid syntax`, + errMsg: `parse error: flag "number" received invalid value "-8" (expected uint): strconv.ParseUint: parsing "-8": invalid syntax`, }, { name: "valid short value", @@ -612,7 +613,7 @@ func TestParse(t *testing.T) { }, args: []string{"-n", "-8"}, // Trying to set a uint flag to negative number wantErr: true, - errMsg: `flag "number" received invalid value "-8" (expected uint), detail: strconv.ParseUint: parsing "-8": invalid syntax`, + errMsg: `parse error: flag "number" received invalid value "-8" (expected uint): strconv.ParseUint: parsing "-8": invalid syntax`, }, { name: "valid long equals value", @@ -681,7 +682,7 @@ func TestParse(t *testing.T) { }, args: []string{"--number=-8"}, // Trying to set a uint flag to negative number wantErr: true, - errMsg: `flag "number" received invalid value "-8" (expected uint), detail: strconv.ParseUint: parsing "-8": invalid syntax`, + errMsg: `parse error: flag "number" received invalid value "-8" (expected uint): strconv.ParseUint: parsing "-8": invalid syntax`, }, { name: "valid short equals value", diff --git a/internal/format/format.go b/internal/format/format.go index 6be9a87..bf822d8 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -4,7 +4,10 @@ package format import ( + "fmt" + "reflect" "strconv" + "strings" "go.followtheprocess.codes/cli/internal/constraints" ) @@ -13,6 +16,7 @@ const ( base10 = 10 floatFmt = 'g' floatPrecision = -1 + slice = "[]" ) const ( @@ -22,27 +26,46 @@ const ( // Type names. const ( - TypeInt = "int" - TypeInt8 = "int8" - TypeInt16 = "int16" - TypeInt32 = "int32" - TypeInt64 = "int64" - TypeUint = "uint" - TypeUint8 = "uint8" - TypeUint16 = "uint16" - TypeUint32 = "uint32" - TypeUint64 = "uint64" - TypeUintptr = "uintptr" - TypeFloat32 = "float32" - TypeFloat64 = "float64" - TypeString = "string" - TypeBool = "bool" - TypeBytesHex = "bytesHex" - TypeTime = "time" - TypeDuration = "duration" - TypeIP = "ip" + TypeInt = "int" + TypeInt8 = "int8" + TypeInt16 = "int16" + TypeInt32 = "int32" + TypeInt64 = "int64" + TypeUint = "uint" + TypeCount = "count" + TypeUint8 = "uint8" + TypeUint16 = "uint16" + TypeUint32 = "uint32" + TypeUint64 = "uint64" + TypeUintptr = "uintptr" + TypeFloat32 = "float32" + TypeFloat64 = "float64" + TypeString = "string" + TypeBool = "bool" + TypeBytesHex = "bytesHex" + TypeTime = "time" + TypeDuration = "duration" + TypeIP = "ip" + TypeIntSlice = slice + TypeInt + TypeInt8Slice = slice + TypeInt8 + TypeInt16Slice = slice + TypeInt16 + TypeInt32Slice = slice + TypeInt32 + TypeInt64Slice = slice + TypeInt64 + TypeUintSlice = slice + TypeUint + TypeUint16Slice = slice + TypeUint16 + TypeUint32Slice = slice + TypeUint32 + TypeUint64Slice = slice + TypeUint64 + TypeFloat32Slice = slice + TypeFloat32 + TypeFloat64Slice = slice + TypeFloat64 + TypeStringSlice = slice + TypeString ) +// True is the literal boolean true as a string. +// +// We check for "true" when scanning boolean flags, interpreting +// default values and when flags have a NoArgValue. +const True = "true" + // Int returns a string representation of an integer. func Int[T constraints.Signed](n T) string { return strconv.FormatInt(int64(n), base10) @@ -62,3 +85,43 @@ func Float32(f float32) string { func Float64(f float64) string { return strconv.FormatFloat(float64(f), floatFmt, floatPrecision, bits64) } + +// Slice returns a string representation of a slice. +// +// It will return a bracketed, comma separated list of items. If T is +// a string, the items will be quoted. +// +// Slice([]int{1, 2, 3, 4}) // "[1, 2, 3, 4]" +// Slice([]string{"one", "two", "three"}) // `["one", "two", "three"]` +func Slice[T any](s []T) string { + length := len(s) + + if length == 0 { + // If it's empty or nil, avoid doing the work below + // and just return "[]" + return slice + } + + builder := &strings.Builder{} + builder.WriteByte('[') + + typ := reflect.TypeFor[T]().Kind() + + for index, element := range s { + str := fmt.Sprintf("%v", element) + if typ == reflect.String { + // If it's a string, quote it + str = strconv.Quote(str) + } + + builder.WriteString(str) + + if index < length-1 { + builder.WriteString(", ") + } + } + + builder.WriteByte(']') + + return builder.String() +} diff --git a/internal/format/format_test.go b/internal/format/format_test.go index 8fa7eb5..90a0a88 100644 --- a/internal/format/format_test.go +++ b/internal/format/format_test.go @@ -4,6 +4,8 @@ import ( "strconv" "testing" "testing/quick" + + "go.followtheprocess.codes/test" ) func TestInt(t *testing.T) { @@ -59,3 +61,15 @@ func TestFloat64(t *testing.T) { t.Error(err) } } + +func TestSlice(t *testing.T) { + strings := []string{"one", "two", "three"} + ints := []int{1, 2, 3} + floats := []float64{1.0, 2.0, 3.0} + bools := []bool{true, true, false} + + test.Equal(t, Slice(strings), `["one", "two", "three"]`, test.Context("strings")) + test.Equal(t, Slice(ints), "[1, 2, 3]", test.Context("ints")) + test.Equal(t, Slice(floats), "[1, 2, 3]", test.Context("floats")) + test.Equal(t, Slice(bools), "[true, true, false]", test.Context("bools")) +} diff --git a/internal/parse/parse.go b/internal/parse/parse.go index 8549c57..4701635 100644 --- a/internal/parse/parse.go +++ b/internal/parse/parse.go @@ -56,6 +56,17 @@ func Error[T any](kind Kind, name, str string, typ T, err error) error { return fmt.Errorf("%w: %s %q received invalid value %q (expected %T): %w", Err, kind, name, str, typ, err) } +// ErrorSlice produces a formatted parse error for a slice type. +// +// The kind should must be [KindArgument] or [KindFlag], with name and str being the +// name of the arg/flag and the invalid text that triggered the error. +// +// The type T is the type we were parsing str into and err is any underlying +// error e.g. from strconv. +func ErrorSlice[T any](kind Kind, name, str string, typ T, err error) error { + return fmt.Errorf("%w: %s %q (type %T) cannot append element %q: %w", Err, kind, name, typ, str, err) +} + // Int parses an int from a string. func Int(str string) (int, error) { val, err := strconv.ParseInt(str, base10, 0)