From bf7d1f2f9bab9f0eb7ed44afa3b1217b1df958b6 Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Mon, 5 Sep 2022 23:21:01 +0200 Subject: [PATCH] Add flags support (#39) --- GUIDE.md | 47 ++++++++++++++++++++++++++++------------------- acmd.go | 34 +++++++++++++++++++++++++++++----- example_test.go | 46 ++++++++++++++++++++++++++++------------------ 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index c4c823a..b6222e7 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -64,40 +64,49 @@ func run() error { There is no special methods, config fields to propagate flags to subcommands. However it's not hard to make this, because every command can access predefined flags, which are shared across handlers. ```go -type commonFlags struct { +// generalFlags can be used as flags for all command +type generalFlags struct { IsVerbose bool + Dir string } -// NOTE: should be added before flag.FlagSet method Parse(). -func withCommonFlags(fs *flag.FlagSet) *commonFlags { - c := &commonFlags{} +func (c *generalFlags) Flags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) fs.BoolVar(&c.IsVerbose, "verbose", false, "should app be verbose") - return c + fs.StringVar(&c.Dir, "dir", ".", "directory to process") + return fs } -func cmdFoo(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("foo", flag.ContinueOnError) - // NOTE: here add flags for cmdBar as always +// commandFlags is a flags for a command +// using struct embedding we can inherit other flags +type commandFlags struct { + generalFlags + File string +} + +func (c *commandFlags) Flags() *flag.FlagSet { + fs := c.generalFlags.Flags() + fs.StringVar(&c.File, "file", "input.txt", "file to process") + return fs +} - // add common flags, make sure it's before Parse but after all defined flags - common := withCommonFlags(fs) - if err := fs.Parse(args); err != nil { +func cmdFoo(ctx context.Context, args []string) error { + var cfg generalFlags + if fs := cfg.Flags().Parse(args); err != nil{ return err } - // use commonFlags fields or any other flags that you have defined + + // use cfg fields or any other flags that you have defined return nil } func cmdBar(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("bar", flag.ContinueOnError) - // NOTE: here add flags for cmdFoo as always - - // add common flags, make sure it's before Parse but after all defined flags - common := withCommonFlags(fs) - if err := fs.Parse(args); err != nil { + var cfg commandFlags + if fs := cfg.Flags().Parse(args); err != nil{ return err } - // use commonFlags fields or any other flags that you have defined + + // use cfg fields or any other flags that you have defined return nil } ``` diff --git a/acmd.go b/acmd.go index 2eff864..f180c8e 100644 --- a/acmd.go +++ b/acmd.go @@ -3,6 +3,7 @@ package acmd import ( "context" "errors" + "flag" "fmt" "io" "os" @@ -53,6 +54,15 @@ type Command struct { // IsHidden reports whether command should not be show in help. Default false. IsHidden bool + + // FlagSet is an optional field where you can provide command's flags. + // Is used for autocomplete. Works best with https://github.com/cristalhq/flagx + FlagSet FlagSetter +} + +// FlagSetter returns flags for the command. See examples. +type FlagSetter interface { + Flags() *flag.FlagSet } // simple way to get exec function @@ -101,6 +111,9 @@ type Config struct { // Usage of the application, if nil default will be used. Usage func(cfg Config, cmds []Command) + + // VerboseHelp if "./app help -v" is passed, default is false. + VerboseHelp bool } // HasHelpFlag reports whether help flag is presented in args. @@ -150,7 +163,7 @@ func (r *Runner) init() error { } if r.cfg.Usage == nil { - r.cfg.Usage = defaultUsage(r.cfg.Output) + r.cfg.Usage = defaultUsage(r) } r.args = r.cfg.Args @@ -344,14 +357,15 @@ func suggestCommand(got string, cmds []Command) string { return match } -func defaultUsage(w io.Writer) func(cfg Config, cmds []Command) { +func defaultUsage(r *Runner) func(cfg Config, cmds []Command) { return func(cfg Config, cmds []Command) { + w := r.cfg.Output if cfg.AppDescription != "" { fmt.Fprintf(w, "%s\n\n", cfg.AppDescription) } fmt.Fprintf(w, "Usage:\n\n %s [arguments...]\n\nThe commands are:\n\n", cfg.AppName) - printCommands(w, cmds) + printCommands(r.cfg, cmds) if cfg.PostDescription != "" { fmt.Fprintf(w, "%s\n\n", cfg.PostDescription) @@ -363,18 +377,28 @@ func defaultUsage(w io.Writer) func(cfg Config, cmds []Command) { } // printCommands in a table form (Name and Description) -func printCommands(w io.Writer, cmds []Command) { +func printCommands(cfg Config, cmds []Command) { minwidth, tabwidth, padding, padchar, flags := 0, 0, 11, byte(' '), uint(0) - tw := tabwriter.NewWriter(w, minwidth, tabwidth, padding, padchar, flags) + tw := tabwriter.NewWriter(cfg.Output, minwidth, tabwidth, padding, padchar, flags) for _, cmd := range cmds { if cmd.IsHidden { continue } + desc := cmd.Description if desc == "" { desc = "" } fmt.Fprintf(tw, " %s\t%s\n", cmd.Name, desc) + + if cfg.VerboseHelp && cmd.FlagSet != nil { + fset := cmd.FlagSet.Flags() + old := fset.Output() + fmt.Fprintf(tw, " ") + fset.SetOutput(tw) + fset.Usage() + fset.SetOutput(old) + } } fmt.Fprint(tw, "\n") tw.Flush() diff --git a/example_test.go b/example_test.go index 1ddf044..2d98532 100644 --- a/example_test.go +++ b/example_test.go @@ -42,6 +42,7 @@ func ExampleRunner() { } return nil }, + FlagSet: &commandFlags{}, }, { Name: "status", @@ -97,6 +98,7 @@ func ExampleHelp() { { Name: "boom", ExecFunc: nopFunc, + FlagSet: &generalFlags{}, }, } @@ -113,7 +115,8 @@ func ExampleHelp() { panic(err) } - // Output: Example of acmd package + // Output: + // Example of acmd package // // Usage: // @@ -308,27 +311,24 @@ func ExamplePropagateFlags() { cmds := []acmd.Command{ { Name: "foo", ExecFunc: func(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("foo", flag.ContinueOnError) - isRecursive := fs.Bool("r", false, "should file list be recursive") - common := withCommonFlags(fs) - if err := fs.Parse(args); err != nil { + var cfg generalFlags + if err := cfg.Flags().Parse(args); err != nil { return err } - if common.IsVerbose { - fmt.Fprintf(buf, "TODO: dir %q, is recursive = %v\n", common.Dir, *isRecursive) + if cfg.IsVerbose { + fmt.Fprintf(buf, "TODO: dir %q, is verbose = %v\n", cfg.Dir, cfg.IsVerbose) } return nil }, }, { Name: "bar", ExecFunc: func(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("bar", flag.ContinueOnError) - common := withCommonFlags(fs) - if err := fs.Parse(args); err != nil { + var cfg commandFlags + if err := cfg.Flags().Parse(args); err != nil { return err } - if common.IsVerbose { - fmt.Fprintf(buf, "TODO: dir %q\n", common.Dir) + if cfg.IsVerbose { + fmt.Fprintf(buf, "TODO: dir %q\n", cfg.Dir) } return nil }, @@ -348,18 +348,28 @@ func ExamplePropagateFlags() { } fmt.Println(buf.String()) - // Output: TODO: dir "test-dir", is recursive = false + // Output: TODO: dir "test-dir", is verbose = true } -type commonFlags struct { +type generalFlags struct { IsVerbose bool Dir string } -// NOTE: should be added before flag.FlagSet method Parse(). -func withCommonFlags(fs *flag.FlagSet) *commonFlags { - c := &commonFlags{} +func (c *generalFlags) Flags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) fs.BoolVar(&c.IsVerbose, "verbose", false, "should app be verbose") fs.StringVar(&c.Dir, "dir", ".", "directory to process") - return c + return fs +} + +type commandFlags struct { + generalFlags + File string +} + +func (c *commandFlags) Flags() *flag.FlagSet { + fs := c.generalFlags.Flags() + fs.StringVar(&c.File, "file", "input.txt", "file to process") + return fs }