Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/http/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func (c *ServerCommand) InitializeFlags(fs *flag.FlagSet) {
fs.StringVar(&c.basicAuth, "http.auth-basic", "",
"Configure basic auth credentials with format `user:pass`.")

getopt.Alias(fs, "http.auth-basic", "A")
getopt.Alias(fs, "http.max-header-size", "H")
getopt.Alias(fs, "http.bind-addr", "b")

fs.BoolVar(&c.auth, "http.auth", true, "Enable basic auth. Basic auth credentials must be configured with 'http.auth-basic' option.")
fs.Var(getopt.NegatedBool(&c.auth), "http.no-auth", "Disable basic auth, making the server available to all.")
}
Expand Down
20 changes: 10 additions & 10 deletions execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,21 @@ func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
var err error

if c.showUsage {
return errors.Join(ErrShowUsage, usage(c, ops))
return errors.Join(ErrShowUsage, c.usage(ops))
}
if c.showHelp {
return errors.Join(ErrShowHelp, help(c, ops))
return errors.Join(ErrShowHelp, c.help(ops))
}

if cmd, ok := c.Command.(Initializer); ok {
err = cmd.Initialize(ctx, c.args)
}

if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
return errors.Join(err, c.usage(ops))
}
if errors.Is(err, ErrShowHelp) {
return errors.Join(err, help(c, ops))
return errors.Join(err, c.help(ops))
}

return err
Expand All @@ -181,19 +181,19 @@ func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
// run calls the [Runnable] run routine of c.
func (c command) run(ctx context.Context, ops *ExecuteOptions) error {
if c.showUsage {
return errors.Join(ErrShowUsage, usage(c, ops))
return errors.Join(ErrShowUsage, c.usage(ops))
}
if c.showHelp {
return errors.Join(ErrShowHelp, help(c, ops))
return errors.Join(ErrShowHelp, c.help(ops))
}

err := c.Run(ctx, c.args)

if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
return errors.Join(err, c.usage(ops))
}
if errors.Is(err, ErrShowHelp) {
return errors.Join(err, help(c, ops))
return errors.Join(err, c.help(ops))
}

return err
Expand All @@ -208,10 +208,10 @@ func (c command) onDestroy(ctx context.Context, ops *ExecuteOptions) error {
}

if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
return errors.Join(err, c.usage(ops))
}
if errors.Is(err, ErrShowHelp) {
return errors.Join(err, help(c, ops))
return errors.Join(err, c.help(ops))
}

return err
Expand Down
66 changes: 4 additions & 62 deletions getopt/flagset.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package getopt

import (
"cmp"
"errors"
"flag"
"fmt"
"os"
"reflect"
"slices"
"strings"
"text/template"

"github.com/brandon1024/cmder/internal"
)

