Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active March 28, 2024 07:47
Show Gist options
  • Save Integralist/90499f2bb24073ec5eb487020078a582 to your computer and use it in GitHub Desktop.
Save Integralist/90499f2bb24073ec5eb487020078a582 to your computer and use it in GitHub Desktop.
[Golang CLI Flags and Subcommands] #tags: golang, go, cli, flags, subcommands, logs, logging

Three approaches...

  1. Flags are known up front. 👌🏻
  2. Flags are defined by user (ugly data structure). ❌
  3. Flags are defined by user (dynamic struct creation BUT doesn't work fully) ❌
  4. Flags are defined by user (dynamic population of a struct provided by the user) ✅

flag.FlagSet ?

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
})
/*
go run fastly.go diff -foo
Usage of diff:
-foo string
default (default "desc")
go run fastly.go -debug diff // enables debug logs
*/
package main
import (
"flag"
"flags"
"fmt"
"os"
"github.com/sirupsen/logrus"
)
// AppVersion is the application version
const AppVersion = "0.0.1"
var logger *logrus.Entry
func init() {
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetLevel(logrus.InfoLevel)
logger = logrus.WithFields(logrus.Fields{
"package": "main",
})
}
func main() {
f := flags.New()
if *f.Top.Help == true {
flag.PrintDefaults()
os.Exit(1)
}
if *f.Top.Version == true {
fmt.Println(AppVersion)
os.Exit(1)
}
if *f.Top.Debug == true {
logrus.SetLevel(logrus.DebugLevel)
}
logger.Debug("application starting")
args := os.Args[1:] // strip first arg `fastly`
arg, counter := flags.Check(args)
switch arg {
case "diff":
f.Top.Diff.Parse(args[counter:])
case "upload":
f.Top.Upload.Parse(args[counter:])
default:
fmt.Printf("%v is not valid command.\n", arg)
os.Exit(1)
}
}
// flags/flags.go
package flags
import (
"flag"
"os"
"strings"
"github.com/sirupsen/logrus"
)
var logger *logrus.Entry
func init() {
logger = logrus.WithFields(logrus.Fields{
"package": "flags",
})
}
// TopLevelFlags defines the common settings across all commands
type TopLevelFlags struct {
Help, Debug, Version *bool
Token, Service, Directory, Match, Skip *string
Diff, Upload *flag.FlagSet
}
// SubCommandFlags defines the settings for the subcommands
type SubCommandFlags struct {
Foo, Bar *string
}
// Flags defines type of structure returned to user
type Flags struct {
Top TopLevelFlags
Sub SubCommandFlags
}
// New returns defined flags
func New() Flags {
topLevelFlags := TopLevelFlags{
Help: flag.Bool("help", false, "show available flags"),
Debug: flag.Bool("debug", false, "show any error/diff output + debug logs"),
Version: flag.Bool("version", false, "show application version"),
Token: flag.String("token", os.Getenv("FASTLY_API_TOKEN"), "your fastly api token (fallback: FASTLY_API_TOKEN)"),
Service: flag.String("service", os.Getenv("FASTLY_SERVICE_ID"), "your service id (fallback: FASTLY_SERVICE_ID)"),
Directory: flag.String("dir", os.Getenv("VCL_DIRECTORY"), "vcl directory to compare files against"),
Match: flag.String("match", "", "regex for matching vcl directories (will also try: VCL_MATCH_DIRECTORY)"),
Skip: flag.String("skip", "^____", "regex for skipping vcl directories (will also try: VCL_SKIP_DIRECTORY)"),
Diff: flag.NewFlagSet("diff", flag.ExitOnError),
Upload: flag.NewFlagSet("upload", flag.ExitOnError),
}
flag.Parse()
subCommandFlags := SubCommandFlags{
Foo: topLevelFlags.Diff.String("foo", "foo is upload only", "foo default"),
Bar: topLevelFlags.Upload.String("bar", "bar is upload only", "bar default"),
}
return Flags{
Top: topLevelFlags,
Sub: subCommandFlags,
}
}
// Check determines if a flag was specified before the subcommand
// Then returns the subcommand argument value based on the correct index
// Followed by the index of where the subcommand's flags start in the args list
func Check(args []string) (string, int) {
counter := 0
subcommandSeen := false
for _, arg := range args {
if subcommandSeen {
break
}
if strings.HasPrefix(arg, "-") == true {
counter++
continue
}
if arg == "diff" || arg == "upload" {
subcommandSeen = true
} else {
counter++
}
}
subcommandFlagsIndex := counter + 1
logger.WithFields(logrus.Fields{
"args": args,
"counter": counter,
"subcommand": args[counter],
"index": subcommandFlagsIndex,
}).Debug("subcommand selected")
return args[counter], subcommandFlagsIndex
}
package main
import (
"errors"
"flag"
"fmt"
"os"
"strings"
)
var (
ErrNoArgs = errors.New("no flags or commands provided")
ErrCmdFlagParse = errors.New("failed to parse the command flags")
)
type Flag struct {
NameLong string
NameShort string
Default interface{}
Usage string
Value interface{}
}
type Flags []*Flag
type Command struct {
Name string
Flags Flags
}
type Commands []Command
type Schema struct {
Commands Commands
Flags Flags
}
func (s *Schema) Parse() error {
args := os.Args[1:]
if len(args) == 0 {
return ErrNoArgs
}
for _, f := range s.Flags {
switch def := f.Default.(type) {
case bool:
v, _ := f.Value.(bool)
flag.BoolVar(&v, f.NameLong, def, f.Usage)
flag.BoolVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case string:
v, _ := f.Value.(string)
flag.StringVar(&v, f.NameLong, def, f.Usage)
flag.StringVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case int:
v, _ := f.Value.(int)
flag.IntVar(&v, f.NameLong, def, f.Usage)
flag.IntVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case float64:
v, _ := f.Value.(float64)
flag.Float64Var(&v, f.NameLong, def, f.Usage)
flag.Float64Var(&v, f.NameShort, def, f.Usage+" (shorthand)")
}
}
flag.Parse()
flag.VisitAll(func(f *flag.Flag) {
for _, sf := range s.Flags {
if f.Name == sf.NameLong || f.Name == sf.NameShort {
sf.Value = f.Value
}
}
})
cmd := s.identifyCommand(args)
cmdFlags := s.commandFlags(cmd, flag.Args())
for _, c := range s.Commands {
if c.Name == cmd {
cfs := flag.NewFlagSet(c.Name, flag.ExitOnError)
for _, f := range c.Flags {
switch def := f.Default.(type) {
case bool:
v, _ := f.Value.(bool)
cfs.BoolVar(&v, f.NameLong, def, f.Usage)
cfs.BoolVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case string:
v, _ := f.Value.(string)
cfs.StringVar(&v, f.NameLong, def, f.Usage)
cfs.StringVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case int:
v, _ := f.Value.(int)
cfs.IntVar(&v, f.NameLong, def, f.Usage)
cfs.IntVar(&v, f.NameShort, def, f.Usage+" (shorthand)")
case float64:
v, _ := f.Value.(float64)
cfs.Float64Var(&v, f.NameLong, def, f.Usage)
cfs.Float64Var(&v, f.NameShort, def, f.Usage+" (shorthand)")
}
}
err := cfs.Parse(cmdFlags)
if err != nil {
return fmt.Errorf("%s %w", ErrCmdFlagParse, err)
}
cfs.VisitAll(func(f *flag.Flag) {
for _, cf := range c.Flags {
if cf.NameLong == f.Name || cf.NameShort == f.Name {
cf.Value = f.Value
}
}
})
break
}
}
return nil
}
func (s *Schema) identifyCommand(args []string) string {
commandIndex := 0
commandSeen := false
for _, arg := range args {
if commandSeen {
break
}
if strings.HasPrefix(arg, "-") == true {
commandIndex++
continue
}
for _, cmd := range s.Commands {
if arg == cmd.Name {
commandSeen = true
break
}
}
if !commandSeen {
commandIndex++
}
}
if !commandSeen {
return ""
}
return args[commandIndex]
}
func (s *Schema) commandFlags(cmd string, args []string) []string {
for i, v := range args {
if v == cmd {
return args[i+1:]
}
}
return []string{}
}
func main() {
// NOTE: when using verbose syntax and explicitly naming the Flag struct
// type, then the field `Value` can be omitted. But with the short syntax
// used below the compiler complains and so we have to pass `nil`.
//
flags := Flags{
{"debug", "d", false, "description", nil},
{"number", "n", 0, "description", nil},
}
fooFlags := Flags{
{"AAA", "a", "", "description", nil},
{"BBB", "b", 0, "description", nil},
}
barFlags := Flags{
{"CCC", "c", false, "description", nil},
}
commands := Commands{
{"foo", fooFlags},
{"bar", barFlags},
{"baz", nil},
}
schema := &Schema{
Flags: flags,
Commands: commands,
}
err := schema.Parse()
if err != nil {
fmt.Printf("error parsing schema: %v\n", err)
os.Exit(1)
}
// NOTE: after running the following command we can access the values:
//
// go run test_flags.go -debug -n 123 foo -a foobar -b 666
//
fmt.Printf("schema: %+v\n", schema)
fmt.Printf("schema.Flags[0].Value: %v\n", schema.Flags[0].Value)
fmt.Printf("schema.Flags[1].Value: %v\n", schema.Flags[1].Value)
fmt.Printf("schema.Commands[0].Flags[0].Value: %v\n", schema.Commands[0].Flags[0].Value)
fmt.Printf("schema.Commands[0].Flags[1].Value: %v\n", schema.Commands[0].Flags[1].Value)
// TODO: figure out better data strucuture for sake of user experience,
// because having to dip into an array is nasty/fugly
//
}
// this is the consumer of the code
package main
import (
"fmt"
"os"
flag "github.com/integralist/go-flags/flags"
)
func main() {
// standalone flags (i.e. no command necessary)
//
// examples: app -debug
// app -d
//
flags := flag.Flags{
{"debug", "d", "bool", "description"},
}
// flags for the foo command
//
fooFlags := flag.Flags{
{"AAA", "a", "string", "description"},
{"BBB", "b", "int", "description"},
}
// flags for the bar command
//
barFlags := flag.Flags{
{"CCC", "c", "bool", "description"},
}
// commands
//
// examples: app foo
// app foo -a test -b 123
// app bar -c
// app baz
//
commands := flag.Commands{
{"foo", fooFlags},
{"bar", barFlags},
{"baz", nil},
}
schema := &flag.Schema{
Flags: flags,
Commands: commands,
}
// produces the following data structure...
//
// &{
// Commands:[
// {
// Name:foo
// Flags:[
// {Verbose:AAA Short:a Type:string Usage:description}
// {Verbose:BBB Short:b Type:int Usage:description}
// ]
// }
// {
// Name:bar
// Flags:[
// {Verbose:CCC Short:c Type:bool Usage:description}
// ]
// }
// {
// Name:baz
// Flags:[]
// }
// ]
// Flags:[
// {Verbose:debug Short:d Type:bool Usage:description}
// ]
// }
// TODO: should 'no args' really be an error?
//
fd, err := schema.Parse()
if err != nil && err != flag.ErrNoArgs {
fmt.Printf("something unexpected happened: %w", err)
os.Exit(1)
}
if fd.Cmd == "" {
fmt.Println("no recognized flag or command was provided")
} else {
fmt.Println("command executed:", fd.Cmd)
}
// TODO: figure out how to get at the other fields?
//
// fmt.Println(fd.Debug, fd.D, fd.AAA, fd.A, fd.BBB, fd.B, fd.CCC, fd.C)
//
// ...because we can't type assert the returned data structure.
}
// builder.go uses reflection to dynamically generate a struct.
// the struct will contain the parsed flag values.
//
// the struct generation code was extracted from the great work done by:
// https://github.com/Ompluscator/dynamic-struct
// so all credit for that code goes to him.
//
package flags
import (
"reflect"
)
// DynamicStruct contains defined dynamic struct.
// This definition can't be changed anymore, once is built.
// It provides a method for creating new instances of same defintion.
type DynamicStruct interface {
// New provides new instance of defined dynamic struct.
//
// value := dStruct.New()
//
New() interface{}
}
// Builder holds all fields' definitions for desired structs.
type Builder interface {
// AddField creates new struct's field.
// It expects field's name, type and string.
// Type is provided as an instance of some golang type.
// Tag is provided as classical golang field tag.
//
// builder.AddField("SomeFloatField", 0.0, `json:"boolean" validate:"gte=10"`)
//
AddField(name string, typ interface{}, tag string) Builder
// Build returns definition for dynamic struct.
// Definition can be used to create new instances.
//
// dStruct := builder.Build()
//
Build() DynamicStruct
}
type dynamicStructImpl struct {
definition reflect.Type
}
type fieldConfigImpl struct {
typ interface{}
tag string
}
type builderImpl struct {
fields map[string]*fieldConfigImpl
}
func (b *builderImpl) AddField(name string, typ interface{}, tag string) Builder {
b.fields[name] = &fieldConfigImpl{
typ: typ,
tag: tag,
}
return b
}
func (b *builderImpl) Build() DynamicStruct {
var structFields []reflect.StructField
for name, field := range b.fields {
structFields = append(structFields, reflect.StructField{
Name: name,
Type: reflect.TypeOf(field.typ),
Tag: reflect.StructTag(field.tag),
})
}
return &dynamicStructImpl{
definition: reflect.StructOf(structFields),
}
}
func (ds *dynamicStructImpl) New() interface{} {
return reflect.New(ds.definition).Interface()
}
// NewStruct returns new clean instance of Builder interface
// for defining fresh dynamic struct.
//
// builder := NewStruct()
//
func NewStruct() Builder {
return &builderImpl{
fields: map[string]*fieldConfigImpl{},
}
}
package flags
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strings"
)
var (
ErrNoArgs = errors.New("no flags or commands provided")
ErrCmdFlagParse = errors.New("failed to parse the command flags")
ErrInterpolationMarshal = errors.New("unable to parse interpolation map into json")
ErrStructBuild = errors.New("failed to dynamically generate struct")
)
type Data struct {
Cmd string
}
type Flag struct {
Verbose string
Short string
Type string
Usage string
}
type Flags []Flag
type Command struct {
Name string
Flags Flags
}
type CommandParsed struct {
Command
FlagSet *flag.FlagSet
}
type Commands []Command
type Schema struct {
Commands Commands
Flags Flags
}
func (s *Schema) Parse() (*Data, error) {
args := os.Args[1:]
if len(args) == 0 {
return &Data{}, ErrNoArgs
}
for _, f := range s.Flags {
switch f.Type {
case "bool":
var boolType bool
flagValue := false
flagUsage := f.Usage
flag.BoolVar(&boolType, f.Verbose, flagValue, flagUsage)
flag.BoolVar(&boolType, f.Short, flagValue, flagUsage+" (shorthand)")
case "string":
var stringType string
flagValue := ""
flagUsage := f.Usage
flag.StringVar(&stringType, f.Verbose, flagValue, flagUsage)
flag.StringVar(&stringType, f.Short, flagValue, flagUsage+" (shorthand)")
case "int":
var intType int
flagValue := 0
flagUsage := f.Usage
flag.IntVar(&intType, f.Verbose, flagValue, flagUsage)
flag.IntVar(&intType, f.Short, flagValue, flagUsage+" (shorthand)")
case "float":
var floatType float64
flagValue := 0.0
flagUsage := f.Usage
flag.Float64Var(&floatType, f.Verbose, flagValue, flagUsage)
flag.Float64Var(&floatType, f.Short, flagValue, flagUsage+" (shorthand)")
}
}
flag.Parse()
cmd := s.identifyCommand(args)
cmdFlags := s.commandFlags(cmd, flag.Args())
instance := NewStruct()
data := make(map[string]interface{})
data["cmd"] = cmd
flag.VisitAll(func(f *flag.Flag) {
// create zero value struct dynamically
// later on we'll be able to populate it dynamically too
//
for _, sf := range s.Flags {
if sf.Verbose == f.Name || sf.Short == f.Name {
title := strings.Title(f.Name)
tag := fmt.Sprintf(`json:"%s"`, f.Name) // f.Name must be present in data bytes
data[f.Name] = f.Value
switch sf.Type {
case "bool":
instance.AddField(title, false, "")
case "string":
instance.AddField(title, "", tag)
case "int":
instance.AddField(title, 0, tag)
case "float":
instance.AddField(title, 0.0, tag)
}
}
}
})
cfs := s.commandFlagSet(cmd, cmdFlags)
err := cfs.Parse(cmdFlags)
if err != nil {
return &Data{}, fmt.Errorf("%s %w", ErrCmdFlagParse, err)
}
cfs.VisitAll(func(f *flag.Flag) {
// create zero value struct dynamically
// later on we'll be able to populate it dynamically too
//
for _, c := range s.Commands {
if c.Name == cmd {
for _, cf := range c.Flags {
if cf.Verbose == f.Name || cf.Short == f.Name {
title := strings.Title(f.Name)
tag := fmt.Sprintf(`json:"%s"`, f.Name) // f.Name must be present in data bytes
// TODO:
//
// figure out how to protect against a top-level flag and a command
// flag both having the same field name.
//
// e.g. this should be a valid command:
// app -debug foocmd -debug
//
// our current implementation would mean the command's -debug flag
// would override the value in the top-level -debug. ok it's a
// contrived example so it's unlikely to happen but it's not
// impossible to want the same flag at both the top-level and at a
// command specific level.
//
data[f.Name] = f.Value
switch cf.Type {
case "bool":
instance.AddField(title, false, "")
case "string":
instance.AddField(title, "", tag)
case "int":
instance.AddField(title, 0, tag)
case "float":
instance.AddField(title, 0.0, tag)
}
}
}
}
}
})
instance.AddField("Cmd", "", `json:"cmd"`)
ds := instance.Build().New()
j, err := json.Marshal(data)
if err != nil {
return &Data{}, fmt.Errorf("%s %w", ErrInterpolationMarshal, err)
}
fmt.Println("interpolated data:", string(j))
err = json.Unmarshal(j, &ds)
if err != nil {
return &Data{}, fmt.Errorf("%s %w", ErrStructBuild, err)
}
// TODO:
//
// figure out how to type assert interface{} to concrete type?
// otherwise dynamically generating a struct is pointless :grimace:
fmt.Printf("\ndynamic struct data:\n%+v\n\ntype:\n%T\n\n", ds, ds)
// v := reflect.ValueOf(ds).Type()
// var ei interface{} = ds
// d, ok := ei.(*struct {
// AAA string "json:\"AAA\""
// BBB int "json:\"BBB\""
// A string "json:\"a\""
// B int "json:\"b\""
// Cmd string "json:\"cmd\""
// D bool
// Debug bool
// })
// if !ok {
// fmt.Println("uh-oh:", d, ok)
// }
// fmt.Println(d.Cmd, d.Debug)
return &Data{
Cmd: cmd,
}, nil
}
func (s *Schema) commandFlagSet(cmd string, cmdFlags []string) *flag.FlagSet {
for _, c := range s.Commands {
if c.Name == cmd {
cfs := flag.NewFlagSet(c.Name, flag.ExitOnError)
for _, f := range c.Flags {
switch f.Type {
case "bool":
var boolType bool
flagValue := false
flagUsage := f.Usage
cfs.BoolVar(&boolType, f.Verbose, flagValue, flagUsage)
cfs.BoolVar(&boolType, f.Short, flagValue, flagUsage+" (shorthand)")
case "string":
var stringType string
flagValue := ""
flagUsage := f.Usage
cfs.StringVar(&stringType, f.Verbose, flagValue, flagUsage)
cfs.StringVar(&stringType, f.Short, flagValue, flagUsage+" (shorthand)")
case "int":
var intType int
flagValue := 0
flagUsage := f.Usage
cfs.IntVar(&intType, f.Verbose, flagValue, flagUsage)
cfs.IntVar(&intType, f.Short, flagValue, flagUsage+" (shorthand)")
case "float":
var floatType float64
flagValue := 0.0
flagUsage := f.Usage
cfs.Float64Var(&floatType, f.Verbose, flagValue, flagUsage)
cfs.Float64Var(&floatType, f.Short, flagValue, flagUsage+" (shorthand)")
}
}
return cfs
}
}
return &flag.FlagSet{}
}
func (s *Schema) commandFlags(cmd string, args []string) []string {
for i, v := range args {
if v == cmd {
return args[i+1:]
}
}
return []string{}
}
func (s *Schema) identifyCommand(args []string) string {
commandIndex := 0
commandSeen := false
for _, arg := range args {
if commandSeen {
break
}
if strings.HasPrefix(arg, "-") == true {
commandIndex++
continue
}
for _, cmd := range s.Commands {
if arg == cmd.Name {
commandSeen = true
break
}
}
if !commandSeen {
commandIndex++
}
}
if !commandSeen {
return ""
}
return args[commandIndex]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment