Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command line flag support #328

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 114 additions & 7 deletions mage/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,86 @@ import (
{{range .Imports}}{{.UniqueName}} "{{.Path}}"
{{end}}
)
func stripFlags(fs flag.FlagSet, args []string) []string {
if len(args) == 0 {
return args
}

commands := []string{}

Loop:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a goto in the wild, like finding a yeti 😆

for len(args) > 0 {
s := args[0]
args = args[1:]
switch {
case fs.Lookup(strings.Trim(s, "-")) != nil:
commands = append(commands, s)
case s == "--":
// "--" terminates the flags
break Loop
case strings.HasPrefix(s, "--") && !strings.Contains(s, "="):
// If '--flag arg' then
// delete arg from args.
fallthrough // (do the same as below)
case strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2:
// If '-f arg' then
// delete 'arg' from args or break the loop if len(args) <= 1.
if len(args) <= 1 {
break Loop
} else {
args = args[0:]
continue
}
case strings.HasPrefix(s, "-") && strings.Contains(s, "="):
parts := strings.SplitN(s, "=",2)
if fs.Lookup(strings.Trim(parts[0], "-")) != nil {
commands = append(commands, s)
continue
}
case s != "" && !strings.HasPrefix(s, "-"):
commands = append(commands, s)
}
}

return commands
}

func getFlags(args []string) map[string][]string {
flags := make(map[string][]string)
if len(args) == 0 {
return flags
}

for _, s := range args {
if s == "==" {
break
}

if !strings.HasPrefix(s, "-") {
continue
}

parts := strings.SplitN(s, "=", 2)
key := strings.Trim(parts[0], "-")
if len(parts) == 2 {
flags[key] = append(flags[key], parts[1])
} else {
flags[key] = append(flags[key], "true")
}
}

return flags
}

