Merge pull request #2848 from hashicorp/f-autocomplete

Add command autocompletion.
This commit is contained in:
Alex Dadgar 2017-07-17 18:27:04 -07:00 committed by GitHub
commit 5b14f091b1
27 changed files with 1447 additions and 16 deletions

10
main.go
View File

@ -39,10 +39,12 @@ func RunCustom(args []string, commands map[string]cli.CommandFactory) int {
}
cli := &cli.CLI{
Version: PrettyVersion(GetVersionParts()),
Args: args,
Commands: commands,
HelpFunc: cli.FilteredHelpFunc(commandsInclude, cli.BasicHelpFunc("nomad")),
Name: "nomad",
Version: PrettyVersion(GetVersionParts()),
Args: args,
Commands: commands,
Autocomplete: true,
HelpFunc: cli.FilteredHelpFunc(commandsInclude, cli.BasicHelpFunc("nomad")),
}
exitCode, err := cli.Run()

43
vendor/github.com/mitchellh/cli/autocomplete.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
package cli
import (
"github.com/posener/complete/cmd/install"
)
// autocompleteInstaller is an interface to be implemented to perform the
// autocomplete installation and uninstallation with a CLI.
//
// This interface is not exported because it only exists for unit tests
// to be able to test that the installation is called properly.
type autocompleteInstaller interface {
Install(string) error
Uninstall(string) error
}
// realAutocompleteInstaller uses the real install package to do the
// install/uninstall.
type realAutocompleteInstaller struct{}
func (i *realAutocompleteInstaller) Install(cmd string) error {
return install.Install(cmd)
}
func (i *realAutocompleteInstaller) Uninstall(cmd string) error {
return install.Uninstall(cmd)
}
// mockAutocompleteInstaller is used for tests to record the install/uninstall.
type mockAutocompleteInstaller struct {
InstallCalled bool
UninstallCalled bool
}
func (i *mockAutocompleteInstaller) Install(cmd string) error {
i.InstallCalled = true
return nil
}
func (i *mockAutocompleteInstaller) Uninstall(cmd string) error {
i.UninstallCalled = true
return nil
}

View File

