From b8156931fd5fd06b90c313c610a04466a259dc3d Mon Sep 17 00:00:00 2001 From: bth Date: Mon, 26 Jan 2026 18:48:55 -0800 Subject: [PATCH] flags : Flag interface instead of generic, allowing us to move the reflection out into a field expansion func only --- cat/cat.go | 16 ++-- flags/flags.go | 205 +++++++++++++++++++------------------------------ flags/help.go | 58 ++++++++++++++ last/last.go | 4 +- mv/mv.go | 4 +- next/next.go | 4 +- sort/sort.go | 16 ++-- tag/tag.go | 16 ++-- 8 files changed, 169 insertions(+), 154 deletions(-) create mode 100644 flags/help.go diff --git a/cat/cat.go b/cat/cat.go index 06704db..f321786 100644 --- a/cat/cat.go +++ b/cat/cat.go @@ -22,19 +22,19 @@ const ( type options struct { Filelist flags.Arg - Base flags.Flag[string] - KeepHeader flags.Flag[bool] - KeepTitle flags.Flag[bool] - Help flags.Flag[bool] + Base flags.String + KeepHeader flags.Bool + KeepTitle flags.Bool + Help flags.Bool } func initOpts() options { return options{ flags.Arg{"", "filelist. accepts from stdin if not provided"}, - flags.Flag[string]{"--base", "-b", "", "base directory prepended to files in filelist"}, - flags.Flag[bool]{"--keep-header", "-d", false, "preserve um headers in concatenated file. overrides --keep-title"}, - flags.Flag[bool]{"--keep-title", "-t", false, "preserve um titles in concatenated file"}, - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.String{"--base", "-b", "", "base directory prepended to files in filelist"}, + flags.Bool{"--keep-header", "-d", false, "preserve um headers in concatenated file. overrides --keep-title"}, + flags.Bool{"--keep-title", "-t", false, "preserve um titles in concatenated file"}, + flags.Bool{"--help", "-h", false, "show help"}, } } diff --git a/flags/flags.go b/flags/flags.go index 4fabf75..883e591 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -1,65 +1,87 @@ package flags import ( - "fmt" "log" - "os" "reflect" "strings" - "text/tabwriter" "github.com/brtholomy/um/go/cmd" ) const INTERNAL_ERR_PREFIX = "um internal err: ParseArgs" +// interface as func parameter +type Flag interface { + Set(string) + Match(arg string, i, j int) bool + IsSet() bool +} + type Arg struct { Val string Help string } -// type constraint -type Val interface { - string | bool -} - -// with generic Val field, instantiate like Flag[string] -// NOTE: struct inheritance would be easier, but Flag[bool] is much cooler than FlagBool. -// and actually more readable for the initOpts function, which serves as documentation. -// so the complexity seems to be all paid up front here in this pkg. -type Flag[T Val] struct { +type String struct { Long string Short string - Val T + Val string Help string } -// generic Flag validity check -// NOTE: you cannot define a method on an instantiated type, only the generic -// https://www.reddit.com/r/golang/comments/1n6xasx/comment/nc3dbhd/ -func (f Flag[Val]) IsSet() bool { - // must do this weird dance: convert to empty interface, then cast: - switch any(f.Val).(type) { - case string: - return any(f.Val).(string) != "" - case bool: - return any(f.Val).(bool) - default: - return false - } +type Bool struct { + Long string + Short string + Val bool + Help string } -func (f Arg) IsSet() bool { +func (f *Arg) Set(val string) { + f.Val = val +} + +func (f *Arg) Match(arg string, i, j int) bool { + return i == j && !hasDashPrefix(arg) +} + +func (f *Arg) IsSet() bool { return f.Val != "" } -func HasDashPrefix(s string) bool { +func (f *String) Set(val string) { + f.Val = val +} + +// TODO: this should look ahead +// i, fetch = ValidateIncrementFetchOrExit(sub, args, i) +func (f *String) Match(arg string, _, _ int) bool { + return arg == f.Long || arg == f.Short +} + +func (f *String) IsSet() bool { + return f.Val != "" +} + +func (f *Bool) Set(_ string) { + f.Val = true +} + +func (f *Bool) Match(arg string, _, _ int) bool { + // TODO: catch --help + return arg == f.Long || arg == f.Short +} + +func (f *Bool) IsSet() bool { + return f.Val +} + +func hasDashPrefix(s string) bool { return strings.HasPrefix(s, "-") } // check for non-dashed value ahead in the args slice func missingValue(args []string, i int) bool { - return i+1 == len(args) || HasDashPrefix(args[i+1]) + return i+1 == len(args) || hasDashPrefix(args[i+1]) } // if no value ahead in the args, print error and exit @@ -78,62 +100,12 @@ func ValidateIncrementFetchOrExit(sub cmd.Subcommand, args []string, i int) (int return i + 1, args[i+1] } -func HelpRequired(sub cmd.Subcommand, long string) { - log.Printf("um %s: %s is required", sub, long) -} - -func HelpInvalidArg(sub cmd.Subcommand, arg string) { - log.Printf("um %s: invalid argument: %s", sub, arg) -} - -// print out help string by reflecting over fields of provided opts struct -// and os.Exit(0) -func Help(sub cmd.Subcommand, summary string, opts any) { - v := reflect.ValueOf(opts) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - t := v.Type() - - // everything should go to either stderr or stdout: - w := tabwriter.NewWriter(os.Stderr, 0, 0, 3, ' ', 0) - positional := "" - - for i := 0; i < v.NumField(); i++ { - field := t.Field(i) - value := v.Field(i) - switch value.Interface().(type) { - case Arg: - positional += fmt.Sprintf(" [%s]", strings.ToLower(field.Name)) - fmt.Fprintf(w, "[%v]\tstring\t%v\n", strings.ToLower(field.Name), value.FieldByName("Help")) - case Flag[string], Flag[bool]: - if field.Name != "Help" { - positional += fmt.Sprintf(" [%s]", value.FieldByName("Long")) - } - fmt.Fprintf(w, "%v | %v\t%v\t%v\n", - value.FieldByName("Long"), - value.FieldByName("Short"), - value.FieldByName("Val").Type(), - value.FieldByName("Help"), - ) - } - } - log.Printf("um %s%s\n\n", sub, positional) - log.Printf("%s\n\n", summary) - w.Flush() - os.Exit(0) -} - -// parse the commandline args -// -// reflect on the incoming opts struct filled out with flags.Arg, flags.Flag[bool], flags.Flag[string] -// and fill their "Val" field appropriately. +// expand incoming opts struct into a []Flag // // WARN: incoming opts must be a pointer! // WORB: opts struct must list its Arg types first and in expected order -// NOTE: uglier than the pretty switch statements I had per cmd, but much more convenient. -func ParseArgs(sub cmd.Subcommand, summary string, args []string, opts any) { - // immediately get the underlying value of the incoming pointer: +func expandOpts(opts any) []Flag { + flags := []Flag{} v := reflect.ValueOf(opts) if v.Kind() == reflect.Ptr { v = v.Elem() @@ -143,51 +115,36 @@ func ParseArgs(sub cmd.Subcommand, summary string, args []string, opts any) { if v.Kind() != reflect.Struct { log.Fatalf("%s needs an underlying struct: %#v", INTERNAL_ERR_PREFIX, opts) } + for j := 0; j < v.NumField(); j++ { + field := v.Field(j) + if field.Kind() != reflect.Struct { + log.Fatalf("%s needs a struct: %s", INTERNAL_ERR_PREFIX, field.Kind()) + } + if !field.CanAddr() { + log.Fatalf("%s needs an addressable type: ", INTERNAL_ERR_PREFIX, field.Type()) + } + // NOTE: Addr() gets the underlying address: + if f, ok := field.Addr().Interface().(Flag); ok { + flags = append(flags, f) + } + } + return flags +} - // for all args, iterate through all opts fields and reflect on type +// assign values of args to opts struct using Flag interface methods +// +// NOTE: uglier than the pretty switch statements I had per cmd, but much more convenient. +func ParseArgs(sub cmd.Subcommand, summary string, args []string, opts any) { + flags := expandOpts(opts) argloop: - for i := 0; i < len(args); i++ { - arg := args[i] - - for j := 0; j < v.NumField(); j++ { - field := v.Field(j) - if field.Kind() != reflect.Struct { - log.Fatalf("%s needs a struct of flag types: %#v is type: %s", INTERNAL_ERR_PREFIX, field, field.Kind()) - } - val := field.FieldByName("Val") - // NORT: FieldByName().String() won't panic if not present, as with Arg type, just prints "" - long := field.FieldByName("Long").String() - short := field.FieldByName("Short").String() - if !val.CanSet() { - log.Fatalf("%s failed to set flag value: %#v", INTERNAL_ERR_PREFIX, field) - } - switch field.Interface().(type) { - case Arg: - // NOTE: we assume the opts struct lists its Arg types first and in order - if j == i && !HasDashPrefix(arg) { - val.SetString(arg) - continue argloop - } - case Flag[bool]: - if arg == long || arg == short { - val.SetBool(true) - if long == "--help" { - Help(sub, summary, opts) - } - continue argloop - } - case Flag[string]: - if arg == long || arg == short { - fetch := "" - i, fetch = ValidateIncrementFetchOrExit(sub, args, i) - val.SetString(fetch) - continue argloop - } - default: - log.Fatalf("um internal err: unrecognized flag type in opts struct: %#v", field) + for i, arg := range args { + for j, f := range flags { + if f.Match(arg, i, j) { + f.Set(arg) + continue argloop } } - // BLAK: if we didn't skip ahead, arg didn't match: + // NOTE: if we didn't skip ahead, arg didn't match: HelpInvalidArg(sub, arg) Help(sub, summary, opts) } diff --git a/flags/help.go b/flags/help.go new file mode 100644 index 0000000..36d6be6 --- /dev/null +++ b/flags/help.go @@ -0,0 +1,58 @@ +package flags + +import ( + "fmt" + "log" + "os" + "reflect" + "strings" + "text/tabwriter" + + "github.com/brtholomy/um/go/cmd" +) + +func HelpRequired(sub cmd.Subcommand, long string) { + log.Printf("um %s: %s is required", sub, long) +} + +func HelpInvalidArg(sub cmd.Subcommand, arg string) { + log.Printf("um %s: invalid argument: %s", sub, arg) +} + +// print out help string by reflecting over fields of provided opts struct +// and os.Exit(0) +func Help(sub cmd.Subcommand, summary string, opts any) { + v := reflect.ValueOf(opts) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + t := v.Type() + + // everything should go to either stderr or stdout: + w := tabwriter.NewWriter(os.Stderr, 0, 0, 3, ' ', 0) + positional := "" + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + value := v.Field(i) + switch value.Interface().(type) { + case Arg: + positional += fmt.Sprintf(" [%s]", strings.ToLower(field.Name)) + fmt.Fprintf(w, "[%v]\tstring\t%v\n", strings.ToLower(field.Name), value.FieldByName("Help")) + case String, Bool: + if field.Name != "Help" { + positional += fmt.Sprintf(" [%s]", value.FieldByName("Long")) + } + fmt.Fprintf(w, "%v | %v\t%v\t%v\n", + value.FieldByName("Long"), + value.FieldByName("Short"), + value.FieldByName("Val").Type(), + value.FieldByName("Help"), + ) + } + } + log.Printf("um %s%s\n\n", sub, positional) + log.Printf("%s\n\n", summary) + w.Flush() + os.Exit(0) +} diff --git a/last/last.go b/last/last.go index 731c9b3..9c20363 100644 --- a/last/last.go +++ b/last/last.go @@ -22,12 +22,12 @@ const ( ) type options struct { - Help flags.Flag[bool] + Help flags.Bool } func initOpts() options { return options{ - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.Bool{"--help", "-h", false, "show help"}, } } diff --git a/mv/mv.go b/mv/mv.go index de901b8..ca814fa 100644 --- a/mv/mv.go +++ b/mv/mv.go @@ -21,14 +21,14 @@ const ( type options struct { Filename flags.Arg Descriptor flags.Arg - Help flags.Flag[bool] + Help flags.Bool } func initOpts() options { return options{ flags.Arg{"", "old filename"}, flags.Arg{"", "new descriptor"}, - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.Bool{"--help", "-h", false, "show help"}, } } diff --git a/next/next.go b/next/next.go index f012758..2fb5429 100644 --- a/next/next.go +++ b/next/next.go @@ -25,14 +25,14 @@ var fileRegexp *regexp.Regexp = regexp.MustCompile(FILE_REGEXP) type options struct { Descriptor flags.Arg Tags flags.Arg - Help flags.Flag[bool] + Help flags.Bool } func initOpts() options { return options{ flags.Arg{"", "midfix file descriptor"}, flags.Arg{"", "tags to add to new file"}, - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.Bool{"--help", "-h", false, "show help"}, } } diff --git a/sort/sort.go b/sort/sort.go index 919900b..49fbf5a 100644 --- a/sort/sort.go +++ b/sort/sort.go @@ -18,18 +18,18 @@ const ( ) type options struct { - Source flags.Flag[string] - Key flags.Flag[string] - Write flags.Flag[bool] - Help flags.Flag[bool] + Source flags.String + Key flags.String + Write flags.Bool + Help flags.Bool } func initOpts() options { return options{ - flags.Flag[string]{"--source", "-s", "", "path to source list. reads from stdin if omitted."}, - flags.Flag[string]{"--key", "-k", "", "path to sort key"}, - flags.Flag[bool]{"--write", "-w", false, "write sorted list back to --key file"}, - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.String{"--source", "-s", "", "path to source list. reads from stdin if omitted."}, + flags.String{"--key", "-k", "", "path to sort key"}, + flags.Bool{"--write", "-w", false, "write sorted list back to --key file"}, + flags.Bool{"--help", "-h", false, "show help"}, } } diff --git a/tag/tag.go b/tag/tag.go index 9f83a01..3fb11de 100644 --- a/tag/tag.go +++ b/tag/tag.go @@ -15,19 +15,19 @@ const ( type options struct { Query flags.Arg - Date flags.Flag[string] - Invert flags.Flag[bool] - Verbose flags.Flag[bool] - Help flags.Flag[bool] + Date flags.String + Invert flags.Bool + Verbose flags.Bool + Help flags.Bool } func initOpts() options { return options{ flags.Arg{"", "tag query: understands intersection '+' and union ','"}, - flags.Flag[string]{"--date", "-d", "", "date range in ISO 8601: YYYY.MM.DD[-YYYY.MM.DD]"}, - flags.Flag[bool]{"--invert", "-i", false, "invert match"}, - flags.Flag[bool]{"--verbose", "-v", false, "print a verbose summary"}, - flags.Flag[bool]{"--help", "-h", false, "show help"}, + flags.String{"--date", "-d", "", "date range in ISO 8601: YYYY.MM.DD[-YYYY.MM.DD]"}, + flags.Bool{"--invert", "-i", false, "invert match"}, + flags.Bool{"--verbose", "-v", false, "print a verbose summary"}, + flags.Bool{"--help", "-h", false, "show help"}, } }