func main() {
// Use local types and functions in order to avoid name conflicts with additional magefiles.
type arguments struct {
Verbose bool // print out log statements
List bool // print out a list of targets
Help bool // print out help for a specific target
Timeout time.Duration // set a timeout to running the targets
Args []string // args contain the non-flag command-line arguments
Verbose bool // print out log statements
List bool // print out a list of targets
Help bool // print out help for a specific target
Timeout time.Duration // set a timeout to running the targets
Args []string // args contain the non-flag command-line arguments
Flags map[string][]string // flags contains the flag command-line arguments
}

parseBool := func(env string) bool {
Expand Down Expand Up @@ -83,7 +154,8 @@ Options:
-v show verbose output when running targets
` + "`" + `[1:], filepath.Base(os.Args[0]))
}
if err := fs.Parse(os.Args[1:]); err != nil {

if err := fs.Parse(stripFlags(fs, os.Args[1:])); err != nil {
// flag will have printed out an error already.
return
}
Expand Down Expand Up @@ -385,6 +457,8 @@ Options:
for x := 0; x < len(args.Args); {
target := args.Args[x]
x++
flags := getFlags(os.Args[1:])
args.Flags = flags

// resolve aliases
switch strings.ToLower(target) {
Expand All @@ -397,7 +471,40 @@ Options:
switch strings.ToLower(target) {
{{range .Funcs }}
case "{{lower .TargetName}}":
expected := x + {{len .Args}}

if len(flags["help"]) > 0 || len(flags["h"]) > 0 {
fmt.Fprintf(os.Stdout, ` + "`" + `
{{- with .Comment}}

{{.}}

{{ end -}}

Usage:
{{$.BinaryName}} {{lower .TargetName}} [options] {{range .Positional}} <{{.Name}}>{{end}}

Options:
-h, --help show this help
{{- range .Flags}}
--{{.Name}} provide the {{.Name}} argument ({{.FlagType}})
{{- end}}
` + "`" + `)
return
}

validFlags := map[string][]string{}
{{range .Args}}
{{ if .IsFlag }}
if a, ok := flags["{{.Name}}"]; ok {
validFlags["{{.Name}}"] = append(validFlags["{{.Name}}"], a...)
} else {
validFlags["{{.Name}}"] = append(validFlags["{{.Name}}"], {{.ZeroValue}})
}
{{- end}}
{{- end}}
args.Flags = validFlags

expected := x + {{len .Positional}}
if expected > len(args.Args) {
// note that expected and args at this point include the arg for the target itself
// so we subtract 1 here to show the number of args without the target.
Expand Down
6 changes: 6 additions & 0 deletions mg/fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
"time"
)

type StringFlag = string
type BoolFlag = bool
type IntFlag = int
type DurationFlag = time.Duration
type StringSliceFlag = []string

// Fn represents a function that can be run with mg.Deps. Package, Name, and ID must combine to
// uniquely identify a function, while ensuring the "same" function has identical values. These are
// used as a map key to find and run (or not run) the function.
Expand Down
95 changes: 91 additions & 4 deletions parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,34 @@ type Arg struct {
Name, Type string
}

// IsFlag detects if the argument is intended for use as a flag
func (a Arg) IsFlag() bool {
return strings.HasPrefix(a.Type, "mg") && strings.HasSuffix(a.Type, "Flag")
}

// FlagType returns the underlying type for the flag (string, bool, etc)
func (a Arg) FlagType() string {
return strings.TrimSuffix(strings.TrimPrefix(a.Type, "mg."), "Flag")
}

// ZeroValue returns code needed to set the default value for a flag when it is
// undefined. It needs to be a value that can be converted to the correct tyoe
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Typo on the work 'type', on line 70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// undefined. It needs to be a value that can be converted to the correct tyoe
// undefined. It needs to be a value that can be converted to the correct typo

// in ExecCode
func (a Arg) ZeroValue() string {
var z string
switch a.Type {
case "string", "mg.StringFlag", "mg.StringSliceFlag":
z = `""`
case "int", "mg.IntFlag":
z = `"0"`
case "bool", "mg.BoolFlag":
z = `"false"`
case "time.Duration", "mg.DurationFlag":
z = `"0s"`
}
return z
}

// ID returns user-readable information about where this function is defined.
func (f Function) ID() string {
path := "<current>"
Expand All @@ -82,6 +110,28 @@ func (f Function) TargetName() string {
return strings.Join(names, ":")
}

// Flags extracts the Args intended to be defined by flags
func (f Function) Flags() []Arg {
out := []Arg{}
for _, arg := range f.Args {
if arg.IsFlag() {
out = append(out, arg)
}
}
return out
}

// Positional extracts the Args that are positional in nature
func (f Function) Positional() []Arg {
out := []Arg{}
for _, arg := range f.Args {
if !arg.IsFlag() {
out = append(out, arg)
}
}
return out
}

// ExecCode returns code for the template switch to run the target.
// It wraps each target call to match the func(context.Context) error that
// runTarget requires.
Expand Down Expand Up @@ -125,6 +175,39 @@ func (f Function) ExecCode() string {
os.Exit(2)
}
x++`, x)
case "mg.StringFlag":
parseargs += fmt.Sprintf(`
arg%d := args.Flags["%s"][len(args.Flags["%s"])-1]
`, x, arg.Name, arg.Name)
case "mg.BoolFlag":
parseargs += fmt.Sprintf(`
arg%d, err := strconv.ParseBool(args.Flags["%s"][len(args.Flags["%s"])-1])
if err != nil {
logger.Printf("can't convert argument %%q to bool\n", args.Flags["%s"])
os.Exit(2)
}
`, x, arg.Name, arg.Name, arg.Name)
case "mg.StringSliceFlag":
parseargs += fmt.Sprintf(`
arg%d := args.Flags["%s"]
`, x, arg.Name)
case "mg.IntFlag":
parseargs += fmt.Sprintf(`
arg%d, err := strconv.Atoi(args.Flags["%s"][len(args.Flags["%s"])-1])
if err != nil {
logger.Printf("can't convert argument %%q to int\n", args.Flags["%s"])
os.Exit(2)
}
`, x, arg.Name, arg.Name, arg.Name)
case "mg.DurationFlag":
parseargs += fmt.Sprintf(`
arg%d, err := time.ParseDuration(args.Flags["%s"])
if err != nil {
logger.Printf("can't convert argument %%q to time.Duration\n", args.Flags["%s"])
os.Exit(2)
}
`, x, arg.Name, arg.Name)

}
}

Expand Down Expand Up @@ -790,8 +873,12 @@ func toOneLine(s string) string {
}

var argTypes = map[string]string{
"string": "string",
"int": "int",
"&{time Duration}": "time.Duration",
"bool": "bool",
"string": "string",
"int": "int",
"&{time Duration}": "time.Duration",
"bool": "bool",
"&{mg StringFlag}": "mg.StringFlag",
"&{mg BoolFlag}": "mg.BoolFlag",
"&{mg DurationFlag}": "mg.DurationFlag",
"&{mg StringSliceFlag}": "mg.StringSliceFlag",
}
51 changes: 51 additions & 0 deletions site/content/targets/_index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func Build()
func Install(ctx context.Context) error
func Run(what string) error
func Exec(ctx context.Context, name string, count int, debug bool, timeout time.Duration) error
func Greet(name string, loud mg.BoolFlag, count mg.IntFlag, debug mg.BoolFlag, every mg.DurationFlag)
```

A target is effectively a subcommand of mage while running mage in
Expand All @@ -34,6 +35,56 @@ You can intersperse multiple targets with arguments as you'd expect:

`mage run foo.exe exec somename 5 true 100ms`

## Flags

Flags are taken from the CLI arguments after the target name. They are
converted into usable function parameters the same as other arguments. The name
of the flag is the name of the parameter.

Thus you could call Greet above by running:

```
mage greet Adam --loud
mage greet Adam --count=3 --loud
mage greet --every 5m --count=5 adam
```

All flags are optional and may be specified in any order. They can be combined
with standard arguments and multiple targets as you'd expect.

Available flag types are:

```
mg.StringFlag
mg.IntFlag
mg.BoolFlag
mg.DurationFlag
mg.StringSliceFlag
```

Each one, expect `mg.StringSliceFlag`, only allows a single usage, with the
value of the last usage being the one that is used.

The `mg.StringSliceFlag` allows the flag to be used multiple times.

## Help/Usage Text

If a help flag (-h, --help) is added after the target, usage information will
be displayed.

```plain
$ mage say --help
Usage:
mage say [options] <animal>

Options:
-h, --help show this help
--loud provide the loud argument (Bool)
--msgs provide the msgs argument (StringSlice)

```


## Errors

If the function has an error return, errors returned from the function will
Expand Down