diff --git a/help-util.go b/help-util.go new file mode 100644 index 0000000000000000000000000000000000000000..3a153d09d5b2def425cb1d3c12706ca3d092fd14 --- /dev/null +++ b/help-util.go @@ -0,0 +1,138 @@ +/** +* Copyright (c) 2009 The Go Authors. All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are +* met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above +* copyright notice, this list of conditions and the following disclaimer +* in the documentation and/or other materials provided with the +* distribution. +* * Neither the name of Google Inc. nor the names of its +* contributors may be used to endorse or promote products derived from +* this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +package xflags + +// this file contain adapted standard function from flag.go + +import ( + "flag" + "fmt" + "reflect" + "strings" +) + +func getFlagTable(f *flag.FlagSet) []string { + var isZeroValueErrs []error + + result := []string{} + + f.VisitAll(func(f *flag.Flag) { + var b strings.Builder + fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments. + + name, usage := flag.UnquoteUsage(f) + if len(name) > 0 { + b.WriteString(" ") + b.WriteString(name) + } + // Boolean flags of one ASCII letter are so common we + // treat them specially, putting their usage on the same line. + if b.Len() <= 4 { // space, space, '-', 'x'. + b.WriteString("\t") + } else { + // Four spaces before the tab triggers good alignment + // for both 4- and 8-space tab stops. + b.WriteString("\n \t") + } + b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) + + // Print the default value only if it differs to the zero value + // for this flag type. + if isZero, err := isZeroValue(f, f.DefValue); err != nil { + isZeroValueErrs = append(isZeroValueErrs, err) + } else if !isZero { + if _, ok := f.Value.(*stringValue); ok { + // put quotes on the value + fmt.Fprintf(&b, " (default %q)", f.DefValue) + } else { + fmt.Fprintf(&b, " (default %v)", f.DefValue) + } + } + + result = append(result, b.String()) + + }) + // If calling String on any zero flag.Values triggered a panic, print + // the messages after the full set of defaults so that the programmer + // knows to fix the panic. + if errs := isZeroValueErrs; len(errs) > 0 { + fmt.Fprintln(f.Output()) + for _, err := range errs { + result = append(result, err.Error()) + } + } + + return result +} + +// isZeroValue determines whether the string represents the zero +// value for a flag. +func isZeroValue(f *flag.Flag, value string) (ok bool, err error) { + // Build a zero value of the flag's Value type, and see if the + // result of calling its String method equals the value passed in. + // This works unless the Value type is itself an interface type. + typ := reflect.TypeOf(f.Value) + var z reflect.Value + if typ.Kind() == reflect.Pointer { + z = reflect.New(typ.Elem()) + } else { + z = reflect.Zero(typ) + } + // Catch panics calling the String method, which shouldn't prevent the + // usage message from being printed, but that we should report to the + // user so that they know to fix their code. + defer func() { + if e := recover(); e != nil { + if typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + err = fmt.Errorf("panic calling String method on zero %v for flag %s: %v", typ, f.Name, e) + } + }() + return value == z.Interface().(flag.Value).String(), nil +} + +// -- string Value +type stringValue string + +func newStringValue(val string, p *string) *stringValue { + *p = val + return (*stringValue)(p) +} + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} + +func (s *stringValue) Get() any { return string(*s) } + +func (s *stringValue) String() string { return string(*s) } diff --git a/help.go b/help.go new file mode 100644 index 0000000000000000000000000000000000000000..2ba9127228f81e646c334b1c86ca8c820c7b3bde --- /dev/null +++ b/help.go @@ -0,0 +1,69 @@ +package xflags + +import ( + "strings" +) + +func (c *cmd[C]) getCommandLevel() (*cmd[C], []string) { + + result := c + + path := []string{} + path = append(path, c.name) + + for _, c := range c.commands { + + if c.flagSet.Parsed() { + var p []string + result, p = c.getCommandLevel() + path = append(path, p...) + break + } + } + + return result, path +} + +// Help returns the help text for the command +func (s *Settings[C]) Help() string { + + cmd, path := s.command.getCommandLevel() + + h := strings.Join(path, " ") + var help string + + help = "Usage: " + h + " " + + g := getFlagTable(cmd.settings.command.flagSet) + if len(g) > 0 { + help += "[global options] " + } + + if len(cmd.commands) > 0 { + help += "[command] " + } + + help += "[arguments]" + help += "\n" + + if len(g) > 0 { + help += "\nGlobal Options:\n" + help += strings.Join(g, "\n") + "\n" + } + + if len(cmd.commands) > 0 { + for _, c := range cmd.commands { + help += "\nCommand: " + c.name + "\n" + //help += fmt.Sprintf(" %s\t%s", c.name, c.tagMapping[tagDescription]) + s := getFlagTable(c.flagSet) + + if len(s) > 0 { + help += "\nOptions:\n" + help += strings.Join(s, "\n") + "\n" + } + + } + } + + return help +} diff --git a/help_test.go b/help_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3bebc4adae18dac0d64b557fc0fa5ac96e05d2f4 --- /dev/null +++ b/help_test.go @@ -0,0 +1,47 @@ +package xflags + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type CmdTestHelp1 struct { + A bool `short:"a" description:"Message A"` + Sub1 struct { + B bool `short:"b" description:"Message B"` + Sub2 struct { + C bool `short:"c" description:"Message C"` + Sub3 struct { + D bool `short:"d" description:"Message D"` + } `command:"sub3"` + Sub4 struct { + E bool `short:"e" description:"Message E"` + } `command:"sub4"` + } `command:"sub2"` + } `command:"sub1"` + aa int `short:"x" description:"Message X"` +} + +func TestGetHelp(t *testing.T) { + + tables := []struct { + name string + args []string + substring string + }{ + {"test1", []string{"sub1", "--help"}, "Usage: test1 sub1 [global options] [command] [arguments]"}, + {"test1", []string{"xsd", "help"}, "Usage: test1 [global options] [command] [arguments]"}, + {"test2", []string{"sub1", "sub2"}, "Usage: test2 sub1 sub2 [global options] [command] [arguments]"}, + } + + for _, table := range tables { + t.Run(table.name, func(t *testing.T) { + s := New(table.name, CmdTestHelp1{}) + s.Parse(table.args) + help := s.Help() + assert.NotEmpty(t, help) + assert.Contains(t, help, table.substring) + }) + } + +}