package wand import ( "code.squareroundforest.org/arpio/bind" "slices" "strconv" "strings" "unicode" ) type value struct { isBool bool boolean bool str string } type option struct { name string value value shortForm bool } type commandLine struct { options []option positional []string } func boolValue(b bool) value { return value{isBool: true, boolean: b} } func stringValue(s string) value { return value{str: s} } func isOption(arg string) bool { a := []rune(arg) if len(a) <= 2 { return false } if string(a[:2]) != "--" { return false } if !unicode.IsLower(a[2]) { return false } for _, r := range a[3:] { if unicode.IsLower(r) { continue } if unicode.IsDigit(r) { continue } if r == '-' { continue } if r == '=' { return true } return false } return true } func isShortOptionSet(arg string) bool { a := []rune(arg) if len(a) < 2 { return false } if a[0] != '-' { return false } if !unicode.IsLower(a[1]) { return false } for _, r := range a[2:] { if r == '=' { return true } if !unicode.IsLower(r) { return false } } return true } func defaultCommand(cmd Cmd) Cmd { if cmd.impl != nil { return cmd } for _, sc := range cmd.subcommands { if sc.isDefault { return sc } } return cmd } func subcommand(cmd Cmd, name string) (Cmd, bool) { for _, sc := range cmd.subcommands { if sc.name == name { return sc, true } } return Cmd{}, false } func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) { if len(args) == 0 { return defaultCommand(cmd), []string{cmd.name}, nil } sc, ok := subcommand(cmd, args[0]) if !ok { return defaultCommand(cmd), []string{cmd.name}, args } sc, fullCommand, args := selectCommand(sc, args[1:]) fullCommand = append([]string{cmd.name}, fullCommand...) return sc, fullCommand, args } func boolOption(name string, value bool) option { return option{ name: name, value: boolValue(value), } } func stringOption(name, value string) option { return option{ name: name, value: stringValue(value), } } func shortForm(o option) option { o.shortForm = true return o } func canBeValue(arg string) bool { if arg == "--" { return false } if isOption(arg) { return false } if isShortOptionSet(arg) { return false } return true } func canBeBoolValue(arg string) bool { _, err := strconv.ParseBool(arg) return err == nil } func readOption(boolOptions []string, arg string, args []string) (option, []string) { eqi := strings.Index(arg, "=") if eqi >= 0 { arg, value := arg[:eqi], arg[eqi+1:] if slices.Contains(boolOptions, arg) && canBeBoolValue(value) { v, _ := strconv.ParseBool(value) return boolOption(arg, v), args } return stringOption(arg, value), args } var ( next string nextCanBeValue, nextCanBeBoolValue bool ) if len(args) > 0 { next = args[0] nextCanBeValue = canBeValue(next) nextCanBeBoolValue = canBeBoolValue(next) } if slices.Contains(boolOptions, arg) { value := true if nextCanBeBoolValue { value, _ = strconv.ParseBool(next) args = args[1:] } return boolOption(arg, value), args } if !nextCanBeValue { return boolOption(arg, true), args } return stringOption(arg, next), args[1:] } func readShortOptionSet(boolOptions []string, arg string, args []string) ([]option, []string) { last := len(arg) - 1 eqi := strings.Index(arg, "=") if eqi >= 0 { last = eqi - 1 } var o []option for _, a := range arg[:last] { o = append(o, shortForm(boolOption(string(a), true))) } if slices.Contains(boolOptions, arg[last:]) && (len(args) == 0 || !canBeBoolValue(args[0])) { o = append(o, shortForm(boolOption(arg[last:], true))) return o, args } var lastOption option lastOption, args = readOption(boolOptions, arg[last:], args) o = append(o, shortForm(lastOption)) return o, args } func readArgs(boolOptions, args []string) commandLine { var c commandLine if len(args) == 0 { return c } arg, args := args[0], args[1:] switch { case arg == "--": if len(args) > 0 { arg, args = args[0], args[1:] c.positional = append(c.positional, arg) args = append([]string{"--"}, args...) } case isOption(arg): var f option arg = arg[2:] f, args = readOption(boolOptions, arg, args) c.options = append(c.options, f) case isShortOptionSet(arg): var f []option arg = arg[1:] f, args = readShortOptionSet(boolOptions, arg, args) c.options = append(c.options, f...) default: c.positional = append(c.positional, arg) } cc := readArgs(boolOptions, args) c.options = append(c.options, cc.options...) c.positional = append(c.positional, cc.positional...) return c } func shortFormsToLong(sf []string) map[string]string { s2l := make(map[string]string) for i := 0; i < len(sf); i += 2 { s2l[sf[i]] = sf[i+1] } return s2l } func longFormsToShort(sf []string) map[string][]string { l2s := make(map[string][]string) for i := 0; i < len(sf); i += 2 { l2s[sf[i+1]] = append( l2s[sf[i+1]], sf[i], ) } return l2s } func hasHelpOption(cmd Cmd, o []option) bool { var mf map[string][]bind.Field if cmd.impl != nil { mf = mapFields(cmd.impl) } sf := shortFormsToLong(cmd.shortForms) for _, oi := range o { if !oi.value.isBool { continue } n := oi.name if oi.shortForm && n == "h" { l, ok := sf["h"] if !ok { continue } if l != "help" { continue } n = "help" } if n == "help" { if _, ok := mf["help"]; !ok { return true } } } return false }