package rags import ( "flag" "fmt" "sort" "strings" ) // Built-in flag categories, used when sorting flags to print usage const ( Global = "global" // Flags that exist on all commands in a CLI uncategorized = "uncategorized" // Flags without a category ) // Rag is a rich flag implementation that is applied to a [flag.FlagSet] for // parsing. It can be instantiated directly (i.e., declaratively), or via // [flag.FlagSet] style functions on [RagSet]. type Rag struct { Name string Short string Usage string Category string Required bool Value flag.Getter } func (f *Rag) Names() []string { if f.Short != "" { return []string{f.Name, f.Short} } return []string{f.Name} } func (f *Rag) Set(s string) error { return f.Value.Set(s) } func (f *Rag) Apply(fs *flag.FlagSet) { if f.Name == "" { panic("invalid rich flag: name is required") } if f.Value == nil { panic(fmt.Sprintf("invalid rich flag '%s': value is required", f.Name)) } for _, name := range f.Names() { fs.Var(f.Value, name, f.Usage) } } // withOpts updates f to use options. Existing values are wiped. func (f *Rag) withOpts(opts *options) *Rag { f.Category = opts.category f.Required = opts.required f.Short = opts.short return f } func normalizeCategory(c string) string { // Make characters consistent before chopping suffixes c = strings.ToLower(c) c = strings.TrimSpace(c) c = strings.TrimSuffix(c, ":") c = strings.TrimSuffix(c, "flags") // Clear out any whitespace we created if the category // ended with "flags" c = strings.TrimSpace(c) return c } // sortCategories sorts categories into the appropriate order for printing. // it assumes that the categories have already been normalized. // empty string is treated as uncategorized. func sortCategories(c []string) { sort.SliceStable(c, func(i, j int) bool { switch { case c[i] == uncategorized, c[i] == "", c[j] == Global: return true case c[j] == uncategorized, c[j] == "", c[i] == Global: return false default: return c[i] < c[j] } }) } // FlagOption can be used to leverage rich flag features while using the // [flag.FlagSet] API. type FlagOption func(*options) type options struct { category string required bool short string } // WithCategory adds a category field to the flag being registered func WithCategory(c string) FlagOption { return func(o *options) { o.category = c } } // WithRequired marks the flag being registered as required. func WithRequired() FlagOption { return func(o *options) { o.required = true } } // WithShort adds a short-hand alias to the flag being registered. func WithShort(s string) FlagOption { return func(o *options) { o.short = s } } func makeOptions(opts ...FlagOption) *options { o := &options{} for _, opt := range opts { opt(o) } return o }