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

Feature: zsh completions and formatting target lists with gotemplates #488

Open
wants to merge 5 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
18 changes: 18 additions & 0 deletions completions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# mage shell completions

## Zsh

Generate autocomplete script and set it to your `$fpath`:

```
mage -completion zsh > mage.zsh
```

#### Faster autocomplete

The autocompletion script uses `mage -l -format` command for target definitions and descriptions. The built binary is cached but hitting tab
still has a bit of delay for each refresh. To speed up things, you can enable hash fast feature by adding following to your `.zshrc` file:

```
zstyle ':completions:*:mage:*' hash-fast true
```
24 changes: 24 additions & 0 deletions completions/completions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package completions

import (
"fmt"
"io"
)

type Completion interface {
GenerateCompletions(w io.Writer) error
}

var completions = map[string]Completion{
"zsh": &Zsh{},
}

func GetCompletions(shell string) (Completion, error) {
completions := completions[shell]

if completions == nil {
return nil, fmt.Errorf("no completions for shell %q", shell)
}

return completions, nil
}
87 changes: 87 additions & 0 deletions completions/completions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package completions

import (
"bytes"
"io"
"testing"
)

func TestGetCompletions(t *testing.T) {
testCases := []struct {
name string
shell string
expected Completion
err bool
}{
{
name: "zsh",
shell: "zsh",
expected: &Zsh{},
err: false,
},
{
name: "nonexistent",
shell: "nonexistent",
expected: nil,
err: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
completion, err := GetCompletions(tc.shell)
if err != nil && !tc.err {
t.Fatalf("expected to get completions, but got error: %v", err)
}
if completion != tc.expected {
t.Fatalf("expected to get completions, but got: %v", completion)
}
})
}
}

type failingWriter struct{}

func (f *failingWriter) Write(p []byte) (n int, err error) {
return 0, io.ErrClosedPipe
}

func TestGenerateCompletionsFails(t *testing.T) {
cmpl := &Zsh{}
writer := &failingWriter{}

err := cmpl.GenerateCompletions(writer)
if err == nil {
t.Fatalf("expected failure to generate completions, but got no error")
}
}

func TestGenerateCompletions(t *testing.T) {
testCases := []struct {
name string
cmpl Completion
expected string
err bool
}{
{
name: "zsh",
cmpl: &Zsh{},
expected: string(ZshCompletions),
err: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
err := tc.cmpl.GenerateCompletions(&buf)
if err != nil {
t.Fatalf("expected to generate completions, but got error: %v", err)
}
actual := buf.String()
if actual != tc.expected {
t.Fatalf("expected: %q\ngot: %q", tc.expected, actual)
}
})
}
}
86 changes: 86 additions & 0 deletions completions/zsh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package completions

import (
"fmt"
"io"
)

var ZshCompletions string = `#compdef mage

local curcontext="$curcontext" state line ret=1
typeset -A opt_args
local magepath="magefiles"
local -a targets

_arguments -C \
'-clean[clean out old generated binaries from CACHE_DIR]' \
'-compile[output a static binary to the given path]:compilepath:_path_files' \
'-completion[print out shell completion script for given shell (default: "")]' \
'-init[create a starting template if no mage files exist]' \
'-h[show help]' \
'-l[list mage targets in this directory]' \
'-format[when used in conjunction with -l, will list targets in a specified golang template format (available vars: .Name, .Synopsis)]' \
'-d[directory to read magefiles from (default "." or "magefiles" if exists)]:magepath:_path_files -/' \
'-debug[turn on debug messages]' \
'-f[force recreation of compiled magefile]' \
'-goarch[sets the GOARCH for the binary created by -compile (default: current arch)]' \
'-gocmd[use the given go binary to compile the output (default: "go")]' \
'-goos[sets the GOOS for the binary created by -compile (default: current OS)]' \
'-ldflags[sets the ldflags for the binary created by -compile (default: "")]' \
'-h[show description of a target]' \
'-keep[keep intermediate mage files around after running]' \
'-t[timeout in duration parsable format (e.g. 5m30s)]' \
'-v[show verbose output when running mage targets]' \
'-w[working directory where magefiles will run (default -d value)]' \
'*: :->trg'

(( $+opt_args[-d] )) && magepath=$opt_args[-d]

zstyle ':completion:*:mage:*' list-grouped false
zstyle -s ':completion:*:mage:*' hash-fast hash_fast false

_get_targets() {
# check if magefile exists
[[ ! -f "$magepath/mage.go" ]] && return 1

local IFS=$'\n'
targets=($(MAGEFILE_HASHFAST=$hash_fast mage -d $magepath -l -format "{{ .Name }}|{{ .Synopsis }}" | awk -F '|' '{
target = $1;
gsub(/:/, "\\:", target);
gsub(/^ +| +$/, "", $0);

description = $2;
gsub(/^ +| +$/, "", description);

print target ":" description;
}'))
}

case "$state" in
trg)
_get_targets || ret=1
_describe 'mage' targets && ret=0
;;
esac

return ret

# Local Variables:
# mode: Shell-Script
# sh-indentation: 2
# indent-tabs-mode: nil
# sh-basic-offset: 2
# End:
# vim: ft=zsh sw=2 ts=2 et
`

type Zsh struct{}

func (z *Zsh) GenerateCompletions(w io.Writer) error {
_, err := fmt.Fprint(w, string(ZshCompletions))

if err != nil {
return err
}
return nil
}
85 changes: 57 additions & 28 deletions mage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"text/template"
"time"

"github.com/magefile/mage/completions"
"github.com/magefile/mage/internal"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/parse"
Expand Down Expand Up @@ -88,6 +89,7 @@ const (
Init // create a starting template for mage
Clean // clean out old compiled mage binaries from the cache
CompileStatic // compile a static binary of the current directory
Completions // generate a completion file for mage for given shell
)

// Main is the entrypoint for running mage. It exists external to mage's main
Expand All @@ -99,26 +101,28 @@ func Main() int {

// Invocation contains the args for invoking a run of Mage.
type Invocation struct {
Debug bool // turn on debug messages
Dir string // directory to read magefiles from
WorkDir string // directory where magefiles will run
Force bool // forces recreation of the compiled binary
Verbose bool // tells the magefile to print out log statements
List bool // tells the magefile to print out a list of targets
Help bool // tells the magefile to print out help for a specific target
Keep bool // tells mage to keep the generated main file after compiling
Timeout time.Duration // tells mage to set a timeout to running the targets
CompileOut string // tells mage to compile a static binary to this path, but not execute
GOOS string // sets the GOOS when producing a binary with -compileout
GOARCH string // sets the GOARCH when producing a binary with -compileout
Ldflags string // sets the ldflags when producing a binary with -compileout
Stdout io.Writer // writer to write stdout messages to
Stderr io.Writer // writer to write stderr messages to
Stdin io.Reader // reader to read stdin from
Args []string // args to pass to the compiled binary
GoCmd string // the go binary command to run
CacheDir string // the directory where we should store compiled binaries
HashFast bool // don't rely on GOCACHE, just hash the magefiles
Debug bool // turn on debug messages
Dir string // directory to read magefiles from
WorkDir string // directory where magefiles will run
Force bool // forces recreation of the compiled binary
Verbose bool // tells the magefile to print out log statements
List bool // tells the magefile to print out a list of targets
Format string // tells the magefile to print out a list of targets in a specific format
Help bool // tells the magefile to print out help for a specific target
Keep bool // tells mage to keep the generated main file after compiling
Timeout time.Duration // tells mage to set a timeout to running the targets
CompileOut string // tells mage to compile a static binary to this path, but not execute
GOOS string // sets the GOOS when producing a binary with -compileout
GOARCH string // sets the GOARCH when producing a binary with -compileout
Ldflags string // sets the ldflags when producing a binary with -compileout
Stdout io.Writer // writer to write stdout messages to
Stderr io.Writer // writer to write stderr messages to
Stdin io.Reader // reader to read stdin from
Args []string // args to pass to the compiled binary
GoCmd string // the go binary command to run
CacheDir string // the directory where we should store compiled binaries
CompletionsShell string // the shell to generate completions for
HashFast bool // don't rely on GOCACHE, just hash the magefiles
}

// MagefilesDirName is the name of the default folder to look for if no directory was specified,
Expand Down Expand Up @@ -170,6 +174,18 @@ func ParseAndRun(stdout, stderr io.Writer, stdin io.Reader, args []string) int {
return 0
case CompileStatic:
return Invoke(inv)
case Completions:
shell := inv.CompletionsShell
cmpl, err := completions.GetCompletions(shell)
if err != nil {
errlog.Println("error getting completions:", err)
return 1
}
if err := cmpl.GenerateCompletions(stdout); err != nil {
errlog.Println("error generating completions:", err)
return 1
}
return 0
case None:
return Invoke(inv)
default:
Expand Down Expand Up @@ -202,6 +218,7 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
// commands below

fs.BoolVar(&inv.List, "l", false, "list mage targets in this directory")
fs.StringVar(&inv.Format, "format", "", "format template to use when listing targets")
var showVersion bool
fs.BoolVar(&showVersion, "version", false, "show version info for the mage binary")
var mageInit bool
Expand All @@ -210,6 +227,8 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
fs.BoolVar(&clean, "clean", false, "clean out old generated binaries from CACHE_DIR")
var compileOutPath string
fs.StringVar(&compileOutPath, "compile", "", "output a static binary to the given path")
var completionsShell string
fs.StringVar(&completionsShell, "completion", "", "generate a completion script for mage")

fs.Usage = func() {
fmt.Fprint(stdout, `
Expand All @@ -218,21 +237,24 @@ mage [options] [target]
Mage is a make-like command runner. See https://magefile.org for full docs.

Commands:
-clean clean out old generated binaries from CACHE_DIR
-compile <string>
output a static binary to the given path
-h show this help
-init create a starting template if no mage files exist
-l list mage targets in this directory
-version show version info for the mage binary
-clean clean out old generated binaries from CACHE_DIR
-compile <string>
output a static binary to the given path
-h show this help
-init create a starting template if no mage files exist
-l list mage targets in this directory
-format format template to use when listing targets
-version show version info for the mage binary
-completion <string>
generate a completion script for mage for given shell

Options:
-d <string>
directory to read magefiles from (default "." or "magefiles" if exists)
-debug turn on debug messages
-f force recreation of compiled magefile
-goarch sets the GOARCH for the binary created by -compile (default: current arch)
-gocmd <string>
-gocmd <string>
use the given go binary to compile the output (default: "go")
-goos sets the GOOS for the binary created by -compile (default: current OS)
-ldflags sets the ldflags for the binary created by -compile (default: "")
Expand Down Expand Up @@ -269,6 +291,10 @@ Options:
case showVersion:
numCommands++
cmd = Version
case completionsShell != "":
numCommands++
cmd = Completions
inv.CompletionsShell = completionsShell
case clean:
numCommands++
cmd = Clean
Expand Down Expand Up @@ -721,6 +747,9 @@ func RunCompiled(inv Invocation, exePath string, errlog *log.Logger) int {
if inv.List {
c.Env = append(c.Env, "MAGEFILE_LIST=1")
}
if inv.Format != "" {
c.Env = append(c.Env, fmt.Sprintf("MAGEFILE_FORMAT=%s", inv.Format))
}
if inv.Help {
c.Env = append(c.Env, "MAGEFILE_HELP=1")
}
Expand Down
Loading