Merge pull request #5454 from jvoorhis/f-autocomplete
Autocomplete for top-level commands
This commit is contained in:
commit
6e2f06ef49
|
@ -10,6 +10,7 @@ go:
|
|||
- 1.9.x
|
||||
- 1.x
|
||||
|
||||
|
||||
install:
|
||||
- make deps
|
||||
|
||||
|
|
|
@ -14,13 +14,15 @@ import (
|
|||
"github.com/hashicorp/packer/helper/enumflag"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type BuildCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c BuildCommand) Run(args []string) int {
|
||||
func (c *BuildCommand) Run(args []string) int {
|
||||
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
|
||||
var cfgOnError string
|
||||
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
|
||||
|
@ -283,7 +285,7 @@ func (c BuildCommand) Run(args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (BuildCommand) Help() string {
|
||||
func (*BuildCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer build [options] TEMPLATE
|
||||
|
||||
|
@ -307,6 +309,25 @@ Options:
|
|||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (BuildCommand) Synopsis() string {
|
||||
func (*BuildCommand) Synopsis() string {
|
||||
return "build image(s) from template"
|
||||
}
|
||||
|
||||
func (*BuildCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (*BuildCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-color": complete.PredictNothing,
|
||||
"-debug": complete.PredictNothing,
|
||||
"-except": complete.PredictNothing,
|
||||
"-only": complete.PredictNothing,
|
||||
"-force": complete.PredictNothing,
|
||||
"-machine-readable": complete.PredictNothing,
|
||||
"-on-error": complete.PredictNothing,
|
||||
"-parallel": complete.PredictNothing,
|
||||
"-var": complete.PredictNothing,
|
||||
"-var-file": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/fix"
|
||||
"github.com/hashicorp/packer/template"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type FixCommand struct {
|
||||
|
@ -140,3 +142,13 @@ Options:
|
|||
func (c *FixCommand) Synopsis() string {
|
||||
return "fixes templates from old versions of packer"
|
||||
}
|
||||
|
||||
func (c *FixCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *FixCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-validate": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/template"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type InspectCommand struct {
|
||||
|
@ -160,3 +162,13 @@ Options:
|
|||
func (c *InspectCommand) Synopsis() string {
|
||||
return "see components of a template"
|
||||
}
|
||||
|
||||
func (c *InspectCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *InspectCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-machine-readable": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"github.com/hashicorp/packer/helper/flag-kv"
|
||||
"github.com/hashicorp/packer/helper/flag-slice"
|
||||
"github.com/hashicorp/packer/template"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// archiveTemplateEntry is the name the template always takes within the slug.
|
||||
|
@ -334,6 +336,20 @@ func (*PushCommand) Synopsis() string {
|
|||
return "push a template and supporting files to a Packer build service"
|
||||
}
|
||||
|
||||
func (*PushCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (*PushCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-name": complete.PredictNothing,
|
||||
"-token": complete.PredictNothing,
|
||||
"-sensitive": complete.PredictNothing,
|
||||
"-var": complete.PredictNothing,
|
||||
"-var-file": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PushCommand) upload(
|
||||
r *archive.Archive, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
|
||||
if c.uploadFn != nil {
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type ValidateCommand struct {
|
||||
|
@ -136,3 +138,17 @@ Options:
|
|||
func (*ValidateCommand) Synopsis() string {
|
||||
return "check that a template is valid"
|
||||
}
|
||||
|
||||
func (*ValidateCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (*ValidateCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-syntax-only": complete.PredictNothing,
|
||||
"-except": complete.PredictNothing,
|
||||
"-only": complete.PredictNothing,
|
||||
"-var": complete.PredictNothing,
|
||||
"-var-file": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
|
12
main.go
12
main.go
|
@ -209,11 +209,13 @@ func wrappedMain() int {
|
|||
}
|
||||
|
||||
cli := &cli.CLI{
|
||||
Args: args,
|
||||
Commands: Commands,
|
||||
HelpFunc: excludeHelpFunc(Commands, []string{"plugin"}),
|
||||
HelpWriter: os.Stdout,
|
||||
Version: version.Version,
|
||||
Args: args,
|
||||
Autocomplete: true,
|
||||
Commands: Commands,
|
||||
HelpFunc: excludeHelpFunc(Commands, []string{"plugin"}),
|
||||
HelpWriter: os.Stdout,
|
||||
Name: "packer",
|
||||
Version: version.Version,
|
||||
}
|
||||
|
||||
exitCode, err := cli.Run()
|
||||
|
|
|
@ -18,6 +18,9 @@ cli is the library that powers the CLI for
|
|||
* Optional support for default subcommands so `cli` does something
|
||||
other than error.
|
||||
|
||||
* Support for shell autocompletion of subcommands, flags, and arguments
|
||||
with callbacks in Go. You don't need to write any shell code.
|
||||
|
||||
* Automatic help generation for listing subcommands
|
||||
|
||||
* Automatic help flag recognition of `-h`, `--help`, etc.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
@ -58,14 +59,56 @@ type CLI struct {
|
|||
// For example, if the key is "foo bar", then to access it our CLI
|
||||
// must be accessed with "./cli foo bar". See the docs for CLI for
|
||||
// notes on how this changes some other behavior of the CLI as well.
|
||||
//
|
||||
// The factory should be as cheap as possible, ideally only allocating
|
||||
// a struct. The factory may be called multiple times in the course
|
||||
// of a command execution and certain events such as help require the
|
||||
// instantiation of all commands. Expensive initialization should be
|
||||
// deferred to function calls within the interface implementation.
|
||||
Commands map[string]CommandFactory
|
||||
|
||||
// HiddenCommands is a list of commands that are "hidden". Hidden
|
||||
// commands are not given to the help function callback and do not
|
||||
// show up in autocomplete. The values in the slice should be equivalent
|
||||
// to the keys in the command map.
|
||||
HiddenCommands []string
|
||||
|
||||
// Name defines the name of the CLI.
|
||||
Name string
|
||||
|
||||
// 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.
|
||||
//
|
||||
// AutocompleteNoDefaultFlags is a boolean which controls if the default auto-
|
||||
// complete flags like -help and -version are added to the output.
|
||||
//
|
||||
// AutocompleteGlobalFlags are a mapping of global flags for
|
||||
// autocompletion. The help and version flags are automatically added.
|
||||
Autocomplete bool
|
||||
AutocompleteInstall string
|
||||
AutocompleteUninstall string
|
||||
AutocompleteNoDefaultFlags bool
|
||||
AutocompleteGlobalFlags complete.Flags
|
||||
autocompleteInstaller autocompleteInstaller // For tests
|
||||
|
||||
// HelpFunc and HelpWriter are used to output help information, if
|
||||
// requested.
|
||||
//
|
||||
|
@ -78,23 +121,33 @@ 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
|
||||
commandHidden map[string]struct{}
|
||||
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,
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -117,6 +170,14 @@ func (c *CLI) IsVersion() bool {
|
|||
func (c *CLI) Run() (int, error) {
|
||||
c.once.Do(c.init)
|
||||
|
||||
// If this is a autocompletion request, satisfy it. This must be called
|
||||
// first before anything else since its possible to be autocompleting
|
||||
// -help or -version or other flags and we want to show completions
|
||||
// and not actually write the help or version.
|
||||
if c.Autocomplete && c.autocomplete.Complete() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Just show the version and exit if instructed.
|
||||
if c.IsVersion() && c.Version != "" {
|
||||
c.HelpWriter.Write([]byte(c.Version + "\n"))
|
||||
|
@ -125,16 +186,50 @@ func (c *CLI) Run() (int, error) {
|
|||
|
||||
// Just print the help when only '-h' or '--help' is passed.
|
||||
if c.IsHelp() && c.Subcommand() == "" {
|
||||
c.HelpWriter.Write([]byte(c.HelpFunc(c.Commands) + "\n"))
|
||||
c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.Subcommand())) + "\n"))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
if !ok {
|
||||
c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.subcommandParent())) + "\n"))
|
||||
return 1, nil
|
||||
return 127, nil
|
||||
}
|
||||
|
||||
command, err := raw.(CommandFactory)()
|
||||
|
@ -216,6 +311,14 @@ func (c *CLI) init() {
|
|||
c.HelpWriter = os.Stderr
|
||||
}
|
||||
|
||||
// Build our hidden commands
|
||||
if len(c.HiddenCommands) > 0 {
|
||||
c.commandHidden = make(map[string]struct{})
|
||||
for _, h := range c.HiddenCommands {
|
||||
c.commandHidden[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Build our command tree
|
||||
c.commandTree = radix.New()
|
||||
c.commandNested = false
|
||||
|
@ -268,10 +371,113 @@ 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.
|
||||
if !c.AutocompleteNoDefaultFlags {
|
||||
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 {
|
||||
// Keep track of the full key so that we can nest further if necessary
|
||||
fullKey := k
|
||||
|
||||
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:]
|
||||
}
|
||||
|
||||
if idx := strings.Index(k, " "); idx >= 0 {
|
||||
// If there is a space, we trim up to the space. This turns
|
||||
// "sub sub2 sub3" into "sub". The prefix trim above will
|
||||
// trim our current depth properly.
|
||||
k = k[:idx]
|
||||
}
|
||||
|
||||
if _, ok := cmd.Sub[k]; ok {
|
||||
// If we already tracked this subcommand then ignore
|
||||
return false
|
||||
}
|
||||
|
||||
// If the command is hidden, don't record it at all
|
||||
if _, ok := c.commandHidden[fullKey]; ok {
|
||||
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)
|
||||
|
@ -386,6 +592,11 @@ func (c *CLI) helpCommands(prefix string) map[string]CommandFactory {
|
|||
panic("not found: " + k)
|
||||
}
|
||||
|
||||
// If this is a hidden command, don't show it
|
||||
if _, ok := c.commandHidden[k]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result[k] = raw.(CommandFactory)
|
||||
}
|
||||
|
||||
|
@ -404,6 +615,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 +680,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}}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,12 +7,25 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// MockUi is a mock UI that is used for tests and is exported publicly for
|
||||
// use in external tests if needed as well.
|
||||
// NewMockUi returns a fully initialized MockUi instance
|
||||
// which is safe for concurrent use.
|
||||
func NewMockUi() *MockUi {
|
||||
m := new(MockUi)
|
||||
m.once.Do(m.init)
|
||||
return m
|
||||
}
|
||||
|
||||
// MockUi is a mock UI that is used for tests and is exported publicly
|
||||
// for use in external tests if needed as well. Do not instantite this
|
||||
// directly since the buffers will be initialized on the first write. If
|
||||
// there is no write then you will get a nil panic. Please use the
|
||||
// NewMockUi() constructor function instead. You can fix your code with
|
||||
//
|
||||
// sed -i -e 's/new(cli.MockUi)/cli.NewMockUi()/g' *_test.go
|
||||
type MockUi struct {
|
||||
InputReader io.Reader
|
||||
ErrorWriter *bytes.Buffer
|
||||
OutputWriter *bytes.Buffer
|
||||
ErrorWriter *syncBuffer
|
||||
OutputWriter *syncBuffer
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
@ -59,6 +72,40 @@ func (u *MockUi) Warn(message string) {
|
|||
}
|
||||
|
||||
func (u *MockUi) init() {
|
||||
u.ErrorWriter = new(bytes.Buffer)
|
||||
u.OutputWriter = new(bytes.Buffer)
|
||||
u.ErrorWriter = new(syncBuffer)
|
||||
u.OutputWriter = new(syncBuffer)
|
||||
}
|
||||
|
||||
type syncBuffer struct {
|
||||
sync.RWMutex
|
||||
b bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Write(data []byte) (int, error) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
return b.b.Write(data)
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Read(data []byte) (int, error) {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
return b.b.Read(data)
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Reset() {
|
||||
b.Lock()
|
||||
b.b.Reset()
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *syncBuffer) String() string {
|
||||
return string(b.Bytes())
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Bytes() []byte {
|
||||
b.RLock()
|
||||
data := b.b.Bytes()
|
||||
b.RUnlock()
|
||||
return data
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 find 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 find 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 -o nospace -C %s %s", bin, cmd)
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
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 the flag starts with a hyphen, we avoid emitting the prediction
|
||||
// unless the last typed arg contains a hyphen as well.
|
||||
flagHyphenStart := len(flag) != 0 && flag[0] == '-'
|
||||
lastHyphenStart := len(a.Last) != 0 && a.Last[0] == '-'
|
||||
if flagHyphenStart && !lastHyphenStart {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// We matched so stop searching. Continuing to search can accidentally
|
||||
// match a subcommand with current set of commands, see issue #46.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 })
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -1011,10 +1011,11 @@
|
|||
"revisionTime": "2017-05-10T07:48:58Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "UP+pXl+ic9y6qrpZA5MqDIAuGfw=",
|
||||
"checksumSHA1": "UIqCj7qI0hhIMpAhS9YYqs2jD48=",
|
||||
"path": "github.com/mitchellh/cli",
|
||||
"revision": "ee8578a9c12a5bb9d55303b9665cc448772c81b8",
|
||||
"revisionTime": "2017-03-28T05:23:52Z"
|
||||
|
||||
"revision": "65fcae5817c8600da98ada9d7edf26dd1a84837b",
|
||||
"revisionTime": "2017-09-08T18:10:43Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "mVqDwKcibat0IKAdzAhfGIHPwI8=",
|
||||
|
@ -1163,6 +1164,30 @@
|
|||
"path": "github.com/pmezard/go-difflib/difflib",
|
||||
"revision": "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "rTNABfFJ9wtLQRH8uYNkEZGQOrY=",
|
||||
"path": "github.com/posener/complete",
|
||||
"revision": "88e59760adaddb8276c9b15511302890690e2dae",
|
||||
"revisionTime": "2017-09-08T12:52:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "NB7uVS0/BJDmNu68vPAlbrq4TME=",
|
||||
"path": "github.com/posener/complete/cmd",
|
||||
"revision": "88e59760adaddb8276c9b15511302890690e2dae",
|
||||
"revisionTime": "2017-09-08T12:52:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Hwojin3GxRyKwPAiz5r7UszqkPc=",
|
||||
"path": "github.com/posener/complete/cmd/install",
|
||||
"revision": "88e59760adaddb8276c9b15511302890690e2dae",
|
||||
"revisionTime": "2017-09-08T12:52:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "DMo94FwJAm9ZCYCiYdJU2+bh4no=",
|
||||
"path": "github.com/posener/complete/match",
|
||||
"revision": "88e59760adaddb8276c9b15511302890690e2dae",
|
||||
"revisionTime": "2017-09-08T12:52:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Kq0fF7R65dDcGReuhf47O3LQgrY=",
|
||||
"path": "github.com/profitbricks/profitbricks-sdk-go",
|
||||
|
|
|
@ -106,3 +106,18 @@ The set of machine-readable message types can be found in the
|
|||
documentation section. This section contains documentation on all the message
|
||||
types exposed by Packer core as well as all the components that ship with
|
||||
Packer by default.
|
||||
|
||||
## Autocompletion
|
||||
|
||||
The `packer` command features opt-in subcommand autocompletion that you can
|
||||
enable for your shell with `packer -autocomplete-install`. After doing so,
|
||||
you can invoke a new shell and use the feature.
|
||||
|
||||
For example, assume a tab is typed at the end of each prompt line:
|
||||
|
||||
```
|
||||
$ packer p
|
||||
plugin push
|
||||
$ packer push -
|
||||
-name -sensitive -token -var -var-file
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue