package wand import ( "errors" "fmt" "reflect" "regexp" "code.squareroundforest.org/arpio/bind" ) var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$") func wrap(impl any) Cmd { cmd, ok := impl.(Cmd) if ok { return cmd } return Command("", impl) } func validateFields(f []bind.Field, conf Config) error { hasConfigFromOption := hasConfigFromOption(conf) mf := make(map[string]bind.Field) for _, fi := range f { if ef, ok := mf[fi.Name()]; ok && !compatibleTypes(fi.Type(), ef.Type()) { return fmt.Errorf("duplicate fields with different types: %s", fi.Name()) } if hasConfigFromOption && fi.Name() == "config" { return errors.New("option reserved for config file shadowed by struct field") } mf[fi.Name()] = fi } return nil } func validatePositional(p []reflect.Type, variadic bool, min, max int) error { fixedPositional := len(p) if variadic { fixedPositional-- } if min > 0 && min < fixedPositional { return fmt.Errorf( "minimum positional arguments defined as %d but the implementation expects minimum %d fixed parameters", min, fixedPositional, ) } if min > 0 && min > fixedPositional && !variadic { return fmt.Errorf( "minimum positional arguments defined as %d but the implementation has only %d fixed parameters and no variadic parameter", min, fixedPositional, ) } if max > 0 && max < fixedPositional { return fmt.Errorf( "maximum positional arguments defined as %d but the implementation expects minimum %d fixed parameters", max, fixedPositional, ) } if min > 0 && max > 0 && min > max { return fmt.Errorf( "minimum positional arguments defined as larger then the maxmimum positional: %d > %d", min, max, ) } return nil } func validateIOParameters(ior, iow []reflect.Type) error { if len(ior) > 1 || len(iow) > 1 { return errors.New("only zero or one reader and zero or one writer parameter is supported") } return nil } func validateImpl(cmd Cmd, conf Config) error { if !isFunc(cmd.impl) { return errors.New("command implementation must be a function or a pointer to a function") } p := parameters(cmd.impl) for _, pi := range p { if !isReader(pi) && !isWriter(pi) && !bindable(pi) { return fmt.Errorf("unsupported parameter type: %s", pi.Name()) } } f := fields(cmd.impl) if err := validateFields(f, conf); err != nil { return err } pos, variadic := positional(cmd.impl) if err := validatePositional(pos, variadic, cmd.minPositional, cmd.maxPositional); err != nil { return err } ior, iow := ioParameters(cmd.impl) if err := validateIOParameters(ior, iow); err != nil { return err } return nil } func validateCommandTree(cmd Cmd, conf Config) error { if cmd.isHelp { return nil } if cmd.version != "" { return nil } if cmd.impl == nil && !cmd.group { return fmt.Errorf("command does not have an implementation: %s", cmd.name) } if cmd.impl == nil && len(cmd.subcommands) == 0 { return fmt.Errorf("empty command category: %s", cmd.name) } if cmd.impl != nil { if err := validateImpl(cmd, conf); err != nil { return fmt.Errorf("%s: %w", cmd.name, err) } } var hasDefault bool names := make(map[string]bool) for _, s := range cmd.subcommands { if !commandNameExpression.MatchString(s.name) { return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name) } if names[s.name] { return fmt.Errorf("subcommand name conflict: %s", s.name) } names[s.name] = true if s.isDefault && cmd.impl != nil { return fmt.Errorf( "default subcommand defined for a command that has an explicit implementation: %s, %s", cmd.name, s.name, ) } if s.isDefault && hasDefault { return fmt.Errorf("multiple default subcommands for: %s", cmd.name) } if s.isDefault { hasDefault = true } if err := validateCommandTree(s, conf); err != nil { return fmt.Errorf("%s: %w", s.name, err) } } return nil } func checkShortFormDefinition(existing map[string]string, short, long string) error { e, ok := existing[short] if !ok { return nil } if e == long { return nil } return fmt.Errorf( "using the same short form for different options is not allowed: %s->%s, %s->%s", short, long, short, e, ) } func collectMappedShortForms(to, from map[string]string) (map[string]string, error) { for s, l := range from { if err := checkShortFormDefinition(to, s, l); err != nil { return nil, err } if to == nil { to = make(map[string]string) } to[s] = l } return to, nil } func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, error) { var mapped, unmapped map[string]string for _, sc := range cmd.subcommands { m, um, err := validateShortFormsTree(sc) if err != nil { return nil, nil, err } if mapped, err = collectMappedShortForms(mapped, m); err != nil { return nil, nil, err } if unmapped, err = collectMappedShortForms(unmapped, um); err != nil { return nil, nil, err } } if len(cmd.shortForms) % 2 != 0 { return nil, nil, fmt.Errorf("unassigned short form: %s", cmd.shortForms[len(cmd.shortForms) - 1]) } mf := mapFields(cmd.impl) for i := 0; i < len(cmd.shortForms); i += 2 { s, l := cmd.shortForms[i], cmd.shortForms[i + 1] r := []rune(s) if len(r) != 1 || r[0] < 'a' || r[0] > 'z' { return nil, nil, fmt.Errorf("invalid short form: %s", s) } if err := checkShortFormDefinition(mapped, s, l); err != nil { return nil, nil, err } if err := checkShortFormDefinition(unmapped, s, l); err != nil { return nil, nil, err } _, hasField := mf[l] _, isMapped := mapped[s] if !hasField && !isMapped { if unmapped == nil { unmapped = make(map[string]string) } unmapped[s] = l continue } if mapped == nil { mapped = make(map[string]string) } delete(unmapped, s) mapped[s] = l } return mapped, unmapped, nil } func validateShortForms(cmd Cmd) error { _, um, err := validateShortFormsTree(cmd) if err != nil { return err } if len(um) != 0 { return errors.New("unmapped short forms") } return nil } func validateCommand(cmd Cmd, conf Config) error { if err := validateCommandTree(cmd, conf); err != nil { return err } if err := validateShortForms(cmd); err != nil { return err } return nil } func insertHelpOption(names []string) []string { for _, n := range names { if n == "help" { return names } } return append(names, "help") } func insertHelpShortForm(shortForms []string) []string { for _, sf := range shortForms { if sf == "h" { return shortForms } } return append(shortForms, "h") } func boolOptions(cmd Cmd) []string { f := fields(cmd.impl) b := boolFields(f) var n []string for _, fi := range b { n = append(n, fi.Name()) } n = insertHelpOption(n) sfm := make(map[string][]string) for i := 0; i < len(cmd.shortForms); i += 2 { s, l := cmd.shortForms[i], cmd.shortForms[i+1] sfm[l] = append(sfm[l], s) } var sf []string for _, ni := range n { if sn, ok := sfm[ni]; ok { sf = append(sf, sn...) } } sf = insertHelpShortForm(sf) return append(n, sf...) }