@ -11,6 +11,7 @@ import (
"text/template"
"github.com/armon/go-radix"
"github.com/posener/complete"
)
// CLI contains the state necessary to run subcommands and parse the
@ -66,6 +67,32 @@ type CLI struct {
// Version of the CLI.
Version string
// Autocomplete enables or disables subcommand auto-completion support.
// This is enabled by default when NewCLI is called. Otherwise, this
// must enabled explicitly.
//
// Autocomplete requires the "Name" option to be set on CLI. This name
// should be set exactly to the binary name that is autocompleted.
//
// Autocompletion is supported via the github.com/posener/complete
// library. This library supports both bash and zsh. To add support
// for other shells, please see that library.
//
// AutocompleteInstall and AutocompleteUninstall are the global flag
// names for installing and uninstalling the autocompletion handlers
// for the user's shell. The flag should omit the hyphen(s) in front of
// the value. Both single and double hyphens will automatically be supported
// for the flag name. These default to `autocomplete-install` and
// `autocomplete-uninstall` respectively.
//
// AutocompleteGlobalFlags are a mapping of global flags for
// autocompletion. The help and version flags are automatically added.
Autocomplete bool
AutocompleteInstall string
AutocompleteUninstall string
AutocompleteGlobalFlags complete.Flags
autocompleteInstaller autocompleteInstaller // For tests
// HelpFunc and HelpWriter are used to output help information, if
// requested.
//
@ -78,23 +105,32 @@ type CLI struct {
HelpFunc HelpFunc
HelpWriter io.Writer
//---------------------------------------------------------------
// Internal fields set automatically
once sync.Once
autocomplete *complete.Complete
commandTree *radix.Tree
commandNested bool
isHelp bool
subcommand string
subcommandArgs []string
topFlags []string
isVersion bool
// These are true when special global flags are set. We can/should
// probably use a bitset for this one day.
isHelp bool
isVersion bool
isAutocompleteInstall bool
isAutocompleteUninstall bool
}
// NewClI returns a new CLI instance with sensible defaults.
func NewCLI(app, version string) *CLI {
return &CLI{
Name: app,
Version: version,
HelpFunc: BasicHelpFunc(app),
Name: app,
Version: version,
HelpFunc: BasicHelpFunc(app),
Autocomplete: true,
}
}
@ -129,6 +165,45 @@ func (c *CLI) Run() (int, error) {
return 0, nil
}
// If we're attempting to install or uninstall autocomplete then handle
if c.Autocomplete {
// Autocomplete requires the "Name" to be set so that we know what
// command to setup the autocomplete on.
if c.Name == "" {
return 1, fmt.Errorf(
"internal error: CLI.Name must be specified for autocomplete to work")
}
// If both install and uninstall flags are specified, then error
if c.isAutocompleteInstall && c.isAutocompleteUninstall {
return 1, fmt.Errorf(
"Either the autocomplete install or uninstall flag may " +
"be specified, but not both.")
}
// If the install flag is specified, perform the install or uninstall
if c.isAutocompleteInstall {
if err := c.autocompleteInstaller.Install(c.Name); err != nil {
return 1, err
}
return 0, nil
}
if c.isAutocompleteUninstall {
if err := c.autocompleteInstaller.Uninstall(c.Name); err != nil {
return 1, err
}
return 0, nil
}
// If this is a autocompletion request, satisfy it
if c.autocomplete.Complete() {
return 0, nil
}
}
// Attempt to get the factory function for creating the command
// implementation. If the command is invalid or blank, it is an error.
raw, ok := c.commandTree.Get(c.Subcommand())
@ -268,10 +343,111 @@ func (c *CLI) init() {
}
}
// Setup autocomplete if we have it enabled. We have to do this after
// the command tree is setup so we can use the radix tree to easily find
// all subcommands.
if c.Autocomplete {
c.initAutocomplete()
}
// Process the args
c.processArgs()
}
func (c *CLI) initAutocomplete() {
if c.AutocompleteInstall == "" {
c.AutocompleteInstall = defaultAutocompleteInstall
}
if c.AutocompleteUninstall == "" {
c.AutocompleteUninstall = defaultAutocompleteUninstall
}
if c.autocompleteInstaller == nil {
c.autocompleteInstaller = &realAutocompleteInstaller{}
}
// Build the root command
cmd := c.initAutocompleteSub("")
// For the root, we add the global flags to the "Flags". This way
// they don't show up on every command.
cmd.Flags = map[string]complete.Predictor{
"-" + c.AutocompleteInstall: complete.PredictNothing,
"-" + c.AutocompleteUninstall: complete.PredictNothing,
"-help": complete.PredictNothing,
"-version": complete.PredictNothing,
}
cmd.GlobalFlags = c.AutocompleteGlobalFlags
c.autocomplete = complete.New(c.Name, cmd)
}
// initAutocompleteSub creates the complete.Command for a subcommand with
// the given prefix. This will continue recursively for all subcommands.
// The prefix "" (empty string) can be used for the root command.
func (c *CLI) initAutocompleteSub(prefix string) complete.Command {
var cmd complete.Command
walkFn := func(k string, raw interface{}) bool {
if len(prefix) > 0 {
// If we have a prefix, trim the prefix + 1 (for the space)
// Example: turns "sub one" to "one" with prefix "sub"
k = k[len(prefix)+1:]
}
// Keep track of the full key so that we can nest further if necessary
fullKey := k
if idx := strings.LastIndex(k, " "); idx >= 0 {
// If there is a space, we trim up to the space
k = k[:idx]
}
if idx := strings.LastIndex(k, " "); idx >= 0 {
// This catches the scenario just in case where we see "sub one"
// before "sub". This will let us properly setup the subcommand
// regardless.
k = k[idx+1:]
}
if _, ok := cmd.Sub[k]; ok {
// If we already tracked this subcommand then ignore
return false
}
if cmd.Sub == nil {
cmd.Sub = complete.Commands(make(map[string]complete.Command))
}
subCmd := c.initAutocompleteSub(fullKey)
// Instantiate the command so that we can check if the command is
// a CommandAutocomplete implementation. If there is an error
// creating the command, we just ignore it since that will be caught
// later.
impl, err := raw.(CommandFactory)()
if err != nil {
impl = nil
}
// Check if it implements ComandAutocomplete. If so, setup the autocomplete
if c, ok := impl.(CommandAutocomplete); ok {
subCmd.Args = c.AutocompleteArgs()
subCmd.Flags = c.AutocompleteFlags()
}
cmd.Sub[k] = subCmd
return false
}
walkPrefix := prefix
if walkPrefix != "" {
walkPrefix += " "
}
c.commandTree.WalkPrefix(walkPrefix, walkFn)
return cmd
}
func (c *CLI) commandHelp(command Command) {
// Get the template to use
tpl := strings.TrimSpace(defaultHelpTemplate)
@ -404,6 +580,19 @@ func (c *CLI) processArgs() {
continue
}
// Check for autocomplete flags
if c.Autocomplete {
if arg == "-"+c.AutocompleteInstall || arg == "--"+c.AutocompleteInstall {
c.isAutocompleteInstall = true
continue
}
if arg == "-"+c.AutocompleteUninstall || arg == "--"+c.AutocompleteUninstall {
c.isAutocompleteUninstall = true
continue
}
}
if c.subcommand == "" {
// Check for version flags if not in a subcommand.
if arg == "-v" || arg == "-version" || arg == "--version" {
@ -456,6 +645,11 @@ func (c *CLI) processArgs() {
}
}
// defaultAutocompleteInstall and defaultAutocompleteUninstall are the
// default values for the autocomplete install and uninstall flags.
const defaultAutocompleteInstall = "autocomplete-install"
const defaultAutocompleteUninstall = "autocomplete-uninstall"
const defaultHelpTemplate = `
{{.Help}}{{if gt (len .Subcommands) 0}}

View File

@ -1,5 +1,9 @@
package cli
import (
"github.com/posener/complete"
)
const (
// RunResultHelp is a value that can be returned from Run to signal
// to the CLI to render the help output.
@ -26,6 +30,22 @@ type Command interface {
Synopsis() string
}
// CommandAutocomplete is an extension of Command that enables fine-grained
// autocompletion. Subcommand autocompletion will work even if this interface
// is not implemented. By implementing this interface, more advanced
// autocompletion is enabled.
type CommandAutocomplete interface {
// AutocompleteArgs returns the argument predictor for this command.
// If argument completion is not supported, this should return
// complete.PredictNothing.
AutocompleteArgs() complete.Predictor
// AutocompleteFlags returns a mapping of supported flags and autocomplete
// options for this command. The map key for the Flags map should be the
// complete flag such as "-foo" or "--foo".
AutocompleteFlags() complete.Flags
}
// CommandHelpTemplate is an extension of Command that also has a function
// for returning a template for the help rather than the help itself. In
// this scenario, both Help and HelpTemplate should be implemented.

View File

@ -1,5 +1,9 @@
package cli
import (
"github.com/posener/complete"
)
// MockCommand is an implementation of Command that can be used for tests.
// It is publicly exported from this package in case you want to use it
// externally.
@ -29,6 +33,23 @@ func (c *MockCommand) Synopsis() string {
return c.SynopsisText
}
// MockCommandAutocomplete is an implementation of CommandAutocomplete.
type MockCommandAutocomplete struct {
MockCommand
// Settable
AutocompleteArgsValue complete.Predictor
AutocompleteFlagsValue complete.Flags
}
func (c *MockCommandAutocomplete) AutocompleteArgs() complete.Predictor {
return c.AutocompleteArgsValue
}
func (c *MockCommandAutocomplete) AutocompleteFlags() complete.Flags {
return c.AutocompleteFlagsValue
}
// MockCommandHelpTemplate is an implementation of CommandHelpTemplate.
type MockCommandHelpTemplate struct {
MockCommand

21
vendor/github.com/posener/complete/LICENSE.txt generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2017 Eyal Posener
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

75
vendor/github.com/posener/complete/args.go generated vendored Normal file
View File

@ -0,0 +1,75 @@
package complete
import (
"os"
"path/filepath"
)
// Args describes command line arguments
type Args struct {
// All lists of all arguments in command line (not including the command itself)
All []string
// Completed lists of all completed arguments in command line,
// If the last one is still being typed - no space after it,
// it won't appear in this list of arguments.
Completed []string
// Last argument in command line, the one being typed, if the last
// character in the command line is a space, this argument will be empty,
// otherwise this would be the last word.
Last string
// LastCompleted is the last argument that was fully typed.
// If the last character in the command line is space, this would be the
// last word, otherwise, it would be the word before that.
LastCompleted string
}
// Directory gives the directory of the current written
// last argument if it represents a file name being written.
// in case that it is not, we fall back to the current directory.
func (a Args) Directory() string {
if info, err := os.Stat(a.Last); err == nil && info.IsDir() {
return fixPathForm(a.Last, a.Last)
}
dir := filepath.Dir(a.Last)
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
return "./"
}
return fixPathForm(a.Last, dir)
}
func newArgs(line []string) Args {
completed := removeLast(line[1:])
return Args{
All: line[1:],
Completed: completed,
Last: last(line),
LastCompleted: last(completed),
}
}
func (a Args) from(i int) Args {
if i > len(a.All) {
i = len(a.All)
}
a.All = a.All[i:]
if i > len(a.Completed) {
i = len(a.Completed)
}
a.Completed = a.Completed[i:]
return a
}
func removeLast(a []string) []string {
if len(a) > 0 {
return a[:len(a)-1]
}
return a
}
func last(args []string) (last string) {
if len(args) > 0 {
last = args[len(args)-1]
}
return
}

128
vendor/github.com/posener/complete/cmd/cmd.go generated vendored Normal file
View File

@ -0,0 +1,128 @@
// Package cmd used for command line options for the complete tool
package cmd
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/posener/complete/cmd/install"
)
// CLI for command line
type CLI struct {
Name string
InstallName string
UninstallName string
install bool
uninstall bool
yes bool
}
const (
defaultInstallName = "install"
defaultUninstallName = "uninstall"
)
// Run is used when running complete in command line mode.
// this is used when the complete is not completing words, but to
// install it or uninstall it.
func (f *CLI) Run() bool {
err := f.validate()
if err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
switch {
case f.install:
f.prompt()
err = install.Install(f.Name)
case f.uninstall:
f.prompt()
err = install.Uninstall(f.Name)
default:
// non of the action flags matched,
// returning false should make the real program execute
return false
}
if err != nil {
fmt.Printf("%s failed! %s\n", f.action(), err)
os.Exit(3)
}
fmt.Println("Done!")
return true
}
// prompt use for approval
// exit if approval was not given
func (f *CLI) prompt() {
defer fmt.Println(f.action() + "ing...")
if f.yes {
return
}
fmt.Printf("%s completion for %s? ", f.action(), f.Name)
var answer string
fmt.Scanln(&answer)
switch strings.ToLower(answer) {
case "y", "yes":
return
default:
fmt.Println("Cancelling...")
os.Exit(1)
}
}
// AddFlags adds the CLI flags to the flag set.
// If flags is nil, the default command line flags will be taken.
// Pass non-empty strings as installName and uninstallName to override the default
// flag names.
func (f *CLI) AddFlags(flags *flag.FlagSet) {
if flags == nil {
flags = flag.CommandLine
}
if f.InstallName == "" {
f.InstallName = defaultInstallName
}
if f.UninstallName == "" {
f.UninstallName = defaultUninstallName
}
if flags.Lookup(f.InstallName) == nil {
flags.BoolVar(&f.install, f.InstallName, false,
fmt.Sprintf("Install completion for %s command", f.Name))
}
if flags.Lookup(f.UninstallName) == nil {
flags.BoolVar(&f.uninstall, f.UninstallName, false,
fmt.Sprintf("Uninstall completion for %s command", f.Name))
}
if flags.Lookup("y") == nil {
flags.BoolVar(&f.yes, "y", false, "Don't prompt user for typing 'yes'")
}
}
// validate the CLI
func (f *CLI) validate() error {
if f.install && f.uninstall {
return errors.New("Install and uninstall are mutually exclusive")
}
return nil
}
// action name according to the CLI values.
func (f *CLI) action() string {
switch {
case f.install:
return "Install"
case f.uninstall:
return "Uninstall"
default:
return "unknown"
}
}

32
vendor/github.com/posener/complete/cmd/install/bash.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
package install
import "fmt"
// (un)install in bash
// basically adds/remove from .bashrc:
//
// complete -C </path/to/completion/command> <command>
type bash struct {
rc string
}
func (b bash) Install(cmd, bin string) error {
completeCmd := b.cmd(cmd, bin)
if lineInFile(b.rc, completeCmd) {
return fmt.Errorf("already installed in %s", b.rc)
}
return appendToFile(b.rc, completeCmd)
}
func (b bash) Uninstall(cmd, bin string) error {
completeCmd := b.cmd(cmd, bin)
if !lineInFile(b.rc, completeCmd) {
return fmt.Errorf("does not installed in %s", b.rc)
}
return removeFromFile(b.rc, completeCmd)
}
func (bash) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd)
}

View File

@ -0,0 +1,92 @@
package install
import (
"errors"
"os"
"os/user"
"path/filepath"
"github.com/hashicorp/go-multierror"
)
type installer interface {
Install(cmd, bin string) error
Uninstall(cmd, bin string) error
}
// Install complete command given:
// cmd: is the command name
func Install(cmd string) error {
is := installers()
if len(is) == 0 {
return errors.New("Did not found any shells to install")
}
bin, err := getBinaryPath()
if err != nil {
return err
}
for _, i := range is {
errI := i.Install(cmd, bin)
if errI != nil {
err = multierror.Append(err, errI)
}
}
return err
}
// Uninstall complete command given:
// cmd: is the command name
func Uninstall(cmd string) error {
is := installers()
if len(is) == 0 {
return errors.New("Did not found any shells to uninstall")
}
bin, err := getBinaryPath()
if err != nil {
return err
}
for _, i := range is {
errI := i.Uninstall(cmd, bin)
if errI != nil {
multierror.Append(err, errI)
}
}
return err
}
func installers() (i []installer) {
for _, rc := range [...]string{".bashrc", ".bash_profile"} {
if f := rcFile(rc); f != "" {
i = append(i, bash{f})
break
}
}
if f := rcFile(".zshrc"); f != "" {
i = append(i, zsh{f})
}
return
}
func getBinaryPath() (string, error) {
bin, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Abs(bin)
}
func rcFile(name string) string {
u, err := user.Current()
if err != nil {
return ""
}
path := filepath.Join(u.HomeDir, name)
if _, err := os.Stat(path); err != nil {
return ""
}
return path
}

118
vendor/github.com/posener/complete/cmd/install/utils.go generated vendored Normal file
View File

@ -0,0 +1,118 @@
package install
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
func lineInFile(name string, lookFor string) bool {
f, err := os.Open(name)
if err != nil {
return false
}
defer f.Close()
r := bufio.NewReader(f)
prefix := []byte{}
for {
line, isPrefix, err := r.ReadLine()
if err == io.EOF {
return false
}
if err != nil {
return false
}
if isPrefix {
prefix = append(prefix, line...)
continue
}
line = append(prefix, line...)
if string(line) == lookFor {
return true
}
prefix = prefix[:0]
}
}
func appendToFile(name string, content string) error {
f, err := os.OpenFile(name, os.O_RDWR|os.O_APPEND, 0)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("\n%s\n", content))
return err
}
func removeFromFile(name string, content string) error {
backup := name + ".bck"
err := copyFile(name, backup)
if err != nil {
return err
}
temp, err := removeContentToTempFile(name, content)
if err != nil {
return err
}
err = copyFile(temp, name)
if err != nil {
return err
}
return os.Remove(backup)
}
func removeContentToTempFile(name, content string) (string, error) {
rf, err := os.Open(name)
if err != nil {
return "", err
}
defer rf.Close()
wf, err := ioutil.TempFile("/tmp", "complete-")
if err != nil {
return "", err
}
defer wf.Close()
r := bufio.NewReader(rf)
prefix := []byte{}
for {
line, isPrefix, err := r.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if isPrefix {
prefix = append(prefix, line...)
continue
}
line = append(prefix, line...)
str := string(line)
if str == content {
continue
}
wf.WriteString(str + "\n")
prefix = prefix[:0]
}
return wf.Name(), nil
}
func copyFile(src string, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}

39
vendor/github.com/posener/complete/cmd/install/zsh.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
package install
import "fmt"
// (un)install in zsh
// basically adds/remove from .zshrc:
//
// autoload -U +X bashcompinit && bashcompinit"
// complete -C </path/to/completion/command> <command>
type zsh struct {
rc string
}
func (z zsh) Install(cmd, bin string) error {
completeCmd := z.cmd(cmd, bin)
if lineInFile(z.rc, completeCmd) {
return fmt.Errorf("already installed in %s", z.rc)
}
bashCompInit := "autoload -U +X bashcompinit && bashcompinit"
if !lineInFile(z.rc, bashCompInit) {
completeCmd = bashCompInit + "\n" + completeCmd
}
return appendToFile(z.rc, completeCmd)
}
func (z zsh) Uninstall(cmd, bin string) error {
completeCmd := z.cmd(cmd, bin)
if !lineInFile(z.rc, completeCmd) {
return fmt.Errorf("does not installed in %s", z.rc)
}
return removeFromFile(z.rc, completeCmd)
}
func (zsh) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd)
}

106
vendor/github.com/posener/complete/command.go generated vendored Normal file
View File

@ -0,0 +1,106 @@
package complete
import "github.com/posener/complete/match"
// Command represents a command line
// It holds the data that enables auto completion of command line
// Command can also be a sub command.
type Command struct {
// Sub is map of sub commands of the current command
// The key refer to the sub command name, and the value is it's
// Command descriptive struct.
Sub Commands
// Flags is a map of flags that the command accepts.
// The key is the flag name, and the value is it's predictions.
Flags Flags
// GlobalFlags is a map of flags that the command accepts.
// Global flags that can appear also after a sub command.
GlobalFlags Flags
// Args are extra arguments that the command accepts, those who are
// given without any flag before.
Args Predictor
}
// Predict returns all possible predictions for args according to the command struct
func (c *Command) Predict(a Args) (predictions []string) {
predictions, _ = c.predict(a)
return
}
// Commands is the type of Sub member, it maps a command name to a command struct
type Commands map[string]Command
// Predict completion of sub command names names according to command line arguments
func (c Commands) Predict(a Args) (prediction []string) {
for sub := range c {
if match.Prefix(sub, a.Last) {
prediction = append(prediction, sub)
}
}
return
}
// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions.
type Flags map[string]Predictor
// Predict completion of flags names according to command line arguments
func (f Flags) Predict(a Args) (prediction []string) {
for flag := range f {
if match.Prefix(flag, a.Last) {
prediction = append(prediction, flag)
}
}
return
}
// predict options
// only is set to true if no more options are allowed to be returned
// those are in cases of special flag that has specific completion arguments,
// and other flags or sub commands can't come after it.
func (c *Command) predict(a Args) (options []string, only bool) {
// search sub commands for predictions first
subCommandFound := false
for i, arg := range a.Completed {
if cmd, ok := c.Sub[arg]; ok {
subCommandFound = true
// recursive call for sub command
options, only = cmd.predict(a.from(i))
if only {
return
}
}
}
// if last completed word is a global flag that we need to complete
if predictor, ok := c.GlobalFlags[a.LastCompleted]; ok && predictor != nil {
Log("Predicting according to global flag %s", a.LastCompleted)
return predictor.Predict(a), true
}
options = append(options, c.GlobalFlags.Predict(a)...)
// if a sub command was entered, we won't add the parent command
// completions and we return here.
if subCommandFound {
return
}
// if last completed word is a command flag that we need to complete
if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil {
Log("Predicting according to flag %s", a.LastCompleted)
return predictor.Predict(a), true
}
options = append(options, c.Sub.Predict(a)...)
options = append(options, c.Flags.Predict(a)...)
if c.Args != nil {
options = append(options, c.Args.Predict(a)...)
}
return
}

86
vendor/github.com/posener/complete/complete.go generated vendored Normal file
View File

@ -0,0 +1,86 @@
// Package complete provides a tool for bash writing bash completion in go.
//
// Writing bash completion scripts is a hard work. This package provides an easy way
// to create bash completion scripts for any command, and also an easy way to install/uninstall
// the completion of the command.
package complete
import (
"flag"
"fmt"
"os"
"strings"
"github.com/posener/complete/cmd"
)
const (
envComplete = "COMP_LINE"
envDebug = "COMP_DEBUG"
)
// Complete structs define completion for a command with CLI options
type Complete struct {
Command Command
cmd.CLI
}
// New creates a new complete command.
// name is the name of command we want to auto complete.
// IMPORTANT: it must be the same name - if the auto complete
// completes the 'go' command, name must be equal to "go".
// command is the struct of the command completion.
func New(name string, command Command) *Complete {
return &Complete{
Command: command,
CLI: cmd.CLI{Name: name},
}
}
// Run runs the completion and add installation flags beforehand.
// The flags are added to the main flag CommandLine variable.
func (c *Complete) Run() bool {
c.AddFlags(nil)
flag.Parse()
return c.Complete()
}
// Complete a command from completion line in environment variable,
// and print out the complete options.
// returns success if the completion ran or if the cli matched
// any of the given flags, false otherwise
// For installation: it assumes that flags were added and parsed before
// it was called.
func (c *Complete) Complete() bool {
line, ok := getLine()
if !ok {
// make sure flags parsed,
// in case they were not added in the main program
return c.CLI.Run()
}
Log("Completing line: %s", line)
a := newArgs(line)
options := c.Command.Predict(a)
Log("Completion: %s", options)
output(options)
return true
}
func getLine() ([]string, bool) {
line := os.Getenv(envComplete)
if line == "" {
return nil, false
}
return strings.Split(line, " "), true
}
func output(options []string) {
Log("")
// stdout of program defines the complete options
for _, option := range options {
fmt.Println(option)
}
}

23
vendor/github.com/posener/complete/log.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
package complete
import (
"io"
"io/ioutil"
"log"
"os"
)
// Log is used for debugging purposes
// since complete is running on tab completion, it is nice to
// have logs to the stderr (when writing your own completer)
// to write logs, set the COMP_DEBUG environment variable and
// use complete.Log in the complete program
var Log = getLogger()
func getLogger() func(format string, args ...interface{}) {
var logfile io.Writer = ioutil.Discard
if os.Getenv(envDebug) != "" {
logfile = os.Stderr
}
return log.New(logfile, "complete ", log.Flags()).Printf
}

19
vendor/github.com/posener/complete/match/file.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
package match
import "strings"
// File returns true if prefix can match the file
func File(file, prefix string) bool {
// special case for current directory completion
if file == "./" && (prefix == "." || prefix == "") {
return true
}
if prefix == "." && strings.HasPrefix(file, ".") {
return true
}
file = strings.TrimPrefix(file, "./")
prefix = strings.TrimPrefix(prefix, "./")
return strings.HasPrefix(file, prefix)
}

6
vendor/github.com/posener/complete/match/match.go generated vendored Normal file
View File

@ -0,0 +1,6 @@
package match
// Match matches two strings
// it is used for comparing a term to the last typed
// word, the prefix, and see if it is a possible auto complete option.
type Match func(term, prefix string) bool

9
vendor/github.com/posener/complete/match/prefix.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
package match
import "strings"
// Prefix is a simple Matcher, if the word is it's prefix, there is a match
// Match returns true if a has the prefix as prefix
func Prefix(long, prefix string) bool {
return strings.HasPrefix(long, prefix)
}

21
vendor/github.com/posener/complete/metalinter.json generated vendored Normal file
View File

@ -0,0 +1,21 @@
{
"Vendor": true,
"DisableAll": true,
"Enable": [
"gofmt",
"goimports",
"interfacer",
"goconst",
"misspell",
"unconvert",
"gosimple",
"golint",
"structcheck",
"deadcode",
"vet"
],
"Exclude": [
"initTests is unused"
],
"Deadline": "2m"
}

41
vendor/github.com/posener/complete/predict.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package complete
// Predictor implements a predict method, in which given
// command line arguments returns a list of options it predicts.
type Predictor interface {
Predict(Args) []string
}
// PredictOr unions two predicate functions, so that the result predicate
// returns the union of their predication
func PredictOr(predictors ...Predictor) Predictor {
return PredictFunc(func(a Args) (prediction []string) {
for _, p := range predictors {
if p == nil {
continue
}
prediction = append(prediction, p.Predict(a)...)
}
return
})
}
// PredictFunc determines what terms can follow a command or a flag
// It is used for auto completion, given last - the last word in the already
// in the command line, what words can complete it.
type PredictFunc func(Args) []string
// Predict invokes the predict function and implements the Predictor interface
func (p PredictFunc) Predict(a Args) []string {
if p == nil {
return nil
}
return p(a)
}
// PredictNothing does not expect anything after.
var PredictNothing Predictor
// PredictAnything expects something, but nothing particular, such as a number
// or arbitrary name.
var PredictAnything = PredictFunc(func(Args) []string { return nil })

108
vendor/github.com/posener/complete/predict_files.go generated vendored Normal file
View File

@ -0,0 +1,108 @@
package complete
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/posener/complete/match"
)
// PredictDirs will search for directories in the given started to be typed
// path, if no path was started to be typed, it will complete to directories
// in the current working directory.
func PredictDirs(pattern string) Predictor {
return files(pattern, false)
}
// PredictFiles will search for files matching the given pattern in the started to
// be typed path, if no path was started to be typed, it will complete to files that
// match the pattern in the current working directory.
// To match any file, use "*" as pattern. To match go files use "*.go", and so on.
func PredictFiles(pattern string) Predictor {
return files(pattern, true)
}
func files(pattern string, allowFiles bool) PredictFunc {
// search for files according to arguments,
// if only one directory has matched the result, search recursively into
// this directory to give more results.
return func(a Args) (prediction []string) {
prediction = predictFiles(a, pattern, allowFiles)
// if the number of prediction is not 1, we either have many results or
// have no results, so we return it.
if len(prediction) != 1 {
return
}
// only try deeper, if the one item is a directory
if stat, err := os.Stat(prediction[0]); err != nil || !stat.IsDir() {
return
}
a.Last = prediction[0]
return predictFiles(a, pattern, allowFiles)
}
}
func predictFiles(a Args, pattern string, allowFiles bool) []string {
if strings.HasSuffix(a.Last, "/..") {
return nil
}
dir := a.Directory()
files := listFiles(dir, pattern, allowFiles)
// add dir if match
files = append(files, dir)
return PredictFilesSet(files).Predict(a)
}
// PredictFilesSet predict according to file rules to a given set of file names
func PredictFilesSet(files []string) PredictFunc {
return func(a Args) (prediction []string) {
// add all matching files to prediction
for _, f := range files {
f = fixPathForm(a.Last, f)
// test matching of file to the argument
if match.File(f, a.Last) {
prediction = append(prediction, f)
}
}
return
}
}
func listFiles(dir, pattern string, allowFiles bool) []string {
// set of all file names
m := map[string]bool{}
// list files
if files, err := filepath.Glob(filepath.Join(dir, pattern)); err == nil {
for _, f := range files {
if stat, err := os.Stat(f); err != nil || stat.IsDir() || allowFiles {
m[f] = true
}
}
}
// list directories
if dirs, err := ioutil.ReadDir(dir); err == nil {
for _, d := range dirs {
if d.IsDir() {
m[filepath.Join(dir, d.Name())] = true
}
}
}
list := make([]string, 0, len(m))
for k := range m {
list = append(list, k)
}
return list
}

19
vendor/github.com/posener/complete/predict_set.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
package complete
import "github.com/posener/complete/match"
// PredictSet expects specific set of terms, given in the options argument.
func PredictSet(options ...string) Predictor {
return predictSet(options)
}
type predictSet []string
func (p predictSet) Predict(a Args) (prediction []string) {
for _, m := range p {
if match.Prefix(m, a.Last) {
prediction = append(prediction, m)
}
}
return
}

116
vendor/github.com/posener/complete/readme.md generated vendored Normal file
View File

@ -0,0 +1,116 @@
# complete
[![Build Status](https://travis-ci.org/posener/complete.svg?branch=master)](https://travis-ci.org/posener/complete)
[![codecov](https://codecov.io/gh/posener/complete/branch/master/graph/badge.svg)](https://codecov.io/gh/posener/complete)
[![GoDoc](https://godoc.org/github.com/posener/complete?status.svg)](http://godoc.org/github.com/posener/complete)
[![Go Report Card](https://goreportcard.com/badge/github.com/posener/complete)](https://goreportcard.com/report/github.com/posener/complete)
A tool for bash writing bash completion in go.
Writing bash completion scripts is a hard work. This package provides an easy way
to create bash completion scripts for any command, and also an easy way to install/uninstall
the completion of the command.
## go command bash completion
In [gocomplete](./gocomplete) there is an example for bash completion for the `go` command line.
This is an example that uses the `complete` package on the `go` command - the `complete` package
can also be used to implement any completions, see [Usage](#usage).
### Install
1. Type in your shell:
```
go get -u github.com/posener/complete/gocomplete
gocomplete -install
```
2. Restart your shell
Uninstall by `gocomplete -uninstall`
### Features
- Complete `go` command, including sub commands and all flags.
- Complete packages names or `.go` files when necessary.
- Complete test names after `-run` flag.
## complete package
Supported shells:
- [x] bash
- [x] zsh
### Usage
Assuming you have program called `run` and you want to have bash completion
for it, meaning, if you type `run` then space, then press the `Tab` key,
the shell will suggest relevant complete options.
In that case, we will create a program called `runcomplete`, a go program,
with a `func main()` and so, that will make the completion of the `run`
program. Once the `runcomplete` will be in a binary form, we could
`runcomplete -install` and that will add to our shell all the bash completion
options for `run`.
So here it is:
```go
import "github.com/posener/complete"
func main() {
// create a Command object, that represents the command we want
// to complete.
run := complete.Command{
// Sub defines a list of sub commands of the program,
// this is recursive, since every command is of type command also.
Sub: complete.Commands{
// add a build sub command
"build": complete.Command {
// define flags of the build sub command
Flags: complete.Flags{
// build sub command has a flag '-cpus', which
// expects number of cpus after it. in that case
// anything could complete this flag.
"-cpus": complete.PredictAnything,
},
},
},
// define flags of the 'run' main command
Flags: complete.Flags{
// a flag -o, which expects a file ending with .out after
// it, the tab completion will auto complete for files matching
// the given pattern.
"-o": complete.PredictFiles("*.out"),
},
// define global flags of the 'run' main command
// those will show up also when a sub command was entered in the
// command line
GlobalFlags: complete.Flags{
// a flag '-h' which does not expects anything after it
"-h": complete.PredictNothing,
},
}
// run the command completion, as part of the main() function.
// this triggers the autocompletion when needed.
// name must be exactly as the binary that we want to complete.
complete.New("run", run).Run()
}
```
### Self completing program
In case that the program that we want to complete is written in go we
can make it self completing.
Here is an [example](./example/self/main.go)

12
vendor/github.com/posener/complete/test.sh generated vendored Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -v -race -coverprofile=profile.out -covermode=atomic $d
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done

46
vendor/github.com/posener/complete/utils.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
package complete
import (
"os"
"path/filepath"
"strings"
)
// fixPathForm changes a file name to a relative name
func fixPathForm(last string, file string) string {
// get wording directory for relative name
workDir, err := os.Getwd()
if err != nil {
return file
}
abs, err := filepath.Abs(file)
if err != nil {
return file
}
// if last is absolute, return path as absolute
if filepath.IsAbs(last) {
return fixDirPath(abs)
}
rel, err := filepath.Rel(workDir, abs)
if err != nil {
return file
}
// fix ./ prefix of path
if rel != "." && strings.HasPrefix(last, ".") {
rel = "./" + rel
}
return fixDirPath(rel)
}
func fixDirPath(path string) string {
info, err := os.Stat(path)
if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") {
path += "/"
}
return path
}

38
vendor/vendor.json vendored
View File

@ -378,14 +378,14 @@
{
"checksumSHA1": "tdhmIGUaoOMEDymMC23qTS7bt0g=",
"path": "github.com/docker/docker/pkg/ioutils",
"revision": "52debcd58ac91bf68503ce60561536911b74ff05",
"revisionTime": "2016-05-20T15:17:10Z"
"revision": "da39e9a4f920a15683dd0f23923c302d4db6eed5",
"revisionTime": "2016-05-28T08:11:04Z"
},
{
"checksumSHA1": "tdhmIGUaoOMEDymMC23qTS7bt0g=",
"path": "github.com/docker/docker/pkg/ioutils",
"revision": "da39e9a4f920a15683dd0f23923c302d4db6eed5",
"revisionTime": "2016-05-28T08:11:04Z"
"revision": "52debcd58ac91bf68503ce60561536911b74ff05",
"revisionTime": "2016-05-20T15:17:10Z"
},
{
"checksumSHA1": "BlFSSK7zUjPzPuxkLmM/0wpvku8=",
@ -960,10 +960,10 @@
"revision": "7e024ce8ce18b21b475ac6baf8fa3c42536bf2fa"
},
{
"checksumSHA1": "bUuI7AVR3IZPLlBaEKmw/ke7wqA=",
"checksumSHA1": "F9rKfF4/KI5jhVBBIMuf6eDnTN0=",
"path": "github.com/mitchellh/cli",
"revision": "b481eac70eea3ad671b7c360a013f89bb759b252",
"revisionTime": "2017-05-23T17:27:49Z"
"revision": "4796e5fef694378c14b647f7221591afa58e38cd",
"revisionTime": "2017-07-17T21:49:25Z"
},
{
"checksumSHA1": "ttEN1Aupb7xpPMkQLqb3tzLFdXs=",
@ -1070,6 +1070,30 @@
"revision": "792786c7400a136282c1664665ae0a8db921c6c2",
"revisionTime": "2016-01-10T10:55:54Z"
},
{
"checksumSHA1": "6OEUkwOM0qgI6YxR+BDEn6YMvpU=",
"path": "github.com/posener/complete",
"revision": "e8d6fef54b6283f8bd3fbcb4a7de8884eb540293",
"revisionTime": "2017-05-27T17:24:47Z"
},
{
"checksumSHA1": "NB7uVS0/BJDmNu68vPAlbrq4TME=",
"path": "github.com/posener/complete/cmd",
"revision": "e8d6fef54b6283f8bd3fbcb4a7de8884eb540293",
"revisionTime": "2017-05-27T17:24:47Z"
},
{
"checksumSHA1": "kuS9vs+TMQzTGzXteL6EZ5HuKrU=",
"path": "github.com/posener/complete/cmd/install",
"revision": "e8d6fef54b6283f8bd3fbcb4a7de8884eb540293",
"revisionTime": "2017-05-27T17:24:47Z"
},
{
"checksumSHA1": "DMo94FwJAm9ZCYCiYdJU2+bh4no=",
"path": "github.com/posener/complete/match",
"revision": "e8d6fef54b6283f8bd3fbcb4a7de8884eb540293",
"revisionTime": "2017-05-27T17:24:47Z"
},
{
"checksumSHA1": "M57Rrfc8Z966p+IBtQ91QOcUtcg=",
"comment": "v2.0.1-8-g983d3a5",

View File

@ -25,6 +25,16 @@ with the `-h` argument.
Each command has been conveniently documented on this website. Links to each
command can be found on the left.
### Autocomplete
Nomad's CLI supports command autocomplete. Autocomplete can be installed or
uninstalled by running the following on bash or zsh shells:
```
$ nomad -autocomplete-install
$ nomad -autocomplete-uninstall
```
### Command Contexts
Nomad's CLI commands have implied contexts in their naming convention. Because