// PosixFlagSet a wrapper over the standard [flag.FlagSet] that parses arguments with getopt-style (GNU/POSIX) semantics
Expand Down Expand Up @@ -430,48 +430,12 @@ func (f *PosixFlagSet) group() map[string][]*flag.Flag {
var collected []*flag.Flag

f.VisitAll(func(f *flag.Flag) {
if !isHiddenFlag(f) {
if hidden, ok := f.Value.(HiddenFlag); !ok || !hidden.IsHiddenFlag() {
collected = append(collected, f)
}
})

// sort flags by name length in descending order to ensure that keys in resulting map will use long names first
slices.SortFunc(collected, func(a, b *flag.Flag) int {
return cmp.Compare(len(b.Name), len(a.Name))
})

groups := map[string][]*flag.Flag{}

for len(collected) > 0 {
var flg *flag.Flag

// pop the head of the slice
flg, collected = collected[0], collected[1:]

// update groups
groups[flg.Name] = []*flag.Flag{flg}

// traverse the flags again and find (and remove) any which match flg
for i := range slices.Backward(collected) {
other := collected[i]

if areSame(flg.Value, other.Value) {
groups[flg.Name] = append(groups[flg.Name], other)
collected = append(collected[:i], collected[i+1:]...)
}
}

// sort by length (then lexical order), this time ascending (-a before --all)
slices.SortFunc(groups[flg.Name], func(a, b *flag.Flag) int {
if c := cmp.Compare(len(a.Name), len(b.Name)); c != 0 {
return c
}

return cmp.Compare(a.Name, b.Name)
})
}

return groups
return internal.GroupFlags(collected)
}

// defaultUsage is the default usage renderer invoked when parsing fails, invoked when [PosixFlagSet.Usage] is nil.
Expand Down Expand Up @@ -521,25 +485,3 @@ func zero(flg *flag.Flag) (ok bool, err error) {
ok = flg.DefValue == z.Interface().(flag.Value).String()
return
}

// areSame check if f1 and f2 have the same underlying [flag.Value].
func areSame(f1, f2 flag.Value) bool {
var (
ref1 = reflect.ValueOf(f1)
ref2 = reflect.ValueOf(f2)
)

if ref1.Comparable() && ref2.Comparable() && f1 == f2 {
return true
}

if ref1.Kind() != ref2.Kind() {
return false
}

if !slices.Contains([]reflect.Kind{reflect.Map, reflect.Pointer, reflect.Func, reflect.Slice}, ref1.Kind()) {
return false
}

return ref1.Pointer() == ref2.Pointer()
}
6 changes: 0 additions & 6 deletions getopt/hiddenvar.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,3 @@ func (h *HiddenVar) String() string {

return h.Value.String()
}

// isHiddenFlag checks if the given flag has a [flag.Value] which indicates that flg is hidden.
func isHiddenFlag(flg *flag.Flag) bool {
hf, ok := flg.Value.(HiddenFlag)
return ok && hf.IsHiddenFlag()
}
82 changes: 82 additions & 0 deletions internal/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package internal

import (
"cmp"
"flag"
"reflect"
"slices"
)

// GroupFlags groups flags by [flag.Value] equivalence and returns them.
//
// The flags are grouped by [flag.Value] equivalence. This allows flags to be grouped together in the rendered
// usage text when two flags are aliases of each other. This is often the case for short flags which are aliases of
// longer flags (e.g. '-a' is an alias of '--all').
//
// -a <string>, --addr=<string>
// -s <string>, --serial-number=<string>
//
// The resulting map entries are keyed by the flag group name, which is the longest flag name in the group. The map
// values are slices of (one or more) flags in the flag group, sorted by flag name length ('-a' before '--all').
//
// Hidden flags are excluded from the resulting map.
func GroupFlags(collected []*flag.Flag) map[string][]*flag.Flag {
// sort flags by name length in descending order to ensure that keys in resulting map will use long names first
slices.SortFunc(collected, func(a, b *flag.Flag) int {
return cmp.Compare(len(b.Name), len(a.Name))
})

groups := map[string][]*flag.Flag{}

for len(collected) > 0 {
var flg *flag.Flag

// pop the head of the slice
flg, collected = collected[0], collected[1:]

// update groups
groups[flg.Name] = []*flag.Flag{flg}

// traverse the flags again and find (and remove) any which match flg
for i := range slices.Backward(collected) {
other := collected[i]

if areSame(flg.Value, other.Value) {
groups[flg.Name] = append(groups[flg.Name], other)
collected = append(collected[:i], collected[i+1:]...)
}
}

// sort by length (then lexical order), this time ascending (-a before --all)
slices.SortFunc(groups[flg.Name], func(a, b *flag.Flag) int {
return cmp.Or(
cmp.Compare(len(a.Name), len(b.Name)),
cmp.Compare(a.Name, b.Name),
)
})
}

return groups
}

// areSame check if f1 and f2 have the same underlying [flag.Value].
func areSame(f1, f2 flag.Value) bool {
var (
ref1 = reflect.ValueOf(f1)
ref2 = reflect.ValueOf(f2)
)

if ref1.Comparable() && ref2.Comparable() && f1 == f2 {
return true
}

if ref1.Kind() != ref2.Kind() {
return false
}

if !slices.Contains([]reflect.Kind{reflect.Map, reflect.Pointer, reflect.Func, reflect.Slice}, ref1.Kind()) {
return false
}

return ref1.Pointer() == ref2.Pointer()
}
Loading
Loading