Repository: magefile/mage
Pull request #90 (2017-10-30 22:25:19)

Support for aliases (#58)
I've added support for aliases. What do you think about these changes?

natefinch: From a first once-over, this looks awesome.  I'll give it a more detailed review tonight. Thanks for contributing!
natefinch: If you'd like to tackle it, it would be nice to add the aliases to the mage -h output for a specific target.

For example, if mage's own magefile had 

```go
var Aliases = map[string]interface{} {
  "i":     Install,
  "build": Install,
}
```

```
$ mage -h install
mage install:

Runs "go install" for mage.  This generates the version info the binary.

Aliases: i, build
```
mbialon: Thanks for reviewing the changes. I've fixed the issues and I've added a list of aliases to target's help output.
natefinch: Awesome, thanks, I'll review shortly.

On Sun, Nov 5, 2017, 6:08 PM Marcin Białoń <notifications@github.com> wrote:

> Thanks for reviewing the changes. I've fixed the issues and I've added a
> list of aliases to target's help output.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/magefile/mage/pull/90#issuecomment-342013740>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ADCcyEgfERrjEGh6oHqwIrtzATF9lAUlks5szkAGgaJpZM4QL8TE>
> .
>

natefinch: Looks great. Thanks for this! :)

Patch:
diff --git a/mage/main.go b/mage/main.go
index 904ca17..1021c6e 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -267,6 +267,7 @@ type data struct {
 	DefaultError bool
 	Default      string
 	DefaultFunc  parse.Function
+	Aliases      map[string]string
 }
 
 // Magefiles returns the list of magefiles in dir.
@@ -315,6 +316,7 @@ func GenerateMainfile(path string, info *parse.PkgInfo) error {
 		Funcs:       info.Funcs,
 		Default:     info.DefaultName,
 		DefaultFunc: info.DefaultFunc,
+		Aliases:     info.Aliases,
 	}
 
 	data.DefaultError = info.DefaultIsError
diff --git a/mage/template.go b/mage/template.go
index cf88873..ece60a6 100644
--- a/mage/template.go
+++ b/mage/template.go
@@ -31,6 +31,8 @@ func main() {
 	}
 
 	targets := map[string]bool {
+		{{range $alias, $funci := .Aliases}}"{{lower $alias}}": true,
+		{{end}}
 		{{range .Funcs}}"{{lower .Name}}": true,
 		{{end}}
 	}
