Files
commandkit/binding.go

268 lines
9.3 KiB
Go

package commandkit
import (
"fmt"
"strconv"
)
// ValueFlag declares a singleton flag with a typed parser, optional default,
// and setter. Every supplied occurrence is parsed; the last parsed value is
// applied with Set=true.
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.clear = func(input *I) { set(input, Supplied[V]{}) }
binding.prepare = func(values []string) (preparedValue[I], error) {
value := fallback
supplied := len(values) > 0
for _, raw := range values {
parsed, err := parse(raw)
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 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.
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.clear = func(input *I) { set(input, Supplied[bool]{}) }
binding.prepare = func(values []string) (preparedValue[I], error) {
value := defaultValue
supplied := len(values) > 0
for _, raw := range values {
parsed, err := strconv.ParseBool(raw)
if err != nil {
return preparedValue[I]{}, 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. Supplied occurrences replace defaults
// and are parsed in argv order, with each occurrence treated as one value.
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.clear = func(input *I) { set(input, Supplied[[]V]{}) }
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 _, 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: 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.
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 a bounded number
// of values. A maximum of -1 means unbounded.
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]
}
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) {
if len(flagValues) != len(spec.Flags) {
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), 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 {
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
}