diff --git a/byte_types.go b/byte_types.go index 75c315d..6acb7f0 100644 --- a/byte_types.go +++ b/byte_types.go @@ -2,4 +2,3 @@ package flaggy // Base64Bytes is a []byte interpreted as base64 when parsed from flags. type Base64Bytes []byte - diff --git a/flag.go b/flag.go index 86fa596..c1a3300 100644 --- a/flag.go +++ b/flag.go @@ -1,19 +1,19 @@ package flaggy import ( - "encoding/base64" - "errors" - "fmt" - "math/big" - "net" - netip "net/netip" - "net/url" - "os" - "reflect" - "regexp" - "strconv" - "strings" - "time" + "encoding/base64" + "errors" + "fmt" + "math/big" + "net" + netip "net/netip" + "net/url" + "os" + "reflect" + "regexp" + "strconv" + "strings" + "time" ) // Flag holds the base methods for all flag types @@ -62,14 +62,14 @@ func (f *Flag) identifyAndAssignValue(value string) error { // depending on the type of the assignment variable, we convert the // incoming string and assign it. We only use pointers to variables // in flagy. No returning vars by value. - switch f.AssignmentVar.(type) { + switch f.AssignmentVar.(type) { case *string: v, _ := (f.AssignmentVar).(*string) *v = value - case *[]string: - v := f.AssignmentVar.(*[]string) - new := append(*v, value) - *v = new + case *[]string: + v := f.AssignmentVar.(*[]string) + new := append(*v, value) + *v = new case *bool: v, err := strconv.ParseBool(value) if err != nil { @@ -323,175 +323,176 @@ func (f *Flag) identifyAndAssignValue(value string) error { existing := f.AssignmentVar.(*[]net.HardwareAddr) new := append(*existing, v) *existing = new - case *net.IPMask: - v := net.IPMask(net.ParseIP(value).To4()) - existing := f.AssignmentVar.(*net.IPMask) - *existing = v - case *[]net.IPMask: - v := net.IPMask(net.ParseIP(value).To4()) - existing := f.AssignmentVar.(*[]net.IPMask) - new := append(*existing, v) - *existing = new - case *time.Time: - // Support unix seconds if numeric, else try common layouts - if isAllDigits(value) { - sec, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return err - } - t := time.Unix(sec, 0).UTC() - a := f.AssignmentVar.(*time.Time) - *a = t - return nil - } - var parsed time.Time - var err error - layouts := []string{time.RFC3339Nano, time.RFC3339, time.RFC1123Z, time.RFC1123} - for _, layout := range layouts { - parsed, err = time.Parse(layout, value) - if err == nil { - a := f.AssignmentVar.(*time.Time) - *a = parsed - return nil - } - } - return err - case *url.URL: - u, err := url.Parse(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*url.URL) - *a = *u - case *net.IPNet: - _, ipnet, err := net.ParseCIDR(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*net.IPNet) - *a = *ipnet - case *net.TCPAddr: - host, portStr, err := net.SplitHostPort(value) - if err != nil { - return err - } - port, err := strconv.Atoi(portStr) - if err != nil { - return err - } - var ip net.IP - if len(host) > 0 { - ip = net.ParseIP(host) - } - addr := net.TCPAddr{IP: ip, Port: port} - a := f.AssignmentVar.(*net.TCPAddr) - *a = addr - case *net.UDPAddr: - host, portStr, err := net.SplitHostPort(value) - if err != nil { - return err - } - port, err := strconv.Atoi(portStr) - if err != nil { - return err - } - var ip net.IP - if len(host) > 0 { - ip = net.ParseIP(host) - } - addr := net.UDPAddr{IP: ip, Port: port} - a := f.AssignmentVar.(*net.UDPAddr) - *a = addr - case *os.FileMode: - v, err := strconv.ParseUint(value, 0, 32) - if err != nil { - return err - } - a := f.AssignmentVar.(*os.FileMode) - *a = os.FileMode(v) - case *regexp.Regexp: - r, err := regexp.Compile(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*regexp.Regexp) - *a = *r - case *time.Location: - // Try IANA name, with fallback to UTC offset like +02:00 or -0700 - if loc, err := time.LoadLocation(value); err == nil { - a := f.AssignmentVar.(*time.Location) - *a = *loc - return nil - } - if off, ok := parseUTCOffset(value); ok { - name := offsetName(off) - loc := time.FixedZone(name, off) - a := f.AssignmentVar.(*time.Location) - *a = *loc - return nil - } - return fmt.Errorf("invalid time.Location: %s", value) - case *time.Month: - if m, ok := parseMonth(value); ok { - a := f.AssignmentVar.(*time.Month) - *a = m - return nil - } - return fmt.Errorf("invalid time.Month: %s", value) - case *time.Weekday: - if d, ok := parseWeekday(value); ok { - a := f.AssignmentVar.(*time.Weekday) - *a = d - return nil - } - return fmt.Errorf("invalid time.Weekday: %s", value) - case *big.Int: - bi := f.AssignmentVar.(*big.Int) - if _, ok := bi.SetString(value, 0); !ok { - return fmt.Errorf("invalid big.Int: %s", value) - } - case *big.Rat: - br := f.AssignmentVar.(*big.Rat) - if _, ok := br.SetString(value); !ok { - return fmt.Errorf("invalid big.Rat: %s", value) - } - case *Base64Bytes: - // Try standard then URL encoding - if decoded, err := base64.StdEncoding.DecodeString(value); err == nil { - a := f.AssignmentVar.(*Base64Bytes) - *a = Base64Bytes(decoded) - return nil - } else if decoded, err2 := base64.URLEncoding.DecodeString(value); err2 == nil { - a := f.AssignmentVar.(*Base64Bytes) - *a = Base64Bytes(decoded) - return nil - } else { - return err - } - case *netip.Addr: - addr, err := netip.ParseAddr(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*netip.Addr) - *a = addr - case *netip.Prefix: - pfx, err := netip.ParsePrefix(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*netip.Prefix) - *a = pfx - case *netip.AddrPort: - ap, err := netip.ParseAddrPort(value) - if err != nil { - return err - } - a := f.AssignmentVar.(*netip.AddrPort) - *a = ap - default: - return errors.New("Unknown flag assignmentVar supplied in flag " + f.LongName + " " + f.ShortName) - } + case *net.IPMask: + v := net.IPMask(net.ParseIP(value).To4()) + existing := f.AssignmentVar.(*net.IPMask) + *existing = v + case *[]net.IPMask: + v := net.IPMask(net.ParseIP(value).To4()) + existing := f.AssignmentVar.(*[]net.IPMask) + new := append(*existing, v) + *existing = new + case *time.Time: + // Support unix seconds if numeric, else try common layouts + if isAllDigits(value) { + sec, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + t := time.Unix(sec, 0).UTC() + a := f.AssignmentVar.(*time.Time) + *a = t + return nil + } + var parsed time.Time + var err error + layouts := []string{time.RFC3339Nano, time.RFC3339, time.RFC1123Z, time.RFC1123} + for _, layout := range layouts { + parsed, err = time.Parse(layout, value) + if err == nil { + a := f.AssignmentVar.(*time.Time) + *a = parsed + return nil + } + } + return err + case *url.URL: + u, err := url.Parse(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*url.URL) + *a = *u + case *net.IPNet: + _, ipnet, err := net.ParseCIDR(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*net.IPNet) + *a = *ipnet + case *net.TCPAddr: + host, portStr, err := net.SplitHostPort(value) + if err != nil { + return err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return err + } + var ip net.IP + if len(host) > 0 { + ip = net.ParseIP(host) + } + addr := net.TCPAddr{IP: ip, Port: port} + a := f.AssignmentVar.(*net.TCPAddr) + *a = addr + case *net.UDPAddr: + host, portStr, err := net.SplitHostPort(value) + if err != nil { + return err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return err + } + var ip net.IP + if len(host) > 0 { + ip = net.ParseIP(host) + } + addr := net.UDPAddr{IP: ip, Port: port} + a := f.AssignmentVar.(*net.UDPAddr) + *a = addr + case *os.FileMode: + v, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return err + } + a := f.AssignmentVar.(*os.FileMode) + *a = os.FileMode(v) + case *regexp.Regexp: + r, err := regexp.Compile(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*regexp.Regexp) + *a = *r + case *time.Location: + // Try IANA name, with fallback to UTC offset like +02:00 or -0700 + if loc, err := time.LoadLocation(value); err == nil { + a := f.AssignmentVar.(*time.Location) + *a = *loc + return nil + } + if off, ok := parseUTCOffset(value); ok { + name := offsetName(off) + loc := time.FixedZone(name, off) + a := f.AssignmentVar.(*time.Location) + *a = *loc + return nil + } + return fmt.Errorf("invalid time.Location: %s", value) + case *time.Month: + if m, ok := parseMonth(value); ok { + a := f.AssignmentVar.(*time.Month) + *a = m + return nil + } + return fmt.Errorf("invalid time.Month: %s", value) + case *time.Weekday: + if d, ok := parseWeekday(value); ok { + a := f.AssignmentVar.(*time.Weekday) + *a = d + return nil + } + return fmt.Errorf("invalid time.Weekday: %s", value) + case *big.Int: + bi := f.AssignmentVar.(*big.Int) + if _, ok := bi.SetString(value, 0); !ok { + return fmt.Errorf("invalid big.Int: %s", value) + } + case *big.Rat: + br := f.AssignmentVar.(*big.Rat) + if _, ok := br.SetString(value); !ok { + return fmt.Errorf("invalid big.Rat: %s", value) + } + case *Base64Bytes: + // Try standard then URL encoding + decoded, err := base64.StdEncoding.DecodeString(value) + if err == nil { + a := f.AssignmentVar.(*Base64Bytes) + *a = Base64Bytes(decoded) + return nil + } + if decodedURL, errURL := base64.URLEncoding.DecodeString(value); errURL == nil { + a := f.AssignmentVar.(*Base64Bytes) + *a = Base64Bytes(decodedURL) + return nil + } + return err + case *netip.Addr: + addr, err := netip.ParseAddr(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.Addr) + *a = addr + case *netip.Prefix: + pfx, err := netip.ParsePrefix(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.Prefix) + *a = pfx + case *netip.AddrPort: + ap, err := netip.ParseAddrPort(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.AddrPort) + *a = ap + default: + return errors.New("Unknown flag assignmentVar supplied in flag " + f.LongName + " " + f.ShortName) + } return err } @@ -560,8 +561,9 @@ func parseArgWithValue(arg string) (key string, value string) { } // parseFlagToName parses a flag with space value down to a key name: -// --path -> path -// -p -> p +// +// --path -> path +// -p -> p func parseFlagToName(arg string) string { // remove minus from start arg = strings.TrimLeft(arg, "-") @@ -570,7 +572,8 @@ func parseFlagToName(arg string) string { } // collectAllNestedFlags recurses through the command tree to get all -// flags specified on a subcommand and its descending subcommands +// +// flags specified on a subcommand and its descending subcommands func collectAllNestedFlags(sc *Subcommand) []*Flag { fullList := sc.Flags for _, sc := range sc.Subcommands { @@ -583,7 +586,17 @@ func collectAllNestedFlags(sc *Subcommand) []*Flag { // flagIsBool determines if the flag is a bool within the specified parser // and subcommand's context func flagIsBool(sc *Subcommand, p *Parser, key string) bool { - for _, f := range append(collectAllNestedFlags(sc), p.Flags...) { + for _, f := range sc.Flags { + if f.HasName(key) { + _, isBool := f.AssignmentVar.(*bool) + _, isBoolSlice := f.AssignmentVar.(*[]bool) + if isBool || isBoolSlice { + return true + } + } + } + + for _, f := range p.Flags { if f.HasName(key) { _, isBool := f.AssignmentVar.(*bool) _, isBoolSlice := f.AssignmentVar.(*[]bool) @@ -597,6 +610,24 @@ func flagIsBool(sc *Subcommand, p *Parser, key string) bool { return false } +// flagIsDefined reports whether a flag with the provided key is registered on +// the supplied subcommand or parser. +func flagIsDefined(sc *Subcommand, p *Parser, key string) bool { + for _, f := range sc.Flags { + if f.HasName(key) { + return true + } + } + + for _, f := range p.Flags { + if f.HasName(key) { + return true + } + } + + return false +} + // returnAssignmentVarValueAsString returns the value of the flag's // assignment variable as a string. This is used to display the // default value of flags before they are assigned (like when help is output). @@ -609,7 +640,7 @@ func (f *Flag) returnAssignmentVarValueAsString() (string, error) { // depending on the type of the assignment variable, we convert the // incoming string and assign it. We only use pointers to variables // in flagy. No returning vars by value. - switch f.AssignmentVar.(type) { + switch f.AssignmentVar.(type) { case *string: v, _ := (f.AssignmentVar).(*string) return *v, err @@ -782,178 +813,178 @@ func (f *Flag) returnAssignmentVarValueAsString() (string, error) { case *net.IPMask: val := f.AssignmentVar.(*net.IPMask) return val.String(), err - case *[]net.IPMask: - val := f.AssignmentVar.(*[]net.IPMask) - var strSlice []string - for _, m := range *val { - strSlice = append(strSlice, m.String()) - } - return strings.Join(strSlice, ","), err - case *time.Time: - v := f.AssignmentVar.(*time.Time) - if v.IsZero() { - return "", err - } - return v.UTC().Format(time.RFC3339Nano), err - case *url.URL: - v := f.AssignmentVar.(*url.URL) - return v.String(), err - case *net.IPNet: - v := f.AssignmentVar.(*net.IPNet) - return v.String(), err - case *net.TCPAddr: - v := f.AssignmentVar.(*net.TCPAddr) - return v.String(), err - case *net.UDPAddr: - v := f.AssignmentVar.(*net.UDPAddr) - return v.String(), err - case *os.FileMode: - v := f.AssignmentVar.(*os.FileMode) - return fmt.Sprintf("%#o", *v), err - case *regexp.Regexp: - v := f.AssignmentVar.(*regexp.Regexp) - return v.String(), err - case *time.Location: - v := f.AssignmentVar.(*time.Location) - return v.String(), err - case *time.Month: - v := f.AssignmentVar.(*time.Month) - if *v == 0 { - return "", err - } - return v.String(), err - case *time.Weekday: - v := f.AssignmentVar.(*time.Weekday) - return v.String(), err - case *big.Int: - v := f.AssignmentVar.(*big.Int) - return v.String(), err - case *big.Rat: - v := f.AssignmentVar.(*big.Rat) - return v.RatString(), err - case *Base64Bytes: - v := f.AssignmentVar.(*Base64Bytes) - if v == nil || len(*v) == 0 { - return "", err - } - return base64.StdEncoding.EncodeToString([]byte(*v)), err - case *netip.Addr: - v := f.AssignmentVar.(*netip.Addr) - return v.String(), err - case *netip.Prefix: - v := f.AssignmentVar.(*netip.Prefix) - return v.String(), err - case *netip.AddrPort: - v := f.AssignmentVar.(*netip.AddrPort) - return v.String(), err - default: - return "", errors.New("Unknown flag assignmentVar found in flag " + f.LongName + " " + f.ShortName + ". Type not supported: " + reflect.TypeOf(f.AssignmentVar).String()) - } + case *[]net.IPMask: + val := f.AssignmentVar.(*[]net.IPMask) + var strSlice []string + for _, m := range *val { + strSlice = append(strSlice, m.String()) + } + return strings.Join(strSlice, ","), err + case *time.Time: + v := f.AssignmentVar.(*time.Time) + if v.IsZero() { + return "", err + } + return v.UTC().Format(time.RFC3339Nano), err + case *url.URL: + v := f.AssignmentVar.(*url.URL) + return v.String(), err + case *net.IPNet: + v := f.AssignmentVar.(*net.IPNet) + return v.String(), err + case *net.TCPAddr: + v := f.AssignmentVar.(*net.TCPAddr) + return v.String(), err + case *net.UDPAddr: + v := f.AssignmentVar.(*net.UDPAddr) + return v.String(), err + case *os.FileMode: + v := f.AssignmentVar.(*os.FileMode) + return fmt.Sprintf("%#o", *v), err + case *regexp.Regexp: + v := f.AssignmentVar.(*regexp.Regexp) + return v.String(), err + case *time.Location: + v := f.AssignmentVar.(*time.Location) + return v.String(), err + case *time.Month: + v := f.AssignmentVar.(*time.Month) + if *v == 0 { + return "", err + } + return v.String(), err + case *time.Weekday: + v := f.AssignmentVar.(*time.Weekday) + return v.String(), err + case *big.Int: + v := f.AssignmentVar.(*big.Int) + return v.String(), err + case *big.Rat: + v := f.AssignmentVar.(*big.Rat) + return v.RatString(), err + case *Base64Bytes: + v := f.AssignmentVar.(*Base64Bytes) + if v == nil || len(*v) == 0 { + return "", err + } + return base64.StdEncoding.EncodeToString([]byte(*v)), err + case *netip.Addr: + v := f.AssignmentVar.(*netip.Addr) + return v.String(), err + case *netip.Prefix: + v := f.AssignmentVar.(*netip.Prefix) + return v.String(), err + case *netip.AddrPort: + v := f.AssignmentVar.(*netip.AddrPort) + return v.String(), err + default: + return "", errors.New("Unknown flag assignmentVar found in flag " + f.LongName + " " + f.ShortName + ". Type not supported: " + reflect.TypeOf(f.AssignmentVar).String()) + } } // helpers func isAllDigits(s string) bool { - if len(s) == 0 { - return false - } - for _, r := range s { - if r < '0' || r > '9' { - return false - } - } - return true + if len(s) == 0 { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true } func parseUTCOffset(s string) (int, bool) { - // Supports formats: +HH, -HH, +HHMM, -HHMM, +HH:MM, -HH:MM, Z - if s == "Z" || s == "z" || strings.EqualFold(s, "UTC") { - return 0, true - } - if len(s) < 2 { - return 0, false - } - sign := 1 - switch s[0] { - case '+': - sign = 1 - case '-': - sign = -1 - default: - return 0, false - } - rest := s[1:] - rest = strings.ReplaceAll(rest, ":", "") - if len(rest) != 2 && len(rest) != 4 { - return 0, false - } - hh, err := strconv.Atoi(rest[:2]) - if err != nil { - return 0, false - } - mm := 0 - if len(rest) == 4 { - mm, err = strconv.Atoi(rest[2:]) - if err != nil { - return 0, false - } - } - if hh < 0 || hh > 23 || mm < 0 || mm > 59 { - return 0, false - } - return sign * (hh*3600 + mm*60), true + // Supports formats: +HH, -HH, +HHMM, -HHMM, +HH:MM, -HH:MM, Z + if s == "Z" || s == "z" || strings.EqualFold(s, "UTC") { + return 0, true + } + if len(s) < 2 { + return 0, false + } + sign := 1 + switch s[0] { + case '+': + sign = 1 + case '-': + sign = -1 + default: + return 0, false + } + rest := s[1:] + rest = strings.ReplaceAll(rest, ":", "") + if len(rest) != 2 && len(rest) != 4 { + return 0, false + } + hh, err := strconv.Atoi(rest[:2]) + if err != nil { + return 0, false + } + mm := 0 + if len(rest) == 4 { + mm, err = strconv.Atoi(rest[2:]) + if err != nil { + return 0, false + } + } + if hh < 0 || hh > 23 || mm < 0 || mm > 59 { + return 0, false + } + return sign * (hh*3600 + mm*60), true } func offsetName(offset int) string { - if offset == 0 { - return "UTC" - } - sign := "+" - if offset < 0 { - sign = "-" - offset = -offset - } - hh := offset / 3600 - mm := (offset % 3600) / 60 - return fmt.Sprintf("UTC%s%02d:%02d", sign, hh, mm) + if offset == 0 { + return "UTC" + } + sign := "+" + if offset < 0 { + sign = "-" + offset = -offset + } + hh := offset / 3600 + mm := (offset % 3600) / 60 + return fmt.Sprintf("UTC%s%02d:%02d", sign, hh, mm) } func parseMonth(s string) (time.Month, bool) { - // Try name - names := map[string]time.Month{ - "january": time.January, "february": time.February, "march": time.March, "april": time.April, - "may": time.May, "june": time.June, "july": time.July, "august": time.August, - "september": time.September, "october": time.October, "november": time.November, "december": time.December, - } - if m, ok := names[strings.ToLower(s)]; ok { - return m, true - } - // Try number 1-12 - n, err := strconv.Atoi(s) - if err == nil && n >= 1 && n <= 12 { - return time.Month(n), true - } - return 0, false + // Try name + names := map[string]time.Month{ + "january": time.January, "february": time.February, "march": time.March, "april": time.April, + "may": time.May, "june": time.June, "july": time.July, "august": time.August, + "september": time.September, "october": time.October, "november": time.November, "december": time.December, + } + if m, ok := names[strings.ToLower(s)]; ok { + return m, true + } + // Try number 1-12 + n, err := strconv.Atoi(s) + if err == nil && n >= 1 && n <= 12 { + return time.Month(n), true + } + return 0, false } func parseWeekday(s string) (time.Weekday, bool) { - names := map[string]time.Weekday{ - "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, - "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, - } - if d, ok := names[strings.ToLower(s)]; ok { - return d, true - } - n, err := strconv.Atoi(s) - if err == nil { - // Accept 0-6 as Sunday-Saturday - if n >= 0 && n <= 6 { - return time.Weekday(n), true - } - // Also accept 1-7 as Monday-Sunday - if n >= 1 && n <= 7 { - v := (n % 7) // 7->0 - return time.Weekday(v), true - } - } - return 0, false + names := map[string]time.Weekday{ + "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, + "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, + } + if d, ok := names[strings.ToLower(s)]; ok { + return d, true + } + n, err := strconv.Atoi(s) + if err == nil { + // Accept 0-6 as Sunday-Saturday + if n >= 0 && n <= 6 { + return time.Weekday(n), true + } + // Also accept 1-7 as Monday-Sunday + if n >= 1 && n <= 7 { + v := (n % 7) // 7->0 + return time.Weekday(v), true + } + } + return 0, false } diff --git a/flag_test.go b/flag_test.go index ab303a0..6e7c7c6 100644 --- a/flag_test.go +++ b/flag_test.go @@ -1,15 +1,15 @@ package flaggy import ( - "fmt" - "math/big" - "net" - netip "net/netip" - "net/url" - "os" - "regexp" - "testing" - "time" + "fmt" + "math/big" + "net" + netip "net/netip" + "net/url" + "os" + "regexp" + "testing" + "time" ) // debugOff makes defers easier and turns off debug mode @@ -279,110 +279,110 @@ func TestInputParsing(t *testing.T) { inputArgs = append(inputArgs, "-m", "255.255.255.255") var maskFlagExpected = net.IPMask([]byte{255, 255, 255, 255}) - var maskSliceFlag []net.IPMask - IPMaskSlice(&maskSliceFlag, "ms", "mFlagSlice", "mask slice flag") - if err != nil { - t.Fatal(err) - } - inputArgs = append(inputArgs, "-ms", "255.255.255.255", "-ms", "255.255.255.0") - var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} - - // time.Time via unix seconds - var timeFlag time.Time - Time(&timeFlag, "ttm", "timeFlag", "time flag") - inputArgs = append(inputArgs, "-ttm", "1717171717") - var timeFlagExpected = time.Unix(1717171717, 0).UTC() - - // url.URL - var urlFlag url.URL - URL(&urlFlag, "urlf", "urlFlag", "url flag") - inputArgs = append(inputArgs, "-urlf", "https://example.com/x?y=1#z") - var urlFlagExpected = "https://example.com/x?y=1#z" - - // net.IPNet CIDR - var cidrFlag net.IPNet - IPNet(&cidrFlag, "cidrf", "cidrFlag", "cidr flag") - inputArgs = append(inputArgs, "-cidrf", "192.168.0.0/16") - var cidrFlagExpected = "192.168.0.0/16" - - // net.TCPAddr - var tcpAddr net.TCPAddr - TCPAddr(&tcpAddr, "tcpa", "tcpAddr", "tcp addr") - inputArgs = append(inputArgs, "-tcpa", "127.0.0.1:8080") - var tcpIPExpected = net.IPv4(127, 0, 0, 1) - var tcpPortExpected = 8080 - - // net.UDPAddr - var udpAddr net.UDPAddr - UDPAddr(&udpAddr, "udpa", "udpAddr", "udp addr") - inputArgs = append(inputArgs, "-udpa", "127.0.0.1:5353") - var udpIPExpected = net.IPv4(127, 0, 0, 1) - var udpPortExpected = 5353 - - // os.FileMode - var fileModeFlag os.FileMode - FileMode(&fileModeFlag, "fmode", "fileMode", "file mode flag") - inputArgs = append(inputArgs, "-fmode", "0755") - var fileModeExpected os.FileMode = 0o755 - - // regexp.Regexp - var regexFlag regexp.Regexp - Regexp(®exFlag, "re", "regexp", "regex flag") - inputArgs = append(inputArgs, "-re", "^ab+$") - - // time.Location - var locFlag time.Location - Location(&locFlag, "tz", "timezone", "timezone flag") - inputArgs = append(inputArgs, "-tz", "+02:00") - var locFlagExpected = "UTC+02:00" - - // time.Month - var monthFlag time.Month - Month(&monthFlag, "mon", "month", "month flag") - inputArgs = append(inputArgs, "-mon", "February") - var monthFlagExpected = time.February - - // time.Weekday - var weekdayFlag time.Weekday - Weekday(&weekdayFlag, "wday", "weekday", "weekday flag") - inputArgs = append(inputArgs, "-wday", "Tuesday") - var weekdayFlagExpected = time.Tuesday - - // big.Int - var bigIntFlag big.Int - BigInt(&bigIntFlag, "bigi", "bigint", "big int flag") - inputArgs = append(inputArgs, "-bigi", "0xFF") - var bigIntExpected = big.NewInt(255) - - // big.Rat - var bigRatFlag big.Rat - BigRat(&bigRatFlag, "bigr", "bigrat", "big rat flag") - inputArgs = append(inputArgs, "-bigr", "1/8") - var bigRatExpected = big.NewRat(1, 8) - - // netip.Addr - var netipAddrFlag netip.Addr - NetipAddr(&netipAddrFlag, "nip", "netipaddr", "netip addr flag") - inputArgs = append(inputArgs, "-nip", "192.0.2.1") - var netipAddrExpected = netip.MustParseAddr("192.0.2.1") - - // netip.Prefix - var netipPrefixFlag netip.Prefix - NetipPrefix(&netipPrefixFlag, "nipr", "netipprefix", "netip prefix flag") - inputArgs = append(inputArgs, "-nipr", "2001:db8::/32") - var netipPrefixExpected = netip.MustParsePrefix("2001:db8::/32") - - // netip.AddrPort - var netipAddrPortFlag netip.AddrPort - NetipAddrPort(&netipAddrPortFlag, "niap", "netipaddrport", "netip addrport flag") - inputArgs = append(inputArgs, "-niap", "127.0.0.1:80") - var netipAddrPortExpected = netip.MustParseAddrPort("127.0.0.1:80") - - // Base64 bytes - var b64Flag Base64Bytes - BytesBase64(&b64Flag, "b64", "base64", "base64 bytes flag") - inputArgs = append(inputArgs, "-b64", "SGVsbG8=") - var b64Expected = []byte("Hello") + var maskSliceFlag []net.IPMask + IPMaskSlice(&maskSliceFlag, "ms", "mFlagSlice", "mask slice flag") + if err != nil { + t.Fatal(err) + } + inputArgs = append(inputArgs, "-ms", "255.255.255.255", "-ms", "255.255.255.0") + var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} + + // time.Time via unix seconds + var timeFlag time.Time + Time(&timeFlag, "ttm", "timeFlag", "time flag") + inputArgs = append(inputArgs, "-ttm", "1717171717") + var timeFlagExpected = time.Unix(1717171717, 0).UTC() + + // url.URL + var urlFlag url.URL + URL(&urlFlag, "urlf", "urlFlag", "url flag") + inputArgs = append(inputArgs, "-urlf", "https://example.com/x?y=1#z") + var urlFlagExpected = "https://example.com/x?y=1#z" + + // net.IPNet CIDR + var cidrFlag net.IPNet + IPNet(&cidrFlag, "cidrf", "cidrFlag", "cidr flag") + inputArgs = append(inputArgs, "-cidrf", "192.168.0.0/16") + var cidrFlagExpected = "192.168.0.0/16" + + // net.TCPAddr + var tcpAddr net.TCPAddr + TCPAddr(&tcpAddr, "tcpa", "tcpAddr", "tcp addr") + inputArgs = append(inputArgs, "-tcpa", "127.0.0.1:8080") + var tcpIPExpected = net.IPv4(127, 0, 0, 1) + var tcpPortExpected = 8080 + + // net.UDPAddr + var udpAddr net.UDPAddr + UDPAddr(&udpAddr, "udpa", "udpAddr", "udp addr") + inputArgs = append(inputArgs, "-udpa", "127.0.0.1:5353") + var udpIPExpected = net.IPv4(127, 0, 0, 1) + var udpPortExpected = 5353 + + // os.FileMode + var fileModeFlag os.FileMode + FileMode(&fileModeFlag, "fmode", "fileMode", "file mode flag") + inputArgs = append(inputArgs, "-fmode", "0755") + var fileModeExpected os.FileMode = 0o755 + + // regexp.Regexp + var regexFlag regexp.Regexp + Regexp(®exFlag, "re", "regexp", "regex flag") + inputArgs = append(inputArgs, "-re", "^ab+$") + + // time.Location + var locFlag time.Location + Location(&locFlag, "tz", "timezone", "timezone flag") + inputArgs = append(inputArgs, "-tz", "+02:00") + var locFlagExpected = "UTC+02:00" + + // time.Month + var monthFlag time.Month + Month(&monthFlag, "mon", "month", "month flag") + inputArgs = append(inputArgs, "-mon", "February") + var monthFlagExpected = time.February + + // time.Weekday + var weekdayFlag time.Weekday + Weekday(&weekdayFlag, "wday", "weekday", "weekday flag") + inputArgs = append(inputArgs, "-wday", "Tuesday") + var weekdayFlagExpected = time.Tuesday + + // big.Int + var bigIntFlag big.Int + BigInt(&bigIntFlag, "bigi", "bigint", "big int flag") + inputArgs = append(inputArgs, "-bigi", "0xFF") + var bigIntExpected = big.NewInt(255) + + // big.Rat + var bigRatFlag big.Rat + BigRat(&bigRatFlag, "bigr", "bigrat", "big rat flag") + inputArgs = append(inputArgs, "-bigr", "1/8") + var bigRatExpected = big.NewRat(1, 8) + + // netip.Addr + var netipAddrFlag netip.Addr + NetipAddr(&netipAddrFlag, "nip", "netipaddr", "netip addr flag") + inputArgs = append(inputArgs, "-nip", "192.0.2.1") + var netipAddrExpected = netip.MustParseAddr("192.0.2.1") + + // netip.Prefix + var netipPrefixFlag netip.Prefix + NetipPrefix(&netipPrefixFlag, "nipr", "netipprefix", "netip prefix flag") + inputArgs = append(inputArgs, "-nipr", "2001:db8::/32") + var netipPrefixExpected = netip.MustParsePrefix("2001:db8::/32") + + // netip.AddrPort + var netipAddrPortFlag netip.AddrPort + NetipAddrPort(&netipAddrPortFlag, "niap", "netipaddrport", "netip addrport flag") + inputArgs = append(inputArgs, "-niap", "127.0.0.1:80") + var netipAddrPortExpected = netip.MustParseAddrPort("127.0.0.1:80") + + // Base64 bytes + var b64Flag Base64Bytes + BytesBase64(&b64Flag, "b64", "base64", "base64 bytes flag") + inputArgs = append(inputArgs, "-b64", "SGVsbG8=") + var b64Expected = []byte("Hello") // display help with all flags used ShowHelp("Showing help for test: " + t.Name()) @@ -567,93 +567,93 @@ func TestInputParsing(t *testing.T) { } } - if maskFlag.String() != maskFlagExpected.String() { - t.Fatal("mask flag incorrect", maskFlag, maskFlagExpected) - } - - for i, f := range maskSliceFlagExpected { - if f.String() != maskSliceFlag[i].String() { - t.Fatal("mask flag slice value incorrect", maskSliceFlag[i].String(), f.String()) - } - } - - // time.Time - if !timeFlag.Equal(timeFlagExpected) { - t.Fatal("time flag incorrect", timeFlag, timeFlagExpected) - } - - // url.URL - if urlFlag.String() != urlFlagExpected { - t.Fatal("url flag incorrect", urlFlag.String(), urlFlagExpected) - } - - // CIDR - if cidrFlag.String() != cidrFlagExpected { - t.Fatal("cidr flag incorrect", cidrFlag.String(), cidrFlagExpected) - } - - // TCP addr - if !tcpAddr.IP.Equal(tcpIPExpected) || tcpAddr.Port != tcpPortExpected { - t.Fatal("tcp addr incorrect", tcpAddr, tcpIPExpected, tcpPortExpected) - } - - // UDP addr - if !udpAddr.IP.Equal(udpIPExpected) || udpAddr.Port != udpPortExpected { - t.Fatal("udp addr incorrect", udpAddr, udpIPExpected, udpPortExpected) - } - - // File mode - if fileModeFlag != fileModeExpected { - t.Fatal("file mode incorrect", fileModeFlag, fileModeExpected) - } - - // Regexp - if !regexFlag.MatchString("abbb") || regexFlag.MatchString("ac") { - t.Fatal("regexp flag incorrect") - } - - // Location - if locFlag.String() != locFlagExpected { - t.Fatal("location flag incorrect", locFlag.String(), locFlagExpected) - } - - // Month - if monthFlag != monthFlagExpected { - t.Fatal("month flag incorrect", monthFlag, monthFlagExpected) - } - - // Weekday - if weekdayFlag != weekdayFlagExpected { - t.Fatal("weekday flag incorrect", weekdayFlag, weekdayFlagExpected) - } - - // big.Int - if bigIntFlag.Cmp(bigIntExpected) != 0 { - t.Fatal("bigint flag incorrect", bigIntFlag.String(), bigIntExpected.String()) - } - - // big.Rat - if bigRatFlag.Cmp(bigRatExpected) != 0 { - t.Fatal("bigrat flag incorrect", bigRatFlag.RatString(), bigRatExpected.RatString()) - } - - // netip.Addr - if netipAddrFlag != netipAddrExpected { - t.Fatal("netip addr incorrect", netipAddrFlag.String(), netipAddrExpected.String()) - } - - // netip.Prefix - if netipPrefixFlag.String() != netipPrefixExpected.String() { - t.Fatal("netip prefix incorrect", netipPrefixFlag.String(), netipPrefixExpected.String()) - } - - // netip.AddrPort - if netipAddrPortFlag.String() != netipAddrPortExpected.String() { - t.Fatal("netip addrport incorrect", netipAddrPortFlag.String(), netipAddrPortExpected.String()) - } - - // Base64 bytes - if string([]byte(b64Flag)) != string(b64Expected) { - t.Fatal("base64 bytes flag incorrect", []byte(b64Flag), b64Expected) - } + if maskFlag.String() != maskFlagExpected.String() { + t.Fatal("mask flag incorrect", maskFlag, maskFlagExpected) + } + + for i, f := range maskSliceFlagExpected { + if f.String() != maskSliceFlag[i].String() { + t.Fatal("mask flag slice value incorrect", maskSliceFlag[i].String(), f.String()) + } + } + + // time.Time + if !timeFlag.Equal(timeFlagExpected) { + t.Fatal("time flag incorrect", timeFlag, timeFlagExpected) + } + + // url.URL + if urlFlag.String() != urlFlagExpected { + t.Fatal("url flag incorrect", urlFlag.String(), urlFlagExpected) + } + + // CIDR + if cidrFlag.String() != cidrFlagExpected { + t.Fatal("cidr flag incorrect", cidrFlag.String(), cidrFlagExpected) + } + + // TCP addr + if !tcpAddr.IP.Equal(tcpIPExpected) || tcpAddr.Port != tcpPortExpected { + t.Fatal("tcp addr incorrect", tcpAddr, tcpIPExpected, tcpPortExpected) + } + + // UDP addr + if !udpAddr.IP.Equal(udpIPExpected) || udpAddr.Port != udpPortExpected { + t.Fatal("udp addr incorrect", udpAddr, udpIPExpected, udpPortExpected) + } + + // File mode + if fileModeFlag != fileModeExpected { + t.Fatal("file mode incorrect", fileModeFlag, fileModeExpected) + } + + // Regexp + if !regexFlag.MatchString("abbb") || regexFlag.MatchString("ac") { + t.Fatal("regexp flag incorrect") + } + + // Location + if locFlag.String() != locFlagExpected { + t.Fatal("location flag incorrect", locFlag.String(), locFlagExpected) + } + + // Month + if monthFlag != monthFlagExpected { + t.Fatal("month flag incorrect", monthFlag, monthFlagExpected) + } + + // Weekday + if weekdayFlag != weekdayFlagExpected { + t.Fatal("weekday flag incorrect", weekdayFlag, weekdayFlagExpected) + } + + // big.Int + if bigIntFlag.Cmp(bigIntExpected) != 0 { + t.Fatal("bigint flag incorrect", bigIntFlag.String(), bigIntExpected.String()) + } + + // big.Rat + if bigRatFlag.Cmp(bigRatExpected) != 0 { + t.Fatal("bigrat flag incorrect", bigRatFlag.RatString(), bigRatExpected.RatString()) + } + + // netip.Addr + if netipAddrFlag != netipAddrExpected { + t.Fatal("netip addr incorrect", netipAddrFlag.String(), netipAddrExpected.String()) + } + + // netip.Prefix + if netipPrefixFlag.String() != netipPrefixExpected.String() { + t.Fatal("netip prefix incorrect", netipPrefixFlag.String(), netipPrefixExpected.String()) + } + + // netip.AddrPort + if netipAddrPortFlag.String() != netipAddrPortExpected.String() { + t.Fatal("netip addrport incorrect", netipAddrPortFlag.String(), netipAddrPortExpected.String()) + } + + // Base64 bytes + if string([]byte(b64Flag)) != string(b64Expected) { + t.Fatal("base64 bytes flag incorrect", []byte(b64Flag), b64Expected) + } } diff --git a/flaggy.go b/flaggy.go index b999d82..72ddd49 100644 --- a/flaggy.go +++ b/flaggy.go @@ -7,17 +7,17 @@ package flaggy // import "github.com/integrii/flaggy" import ( - "fmt" - "log" - "math/big" - "net" - netip "net/netip" - "net/url" - "os" - "regexp" - "strconv" - "strings" - "time" + "fmt" + "log" + "math/big" + "net" + netip "net/netip" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" ) // strings used for builtin help and version flags both short and long @@ -53,24 +53,24 @@ func init() { // ResetParser resets the default parser to a fresh instance. Uses the // name of the binary executing as the program name by default. func ResetParser() { - if len(os.Args) > 0 { - chunks := strings.Split(os.Args[0], "/") - DefaultParser = NewParser(chunks[len(chunks)-1]) - } else { - DefaultParser = NewParser("default") - } + if len(os.Args) > 0 { + chunks := strings.Split(os.Args[0], "/") + DefaultParser = NewParser(chunks[len(chunks)-1]) + return + } + DefaultParser = NewParser("default") } // SortFlagsByLongName enables alphabetical sorting of flags by long name // in help output on the default parser. func SortFlagsByLongName() { - DefaultParser.SortFlagsByLongName() + DefaultParser.SortFlagsByLongName() } // SortFlagsByLongNameReversed enables reverse alphabetical sorting of flags // by long name in help output on the default parser. func SortFlagsByLongNameReversed() { - DefaultParser.SortFlagsByLongNameReversed() + DefaultParser.SortFlagsByLongNameReversed() } // Parse parses flags as requested in the default package parser. All trailing arguments @@ -119,19 +119,19 @@ func BoolSlice(assignmentVar *[]bool, shortName string, longName string, descrip // ByteSlice adds a new slice of bytes flag // Specify the flag multiple times to fill the slice. Takes hex as input. func ByteSlice(assignmentVar *[]byte, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // BytesBase64 adds a new []byte flag parsed from base64 input. func BytesBase64(assignmentVar *Base64Bytes, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Duration adds a new time.Duration flag. // Input format is described in time.ParseDuration(). // Example values: 1h, 1h50m, 32s func Duration(assignmentVar *time.Duration, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // DurationSlice adds a new time.Duration flag. @@ -216,7 +216,7 @@ func UInt16(assignmentVar *uint16, shortName string, longName string, descriptio // UInt16Slice adds a new uint16 slice flag. // Specify the flag multiple times to fill the slice. func UInt16Slice(assignmentVar *[]uint16, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // UInt8 adds a new uint8 flag @@ -304,82 +304,82 @@ func IPMask(assignmentVar *net.IPMask, shortName string, longName string, descri // IPMaskSlice adds a new net.HardwareAddr slice flag. IPv4 only. // Specify the flag multiple times to fill the slice. func IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Time adds a new time.Time flag. Supports RFC3339/RFC3339Nano, RFC1123, and unix seconds. func Time(assignmentVar *time.Time, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // URL adds a new url.URL flag. func URL(assignmentVar *url.URL, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // IPNet adds a new net.IPNet flag parsed from CIDR. func IPNet(assignmentVar *net.IPNet, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // TCPAddr adds a new net.TCPAddr flag parsed from host:port. func TCPAddr(assignmentVar *net.TCPAddr, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // UDPAddr adds a new net.UDPAddr flag parsed from host:port. func UDPAddr(assignmentVar *net.UDPAddr, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // FileMode adds a new os.FileMode flag parsed from octal/decimal (base auto-detected). func FileMode(assignmentVar *os.FileMode, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Regexp adds a new regexp.Regexp flag. func Regexp(assignmentVar *regexp.Regexp, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Location adds a new time.Location flag. func Location(assignmentVar *time.Location, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Month adds a new time.Month flag. func Month(assignmentVar *time.Month, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // Weekday adds a new time.Weekday flag. func Weekday(assignmentVar *time.Weekday, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // BigInt adds a new big.Int flag. func BigInt(assignmentVar *big.Int, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // BigRat adds a new big.Rat flag. func BigRat(assignmentVar *big.Rat, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // NetipAddr adds a new netip.Addr flag. func NetipAddr(assignmentVar *netip.Addr, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // NetipPrefix adds a new netip.Prefix flag. func NetipPrefix(assignmentVar *netip.Prefix, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // NetipAddrPort adds a new netip.AddrPort flag. func NetipAddrPort(assignmentVar *netip.AddrPort, shortName string, longName string, description string) { - DefaultParser.add(assignmentVar, shortName, longName, description) + DefaultParser.add(assignmentVar, shortName, longName, description) } // AttachSubcommand adds a subcommand for parsing diff --git a/help_format_dump_test.go b/help_format_dump_test.go index 96d18f8..14e9177 100644 --- a/help_format_dump_test.go +++ b/help_format_dump_test.go @@ -1,61 +1,61 @@ package flaggy_test import ( - "os" - "testing" - "time" + "os" + "testing" + "time" - "github.com/integrii/flaggy" + "github.com/integrii/flaggy" ) // TestPrintHelpToStdout builds a representative parser and prints the current // help output to stdout for manual inspection. Run with: -// go test -run TestPrintHelpToStdout -v +// +// go test -run TestPrintHelpToStdout -v func TestPrintHelpToStdout(t *testing.T) { - p := flaggy.NewParser("help-dump") - p.Description = "Sample command showing current help formatting." - - // Set up a couple of subcommands - scA := flaggy.NewSubcommand("subA") - scA.ShortName = "a" - scA.Description = "Subcommand A description." - - scB := flaggy.NewSubcommand("subB") - scB.ShortName = "b" - scB.Description = "Subcommand B description." - - p.AttachSubcommand(scA, 1) - scA.AttachSubcommand(scB, 1) - - // Add a positional to demonstrate the section - var posA = "defaultPosA" - scA.AddPositionalValue(&posA, "posA", 2, false, "Example positional for A.") - - // Add a few flags of different types - var s string = "defaultStringHere" - var i int - var b bool - var d time.Duration - p.String(&s, "s", "stringFlag", "Example string flag.") - p.Int(&i, "i", "intFlag", "Example int flag.") - p.Bool(&b, "b", "boolFlag", "Example bool flag.") - p.Duration(&d, "d", "durationFlag", "Example duration flag.") - - // Optional extra help lines to show placement in template - p.AdditionalHelpPrepend = "This is a prepend for help" - p.AdditionalHelpAppend = "This is an append for help" - - // Parse to set subcommand context to scB - if err := p.ParseArgs([]string{"subA", "subB"}); err != nil { - t.Fatalf("parse: unexpected error: %v", err) - } - - // Redirect help output from stderr to stdout for visibility under `go test -v`. - savedStderr := os.Stderr - os.Stderr = os.Stdout - defer func() { os.Stderr = savedStderr }() - - // Print current help to stdout - p.ShowHelpWithMessage("This is a help message on exit") + p := flaggy.NewParser("help-dump") + p.Description = "Sample command showing current help formatting." + + // Set up a couple of subcommands + scA := flaggy.NewSubcommand("subA") + scA.ShortName = "a" + scA.Description = "Subcommand A description." + + scB := flaggy.NewSubcommand("subB") + scB.ShortName = "b" + scB.Description = "Subcommand B description." + + p.AttachSubcommand(scA, 1) + scA.AttachSubcommand(scB, 1) + + // Add a positional to demonstrate the section + var posA = "defaultPosA" + scA.AddPositionalValue(&posA, "posA", 2, false, "Example positional for A.") + + // Add a few flags of different types + var s string = "defaultStringHere" + var i int + var b bool + var d time.Duration + p.String(&s, "s", "stringFlag", "Example string flag.") + p.Int(&i, "i", "intFlag", "Example int flag.") + p.Bool(&b, "b", "boolFlag", "Example bool flag.") + p.Duration(&d, "d", "durationFlag", "Example duration flag.") + + // Optional extra help lines to show placement in template + p.AdditionalHelpPrepend = "This is a prepend for help" + p.AdditionalHelpAppend = "This is an append for help" + + // Parse to set subcommand context to scB + if err := p.ParseArgs([]string{"subA", "subB"}); err != nil { + t.Fatalf("parse: unexpected error: %v", err) + } + + // Redirect help output from stderr to stdout for visibility under `go test -v`. + savedStderr := os.Stderr + os.Stderr = os.Stdout + defer func() { os.Stderr = savedStderr }() + + // Print current help to stdout + p.ShowHelpWithMessage("This is a help message on exit") } - diff --git a/help_sort_test.go b/help_sort_test.go index 7a8d944..148f939 100644 --- a/help_sort_test.go +++ b/help_sort_test.go @@ -1,115 +1,145 @@ package flaggy import ( - "os" - "strings" - "testing" + "os" + "strings" + "testing" ) func TestHelpFlagsSortedWhenEnabled(t *testing.T) { - // Use default parser and functions to enable sort - ResetParser() - DefaultParser.ShowHelpWithHFlag = false - DefaultParser.ShowVersionWithVersionFlag = false - SortFlagsByLongName() + // Use default parser and functions to enable sort + ResetParser() + DefaultParser.ShowHelpWithHFlag = false + DefaultParser.ShowVersionWithVersionFlag = false + SortFlagsByLongName() - var a, b, z string - // Intentionally add in non-sorted order - String(&z, "z", "zeta", "") - String(&a, "a", "alpha", "") - String(&b, "b", "beta", "") + var a, b, z string + // Intentionally add in non-sorted order + String(&z, "z", "zeta", "") + String(&a, "a", "alpha", "") + String(&b, "b", "beta", "") - rd, wr, err := os.Pipe() - if err != nil { t.Fatalf("pipe error: %v", err) } - saved := os.Stderr - os.Stderr = wr - defer func(){ os.Stderr = saved }() + rd, wr, err := os.Pipe() + if err != nil { + t.Fatalf("pipe error: %v", err) + } + saved := os.Stderr + os.Stderr = wr + defer func() { os.Stderr = saved }() - DefaultParser.ShowHelp() + DefaultParser.ShowHelp() - buf := make([]byte, 4096) - n, err := rd.Read(buf) - if err != nil { t.Fatalf("read error: %v", err) } - lines := strings.Split(string(buf[:n]), "\n") + buf := make([]byte, 4096) + n, err := rd.Read(buf) + if err != nil { + t.Fatalf("read error: %v", err) + } + lines := strings.Split(string(buf[:n]), "\n") - // collect just the flag lines (start with two spaces then a dash or spaces then --) - var flagLines []string - inFlags := false - for _, l := range lines { - if strings.HasPrefix(l, " Flags:") { inFlags = true; continue } - if inFlags { - if strings.TrimSpace(l) == "" { break } - flagLines = append(flagLines, l) - } - } - if len(flagLines) < 3 { - t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) - } + // collect just the flag lines (start with two spaces then a dash or spaces then --) + var flagLines []string + inFlags := false + for _, l := range lines { + if strings.HasPrefix(l, " Flags:") { + inFlags = true + continue + } + if inFlags { + if strings.TrimSpace(l) == "" { + break + } + flagLines = append(flagLines, l) + } + } + if len(flagLines) < 3 { + t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) + } - // find the three of interest - var idxAlpha, idxBeta, idxZeta = -1, -1, -1 - for i, l := range flagLines { - if strings.Contains(l, "--alpha") { idxAlpha = i } - if strings.Contains(l, "--beta") { idxBeta = i } - if strings.Contains(l, "--zeta") { idxZeta = i } - } - if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { - t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) - } - if !(idxAlpha < idxBeta && idxBeta < idxZeta) { - t.Fatalf("flags not sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) - } + // find the three of interest + var idxAlpha, idxBeta, idxZeta = -1, -1, -1 + for i, l := range flagLines { + if strings.Contains(l, "--alpha") { + idxAlpha = i + } + if strings.Contains(l, "--beta") { + idxBeta = i + } + if strings.Contains(l, "--zeta") { + idxZeta = i + } + } + if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { + t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) + } + if !(idxAlpha < idxBeta && idxBeta < idxZeta) { + t.Fatalf("flags not sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) + } } func TestHelpFlagsSortedReversed(t *testing.T) { - // Use default parser and reversed sort - ResetParser() - DefaultParser.ShowHelpWithHFlag = false - DefaultParser.ShowVersionWithVersionFlag = false - SortFlagsByLongNameReversed() + // Use default parser and reversed sort + ResetParser() + DefaultParser.ShowHelpWithHFlag = false + DefaultParser.ShowVersionWithVersionFlag = false + SortFlagsByLongNameReversed() - var a, b, z string - // Intentionally add in non-sorted order - String(&z, "z", "zeta", "") - String(&a, "a", "alpha", "") - String(&b, "b", "beta", "") + var a, b, z string + // Intentionally add in non-sorted order + String(&z, "z", "zeta", "") + String(&a, "a", "alpha", "") + String(&b, "b", "beta", "") - rd, wr, err := os.Pipe() - if err != nil { t.Fatalf("pipe error: %v", err) } - saved := os.Stderr - os.Stderr = wr - defer func(){ os.Stderr = saved }() + rd, wr, err := os.Pipe() + if err != nil { + t.Fatalf("pipe error: %v", err) + } + saved := os.Stderr + os.Stderr = wr + defer func() { os.Stderr = saved }() - DefaultParser.ShowHelp() + DefaultParser.ShowHelp() - buf := make([]byte, 4096) - n, err := rd.Read(buf) - if err != nil { t.Fatalf("read error: %v", err) } - lines := strings.Split(string(buf[:n]), "\n") + buf := make([]byte, 4096) + n, err := rd.Read(buf) + if err != nil { + t.Fatalf("read error: %v", err) + } + lines := strings.Split(string(buf[:n]), "\n") - var flagLines []string - inFlags := false - for _, l := range lines { - if strings.HasPrefix(l, " Flags:") { inFlags = true; continue } - if inFlags { - if strings.TrimSpace(l) == "" { break } - flagLines = append(flagLines, l) - } - } - if len(flagLines) < 3 { - t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) - } + var flagLines []string + inFlags := false + for _, l := range lines { + if strings.HasPrefix(l, " Flags:") { + inFlags = true + continue + } + if inFlags { + if strings.TrimSpace(l) == "" { + break + } + flagLines = append(flagLines, l) + } + } + if len(flagLines) < 3 { + t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) + } - var idxAlpha, idxBeta, idxZeta = -1, -1, -1 - for i, l := range flagLines { - if strings.Contains(l, "--alpha") { idxAlpha = i } - if strings.Contains(l, "--beta") { idxBeta = i } - if strings.Contains(l, "--zeta") { idxZeta = i } - } - if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { - t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) - } - if !(idxZeta < idxBeta && idxBeta < idxAlpha) { - t.Fatalf("flags not reverse-sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) - } + var idxAlpha, idxBeta, idxZeta = -1, -1, -1 + for i, l := range flagLines { + if strings.Contains(l, "--alpha") { + idxAlpha = i + } + if strings.Contains(l, "--beta") { + idxBeta = i + } + if strings.Contains(l, "--zeta") { + idxZeta = i + } + } + if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { + t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) + } + if !(idxZeta < idxBeta && idxBeta < idxAlpha) { + t.Fatalf("flags not reverse-sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) + } } diff --git a/parsedValue.go b/parsedValue.go index e3a8455..a6cf024 100644 --- a/parsedValue.go +++ b/parsedValue.go @@ -7,11 +7,12 @@ type parsedValue struct { Key string Value string IsPositional bool // indicates that this value was positional and not a key/value + ConsumesNext bool // indicates that parsing this value consumed the following CLI token } // newParsedValue creates and returns a new parsedValue struct with the // supplied values set -func newParsedValue(key string, value string, isPositional bool) parsedValue { +func newParsedValue(key string, value string, isPositional bool, consumesNext bool) parsedValue { if len(key) == 0 && len(value) == 0 { panic("can't add parsed value with no key or value") } @@ -19,5 +20,6 @@ func newParsedValue(key string, value string, isPositional bool) parsedValue { Key: key, Value: value, IsPositional: isPositional, + ConsumesNext: consumesNext, } } diff --git a/parser.go b/parser.go index bfa3958..9b30d64 100644 --- a/parser.go +++ b/parser.go @@ -115,7 +115,7 @@ func (p *Parser) ParseArgs(args []string) error { } debugPrint("Kicking off parsing with args:", args) - err := p.parse(p, args, 0) + err := p.parse(p, args) if err != nil { return err } @@ -162,7 +162,9 @@ func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []stri var argsNotUsed []string var skipNext bool - for _, a := range args { + + for i := 0; i < len(args); i++ { + a := args[i] // if the final argument (--) is seen, then we stop checking because all // further values are trailing arguments. @@ -177,7 +179,6 @@ func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []stri } // Determine token type and normalized key/value - argType := determineArgType(a) arg := parseFlagToName(a) isFlagToken := strings.HasPrefix(a, "-") @@ -200,16 +201,21 @@ func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []stri if !pv.IsPositional && pv.Key == arg { debugPrint("Found matching parsed flag for " + pv.Key) foundArgUsed = true - if argType == argIsFlagWithSpace && len(pv.Value) > 0 { + if pv.ConsumesNext { + skipNext = true + } else if i+1 < len(args) && pv.Value == args[i+1] { skipNext = true } break } } + if foundArgUsed { + continue + } } // For non-flag tokens, prefer positional matches first. - if !isFlagToken && !foundArgUsed { + if !isFlagToken { for _, pv := range parsedValues { debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") if pv.IsPositional && pv.Value == arg { @@ -218,21 +224,27 @@ func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []stri break } } - } + if foundArgUsed { + continue + } - // Fallback for non-flag tokens: allow matching a non-positional flag by bare name. - if !isFlagToken && !foundArgUsed { + // Fallback for non-flag tokens: allow matching a non-positional flag by bare name. for _, pv := range parsedValues { debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") if !pv.IsPositional && pv.Key == arg { debugPrint("Found matching parsed flag for " + pv.Key) foundArgUsed = true - if len(pv.Value) > 0 { + if pv.ConsumesNext { + skipNext = true + } else if i+1 < len(args) && pv.Value == args[i+1] { skipNext = true } break } } + if foundArgUsed { + continue + } } // if the arg was not used in any parsed values, then we add it to the slice diff --git a/parser_refactor_regression_test.go b/parser_refactor_regression_test.go new file mode 100644 index 0000000..daa9311 --- /dev/null +++ b/parser_refactor_regression_test.go @@ -0,0 +1,215 @@ +package flaggy + +import ( + "strings" + "testing" +) + +func TestNestedSubcommandsParseLocalFlags(t *testing.T) { + // Command: ./app subcommandA subcommandB -flagA 5 + // Expect: subcommandA and subcommandB used; flagA parsed as int 5 on subcommandB. + t.Parallel() + + // Setup root parser and nested subcommands to mirror the CLI hierarchy. + p := NewParser("app") + subA := NewSubcommand("subcommandA") + subB := NewSubcommand("subcommandB") + p.AttachSubcommand(subA, 1) + subA.AttachSubcommand(subB, 1) + + // Define flagA on subcommandB with storage for the parsed integer. + var flagA int + subB.Int(&flagA, "", "flagA", "int flag scoped to subcommandB") + + // Parse the CLI input exactly as described in the command comment. + if err := p.ParseArgs([]string{"subcommandA", "subcommandB", "-flagA", "5"}); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Assert both subcommands are marked as used and the flag value is correct. + if !subA.Used || !subB.Used { + t.Fatalf("expected subcommands to be marked used: subA=%v subB=%v", subA.Used, subB.Used) + } + if flagA != 5 { + t.Fatalf("expected flagA to be 5, got %d", flagA) + } +} + +func TestRootAndChildFlagsAreIsolated(t *testing.T) { + // Command: ./app -flagA=5 --flagB 5 subcommandA --flagC hello + // Expect: root flagA=5, root flagB=5, subcommandA used with flagC="hello". + t.Parallel() + + // Setup root parser with a single subcommand to host child-scoped flags. + p := NewParser("app") + subA := NewSubcommand("subcommandA") + p.AttachSubcommand(subA, 1) + + // Define storage locations for the root and child flag values. + var flagA int + var flagB int + var flagC string + + // Register two root flags and one child flag matching the command contract. + p.Int(&flagA, "", "flagA", "root int flag") + p.Int(&flagB, "", "flagB", "second root int flag") + subA.String(&flagC, "", "flagC", "child string flag") + + // Parse the CLI input exactly as described in the command comment. + args := []string{"-flagA=5", "--flagB", "5", "subcommandA", "--flagC", "hello"} + if err := p.ParseArgs(args); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Verify root flags resolve correctly and the child flag receives "hello". + if flagA != 5 { + t.Fatalf("expected flagA to be 5, got %d", flagA) + } + if flagB != 5 { + t.Fatalf("expected flagB to be 5, got %d", flagB) + } + if flagC != "hello" { + t.Fatalf("expected flagC to be hello, got %q", flagC) + } + // Confirm the subcommand was invoked during parsing. + if !subA.Used { + t.Fatalf("expected subcommandA to be used") + } +} + +func TestFlagNameCollisionWithSubcommand(t *testing.T) { + // Command: ./app -flagA=test flagA + // Expect: root flagA string set to "test" while flagA subcommand is used. + t.Parallel() + + // Setup root parser with a subcommand whose name collides with a root flag. + p := NewParser("app") + subFlagA := NewSubcommand("flagA") + p.AttachSubcommand(subFlagA, 1) + + // Register the root-level string flag sharing the subcommand's name. + var rootFlag string + p.String(&rootFlag, "", "flagA", "root string flag") + + // Parse the CLI input exactly as described in the command comment. + if err := p.ParseArgs([]string{"-flagA=test", "flagA"}); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Validate both the root flag value and the subcommand usage status. + if rootFlag != "test" { + t.Fatalf("expected root flag to be \"test\", got %q", rootFlag) + } + if !subFlagA.Used { + t.Fatalf("expected subcommand flagA to be used") + } +} + +func TestBlankAndWhitespaceValues(t *testing.T) { + // Command: ./app -flagA "" subcommandA -a "" -b " " -c XYZ + // Expect: root flagA blank, -a blank, -b single space, -c "XYZ", subcommandA used. + t.Parallel() + + // Setup root parser with subcommandA to hold scoped string flags. + p := NewParser("app") + subA := NewSubcommand("subcommandA") + p.AttachSubcommand(subA, 1) + + // Define storage for root and subcommand flag values. + var rootFlag string + var flagA string + var flagB string + var flagC string + + // Register the root flag and subcommand flags matching the CLI usage. + p.String(&rootFlag, "", "flagA", "root string flag") + subA.String(&flagA, "a", "", "blank string flag") + subA.String(&flagB, "b", "", "single space flag") + subA.String(&flagC, "c", "", "non blank flag") + + // Parse the CLI input exactly as described in the command comment. + args := []string{"-flagA", "", "subcommandA", "-a", "", "-b", " ", "-c", "XYZ"} + if err := p.ParseArgs(args); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Assert blank and whitespace values were preserved for each flag. + if rootFlag != "" { + t.Fatalf("expected root flagA to be blank, got %q", rootFlag) + } + if flagA != "" { + t.Fatalf("expected -a flag to be blank, got %q", flagA) + } + if flagB != " " { + t.Fatalf("expected -b flag to be a single space, got %q", flagB) + } + if flagC != "XYZ" { + t.Fatalf("expected -c flag to be XYZ, got %q", flagC) + } + // Confirm the subcommand was invoked during parsing. + if !subA.Used { + t.Fatalf("expected subcommandA to be used") + } +} + +func TestRootBoolAfterSubcommand(t *testing.T) { + // Command: ./app subcommandA --output + // Expect: subcommandA used; root --output bool flag set to true. + t.Parallel() + + // Setup root parser with subcommandA to mirror the CLI input. + p := NewParser("app") + subA := NewSubcommand("subcommandA") + p.AttachSubcommand(subA, 1) + + // Register the root-level bool flag that follows the subcommand. + var output bool + p.Bool(&output, "", "output", "root bool flag") + + // Parse the CLI input exactly as described in the command comment. + if err := p.ParseArgs([]string{"subcommandA", "--output"}); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Assert the bool flag is true and the subcommand is marked as used. + if !output { + t.Fatalf("expected --output to set output to true") + } + if !subA.Used { + t.Fatalf("expected subcommandA to be used") + } +} + +func TestNestedSubcommandTrailingArguments(t *testing.T) { + // Command: ./app one two --test -- abc 123 xyz + // Expect: subcommands one & two used; --test bool true; trailing args joined to "abc 123 xyz". + t.Parallel() + + // Setup root parser with nested subcommands to reflect the CLI command. + p := NewParser("app") + subOne := NewSubcommand("one") + subTwo := NewSubcommand("two") + p.AttachSubcommand(subOne, 1) + subOne.AttachSubcommand(subTwo, 1) + + // Register the bool flag on the deepest subcommand per the command contract. + var test bool + subTwo.Bool(&test, "", "test", "bool flag on nested subcommand") + + // Parse the CLI input exactly as described in the command comment. + args := []string{"one", "two", "--test", "--", "abc", "123", "xyz"} + if err := p.ParseArgs(args); err != nil { + t.Fatalf("parse failed: %v", err) + } + + // Validate subcommands were used, flag set to true, and trailing args preserved. + if !subOne.Used || !subTwo.Used { + t.Fatalf("expected nested subcommands to be used: one=%v two=%v", subOne.Used, subTwo.Used) + } + if !test { + t.Fatalf("expected --test to set the nested bool flag to true") + } + if got := strings.Join(p.TrailingArguments, " "); got != "abc 123 xyz" { + t.Fatalf("expected trailing arguments to join to %q, got %q", "abc 123 xyz", got) + } +} diff --git a/parser_test.go b/parser_test.go index ddc2650..52c8c88 100644 --- a/parser_test.go +++ b/parser_test.go @@ -46,11 +46,12 @@ func TestFindArgsNotInParsedValues(t *testing.T) { // ensure regular values are not skipped parsedValues = []parsedValue{ { - Key: "flaggy", - Value: "testing", + Key: "flaggy", + Value: "testing", + ConsumesNext: true, }, } - args = []string{"flaggy", "testing", "unusedFlag"} + args = []string{"--flaggy", "testing", "unusedFlag"} unusedArgs = findArgsNotInParsedValues(args, parsedValues) t.Log(unusedArgs) if len(unusedArgs) == 0 { @@ -60,3 +61,21 @@ func TestFindArgsNotInParsedValues(t *testing.T) { t.Fatal("Invalid number of unused args found. Expected 1 but found", len(unusedArgs)) } } + +func TestFindArgsNotInParsedValuesSkipsEmptyConsumedValues(t *testing.T) { + t.Parallel() + + args := []string{"-log.file.dir", ""} + parsedValues := []parsedValue{ + { + Key: "log.file.dir", + Value: "", + ConsumesNext: true, + }, + } + + unusedArgs := findArgsNotInParsedValues(args, parsedValues) + if len(unusedArgs) != 0 { + t.Fatalf("expected no unused args, found %v", unusedArgs) + } +} diff --git a/scan_result.go b/scan_result.go new file mode 100644 index 0000000..485fb6c --- /dev/null +++ b/scan_result.go @@ -0,0 +1,39 @@ +package flaggy + +// flagScanResult summarizes the outcome of scanning arguments for a parser. +type flagScanResult struct { + // Positionals lists positional tokens (subcommands or positional args) in + // the order they were encountered, along with their indexes in the source + // argument slice. + Positionals []positionalToken + // ForwardArgs contains arguments that were intentionally left untouched so + // that downstream parsers can process them. These tokens maintain their + // original order. + ForwardArgs []string + // HelpRequested reports whether a help flag (-h/--help) was encountered + // while scanning this parser. + HelpRequested bool + // Subcommand holds the first subcommand encountered while scanning. When + // non-nil, scanning stops and the remaining arguments are handed off to the + // referenced parser. + Subcommand *subcommandMatch +} + +// positionalToken tracks a positional argument's value and the index it was +// read from in the source slice. +type positionalToken struct { + Value string + Index int +} + +// subcommandMatch captures the metadata necessary to hand control over to a +// downstream subcommand parser. +type subcommandMatch struct { + // Command references the subcommand that matched the positional token. + Command *Subcommand + // Token points to the positional token that triggered the match. + Token positionalToken + // RelativeDepth tracks the positional depth (1-based) where the match was + // found. This mirrors how subcommand positions are configured. + RelativeDepth int +} diff --git a/subCommand.go b/subCommand.go index 6483c99..ff36fa9 100644 --- a/subCommand.go +++ b/subCommand.go @@ -1,17 +1,17 @@ package flaggy import ( - "fmt" - "log" - "math/big" - "net" - netip "net/netip" - "net/url" - "os" - "regexp" - "strconv" - "strings" - "time" + "fmt" + "log" + "math/big" + "net" + netip "net/netip" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" ) // Subcommand represents a subcommand which contains a set of child @@ -51,154 +51,114 @@ func NewSubcommand(name string) *Subcommand { // out of the supplied args and returns the resulting positional items in order, // all the flag names found (without values), a bool to indicate if help was // requested, and any errors found during parsing -func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) { +func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) (flagScanResult, error) { - var positionalOnlyArguments []string - var helpRequested bool // indicates the user has supplied -h and we - // should render help if we are the last subcommand + result := flagScanResult{} + positionalCount := 0 - // indicates we should skip the next argument, like when parsing a flag - // that separates key and value by space - var skipNext bool - - // endArgfound indicates that a -- was found and everything - // remaining should be added to the trailing arguments slices - var endArgFound bool - - // find all the normal flags (not positional) and parse them out - for i, a := range args { + for i := 0; i < len(args); i++ { + a := args[i] debugPrint("parsing arg:", a) - // evaluate if there is a following arg to avoid panics - var nextArgExists bool - var nextArg string - if len(args)-1 >= i+1 { - nextArgExists = true - nextArg = args[i+1] - } + argType := determineArgType(a) - // if end arg -- has been found, just add everything to TrailingArguments - if endArgFound { + if argType == argIsFinal { if !p.trailingArgumentsExtracted { - p.TrailingArguments = append(p.TrailingArguments, a) + p.TrailingArguments = append(p.TrailingArguments, args[i+1:]...) } - continue + break } - // skip this run if specified - if skipNext { - skipNext = false - debugPrint("skipping flag because it is an arg:", a) - continue - } - - // parse the flag into its name for consideration without dashes flagName := parseFlagToName(a) - // if the flag being passed is version or v and the option to display - // version with version flags, then display version - if p.ShowVersionWithVersionFlag { - if flagName == versionFlagLongName { - p.ShowVersionAndExit() - } + if p.ShowVersionWithVersionFlag && flagName == versionFlagLongName { + p.ShowVersionAndExit() } - // if the show Help on h flag option is set, then show Help when h or Help - // is passed as an option - if p.ShowHelpWithHFlag { - if flagName == helpFlagShortName || flagName == helpFlagLongName { - // Ensure this is the last subcommand passed so we give the correct - // help output - helpRequested = true - continue - } + if p.ShowHelpWithHFlag && (flagName == helpFlagShortName || flagName == helpFlagLongName) { + result.HelpRequested = true + continue } - // determine what kind of flag this is - argType := determineArgType(a) - - // strip flags from arg debugPrint("Parsing flag named", a, "of type", argType) - // depending on the flag type, parse the key and value out, then apply it switch argType { - case argIsFinal: - // debugPrint("Arg", i, "is final:", a) - endArgFound = true case argIsPositional: - // debugPrint("Arg is positional or subcommand:", a) - // this positional argument into a slice of their own, so that - // we can determine if its a subcommand or positional value later - positionalOnlyArguments = append(positionalOnlyArguments, a) - // track this as a parsed value with the subcommand + positionalCount++ + token := positionalToken{Value: a, Index: i} + result.Positionals = append(result.Positionals, token) sc.addParsedPositionalValue(a) - case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value - a = parseFlagToName(a) - - // debugPrint("Arg", i, "is flag with space:", a) - // parse next arg as value to this flag and apply to subcommand flags - // if the flag is a bool flag, then we check for a following positional - // and skip it if necessary - if flagIsBool(sc, p, a) { - debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg) - // set the value in this subcommand and its root parser - valueSet, err := setValueForParsers(a, "true", p, sc) - - // if an error occurs, just return it and quit parsing - if err != nil { - return []string{}, false, err + + // Detect subcommands early so we avoid parsing child flags at this level. + for _, cmd := range sc.Subcommands { + if cmd.Position == positionalCount && (a == cmd.Name || a == cmd.ShortName) { + result.Subcommand = &subcommandMatch{ + Command: cmd, + Token: token, + RelativeDepth: positionalCount, + } + // Stop scanning so the child can handle the remainder. + return result, nil } + } + case argIsFlagWithSpace: + key := flagName - // log all values parsed by this subcommand. We leave the value blank - // because the bool value had no explicit true or false supplied + if flagIsBool(sc, p, key) { + valueSet, err := setValueForParsers(key, "true", p, sc) + if err != nil { + return result, err + } if valueSet { - sc.addParsedFlag(a, "") + sc.addParsedFlag(key, "", false) } - - // we've found and set a standalone bool flag, so we move on to the next - // argument in the list of arguments continue } - skipNext = true - // debugPrint(sc.Name, "NOT bool flag", a) + if !flagIsDefined(sc, p, key) { + result.ForwardArgs = append(result.ForwardArgs, args[i]) + if i+1 < len(args) && shouldReserveNextArgForChild(sc, positionalCount, args[i+1]) { + result.ForwardArgs = append(result.ForwardArgs, args[i+1]) + i++ + } + continue + } - // if the next arg was not found, then show a Help message - if !nextArgExists { - p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.") + if i+1 >= len(args) { + p.ShowHelpWithMessage("Expected a following arg for flag " + key + ", but it did not exist.") exitOrPanic(2) } - valueSet, err := setValueForParsers(a, nextArg, p, sc) + + nextArg := args[i+1] + valueSet, err := setValueForParsers(key, nextArg, p, sc) if err != nil { - return []string{}, false, err + return result, err } - - // log all parsed values in the subcommand if valueSet { - sc.addParsedFlag(a, nextArg) + sc.addParsedFlag(key, nextArg, true) } - case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value - // debugPrint("Arg", i, "is flag with value:", a) - a = parseFlagToName(a) + i++ + case argIsFlagWithValue: + keyWithValue := flagName + key, val := parseArgWithValue(keyWithValue) - // parse flag into key and value and apply to subcommand flags - key, val := parseArgWithValue(a) + if !flagIsDefined(sc, p, key) { + result.ForwardArgs = append(result.ForwardArgs, args[i]) + continue + } - // set the value in this subcommand and its root parser valueSet, err := setValueForParsers(key, val, p, sc) if err != nil { - return []string{}, false, err + return result, err } - - // log all values parsed by the subcommand if valueSet { - sc.addParsedFlag(a, val) + sc.addParsedFlag(keyWithValue, val, false) } } } - return positionalOnlyArguments, helpRequested, nil + return result, nil } // findAllParsedValues finds all values parsed by all subcommands and this @@ -215,13 +175,37 @@ func (sc *Subcommand) findAllParsedValues() []parsedValue { return parsedValues } +// shouldReserveNextArgForChild determines if the following argument should be +// left untouched so that a downstream subcommand can parse it as its own flag +// or positional value. +func shouldReserveNextArgForChild(sc *Subcommand, positionalCount int, nextArg string) bool { + if determineArgType(nextArg) == argIsFinal { + return false + } + + position := positionalCount + 1 + for _, cmd := range sc.Subcommands { + if cmd.Position == position && (cmd.Name == nextArg || cmd.ShortName == nextArg) { + return false + } + } + + for _, pos := range sc.PositionalFlags { + if pos.Position == position { + return false + } + } + + return true +} + // parse causes the argument parser to parse based on the supplied []string. -// depth specifies the non-flag subcommand positional depth. A slice of flags -// and subcommands parsed is returned so that the parser can ultimately decide -// if there were any unexpected values supplied by the user -func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { +// The args slice should contain only values that have not already been +// consumed by parent parsers. The parser records any values it parses so that +// the root parser can detect unexpected arguments after parsing is complete. +func (sc *Subcommand) parse(p *Parser, args []string) error { - debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args) + debugPrint("- Parsing subcommand", sc.Name, "with args", args) // if a command is parsed, its used sc.Used = true @@ -246,65 +230,35 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { sc.ensureNoConflictWithBuiltinVersion() } - // Parse the normal flags out of the argument list and return the positionals - // (subcommands and positional values), along with the flags used. - // Then the flag values are applied to the parent parser and the current - // subcommand being parsed. - positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args) + scan, err := sc.parseAllFlagsFromArgs(p, args) if err != nil { return err } - // indicate that trailing arguments have been extracted, so that they aren't - // appended a second time - p.trailingArgumentsExtracted = true + for idx, token := range scan.Positionals { + relativeDepth := idx + 1 + value := token.Value - // loop over positional values and look for their matching positional - // parameter, or their positional command. If neither are found, then - // we throw an error - var parsedArgCount int - for pos, v := range positionalOnlyArguments { - - // the first relative positional argument will be human natural at position 1 - // but offset for the depth of relative commands being parsed for currently. - relativeDepth := pos - depth + 1 - // debugPrint("Parsing positional only position", relativeDepth, "with value", v) - - if relativeDepth < 1 { - // debugPrint(sc.Name, "skipped value:", v) - continue - } - parsedArgCount++ - - // determine subcommands and parse them by positional value and name - for _, cmd := range sc.Subcommands { - // debugPrint("Subcommand being compared", relativeDepth, "==", cmd.Position, "and", v, "==", cmd.Name, "==", cmd.ShortName) - if relativeDepth == cmd.Position && (v == cmd.Name || v == cmd.ShortName) { - debugPrint("Descending into positional subcommand", cmd.Name, "at relativeDepth", relativeDepth, "and absolute depth", depth+1) - return cmd.parse(p, args, depth+parsedArgCount) // continue recursive positional parsing - } + if scan.Subcommand != nil && token.Index == scan.Subcommand.Token.Index { + debugPrint("Descending into positional subcommand", scan.Subcommand.Command.Name, "at relativeDepth", scan.Subcommand.RelativeDepth) + childArgs := append([]string{}, scan.ForwardArgs...) + childArgs = append(childArgs, args[token.Index+1:]...) + return scan.Subcommand.Command.parse(p, childArgs) } - // determine positional args and parse them by positional value and name var foundPositional bool for _, val := range sc.PositionalFlags { if relativeDepth == val.Position { - debugPrint("Found a positional value at relativePos:", relativeDepth, "value:", v) + debugPrint("Found a positional value at relativePos:", relativeDepth, "value:", value) - // set original value for help output val.defaultValue = *val.AssignmentVar - - // defrerence the struct pointer, then set the pointer property within it - *val.AssignmentVar = v - // debugPrint("set positional to value", *val.AssignmentVar) + *val.AssignmentVar = value foundPositional = true val.Found = true break } } - // if there aren't any positional flags but there are subcommands that - // were not used, display a useful message with subcommand options. if !foundPositional { if p.ShowHelpOnUnexpected { debugPrint("No positional at position", relativeDepth) @@ -315,10 +269,7 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { } } - // if there is a subcommand here but it was not specified, display them all - // as a suggestion to the user before exiting. if foundSubcommandAtDepth { - // determine which name to use in upcoming help output fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") var output string for _, cmd := range sc.Subcommands { @@ -327,7 +278,6 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { } output = output + " " + cmd.Name } - // if there are available subcommands, let the user know if len(output) > 0 { output = strings.TrimLeft(output, " ") fmt.Println("Available subcommands:", output) @@ -335,24 +285,24 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { exitOrPanic(2) } - // if there were not any flags or subcommands at this position at all, then - // throw an error (display Help if necessary) - p.ShowHelpWithMessage("Unexpected argument: " + v) + p.ShowHelpWithMessage("Unexpected argument: " + value) exitOrPanic(2) } else { - // if no positional value was registered at this position, but the parser is not - // configured to show help when any unexpected command is found, add this positional - // to the list of trailing arguments. This allows for any number of unspecified - // values to be added at the end of the arguments list without using the --- - // trailing arguments separator. - p.TrailingArguments = append(p.TrailingArguments, v) - + p.TrailingArguments = append(p.TrailingArguments, value) } } } - // if help was requested and we should show help when h is passed, - if helpRequested && p.ShowHelpWithHFlag { + if scan.Subcommand != nil { + // If we recorded a subcommand but didn't descend, ensure the remaining + // arguments are handed off now. + debugPrint("Descending into positional subcommand", scan.Subcommand.Command.Name, "at relativeDepth", scan.Subcommand.RelativeDepth) + childArgs := append([]string{}, scan.ForwardArgs...) + childArgs = append(childArgs, args[scan.Subcommand.Token.Index+1:]...) + return scan.Subcommand.Command.parse(p, childArgs) + } + + if scan.HelpRequested && p.ShowHelpWithHFlag { p.ShowHelp() exitOrPanic(0) } @@ -372,18 +322,22 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { } } + // indicate that trailing arguments have been extracted, so that they aren't + // appended a second time by parent parsers. + p.trailingArgumentsExtracted = true + return nil } // addParsedFlag makes it easy to append flag values parsed by the subcommand -func (sc *Subcommand) addParsedFlag(key string, value string) { - sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false)) +func (sc *Subcommand) addParsedFlag(key string, value string, consumesNext bool) { + sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false, consumesNext)) } // addParsedPositionalValue makes it easy to append positionals parsed by the // subcommand func (sc *Subcommand) addParsedPositionalValue(value string) { - sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true)) + sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true, false)) } // FlagExists lets you know if the flag name exists as either a short or long @@ -480,12 +434,12 @@ func (sc *Subcommand) BoolSlice(assignmentVar *[]bool, shortName string, longNam // ByteSlice adds a new slice of bytes flag // Specify the flag multiple times to fill the slice. Takes hex as input. func (sc *Subcommand) ByteSlice(assignmentVar *[]byte, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // BytesBase64 adds a new []byte flag parsed from base64 input. func (sc *Subcommand) BytesBase64(assignmentVar *Base64Bytes, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Duration adds a new time.Duration flag. @@ -665,82 +619,82 @@ func (sc *Subcommand) IPMask(assignmentVar *net.IPMask, shortName string, longNa // IPMaskSlice adds a new net.HardwareAddr slice flag. IPv4 only. // Specify the flag multiple times to fill the slice. func (sc *Subcommand) IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Time adds a new time.Time flag. Supports RFC3339/RFC3339Nano, RFC1123, and unix seconds. func (sc *Subcommand) Time(assignmentVar *time.Time, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // URL adds a new url.URL flag. func (sc *Subcommand) URL(assignmentVar *url.URL, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // IPNet adds a new net.IPNet flag parsed from CIDR. func (sc *Subcommand) IPNet(assignmentVar *net.IPNet, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // TCPAddr adds a new net.TCPAddr flag parsed from host:port. func (sc *Subcommand) TCPAddr(assignmentVar *net.TCPAddr, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // UDPAddr adds a new net.UDPAddr flag parsed from host:port. func (sc *Subcommand) UDPAddr(assignmentVar *net.UDPAddr, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // FileMode adds a new os.FileMode flag parsed from octal/decimal (base auto-detected). func (sc *Subcommand) FileMode(assignmentVar *os.FileMode, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Regexp adds a new regexp.Regexp flag. func (sc *Subcommand) Regexp(assignmentVar *regexp.Regexp, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Location adds a new time.Location flag. func (sc *Subcommand) Location(assignmentVar *time.Location, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Month adds a new time.Month flag. func (sc *Subcommand) Month(assignmentVar *time.Month, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // Weekday adds a new time.Weekday flag. func (sc *Subcommand) Weekday(assignmentVar *time.Weekday, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // BigInt adds a new big.Int flag. func (sc *Subcommand) BigInt(assignmentVar *big.Int, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // BigRat adds a new big.Rat flag. func (sc *Subcommand) BigRat(assignmentVar *big.Rat, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // NetipAddr adds a new netip.Addr flag. func (sc *Subcommand) NetipAddr(assignmentVar *netip.Addr, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // NetipPrefix adds a new netip.Prefix flag. func (sc *Subcommand) NetipPrefix(assignmentVar *netip.Prefix, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // NetipAddrPort adds a new netip.AddrPort flag. func (sc *Subcommand) NetipAddrPort(assignmentVar *netip.AddrPort, shortName string, longName string, description string) { - sc.add(assignmentVar, shortName, longName, description) + sc.add(assignmentVar, shortName, longName, description) } // AddPositionalValue adds a positional value to the subcommand. the diff --git a/subcommand_test.go b/subcommand_test.go index f037dcf..80114dd 100644 --- a/subcommand_test.go +++ b/subcommand_test.go @@ -380,10 +380,10 @@ func TestSCInputParsing(t *testing.T) { inputArgs = append(inputArgs, "-ssf", "one", "-ssf", "two") var stringSliceFlagExpected = []string{"one", "two"} - var stringSliceCommaFlag []string - sc.StringSlice(&stringSliceCommaFlag, "sscf", "stringSliceComma", "string slice flag") - inputArgs = append(inputArgs, "-sscf", "one,two") - var stringSliceCommaFlagExpected = []string{"one,two"} + var stringSliceCommaFlag []string + sc.StringSlice(&stringSliceCommaFlag, "sscf", "stringSliceComma", "string slice flag") + inputArgs = append(inputArgs, "-sscf", "one,two") + var stringSliceCommaFlagExpected = []string{"one,two"} var boolFlag bool sc.Bool(&boolFlag, "bf", "bool", "bool flag") @@ -827,19 +827,19 @@ func TestParseErrorsAreReportedRegression(t *testing.T) { // Regression test: when a subcommand and a flag share the same short name, // the flag should take precedence for a dash-prefixed token and not cause an error. func TestSubcommandAndFlagSameShortName(t *testing.T) { - flaggy.ResetParser() - flaggy.PanicInsteadOfExit = true - defer func() { flaggy.PanicInsteadOfExit = false }() - - var test string - sc := flaggy.NewSubcommand("testSubCmd") - sc.ShortName = "t" - sc.String(&test, "t", "testFlag", "test flag") - flaggy.AttachSubcommand(sc, 1) - - // Should set test to "hello" without unknown-argument errors. - flaggy.ParseArgs([]string{"t", "-t", "hello"}) - if test != "hello" { - t.Fatalf("expected test to be 'hello', got %q", test) - } + flaggy.ResetParser() + flaggy.PanicInsteadOfExit = true + defer func() { flaggy.PanicInsteadOfExit = false }() + + var test string + sc := flaggy.NewSubcommand("testSubCmd") + sc.ShortName = "t" + sc.String(&test, "t", "testFlag", "test flag") + flaggy.AttachSubcommand(sc, 1) + + // Should set test to "hello" without unknown-argument errors. + flaggy.ParseArgs([]string{"t", "-t", "hello"}) + if test != "hello" { + t.Fatalf("expected test to be 'hello', got %q", test) + } }