Skip to content

Commit

Permalink
Add flags support (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristaloleg committed Sep 5, 2022
1 parent e0b9131 commit bf7d1f2
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 42 deletions.
47 changes: 28 additions & 19 deletions GUIDE.md
Expand Up @@ -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
}
```
Expand Down
34 changes: 29 additions & 5 deletions acmd.go
Expand Up @@ -3,6 +3,7 @@ package acmd
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <command> [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)
Expand All @@ -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 = "<no description>"
}
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()
Expand Down
46 changes: 28 additions & 18 deletions example_test.go
Expand Up @@ -42,6 +42,7 @@ func ExampleRunner() {
}
return nil
},
FlagSet: &commandFlags{},
},
{
Name: "status",
Expand Down Expand Up @@ -97,6 +98,7 @@ func ExampleHelp() {
{
Name: "boom",
ExecFunc: nopFunc,
FlagSet: &generalFlags{},
},
}

Expand All @@ -113,7 +115,8 @@ func ExampleHelp() {
panic(err)
}

// Output: Example of acmd package
// Output:
// Example of acmd package
//
// Usage:
//
Expand Down Expand Up @@ -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
},
Expand All @@ -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
}

0 comments on commit bf7d1f2

Please sign in to comment.