package commandkit import ( "errors" "fmt" "strconv" ) // ValueFlag declares a singleton flag with a typed parser, optional default, // and setter. The default value is captured at construction. Every supplied // occurrence is parsed before input construction; the last parsed value is // applied with Set=true. A malformed earlier occurrence remains an error. func ValueFlag[I, V any](meta FlagMeta, defaultValue *V, parse func(string) (V, error), set func(*I, Supplied[V])) Flag[I] { var fallback V if defaultValue != nil { fallback = *defaultValue } binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(fallback)} if parse == nil || set == nil { return Flag[I]{binding: binding} } binding.valid = true binding.prepare = func(values []string) (preparedValue[I], error) { value := fallback supplied := len(values) > 0 for index, raw := range values { parsed, err := parse(raw) if err != nil { return preparedValue[I]{}, &flagOccurrenceError{index: index, cause: err} } value = parsed } result := Supplied[V]{Value: value, Set: supplied} return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil } return Flag[I]{binding: binding} } // BoolFlag declares a boolean flag. A bare flag is supplied by the parser as // "true"; explicit values are converted with strconv.ParseBool. Set distinguishes // an omitted flag from an explicit false value. func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool])) Flag[I] { binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true} if set == nil { return Flag[I]{binding: binding} } binding.valid = true binding.prepare = func(values []string) (preparedValue[I], error) { value := defaultValue supplied := len(values) > 0 for index, raw := range values { parsed, err := strconv.ParseBool(raw) if err != nil { return preparedValue[I]{}, &flagOccurrenceError{index: index, cause: err} } value = parsed } result := Supplied[bool]{Value: value, Set: supplied} return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil } return Flag[I]{binding: binding} } // RepeatFlag declares a repeatable flag. It captures a copy of defaults. Supplied // occurrences replace defaults and are parsed in argv order, with each // occurrence treated as one value. The core-owned result slice is copied when // applied to input. func RepeatFlag[I, V any](meta FlagMeta, defaults []V, parse func(string) (V, error), set func(*I, Supplied[[]V])) Flag[I] { fallback := append([]V(nil), defaults...) binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(fallback), repeated: true} if parse == nil || set == nil { return Flag[I]{binding: binding} } binding.valid = true binding.prepare = func(values []string) (preparedValue[I], error) { parsedValues := append([]V(nil), fallback...) supplied := len(values) > 0 if supplied { parsedValues = make([]V, 0, len(values)) for index, raw := range values { parsed, err := parse(raw) if err != nil { return preparedValue[I]{}, &flagOccurrenceError{index: index, cause: err} } parsedValues = append(parsedValues, parsed) } } return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: supplied} set(input, result) }}, nil } return Flag[I]{binding: binding} } // ValueArgument declares an optional or required singleton positional argument. // Required cardinality is enforced for execution, while help can be rendered // without a supplied operand. Parsing completes before input construction. func ValueArgument[I, V any](meta ArgumentMeta, required bool, parse func(string) (V, error), set func(*I, Supplied[V])) Argument[I] { min := 0 if required { min = 1 } binding := argumentBinding[I]{identity: newBindingIdentity(), meta: meta, min: min, max: 1} if parse == nil || set == nil { return Argument[I]{binding: binding} } binding.valid = true binding.prepare = func(values []string) (preparedValue[I], error) { if len(values) > 1 { return preparedValue[I]{}, fmt.Errorf("received %d values; maximum is 1", len(values)) } var value V supplied := len(values) == 1 if supplied { parsed, err := parse(values[0]) if err != nil { return preparedValue[I]{}, err } value = parsed } result := Supplied[V]{Value: value, Set: supplied} return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil } return Argument[I]{binding: binding} } // RepeatArgument declares a positional argument that accepts at least min and // at most max values. A maximum of -1 means unbounded. Parsing completes before // input construction, and the core-owned result slice is copied when applied. func RepeatArgument[I, V any](meta ArgumentMeta, min, max int, parse func(string) (V, error), set func(*I, Supplied[[]V])) Argument[I] { binding := argumentBinding[I]{identity: newBindingIdentity(), meta: meta, min: min, max: max} if parse == nil || set == nil { return Argument[I]{binding: binding} } binding.valid = true binding.prepare = func(values []string) (preparedValue[I], error) { if max >= 0 && len(values) > max { return preparedValue[I]{}, fmt.Errorf("received %d values; maximum is %d", len(values), max) } parsedValues := make([]V, 0, len(values)) for _, raw := range values { parsed, err := parse(raw) if err != nil { return preparedValue[I]{}, err } parsedValues = append(parsedValues, parsed) } return preparedValue[I]{identity: binding.identity, supplied: len(values) > 0, apply: func(input *I) { result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: len(values) > 0} set(input, result) }}, nil } return Argument[I]{binding: binding} } type preparedInput[I any] struct { newInput func() I values []preparedValue[I] } type flagOccurrenceError struct { index int cause error } func (err *flagOccurrenceError) Error() string { return err.cause.Error() } func (err *flagOccurrenceError) Unwrap() error { return err.cause } type flagConversionError struct { binding string spelling string occurrence int cause error } func (err *flagConversionError) Error() string { return fmt.Sprintf("flag %s occurrence %d: %v", err.spelling, err.occurrence, err.cause) } func (err *flagConversionError) Unwrap() error { return err.cause } func (p preparedInput[I]) build() I { var input I if p.newInput != nil { input = p.newInput() } for _, value := range p.values { value.apply(&input) } return input } // prepareInput parses all supplied bindings into typed closures before it // creates an input value. When enforceRequired is false, missing required flags // and operands are allowed so metadata help can validate supplied syntax. func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []string, enforceRequired bool) (preparedInput[I], error) { return prepareInputWithSpellings(spec, flagValues, nil, operands, enforceRequired) } func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, operands []string, enforceRequired bool) (preparedInput[I], error) { if len(flagValues) != len(spec.Flags) { return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags)) } if flagSpellings == nil { flagSpellings = make([][]string, len(spec.Flags)) } if len(flagSpellings) != len(spec.Flags) { return preparedInput[I]{}, fmt.Errorf("received %d flag spelling sets for %d bindings", len(flagSpellings), len(spec.Flags)) } preparedFlags := make([]preparedValue[I], len(spec.Flags)) missingRequired := "" for i, flag := range spec.Flags { binding := flag.binding value, err := binding.prepare(flagValues[i]) if err != nil { var occurrenceErr *flagOccurrenceError if errors.As(err, &occurrenceErr) { spelling := "--" + binding.meta.Name if occurrenceErr.index >= 0 && occurrenceErr.index < len(flagSpellings[i]) && flagSpellings[i][occurrenceErr.index] != "" { spelling = flagSpellings[i][occurrenceErr.index] } return preparedInput[I]{}, &flagConversionError{ binding: binding.meta.Name, spelling: spelling, occurrence: occurrenceErr.index + 1, cause: occurrenceErr.cause, } } return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err) } preparedFlags[i] = value if enforceRequired && binding.meta.Required && !binding.deferredRequired && !value.supplied && missingRequired == "" { missingRequired = binding.meta.Name } } if missingRequired != "" { return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", missingRequired) } argumentValues, err := allocateOperands(operands, spec.Arguments, enforceRequired) if err != nil { return preparedInput[I]{}, err } preparedValues := append([]preparedValue[I](nil), preparedFlags...) for i, argument := range spec.Arguments { binding := argument.binding value, err := binding.prepare(argumentValues[i]) if err != nil { return preparedInput[I]{}, fmt.Errorf("argument %q: %w", binding.meta.Name, err) } preparedValues = append(preparedValues, value) } return preparedInput[I]{newInput: spec.NewInput, values: preparedValues}, nil } func newBindingIdentity() *bindingIdentity { return &bindingIdentity{marker: 1} } func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) { for _, argument := range definitions { binding := argument.binding if binding.min < 0 || binding.max < -1 || (binding.max >= 0 && binding.min > binding.max) { return nil, fmt.Errorf("argument %q has invalid bounds %d..%d", binding.meta.Name, binding.min, binding.max) } } if enforceRequired { minimum := 0 for _, argument := range definitions { if argument.binding.min > len(operands)-minimum { return nil, fmt.Errorf("not enough positional arguments") } minimum += argument.binding.min } } suffixMinimum := make([]int, len(definitions)+1) for i := len(definitions) - 1; i >= 0; i-- { minimum := definitions[i].binding.min remaining := len(operands) - suffixMinimum[i+1] if minimum > remaining { suffixMinimum[i] = len(operands) } else { suffixMinimum[i] = suffixMinimum[i+1] + minimum } } allocated := make([][]string, len(definitions)) position := 0 for i, argument := range definitions { binding := argument.binding remaining := len(operands) - position available := remaining - suffixMinimum[i+1] if available < 0 { available = 0 } count := available if binding.max >= 0 && count > binding.max { count = binding.max } if !enforceRequired && count < binding.min && remaining > 0 { count = remaining if binding.max >= 0 && count > binding.max { count = binding.max } } if enforceRequired && count < binding.min { return nil, fmt.Errorf("not enough values for argument %q", binding.meta.Name) } allocated[i] = operands[position : position+count] position += count } if position < len(operands) { return nil, fmt.Errorf("received %d extra positional arguments", len(operands)-position) } return allocated, nil }