@@ -58,7 +60,15 @@ func main() {
 		switch strings.ToLower(os.Args[1]) {
 			{{range .Funcs}}case "{{lower .Name}}":
 				fmt.Print("mage {{lower .Name}}:\n\n")
-                fmt.Println({{printf "%q" .Comment}})
+				{{if ne .Comment ""}}fmt.Println({{printf "%q" .Comment}}){{end}}
+				var aliases []string
+				{{- $name := .Name -}}
+				{{range $alias, $func := $.Aliases}}
+				{{if eq $name $func}}aliases = append(aliases, "{{$alias}}"){{end -}}
+				{{- end}}
+				if len(aliases) > 0 {
+					fmt.Printf("Aliases: %s\n\n", strings.Join(aliases, ", "))
+				}
 				return
 			{{end}}
 			default:
@@ -82,6 +92,12 @@ func main() {
 	{{- end}}
 	}
 	for _, target := range os.Args[1:] {
+		switch strings.ToLower(target) {
+		{{range $alias, $func := .Aliases}}
+		case "{{lower $alias}}":
+			target = "{{$func}}"
+		{{- end}}
+		}
 		switch strings.ToLower(target) {
 		{{range .Funcs }}
 		case "{{lower .Name}}":
diff --git a/parse/parse.go b/parse/parse.go
index 65f5a34..fba6cc7 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -22,6 +22,7 @@ type PkgInfo struct {
 	DefaultIsContext bool
 	DefaultName      string
 	DefaultFunc      Function
+	Aliases          map[string]string
 }
 
 // Function represented a job function from a mage file
@@ -109,6 +110,7 @@ func Package(path string, files []string) (*PkgInfo, error) {
 	}
 
 	setDefault(p, pi, info)
+	setAliases(p, pi, info)
 
 	return pi, nil
 }
@@ -141,6 +143,57 @@ func setDefault(p *doc.Package, pi *PkgInfo, info types.Info) {
 	}
 }
 
+func setAliases(p *doc.Package, pi *PkgInfo, info types.Info) {
+	for _, v := range p.Vars {
+		for x, name := range v.Names {
+			if name != "Aliases" {
+				continue
+			}
+			spec, ok := v.Decl.Specs[x].(*ast.ValueSpec)
+			if !ok {
+				log.Println("warning: aliases declaration is not a value")
+				return
+			}
+			if len(spec.Values) != 1 {
+				log.Println("warning: aliases declaration has multiple values")
+			}
+			comp, ok := spec.Values[0].(*ast.CompositeLit)
+			if !ok {
+				log.Println("warning: aliases declaration is not a map")
+				return
+			}
+			pi.Aliases = make(map[string]string)
+			for _, elem := range comp.Elts {
+				kv, ok := elem.(*ast.KeyValueExpr)
+				if !ok {
+					log.Println("warning: alias declaration is not a map element")
+					return
+				}
+				k, ok := kv.Key.(*ast.BasicLit)
+				if !ok || k.Kind != token.STRING {
+					log.Println("warning: alias is not a string")
+					return
+				}
+				v, ok := kv.Value.(*ast.Ident)
+				if !ok {
+					log.Println("warning: alias target is not a function")
+					return
+				}
+				alias := strings.Trim(k.Value, "\"")
+				valid := false
+				for _, f := range pi.Funcs {
+					valid = valid || f.Name == v.Name
+				}
+				if !valid {
+					log.Printf("warning: alias declaration (%s) does not reference a mage target", alias)
+				}
+				pi.Aliases[alias] = v.Name
+			}
+			return
+		}
+	}
+}
+
 // getPackage returns the non-test package at the given path.
 func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) {
 	fm := make(map[string]bool, len(files))
diff --git a/site/content/targets/_index.en.md b/site/content/targets/_index.en.md
index 859fa61..ab0593e 100644
--- a/site/content/targets/_index.en.md
+++ b/site/content/targets/_index.en.md
@@ -19,7 +19,7 @@ functions that do not fit this pattern are not considered targets by mage.
 Comments on the target function will become documentation accessible by running
 `mage -l` which will list all the build targets in this directory with the first
 sentence from their docs, or `mage -h <target>` which will show the full comment
-from the docs on the function.
+from the docs on the function, and a list of aliases if specified.
 
 A target may be designated the default target, which is run when the user runs
 `mage` with no target specified. To denote the default, create a `var Default =
@@ -47,4 +47,19 @@ when the timeout set with -t expires.
 
 mg.CtxDeps will pass along whatever context you give it, so if you want to
 modify the original context, or pass in your own, that will work like you expect
-it to.
\ No newline at end of file
+it to.
+
+## Aliases
+
+Target aliases can be specified using the following notation:
+
+```
+var Aliases = map[string]interface{} {
+  "i":     Install,
+  "build": Install,
+  "ls":    List,
+}
+```
+
+The key is an alias and the value is a function identifier.
+An alias can be used interchangeably with it's target.


Test patch:
diff --git a/mage/main_test.go b/mage/main_test.go
index a6e4ac2..bdea160 100644
--- a/mage/main_test.go
+++ b/mage/main_test.go
@@ -477,3 +477,108 @@ func TestParseHelp(t *testing.T) {
 		t.Fatalf("expected -h and --help to produce same output, but got different.\n\n-h:\n%s\n\n--help:\n%s", s, s2)
 	}
 }
+
+func TestHelpTarget(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: stdout,
+		Stderr: os.Stderr,
+		Args:   []string{"panics"},
+		Help:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := "mage panics:\n\nFunction that panics.\n\n"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+}
+
+func TestHelpAlias(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/alias",
+		Stdout: stdout,
+		Stderr: os.Stderr,
+		Args:   []string{"status"},
+		Help:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		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"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+	inv = Invocation{
+		Dir:    "./testdata/alias",
+		Stdout: stdout,
+		Stderr: os.Stderr,
+		Args:   []string{"checkout"},
+		Help:   true,
+	}
+	stdout.Reset()
+	code = Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual = stdout.String()
+	expected = "mage checkout:\n\nAliases: co\n\n"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+}
+
+func TestAlias(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/alias",
+		Stdout: stdout,
+		Stderr: ioutil.Discard,
+		Args:   []string{"status"},
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := "alias!\n"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+	stdout.Reset()
+	inv.Args = []string{"st"}
+	code = Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual = stdout.String()
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+}
+
+func TestInvalidAlias(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/invalid_alias",
+		Stdout: ioutil.Discard,
+		Stderr: stderr,
+		Args:   []string{"co"},
+	}
+	code := Invoke(inv)
+	if code != 1 {
+		t.Errorf("expected to exit with code 1, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "Unknown target: \"co\"\n"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+}
diff --git a/mage/testdata/alias/magefile.go b/mage/testdata/alias/magefile.go
new file mode 100644
index 0000000..921002a
--- /dev/null
+++ b/mage/testdata/alias/magefile.go
@@ -0,0 +1,18 @@
+// +build mage
+
+package main
+
+import "fmt"
+
+var Aliases = map[string]interface{}{
+	"st":   Status,
+	"stat": Status,
+	"co":   Checkout,
+}
+
+// Prints status.
+func Status() {
+	fmt.Println("alias!")
+}
+
+func Checkout() {}
diff --git a/mage/testdata/invalid_alias/magefile.go b/mage/testdata/invalid_alias/magefile.go
new file mode 100644
index 0000000..e3f985f
--- /dev/null
+++ b/mage/testdata/invalid_alias/magefile.go
@@ -0,0 +1,13 @@
+// +build mage
+
+package main
+
+import "fmt"
+
+var Aliases = map[string]interface{}{
+	"co": checkout,
+}
+
+func checkout() {
+	fmt.Println("done!")
+}
diff --git a/parse/parse_test.go b/parse/parse_test.go
index 3107a57..b08d7da 100644
--- a/parse/parse_test.go
+++ b/parse/parse_test.go
@@ -6,7 +6,7 @@ import (
 )
 
 func TestParse(t *testing.T) {
-	info, err := Package("./testdata", []string{"func.go", "command.go"})
+	info, err := Package("./testdata", []string{"func.go", "command.go", "alias.go"})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -43,6 +43,10 @@ func TestParse(t *testing.T) {
 		t.Fatalf("expected DefaultName to be ReturnsNilError")
 	}
 
+	if info.Aliases["void"] != "ReturnsVoid" {
+		t.Fatalf("expected alias of void to be ReturnsVoid")
+	}
+
 	for _, fn := range expected {
 		found := false
 		for _, infoFn := range info.Funcs {
diff --git a/parse/testdata/alias.go b/parse/testdata/alias.go
new file mode 100644
index 0000000..a2b1642
--- /dev/null
+++ b/parse/testdata/alias.go
@@ -0,0 +1,7 @@
+// +build mage
+
+package main
+
+var Aliases = map[string]interface{}{
+	"void": ReturnsVoid,
+}