Three approaches...
- Flags are known up front. 👌🏻
- Flags are defined by user (ugly data structure). ❌
- Flags are defined by user (dynamic struct creation BUT doesn't work fully) ❌
- Flags are defined by user (dynamic population of a struct provided by the user) ✅
Note: the trick to understanding flag.NewFlagSet
is that you have to pass it a string of command line args that you need it to parse. flag.Parse
is able to do this automatically for you, but a flag set is expected to work with a subset of the arguments provided to the program when it's being run. The way I typically do this is as follows...
A simple FlagSet example:
package main
import (
"fmt"
"os"
"github.com/integralist/go-gitbranch/internal/pkg/create"
)
func main() {
args := os.Args[1:]
if len(args) == 0 {
fmt.Println("no subcommand provided")
os.Exit(1)
}
switch args[0] {
case "create":
flags := create.ParseFlags(args[1:])
fmt.Println("name:", flags.Name)
case "rename":
//
case "checkout":
//
case "delete":
//
}
}
////////////////////////////////////////////////////////////////////////
package create
import (
"flag"
)
type Flags struct {
Name string
}
// ParseFlags defines and parses flags for the create subcommand.
func ParseFlags(args []string) Flags {
fs := flag.NewFlagSet("create", flag.ExitOnError)
name := fs.String("name", "", "name of the branch to create")
fs.Parse(args)
return Flags{
Name: *name,
}
}
More context for identifying subcommands dynamically...
// Example: app -debug foo -x 1 -y 2
// define flag(s), in this case a -debug flag, then parse it...
flag.Parse()
// slice args from after the program name "app" (so `args` = `-debug foo -x 1 -y 2`)
args := os.Args[1:]
// identify the command ("foo" in this case)
cmd := identifyCommand(args)
// identify the command's flags ("-x 1 -y 2" in this case)
cmdFlags := commandFlags(cmd, flag.Args())
// identifyCommand parses the arguments provided looking for a 'command'
//
// this implementation presumes that the format of the arguments will be...
//
// <program> <flag(s)> <command> <flag(s) for command>
//
func identifyCommand(args []string) string {
// list of supported/known commands...
commands := []string{"foo", "bar", "baz"}
commandIndex := 0
commandSeen := false
for _, arg := range args {
if commandSeen {
break
}
if strings.HasPrefix(arg, "-") == true {
commandIndex++
continue
}
for _, cmd := range commands {
if arg == cmd.Name {
commandSeen = true
break
}
}
if !commandSeen {
commandIndex++
}
}
if !commandSeen {
return ""
}
return args[commandIndex]
}
// commandFlags parses out the arguments that are meant for the new flag set.
//
// args: flag.Args()
// cmd: the point in the list of arguments where you want to slice from.
//
// Example: app -debug foo -x 1 -y 2
//
// the `cmd` would be foo, and so we want to return a slice starting from the flag -x
// for the FlagSet to parse.
//
func commandFlags(cmd string, args []string) []string {
for i, v := range args {
if v == cmd {
return args[i+1:]
}
}
return []string{}
}
// I've not provided an implementation for commandFlagSet but it basically
// defines a bunch of flags off a cfs variable and returns a *flag.FlagSet
//
// Example:
// cfs := flag.NewFlagSet("foo", flag.ExitOnError)
// cfs.String(...)
//
cfs := commandFlagSet(cmd, cmdFlags)
err := cfs.Parse(cmdFlags)
if err != nil {
// ...
}
// NOTE: flag.Parse type will also have Visit/VisitAll methods.
//
cfs.VisitAll(func(f *flag.Flag) {
// check value of every flag
})