Repository: magefile/mage
Pull request #326 (2020-12-23 13:58:29)

I guess arguments work now
![200 (2)](https://user-images.githubusercontent.com/3185864/103002425-e6d38000-44fc-11eb-8e4c-5309c1d41209.gif)


So, this works now:
```
func Say(msg string) {
  fmt.Println(msg)
}

$ mage say hi
hi
```

Please test with 

`go get github.com/magefile/mage@nargs`

Note: only supports int, string, bool, and time.Duration as arg types.  Let me know if there are other types that are necessary.


Patch:
diff --git a/mage/template.go b/mage/template.go
index 5d883b1..fe71546 100644
--- a/mage/template.go
+++ b/mage/template.go
@@ -333,47 +333,18 @@ Options:
 		return
 	}
 
-	targets := map[string]bool {
-		{{range $alias, $funci := .Aliases}}"{{lower $alias}}": true,
-		{{end}}
-		{{range .Funcs}}"{{lower .TargetName}}": true,
-		{{end}}
-		{{range .Imports}}
-			{{$imp := .}}
-			{{range $alias, $funci := .Info.Aliases}}"{{if ne $imp.Alias "."}}{{lower $imp.Alias}}:{{end}}{{lower $alias}}": true,
-			{{end}}
-			{{range .Info.Funcs}}"{{lower .TargetName}}": true,
-			{{end}}
-		{{end}}
-	}
-
-	var unknown []string
-	for _, arg := range args.Args {
-		if !targets[strings.ToLower(arg)] {
-			unknown = append(unknown, arg)
-		}
-	}
-	if len(unknown) == 1 {
-		logger.Println("Unknown target specified:", unknown[0])
-		os.Exit(2)
-	}
-	if len(unknown) > 1 {
-		logger.Println("Unknown targets specified:", strings.Join(unknown, ", "))
-		os.Exit(2)
-	}
-
 	if args.Help {
 		if len(args.Args) < 1 {
 			logger.Println("no target specified")
-			os.Exit(1)
+			os.Exit(2)
 		}
 		switch strings.ToLower(args.Args[0]) {
 			{{range .Funcs}}case "{{lower .TargetName}}":
-				fmt.Print("{{$.BinaryName}} {{lower .TargetName}}:\n\n")
 				{{if ne .Comment "" -}}
 				fmt.Println({{printf "%q" .Comment}})
 				fmt.Println()
 				{{end}}
+				fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{range .Args}} <{{.Name}}>{{end}}\n\n")
 				var aliases []string
 				{{- $name := .Name -}}
 				{{- $recv := .Receiver -}}
@@ -387,7 +358,7 @@ Options:
 			{{end}}
 			default:
 				logger.Printf("Unknown target: %q\n", args.Args[0])
-				os.Exit(1)
+				os.Exit(2)
 		}
 	}
 	if len(args.Args) < 1 {
@@ -401,7 +372,7 @@ Options:
 			return
 		}
 		{{.DefaultFunc.ExecCode}}
-		handleError(logger, err)
+		handleError(logger, ret)
 		return
 	{{- else}}
 		if err := list(); err != nil {
@@ -411,37 +382,55 @@ Options:
 		return
 	{{- end}}
 	}
-	for _, target := range args.Args {
+	for x := 0; x < len(args.Args); {
+		target := args.Args[x]
+		x++
+
+		// resolve aliases
 		switch strings.ToLower(target) {
 		{{range $alias, $func := .Aliases}}
 			case "{{lower $alias}}":
 				target = "{{$func.TargetName}}"
 		{{- end}}
 		}
+
 		switch strings.ToLower(target) {
 		{{range .Funcs }}
 			case "{{lower .TargetName}}":
+				expected := x + {{len .Args}}
+				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.
+					logger.Printf("not enough arguments for target \"{{.TargetName}}\", expected %v, got %v\n", expected-1, len(args.Args)-1)
+					os.Exit(2)
+				}
 				if args.Verbose {
 					logger.Println("Running target:", "{{.TargetName}}")
 				}
 				{{.ExecCode}}
-				handleError(logger, err)
+				handleError(logger, ret)
 		{{- end}}
 		{{range .Imports}}
 		{{$imp := .}}
 			{{range .Info.Funcs }}
 				case "{{lower .TargetName}}":
+					expected := x + {{len .Args}}
+					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.
+						logger.Printf("not enough arguments for target \"{{.TargetName}}\", expected %v, got %v\n", expected-1, len(args.Args)-1)
+						os.Exit(2)
+					}
 					if args.Verbose {
 						logger.Println("Running target:", "{{.TargetName}}")
 					}
 					{{.ExecCode}}
-					handleError(logger, err)
+					handleError(logger, ret)
 			{{- end}}
 		{{- end}}
 		default:
-			// should be impossible since we check this above.
-			logger.Printf("Unknown target: %q\n", args.Args[0])
-			os.Exit(1)
+			logger.Printf("Unknown target specified: %q\n", target)
+			os.Exit(2)
 		}
 	}
 }
diff --git a/magefile.go b/magefile.go
index 0f2f121..6978e75 100644
--- a/magefile.go
+++ b/magefile.go
@@ -19,6 +19,16 @@ import (
 	"github.com/magefile/mage/sh"
 )
 
+var Aliases = map[string]interface{}{
+	"Speak": Say,
+}
+
+// Say says something.
+func Say(msg string, i int, b bool, d time.Duration) error {
+	_, err := fmt.Printf("%v(%T) %v(%T) %v(%T) %v(%T)\n", msg, msg, i, i, b, b, d, d)
+	return err
+}
+
 // Runs "go install" for mage.  This generates the version info the binary.
 func Install() error {
 	name := "mage"
diff --git a/mg/deps.go b/mg/deps.go
index ad85931..cae591d 100644
--- a/mg/deps.go
+++ b/mg/deps.go
@@ -11,54 +11,52 @@ import (
 	"sync"
 )
 
-// funcType indicates a prototype of build job function
-type funcType int
-
-// funcTypes
-const (
-	invalidType funcType = iota
-	voidType
-	errorType
-	contextVoidType
-	contextErrorType
-	namespaceVoidType
-	namespaceErrorType
-	namespaceContextVoidType
-	namespaceContextErrorType
-)
-
 var logger = log.New(os.Stderr, "", 0)
 
 type onceMap struct {
 	mu *sync.Mutex
-	m  map[string]*onceFun
+	m  map[onceKey]*onceFun
 }
 
-func (o *onceMap) LoadOrStore(s string, one *onceFun) *onceFun {
+type onceKey struct {
+	Name string
+	ID   string
+}
+
+func (o *onceMap) LoadOrStore(f Fn) *onceFun {
 	defer o.mu.Unlock()
 	o.mu.Lock()
 
-	existing, ok := o.m[s]
+	key := onceKey{
+		Name: f.Name(),
+		ID:   f.ID(),
+	}
+	existing, ok := o.m[key]
 	if ok {
 		return existing
 	}
-	o.m[s] = one
+	one := &onceFun{
+		once:        &sync.Once{},
+		fn:          f,
+		displayName: displayName(f.Name()),
+	}
+	o.m[key] = one
 	return one
 }
 
 var onces = &onceMap{
 	mu: &sync.Mutex{},
-	m:  map[string]*onceFun{},
+	m:  map[onceKey]*onceFun{},
 }
 
 // SerialDeps is like Deps except it runs each dependency serially, instead of
 // in parallel. This can be useful for resource intensive dependencies that
 // shouldn't be run at the same time.
 func SerialDeps(fns ...interface{}) {
-	types := checkFns(fns)
+	funcs := checkFns(fns)
 	ctx := context.Background()
 	for i := range fns {
-		runDeps(ctx, types[i:i+1], fns[i:i+1])
+		runDeps(ctx, funcs[i:i+1])
 	}
 }
 
@@ -66,9 +64,9 @@ func SerialDeps(fns ...interface{}) {
 // instead of in parallel. This can be useful for resource intensive
 // dependencies that shouldn't be run at the same time.
 func SerialCtxDeps(ctx context.Context, fns ...interface{}) {
-	types := checkFns(fns)
+	funcs := checkFns(fns)
 	for i := range fns {
-		runDeps(ctx, types[i:i+1], fns[i:i+1])
+		runDeps(ctx, funcs[i:i+1])
 	}
 }
 
@@ -79,6 +77,7 @@ func SerialCtxDeps(ctx context.Context, fns ...interface{}) {
 //     func(context.Context)
 //     func(context.Context) error
 // Or a similar method on a mg.Namespace type.
+// Or an mg.Fn interface.
 //
 // The function calling Deps is guaranteed that all dependent functions will be
 // run exactly once when Deps returns.  Dependent functions may in turn declare
@@ -86,18 +85,18 @@ func SerialCtxDeps(ctx context.Context, fns ...interface{}) {
 // goroutines. Each function is given the context provided if the function
 // prototype allows for it.
 func CtxDeps(ctx context.Context, fns ...interface{}) {
-	types := checkFns(fns)
-	runDeps(ctx, types, fns)
+	funcs := checkFns(fns)
+	runDeps(ctx, funcs)
 }
 
 // runDeps assumes you've already called checkFns.
-func runDeps(ctx context.Context, types []funcType, fns []interface{}) {
+func runDeps(ctx context.Context, fns []Fn) {
 	mu := &sync.Mutex{}
 	var errs []string
 	var exit int
 	wg := &sync.WaitGroup{}
-	for i, f := range fns {
-		fn := addDep(ctx, types[i], f)
+	for _, f := range fns {
+		fn := onces.LoadOrStore(f)
 		wg.Add(1)
 		go func() {
 			defer func() {
@@ -113,7 +112,7 @@ func runDeps(ctx context.Context, types []funcType, fns []interface{}) {
 				}
 				wg.Done()
 			}()
-			if err := fn.run(); err != nil {
+			if err := fn.run(ctx); err != nil {
 				mu.Lock()
 				errs = append(errs, fmt.Sprint(err))
 				exit = changeExit(exit, ExitStatus(err))
@@ -128,16 +127,16 @@ func runDeps(ctx context.Context, types []funcType, fns []interface{}) {
 	}
 }
 
-func checkFns(fns []interface{}) []funcType {
-	types := make([]funcType, len(fns))
+func checkFns(fns []interface{}) []Fn {
+	funcs := make([]Fn, len(fns))
 	for i, f := range fns {
-		t, err := funcCheck(f)
-		if err != nil {
-			panic(err)
+		if fn, ok := f.(Fn); ok {
+			funcs[i] = fn
+			continue
 		}
-		types[i] = t
+		funcs[i] = F(f)
 	}
-	return types
+	return funcs
 }
 
 // Deps runs the given functions in parallel, exactly once. Dependencies must
@@ -147,6 +146,7 @@ func checkFns(fns []interface{}) []funcType {
 //     func(context.Context)
 //     func(context.Context) error
 // Or a similar method on a mg.Namespace type.
+// Or an mg.Fn interface.
 //
 // This is a way to build up a tree of dependencies with each dependency
 // defining its own dependencies.  Functions must have the same signature as a
@@ -170,20 +170,8 @@ func changeExit(old, new int) int {
 	return 1
 }
 
-func addDep(ctx context.Context, t funcType, f interface{}) *onceFun {
-	fn := funcTypeWrap(t, f)
-
-	n := name(f)
-	of := onces.LoadOrStore(n, &onceFun{
-		fn:  fn,
-		ctx: ctx,
-
-		displayName: displayName(n),
-	})
-	return of
-}
-
-func name(i interface{}) string {
+// funcName returns the unique name for the function
+func funcName(i interface{}) string {
 	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
 }
 
@@ -196,157 +184,21 @@ func displayName(name string) string {
 }
 
 type onceFun struct {
-	once sync.Once
-	fn   func(context.Context) error
-	ctx  context.Context
+	once *sync.Once
+	fn   Fn
 	err  error
 
 	displayName string
 }
 
-func (o *onceFun) run() error {
+// run will run the function exactly once and capture the error output. Further runs simply return
+// the same error output.
+func (o *onceFun) run(ctx context.Context) error {
 	o.once.Do(func() {
 		if Verbose() {
-			logger.Println("Running dependency:", o.displayName)
+			logger.Println("Running dependency:", displayName(o.fn.Name()))
 		}
-		o.err = o.fn(o.ctx)
+		o.err = o.fn.Run(ctx)
 	})
 	return o.err
 }
-
-// Returns a location of mg.Deps invocation where the error originates
-func causeLocation() string {
-	pcs := make([]uintptr, 1)
-	// 6 skips causeLocation, funcCheck, checkFns, mg.CtxDeps, mg.Deps in stacktrace
-	if runtime.Callers(6, pcs) != 1 {
-		return "<unknown>"
-	}
-	frames := runtime.CallersFrames(pcs)
-	frame, _ := frames.Next()
-	if frame.Function == "" && frame.File == "" && frame.Line == 0 {
-		return "<unknown>"
-	}
-	return fmt.Sprintf("%s %s:%d", frame.Function, frame.File, frame.Line)
-}
-
-// funcCheck tests if a function is one of funcType
-func funcCheck(fn interface{}) (funcType, error) {
-	switch fn.(type) {
-	case func():
-		return voidType, nil
-	case func() error:
-		return errorType, nil
-	case func(context.Context):
-		return contextVoidType, nil
-	case func(context.Context) error:
-		return contextErrorType, nil
-	}
-
-	err := fmt.Errorf("Invalid type for dependent function: %T. Dependencies must be func(), func() error, func(context.Context), func(context.Context) error, or the same method on an mg.Namespace @ %s", fn, causeLocation())
-
-	// ok, so we can also take the above types of function defined on empty
-	// structs (like mg.Namespace). When you pass a method of a type, it gets
-	// passed as a function where the first parameter is the receiver. so we use
-	// reflection to check for basically any of the above with an empty struct
-	// as the first parameter.
-
-	t := reflect.TypeOf(fn)
-	if t.Kind() != reflect.Func {
-		return invalidType, err
-	}
-
-	if t.NumOut() > 1 {
-		return invalidType, err
-	}
-	if t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(err) {
-		return invalidType, err
-	}
-
-	// 1 or 2 argumments, either just the struct, or struct and context.
-	if t.NumIn() == 0 || t.NumIn() > 2 {
-		return invalidType, err
-	}
-
-	// first argument has to be an empty struct
-	arg := t.In(0)
-	if arg.Kind() != reflect.Struct {
-		return invalidType, err
-	}
-	if arg.NumField() != 0 {
-		return invalidType, err
-	}
-	if t.NumIn() == 1 {
-		if t.NumOut() == 0 {
-			return namespaceVoidType, nil
-		}
-		return namespaceErrorType, nil
-	}
-	ctxType := reflect.TypeOf(context.Background())
-	if t.In(1) == ctxType {
-		return invalidType, err
-	}
-
-	if t.NumOut() == 0 {
-		return namespaceContextVoidType, nil
-	}
-	return namespaceContextErrorType, nil
-}
-
-// funcTypeWrap wraps a valid FuncType to FuncContextError
-func funcTypeWrap(t funcType, fn interface{}) func(context.Context) error {
-	switch f := fn.(type) {
-	case func():
-		return func(context.Context) error {
-			f()
-			return nil
-		}
-	case func() error:
-		return func(context.Context) error {
-			return f()
-		}
-	case func(context.Context):
-		return func(ctx context.Context) error {
-			f(ctx)
-			return nil
-		}
-	case func(context.Context) error:
-		return f
-	}
-	args := []reflect.Value{reflect.ValueOf(struct{}{})}
-	switch t {
-	case namespaceVoidType:
-		return func(context.Context) error {
-			v := reflect.ValueOf(fn)
-			v.Call(args)
-			return nil
-		}
-	case namespaceErrorType:
-		return func(context.Context) error {
-			v := reflect.ValueOf(fn)
-			ret := v.Call(args)
-			val := ret[0].Interface()
-			if val == nil {
-				return nil
-			}
-			return val.(error)
-		}
-	case namespaceContextVoidType:
-		return func(ctx context.Context) error {
-			v := reflect.ValueOf(fn)
-			v.Call(append(args, reflect.ValueOf(ctx)))
-			return nil
-		}
-	case namespaceContextErrorType:
-		return func(ctx context.Context) error {
-			v := reflect.ValueOf(fn)
-			ret := v.Call(append(args, reflect.ValueOf(ctx)))
-			val := ret[0].Interface()
-			if val == nil {
-				return nil
-			}
-			return val.(error)
-		}
-	default:
-		panic(fmt.Errorf("Don't know how to deal with dep of type %T", fn))
-	}
-}
diff --git a/mg/fn.go b/mg/fn.go
new file mode 100644
index 0000000..5737608
--- /dev/null
+++ b/mg/fn.go
@@ -0,0 +1,181 @@
+package mg
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"time"
+)
+
+// 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.
+type Fn interface {
+	// Name should return the fully qualified name of the function. Usually
+	// it's best to use runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name().
+	Name() string
+
+	// ID should be an additional uniqueness qualifier in case the name is insufficiently unique.
+	// This can be the case for functions that take arguments (mg.F json-encodes an array of the
+	// args).
+	ID() string
+
+	// Run should run the function.
+	Run(ctx context.Context) error
+}
+
+// F takes a function that is compatible as a mage target, and any args that need to be passed to
+// it, and wraps it in an mg.Fn that mg.Deps can run. Args must be passed in the same order as they
+// are declared by the function. Note that you do not need to and should not pass a context.Context
+// to F, even if the target takes a context. Compatible args are int, bool, string, and
+// time.Duration.
+func F(target interface{}, args ...interface{}) Fn {
+	hasContext, isNamespace, err := checkF(target, args)
+	if err != nil {
+		panic(err)
+	}
+	id, err := json.Marshal(args)
+	if err != nil {
+		panic(fmt.Errorf("can't convert args into a mage-compatible id for mg.Deps: %s", err))
+	}
+	return fn{
+		name: funcName(target),
+		id:   string(id),
+		f: func(ctx context.Context) error {
+			v := reflect.ValueOf(target)
+			count := len(args)
+			if hasContext {
+				count++
+			}
+			if isNamespace {
+				count++
+			}
+			vargs := make([]reflect.Value, count)
+			x := 0
+			if isNamespace {
+				vargs[0] = reflect.ValueOf(struct{}{})
+				x++
+			}
+			if hasContext {
+				vargs[x] = reflect.ValueOf(ctx)
+				x++
+			}
+			for y := range args {
+				vargs[x+y] = reflect.ValueOf(args[y])
+			}
+			ret := v.Call(vargs)
+			if len(ret) > 0 {
+				// we only allow functions with a single error return, so this should be safe.
+				if ret[0].IsNil() {
+					return nil
+				}
+				return ret[0].Interface().(error)
+			}
+			return nil
+		},
+	}
+}
+
+type fn struct {
+	name string
+	id   string
+	f    func(ctx context.Context) error
+}
+
+// Name returns the fully qualified name of the function.
+func (f fn) Name() string {
+	return f.name
+}
+
+// ID returns a hash of the argument values passed in
+func (f fn) ID() string {
+	return f.id
+}
+
+// Run runs the function.
+func (f fn) Run(ctx context.Context) error {
+	return f.f(ctx)
+}
+
+func checkF(target interface{}, args []interface{}) (hasContext, isNamespace bool, _ error) {
+	t := reflect.TypeOf(target)
+	if t.Kind() != reflect.Func {
+		return false, false, fmt.Errorf("non-function passed to mg.F: %T", target)
+	}
+
+	if t.NumOut() > 1 {
+		return false, false, fmt.Errorf("target has too many return values, must be zero or just an error: %T", target)
+	}
+	if t.NumOut() == 1 && t.Out(0) != errType {
+		return false, false, fmt.Errorf("target's return value is not an error")
+	}
+
+	// more inputs than slots is always an error
+	if len(args) > t.NumIn() {
+		return false, false, fmt.Errorf("too many arguments for target, got %d for %T", len(args), target)
+	}
+
+	if t.NumIn() == 0 {
+		return false, false, nil
+	}
+
+	x := 0
+	inputs := t.NumIn()
+
+	if t.In(0).AssignableTo(emptyType) {
+		// nameSpace func
+		isNamespace = true
+		x++
+		// callers must leave off the namespace value
+		inputs--
+	}
+	if t.NumIn() > x && t.In(x) == ctxType {
+		// callers must leave off the context
+		inputs--
+
+		// let the upper function know it should pass us a context.
+		hasContext = true
+
+		// skip checking the first argument in the below loop if it's a context, since first arg is
+		// special.
+		x++
+	}
+
+	if len(args) != inputs {
+		return false, false, fmt.Errorf("wrong number of arguments for target, got %d for %T", len(args), target)
+	}
+
+	for _, arg := range args {
+		argT := t.In(x)
+		if !argTypes[argT] {
+			return false, false, fmt.Errorf("argument %d (%s), is not a supported argument type", x, argT)
+		}
+		passedT := reflect.TypeOf(arg)
+		if argT != passedT {
+			return false, false, fmt.Errorf("argument %d expected to be %s, but is %s", x, argT, passedT)
+		}
+		x++
+	}
+	return hasContext, isNamespace, nil
+}
+
+// Here we define the types that are supported as arguments/returns
+var (
+	ctxType   = reflect.TypeOf(func(context.Context) {}).In(0)
+	errType   = reflect.TypeOf(func() error { return nil }).Out(0)
+	emptyType = reflect.TypeOf(struct{}{})
+
+	intType    = reflect.TypeOf(int(0))
+	stringType = reflect.TypeOf(string(""))
+	boolType   = reflect.TypeOf(bool(false))
+	durType    = reflect.TypeOf(time.Second)
+
+	// don't put ctx in here, this is for non-context types
+	argTypes = map[reflect.Type]bool{
+		intType:    true,
+		boolType:   true,
+		stringType: true,
+		durType:    true,
+	}
+)
diff --git a/parse/parse.go b/parse/parse.go
index 1549f9e..472a3dc 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -48,6 +48,12 @@ type Function struct {
 	IsContext  bool
 	Synopsis   string
 	Comment    string
+	Args       []Arg
+}
+
+// Arg is an argument to a Function.
+type Arg struct {
+	Name, Type string
 }
 
 // ID returns user-readable information about where this function is defined.
@@ -79,7 +85,7 @@ func (f Function) TargetName() string {
 // 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.
-func (f Function) ExecCode() (string, error) {
+func (f Function) ExecCode() string {
 	name := f.Name
 	if f.Receiver != "" {
 		name = f.Receiver + "{}." + name
@@ -88,41 +94,64 @@ func (f Function) ExecCode() (string, error) {
 		name = f.Package + "." + name
 	}
 
-	if f.IsContext && f.IsError {
-		out := `
-			wrapFn := func(ctx context.Context) error {
-				return %s(ctx)
-			}
-			err := runTarget(wrapFn)`[1:]
-		return fmt.Sprintf(out, name), nil
-	}
-	if f.IsContext && !f.IsError {
-		out := `
-			wrapFn := func(ctx context.Context) error {
-				%s(ctx)
-				return nil
-			}
-			err := runTarget(wrapFn)`[1:]
-		return fmt.Sprintf(out, name), nil
+	var parseargs string
+	for x, arg := range f.Args {
+		switch arg.Type {
+		case "string":
+			parseargs += fmt.Sprintf(`
+			arg%d := args.Args[x]
+			x++`, x)
+		case "int":
+			parseargs += fmt.Sprintf(`
+				arg%d, err := strconv.Atoi(args.Args[x])
+				if err != nil {
+					logger.Printf("can't convert argument %%q to int\n", args.Args[x])
+					os.Exit(2)
+				}
+				x++`, x)
+		case "bool":
+			parseargs += fmt.Sprintf(`
+				arg%d, err := strconv.ParseBool(args.Args[x])
+				if err != nil {
+					logger.Printf("can't convert argument %%q to bool\n", args.Args[x])
+					os.Exit(2)
+				}
+				x++`, x)
+		case "time.Duration":
+			parseargs += fmt.Sprintf(`
+				arg%d, err := time.ParseDuration(args.Args[x])
+				if err != nil {
+					logger.Printf("can't convert argument %%q to time.Duration\n", args.Args[x])
+					os.Exit(2)
+				}
+				x++`, x)
+		}
 	}
-	if !f.IsContext && f.IsError {
-		out := `
-			wrapFn := func(ctx context.Context) error {
-				return %s()
-			}
-			err := runTarget(wrapFn)`[1:]
-		return fmt.Sprintf(out, name), nil
-	}
-	if !f.IsContext && !f.IsError {
-		out := `
-			wrapFn := func(ctx context.Context) error {
-				%s()
-				return nil
-			}
-			err := runTarget(wrapFn)`[1:]
-		return fmt.Sprintf(out, name), nil
+
+	out := parseargs + `
+				wrapFn := func(ctx context.Context) error {
+					`
+	if f.IsError {
+		out += "return "
 	}
-	return "", fmt.Errorf("Error formatting ExecCode code for %#v", f)
+	out += name + "("
+	var args []string
+	if f.IsContext {
+		args = append(args, "ctx")
+	}
+	for x := 0; x < len(f.Args); x++ {
+		args = append(args, fmt.Sprintf("arg%d", x))
+	}
+	out += strings.Join(args, ", ")
+	out += ")"
+	if !f.IsError {
+		out += `
+					return nil`
+	}
+	out += `
+				}
+				ret := runTarget(wrapFn)`
+	return out
 }
 
 // PrimaryPackage parses a package.  If files is non-empty, it will only parse the files given.
@@ -285,18 +314,16 @@ func setFuncs(pi *PkgInfo) {
 			// skip non-exported functions
 			continue
 		}
-		if typ := funcType(f.Decl.Type); typ != invalidType {
-			debug.Printf("found target %v", f.Name)
-			pi.Funcs = append(pi.Funcs, &Function{
-				Name:      f.Name,
-				Comment:   toOneLine(f.Doc),
-				Synopsis:  sanitizeSynopsis(f),
-				IsError:   typ == errorType || typ == contextErrorType,
-				IsContext: typ == contextVoidType || typ == contextErrorType,
-			})
-		} else {
-			debug.Printf("skipping function with invalid signature func %s(%v)(%v)", f.Name, fieldNames(f.Decl.Type.Params), fieldNames(f.Decl.Type.Results))
+		fn, err := funcType(f.Decl.Type)
+		if err != nil {
+			debug.Printf("skipping function with invalid signature func %s: %v", f.Name, err)
+			continue
 		}
+		debug.Printf("found target %v", f.Name)
+		fn.Name = f.Name
+		fn.Comment = toOneLine(f.Doc)
+		fn.Synopsis = sanitizeSynopsis(f)
+		pi.Funcs = append(pi.Funcs, fn)
 	}
 }
 
@@ -310,19 +337,18 @@ func setNamespaces(pi *PkgInfo) {
 			if !ast.IsExported(f.Name) {
 				continue
 			}
-			typ := funcType(f.Decl.Type)
-			if typ == invalidType {
+			fn, err := funcType(f.Decl.Type)
+			if err != nil {
+				debug.Printf("skipping invalid namespace method %s %s.%s: %v", pi.DocPkg.ImportPath, t.Name, f.Name, err)
 				continue
 			}
 			debug.Printf("found namespace method %s %s.%s", pi.DocPkg.ImportPath, t.Name, f.Name)
-			pi.Funcs = append(pi.Funcs, &Function{
-				Name:      f.Name,
-				Receiver:  t.Name,
-				Comment:   toOneLine(f.Doc),
-				Synopsis:  sanitizeSynopsis(f),
-				IsError:   typ == errorType || typ == contextErrorType,
-				IsContext: typ == contextVoidType || typ == contextErrorType,
-			})
+			fn.Name = f.Name
+			fn.Comment = toOneLine(f.Doc)
+			fn.Synopsis = sanitizeSynopsis(f)
+			fn.Receiver = t.Name
+
+			pi.Funcs = append(pi.Funcs, fn)
 		}
 	}
 }
@@ -446,31 +472,6 @@ func isNamespace(t *doc.Type) bool {
 	return ident.Name == "mg" && sel.Sel.Name == "Namespace"
 }
 
-func fieldNames(flist *ast.FieldList) string {
-	if flist == nil {
-		return ""
-	}
-	list := flist.List
-	if len(list) == 0 {
-		return ""
-	}
-	args := make([]string, 0, len(list))
-	for _, f := range list {
-		names := make([]string, 0, len(f.Names))
-		for _, n := range f.Names {
-			if n.Name != "" {
-				names = append(names, n.Name)
-			}
-		}
-		nms := strings.Join(names, ", ")
-		if nms != "" {
-			nms += " "
-		}
-		args = append(args, nms+fmt.Sprint(f.Type))
-	}
-	return strings.Join(args, ", ")
-}
-
 // checkDupeTargets checks a package for duplicate target names.
 func checkDupeTargets(info *PkgInfo) (hasDupes bool, names map[string][]string) {
 	names = map[string][]string{}
@@ -701,23 +702,33 @@ func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package,
 	}
 }
 
-func hasContextParam(ft *ast.FuncType) bool {
-	if ft.Params.NumFields() != 1 {
-		return false
+// hasContextParams returns whether or not the first parameter is a context.Context. If it
+// determines that hte first parameter makes this function invalid for mage, it'll return a non-nil
+// error.
+func hasContextParam(ft *ast.FuncType) (bool, error) {
+	if ft.Params.NumFields() < 1 {
+		return false, nil
 	}
-	ret := ft.Params.List[0]
-	sel, ok := ret.Type.(*ast.SelectorExpr)
+	param := ft.Params.List[0]
+	sel, ok := param.Type.(*ast.SelectorExpr)
 	if !ok {
-		return false
+		return false, nil
 	}
 	pkg, ok := sel.X.(*ast.Ident)
 	if !ok {
-		return false
+		return false, nil
 	}
 	if pkg.Name != "context" {
-		return false
+		return false, nil
 	}
-	return sel.Sel.Name == "Context"
+	if sel.Sel.Name != "Context" {
+		return false, nil
+	}
+	if len(param.Names) > 1 {
+		// something like foo, bar context.Context
+		return false, errors.New("ETOOMANYCONTEXTS")
+	}
+	return true, nil
 }
 
 func hasVoidReturn(ft *ast.FuncType) bool {
@@ -725,48 +736,62 @@ func hasVoidReturn(ft *ast.FuncType) bool {
 	return res.NumFields() == 0
 }
 
-func hasErrorReturn(ft *ast.FuncType) bool {
+func hasErrorReturn(ft *ast.FuncType) (bool, error) {
 	res := ft.Results
-	if res.NumFields() != 1 {
-		return false
+	if res.NumFields() == 0 {
+		// void return is ok
+		return false, nil
+	}
+	if res.NumFields() > 1 {
+		return false, errors.New("ETOOMANYRETURNS")
 	}
 	ret := res.List[0]
 	if len(ret.Names) > 1 {
-		return false
+		return false, errors.New("ETOOMANYERRORS")
+	}
+	if fmt.Sprint(ret.Type) == "error" {
+		return true, nil
 	}
-	return fmt.Sprint(ret.Type) == "error"
+	return false, errors.New("EBADRETURNTYPE")
 }
 
-type functype int
-
-const (
-	invalidType functype = iota
-	voidType
-	errorType
-	contextVoidType
-	contextErrorType
-)
-
-func funcType(ft *ast.FuncType) functype {
-	if hasContextParam(ft) {
-		if hasVoidReturn(ft) {
-			return contextVoidType
-		}
-		if hasErrorReturn(ft) {
-			return contextErrorType
-		}
+func funcType(ft *ast.FuncType) (*Function, error) {
+	var err error
+	f := &Function{}
+	f.IsContext, err = hasContextParam(ft)
+	if err != nil {
+		return nil, err
 	}
-	if ft.Params.NumFields() == 0 {
-		if hasVoidReturn(ft) {
-			return voidType
+	f.IsError, err = hasErrorReturn(ft)
+	if err != nil {
+		return nil, err
+	}
+	x := 0
+	if f.IsContext {
+		x++
+	}
+	for ; x < len(ft.Params.List); x++ {
+		param := ft.Params.List[x]
+		t := fmt.Sprint(param.Type)
+		typ, ok := argTypes[t]
+		if !ok {
+			return nil, fmt.Errorf("unsupported argument type: %s", t)
 		}
-		if hasErrorReturn(ft) {
-			return errorType
+		// support for foo, bar string
+		for _, name := range param.Names {
+			f.Args = append(f.Args, Arg{Name: name.Name, Type: typ})
 		}
 	}
-	return invalidType
+	return f, nil
 }
 
 func toOneLine(s string) string {
 	return strings.TrimSpace(strings.Replace(s, "\n", " ", -1))
 }
+
+var argTypes = map[string]string{
+	"string":           "string",
+	"int":              "int",
+	"&{time Duration}": "time.Duration",
+	"bool":             "bool",
+}
diff --git a/site/content/dependencies/_index.en.md b/site/content/dependencies/_index.en.md
index 9375152..857157b 100644
--- a/site/content/dependencies/_index.en.md
+++ b/site/content/dependencies/_index.en.md
@@ -5,21 +5,43 @@ weight = 30
 
 Mage supports a makefile-style tree of dependencies using the helper library
 [github.com/magefile/mage/mg](https://godoc.org/github.com/magefile/mage/mg). To
-declare dependencies, pass any number of dependent functions of the following
-types:
-```go
-func()
-func() error
-func(ctx context.Context)
-func(ctx context.Context) error
+declare dependencies, pass any number of dependent functions to mg.Deps.
+
+A dependent function may be any function that has an optional first argument of context.Context, has
+either no return or just an error return, and where the other arguments are all of type string, int,
+bool, or time.Duration. Unlike targets, they do not need to be exported.
+
+e.g. these are all acceptable dependent functions:
+
 ```
-(they may be targets, but do not need to be and do not have to be exported) to
-`mg.Deps()`, and the Deps function will not return until all declared
-dependencies have been run (and any dependencies they have are run).
-Dependencies are guaranteed to run exactly once in a single execution of mage,
-so if two of your dependencies both depend on the same function, it is still
-guaranteed to be run only once, and both funcs that depend on it will not
-continue until it has been run. 
+func build()
+func Install(ctx context.Context) error
+func compile(what string) error
+func Exec(ctx context.Context, name string, count int, debug bool, timeout time.Duration) error
+```
+
+The Deps function will not return until all declared dependencies have been run (and any
+dependencies they have are run). Dependencies are guaranteed to run exactly once in a single
+execution of mage, so if two of your dependencies both depend on the same function, it is still
+guaranteed to be run only once, and both funcs that depend on it will not continue until it has been
+run.
+
+The most common way to use mg.Deps is to make it the first line in a function, so that this function won't run until all its dependencies have run.
+
+## Arguments
+
+If a dependent function has no arguments or just takes a context, you can pass it directly to
+mg.Deps:
+
+`mg.Deps(Install)`
+
+Dependent functions with non-context arguments should be passed via mg.F thusly:
+
+`mg.Deps(mg.F(compile, "server"))`
+
+For functions that take arguments, the arguments (except context) are considered part of its
+uniqueness, thus mg.F(compile, "server") and mg.F(compile, "client") are considered distinct, but if
+there are two calls to mg.F(compile, "server"), then compile("server") will only be run once.
 
 ## Parallelism
 
diff --git a/site/content/targets/_index.en.md b/site/content/targets/_index.en.md
index 2184ca5..5b82237 100644
--- a/site/content/targets/_index.en.md
+++ b/site/content/targets/_index.en.md
@@ -2,16 +2,40 @@
 title = "Targets"
 weight = 10
 +++
-A target is any exported function that is one of the following types:
-```go
-func()
-func() error 
-func(context.Context)
-func(context.Context) error
+A target is any exported function that has an optional first argument of context.Context, has either
+no return or just an error return, and where the arguments are all of type string, int, bool, or
+time.Duration.
+
+e.g. these are all acceptable targets
+
+```
+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
 ```
+
 A target is effectively a subcommand of mage while running mage in
 this directory.  i.e. you can run a target by running `mage <target>`
 
+## Arguments
+
+Arguments aside from context are taken from the CLI arguments after the target name.
+Strings are passed as-is, ints are converted with strconv.Atoi, bools are converted with
+strconv.ParseBool, and time.Durations are converted with time.ParseDuration.
+
+Thus you could call Exec above by running
+
+`mage exec somename 5 true 100ms`
+
+All arguments are mandatory and must be specified in the order they appear in the function.
+
+You can intersperse multiple targets with arguments as you'd expect:
+
+`mage run foo.exe exec somename 5 true 100ms`
+
+## Errors
+
 If the function has an error return, errors returned from the function will
 print to stdout and cause the magefile to exit with an exit code of 1.  Any
 functions that do not fit this pattern are not considered targets by mage.


Test patch:
diff --git a/mage/args_test.go b/mage/args_test.go
new file mode 100644
index 0000000..6997df8
--- /dev/null
+++ b/mage/args_test.go
@@ -0,0 +1,178 @@
+package mage
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestArgs(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"status", "say", "hi", "bob", "count", "5", "status", "wait", "5ms", "cough", "false"},
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Log(stderr.String())
+		t.Fatalf("expected 1, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := `status
+saying hi bob
+01234
+status
+waiting 5ms
+not coughing
+`
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestBadIntArg(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"count", "abc123"},
+	}
+	code := Invoke(inv)
+	if code != 2 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 2, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "can't convert argument \"abc123\" to int\n"
+
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestBadBoolArg(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"cough", "abc123"},
+	}
+	code := Invoke(inv)
+	if code != 2 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 2, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "can't convert argument \"abc123\" to bool\n"
+
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestBadDurationArg(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"wait", "abc123"},
+	}
+	code := Invoke(inv)
+	if code != 2 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 2, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "can't convert argument \"abc123\" to time.Duration\n"
+
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestMissingArgs(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"say", "hi"},
+	}
+	code := Invoke(inv)
+	if code != 2 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 2, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "not enough arguments for target \"Say\", expected 2, got 1\n"
+
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestDocs(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Help:   true,
+		Args:   []string{"say"},
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := `Say says something. It's pretty cool. I think you should try it.
+
+Usage:
+
+	mage say <msg> <name>
+
+Aliases: speak
+
+`
+	if actual != expected {
+		t.Fatalf("output is not expected:\n%q", actual)
+	}
+}
+
+func TestMgF(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/args",
+		Stderr: stderr,
+		Stdout: stdout,
+		Args:   []string{"HasDep"},
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Log("stderr:", stderr)
+		t.Log("stdout:", stdout)
+		t.Fatalf("expected code 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := "saying hi Susan\n"
+	if actual != expected {
+		t.Fatalf("output is not expected: %q", actual)
+	}
+}
diff --git a/mage/main_test.go b/mage/main_test.go
index 88ba3a3..cf57da9 100644
--- a/mage/main_test.go
+++ b/mage/main_test.go
@@ -826,10 +826,10 @@ func TestBadSecondTargets(t *testing.T) {
 	}
 	code := Invoke(inv)
 	if code != 2 {
-		t.Errorf("expected 0, but got %v", code)
+		t.Errorf("expected 2, but got %v", code)
 	}
 	actual := stderr.String()
-	expected := "Unknown target specified: NotGonnaWork\n"
+	expected := "Unknown target specified: \"NotGonnaWork\"\n"
 	if actual != expected {
 		t.Errorf("expected %q, but got %q", expected, actual)
 	}
@@ -967,7 +967,7 @@ func TestHelpTarget(t *testing.T) {
 		t.Errorf("expected to exit with code 0, but got %v", code)
 	}
 	actual := stdout.String()
-	expected := "mage panics:\n\nFunction that panics.\n\n"
+	expected := "Function that panics.\n\nUsage:\n\n\tmage panics\n\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
@@ -987,7 +987,7 @@ func TestHelpAlias(t *testing.T) {
 		t.Errorf("expected to exit with code 0, but got %v", code)
 	}
 	actual := stdout.String()
-	expected := "mage status:\n\nPrints status.\n\nAliases: st, stat\n\n"
+	expected := "Prints status.\n\nUsage:\n\n\tmage status\n\nAliases: st, stat\n\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
@@ -1004,7 +1004,7 @@ func TestHelpAlias(t *testing.T) {
 		t.Errorf("expected to exit with code 0, but got %v", code)
 	}
 	actual = stdout.String()
-	expected = "mage checkout:\n\nAliases: co\n\n"
+	expected = "Usage:\n\n\tmage checkout\n\nAliases: co\n\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
@@ -1053,10 +1053,10 @@ func TestInvalidAlias(t *testing.T) {
 	}
 	code := Invoke(inv)
 	if code != 2 {
-		t.Errorf("expected to exit with code 1, but got %v", code)
+		t.Errorf("expected to exit with code 2, but got %v", code)
 	}
 	actual := stderr.String()
-	expected := "Unknown target specified: co\n"
+	expected := "Unknown target specified: \"co\"\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
@@ -1121,7 +1121,7 @@ func TestCompiledFlags(t *testing.T) {
 		t.Fatal(err)
 	}
 	got := strings.TrimSpace(stdout.String())
-	want := filepath.Base(name) + " deploy:\n\nThis is the synopsis for Deploy. This part shouldn't show up."
+	want := "This is the synopsis for Deploy. This part shouldn't show up.\n\nUsage:\n\n\t" + filepath.Base(name) + " deploy"
 	if got != want {
 		t.Errorf("got %q, want %q", got, want)
 	}
@@ -1206,8 +1206,8 @@ func TestCompiledEnvironmentVars(t *testing.T) {
 	if err := run(stdout, stderr, name, "MAGEFILE_HELP=1", "deploy"); err != nil {
 		t.Fatal(err)
 	}
-	got := strings.TrimSpace(stdout.String())
-	want := filepath.Base(name) + " deploy:\n\nThis is the synopsis for Deploy. This part shouldn't show up."
+	got := stdout.String()
+	want := "This is the synopsis for Deploy. This part shouldn't show up.\n\nUsage:\n\n\t" + filepath.Base(name) + " deploy\n\n"
 	if got != want {
 		t.Errorf("got %q, want %q", got, want)
 	}
@@ -1519,8 +1519,6 @@ func TestAliasToImport(t *testing.T) {
 
 }
 
-var wrongDepRx = regexp.MustCompile("^Error: Invalid type for dependent function.*@ main.FooBar .*magefile.go")
-
 func TestWrongDependency(t *testing.T) {
 	stderr := &bytes.Buffer{}
 	inv := Invocation{
@@ -1532,9 +1530,10 @@ func TestWrongDependency(t *testing.T) {
 	if code != 1 {
 		t.Fatalf("expected 1, but got %v", code)
 	}
+	expected := "Error: argument 0 (complex128), is not a supported argument type\n"
 	actual := stderr.String()
-	if !wrongDepRx.MatchString(actual) {
-		t.Fatalf("expected matching %q, but got %q", wrongDepRx, actual)
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
 }
 
diff --git a/mage/testdata/args/magefile.go b/mage/testdata/args/magefile.go
new file mode 100644
index 0000000..e7f7b26
--- /dev/null
+++ b/mage/testdata/args/magefile.go
@@ -0,0 +1,51 @@
+//+build mage
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/magefile/mage/mg"
+)
+
+var Aliases = map[string]interface{}{
+	"speak": Say,
+}
+
+// Prints status.
+func Status() {
+	fmt.Println("status")
+}
+
+// Say says something. It's pretty cool.
+// I think you should try it.
+func Say(ctx context.Context, msg, name string) {
+	fmt.Println("saying", msg, name)
+}
+
+func Count(i int) error {
+	for x := 0; x < i; x++ {
+		fmt.Print(x)
+	}
+	fmt.Println()
+	return nil
+}
+
+func Wait(d time.Duration) {
+	fmt.Println("waiting", d)
+}
+
+func Cough(ctx context.Context, b bool) error {
+	if b {
+		fmt.Println("coughing")
+	} else {
+		fmt.Println("not coughing")
+	}
+	return nil
+}
+
+func HasDep() {
+	mg.Deps(mg.F(Say, "hi", "Susan"))
+}
diff --git a/mage/testdata/wrong_dep/magefile.go b/mage/testdata/wrong_dep/magefile.go
index 0fec8a3..8da309d 100644
--- a/mage/testdata/wrong_dep/magefile.go
+++ b/mage/testdata/wrong_dep/magefile.go
@@ -8,9 +8,9 @@ import (
 
 var Default = FooBar
 
-func WrongSignature(i int) {
+func WrongSignature(c complex128) {
 }
 
 func FooBar() {
-	mg.Deps(WrongSignature)
+	mg.Deps(mg.F(WrongSignature, 0))
 }
diff --git a/mg/deps_test.go b/mg/deps_test.go
index 36c46a7..8424544 100644
--- a/mg/deps_test.go
+++ b/mg/deps_test.go
@@ -1,10 +1,9 @@
 package mg
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
-	"log"
+	"sync/atomic"
 	"testing"
 	"time"
 )
@@ -79,7 +78,7 @@ func TestDepError(t *testing.T) {
 	// be recreated as a full-stack test.
 
 	f := func() error {
-		return errors.New("ouch!")
+		return errors.New("ouch")
 	}
 	defer func() {
 		err := recover()
@@ -87,8 +86,8 @@ func TestDepError(t *testing.T) {
 			t.Fatal("expected panic, but didn't get one")
 		}
 		actual := fmt.Sprint(err)
-		if "ouch!" != actual {
-			t.Fatalf(`expected to get "ouch!" but got "%s"`, actual)
+		if "ouch" != actual {
+			t.Fatalf(`expected to get "ouch" but got "%s"`, actual)
 		}
 	}()
 	Deps(f)
@@ -165,20 +164,19 @@ func TestDepWithUnhandledFunc(t *testing.T) {
 }
 
 func TestDepsErrors(t *testing.T) {
-	buf := &bytes.Buffer{}
-	log := log.New(buf, "", 0)
+	var hRan, gRan, fRan int64
 
 	h := func() error {
-		log.Println("running h")
+		atomic.AddInt64(&hRan, 1)
 		return errors.New("oops")
 	}
 	g := func() {
 		Deps(h)
-		log.Println("running g")
+		atomic.AddInt64(&gRan, 1)
 	}
 	f := func() {
 		Deps(g, h)
-		log.Println("running f")
+		atomic.AddInt64(&fRan, 1)
 	}
 
 	defer func() {
@@ -186,8 +184,14 @@ func TestDepsErrors(t *testing.T) {
 		if err == nil {
 			t.Fatal("expected f to panic")
 		}
-		if buf.String() != "running h\n" {
-			t.Fatalf("expected just h to run, but got\n%s", buf.String())
+		if hRan != 1 {
+			t.Fatalf("expected h to run once, but got %v", hRan)
+		}
+		if gRan > 0 {
+			t.Fatalf("expected g to panic before incrementing gRan to run, but got %v", gRan)
+		}
+		if fRan > 0 {
+			t.Fatalf("expected f to panic before incrementing fRan to run, but got %v", fRan)
 		}
 	}()
 	f()
diff --git a/mg/example_test.go b/mg/example_test.go
new file mode 100644
index 0000000..5776cb3
--- /dev/null
+++ b/mg/example_test.go
@@ -0,0 +1,29 @@
+package mg_test
+
+import (
+	"fmt"
+
+	"github.com/magefile/mage/mg"
+)
+
+func Example() {
+	// Deps will run each dependency exactly once, and will run leaf-dependencies before those
+	// functions that depend on them (if you put mg.Deps first in the function).
+
+	// Normal (non-serial) Deps runs all dependencies in goroutines, so which one finishes first is
+	// non-deterministic. Here we use SerialDeps here to ensure the example always produces the same
+	// output.
+
+	mg.SerialDeps(mg.F(Say, "hi"), Bark)
+	// output:
+	// hi
+	// woof
+}
+
+func Say(something string) {
+	fmt.Println(something)
+}
+
+func Bark() {
+	mg.Deps(mg.F(Say, "woof"))
+}
diff --git a/mg/fn_test.go b/mg/fn_test.go
new file mode 100644
index 0000000..8011167
--- /dev/null
+++ b/mg/fn_test.go
@@ -0,0 +1,218 @@
+package mg
+
+import (
+	"context"
+	"fmt"
+	"sync/atomic"
+	"testing"
+	"time"
+)
+
+func TestFuncCheck(t *testing.T) {
+	hasContext, isNamespace, err := checkF(func() {}, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if hasContext {
+		t.Error("func does not have context")
+	}
+	if isNamespace {
+		t.Error("func is not on a namespace")
+	}
+	hasContext, isNamespace, err = checkF(func() error { return nil }, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if hasContext {
+		t.Error("func does not have context")
+	}
+	if isNamespace {
+		t.Error("func is not on a namespace")
+	}
+	hasContext, isNamespace, err = checkF(func(context.Context) {}, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if !hasContext {
+		t.Error("func has context")
+	}
+	if isNamespace {
+		t.Error("func is not on a namespace")
+	}
+	hasContext, isNamespace, err = checkF(func(context.Context) error { return nil }, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if !hasContext {
+		t.Error("func has context")
+	}
+	if isNamespace {
+		t.Error("func is not on a namespace")
+	}
+
+	hasContext, isNamespace, err = checkF(Foo.Bare, nil)
+	if err != nil {
+		t.Error(err)
+	}
+
+	hasContext, isNamespace, err = checkF(Foo.Error, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if hasContext {
+		t.Error("func does not have context")
+	}
+	if !isNamespace {
+		t.Error("func is  on a namespace")
+	}
+
+	hasContext, isNamespace, err = checkF(Foo.BareCtx, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if !hasContext {
+		t.Error("func has context")
+	}
+	if !isNamespace {
+		t.Error("func is  on a namespace")
+	}
+	hasContext, isNamespace, err = checkF(Foo.CtxError, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	if !hasContext {
+		t.Error("func has context")
+	}
+	if !isNamespace {
+		t.Error("func is  on a namespace")
+	}
+
+	hasContext, isNamespace, err = checkF(Foo.CtxErrorArgs, []interface{}{1, "s", true, time.Second})
+	if err != nil {
+		t.Error(err)
+	}
+	if !hasContext {
+		t.Error("func has context")
+	}
+	if !isNamespace {
+		t.Error("func is on a namespace")
+	}
+
+	hasContext, isNamespace, err = checkF(func(int, bool, string, time.Duration) {}, []interface{}{1, true, "s", time.Second})
+	if err != nil {
+		t.Error(err)
+	}
+	if hasContext {
+		t.Error("func does not have context")
+	}
+	if isNamespace {
+		t.Error("func is not on a namespace")
+	}
+
+	// Test an Invalid case
+	_, _, err = checkF(func(*int) error { return nil }, nil)
+	if err == nil {
+		t.Error("expected func(*int) error to be invalid")
+	}
+}
+
+func TestF(t *testing.T) {
+	var (
+		ctxOut context.Context
+		iOut   int
+		sOut   string
+		bOut   bool
+		dOut   time.Duration
+	)
+	f := func(cctx context.Context, ii int, ss string, bb bool, dd time.Duration) error {
+		ctxOut = cctx
+		iOut = ii
+		sOut = ss
+		bOut = bb
+		dOut = dd
+		return nil
+	}
+
+	ctx := context.Background()
+	i := 1776
+	s := "abc124"
+	b := true
+	d := time.Second
+
+	CtxDeps(ctx, F(f, i, s, b, d))
+	if ctxOut != ctx {
+		t.Error(ctxOut)
+	}
+	if iOut != i {
+		t.Error(iOut)
+	}
+	if bOut != b {
+		t.Error(bOut)
+	}
+	if dOut != d {
+		t.Error(dOut)
+	}
+	if sOut != s {
+		t.Error(sOut)
+	}
+}
+
+func TestFTwice(t *testing.T) {
+	var called int64
+	f := func(int) {
+		atomic.AddInt64(&called, 1)
+	}
+
+	Deps(F(f, 5), F(f, 5), F(f, 1))
+	if called != 2 {
+		t.Fatalf("Expected to be called 2 times, but was called %d", called)
+	}
+}
+
+func ExampleF() {
+	f := func(i int) {
+		fmt.Println(i)
+	}
+
+	// we use SerialDeps here to ensure consistent output, but this works with all Deps functions.
+	SerialDeps(F(f, 5), F(f, 1))
+	// output:
+	// 5
+	// 1
+}
+
+func TestFNamespace(t *testing.T) {
+	ctx := context.Background()
+	i := 1776
+	s := "abc124"
+	b := true
+	d := time.Second
+
+	fn := F(Foo.CtxErrorArgs, i, s, b, d)
+	err := fn.Run(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestFNilError(t *testing.T) {
+	fn := F(func() error { return nil })
+	err := fn.Run(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+type Foo Namespace
+
+func (Foo) Bare() {}
+
+func (Foo) Error() error { return nil }
+
+func (Foo) BareCtx(context.Context) {}
+
+func (Foo) CtxError(context.Context) error { return nil }
+
+func (Foo) CtxErrorArgs(ctx context.Context, i int, s string, b bool, d time.Duration) error {
+	return nil
+}
diff --git a/mg/funcs_test.go b/mg/funcs_test.go
deleted file mode 100644
index 57c8d40..0000000
--- a/mg/funcs_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package mg
-
-import (
-	"context"
-	"testing"
-)
-
-func TestFuncCheck(t *testing.T) {
-	// we can ignore errors here, since the error is always the same
-	// and the FuncType will be InvalidType if there's an error.
-	f, _ := funcCheck(func() {})
-	if f != voidType {
-		t.Errorf("expected func() to be a valid VoidType, but was %v", f)
-	}
-	f, _ = funcCheck(func() error { return nil })
-	if f != errorType {
-		t.Errorf("expected func() error to be a valid ErrorType, but was %v", f)
-	}
-	f, _ = funcCheck(func(context.Context) {})
-	if f != contextVoidType {
-		t.Errorf("expected func(context.Context) to be a valid ContextVoidType, but was %v", f)
-	}
-	f, _ = funcCheck(func(context.Context) error { return nil })
-	if f != contextErrorType {
-		t.Errorf("expected func(context.Context) error to be a valid ContextErrorType but was %v", f)
-	}
-
-	f, _ = funcCheck(Foo.Bare)
-	if f != namespaceVoidType {
-		t.Errorf("expected Foo.Bare to be a valid NamespaceVoidType but was %v", f)
-	}
-
-	f, _ = funcCheck(Foo.Error)
-	if f != namespaceErrorType {
-		t.Errorf("expected Foo.Error to be a valid NamespaceErrorType but was %v", f)
-	}
-	f, _ = funcCheck(Foo.BareCtx)
-	if f != namespaceContextVoidType {
-		t.Errorf("expected Foo.BareCtx to be a valid NamespaceContextVoidType but was %v", f)
-	}
-	f, _ = funcCheck(Foo.CtxError)
-	if f != namespaceContextErrorType {
-		t.Errorf("expected Foo.CtxError to be a valid NamespaceContextErrorType but was %v", f)
-	}
-
-	// Test the Invalid case
-	f, err := funcCheck(func(int) error { return nil })
-	if f != invalidType {
-		t.Errorf("expected func(int) error to be InvalidType but was %v", f)
-	}
-	if err == nil {
-		t.Error("expected func(int) error to not be a valid FuncType, but got nil error.")
-	}
-}
-
-type Foo Namespace
-
-func (Foo) Bare() {}
-
-func (Foo) Error() error { return nil }
-
-func (Foo) BareCtx(context.Context) {}
-
-func (Foo) CtxError(context.Context) error { return nil }
-
-func TestFuncTypeWrap(t *testing.T) {
-	func() {
-		defer func() {
-			err := recover()
-			if err == nil {
-				t.Fatal("Expected a panic, but didn't get one")
-			}
-		}()
-		if funcTypeWrap(voidType, func(i int) {}) != nil {
-			t.Errorf("expected func(int) to return nil")
-		}
-	}()
-
-	if funcTypeWrap(voidType, func() {}) == nil {
-		t.Errorf("expected func() to return a function")
-	}
-
-	if funcTypeWrap(errorType, func() error { return nil }) == nil {
-		t.Errorf("expected func() error to return a function")
-	}
-
-	if funcTypeWrap(contextVoidType, func(context.Context) {}) == nil {
-		t.Errorf("expected func(context.Context) to return a function")
-	}
-
-	if funcTypeWrap(contextErrorType, func(context.Context) error { return nil }) == nil {
-		t.Errorf("expected func(context.Context) error to return a function")
-	}
-}