diff --git a/application.go b/application.go index 5b1a9ce..0d07738 100644 --- a/application.go +++ b/application.go @@ -312,6 +312,8 @@ func (a *Application) setup() { a.prependFlag(HelpFlag) } + registerAutocompleteCommands(a) + for _, c := range a.Commands { if c.HelpName == "" { c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.FullName()) diff --git a/binary.go b/binary.go index 422ea9c..f643ef2 100644 --- a/binary.go +++ b/binary.go @@ -42,3 +42,29 @@ func CurrentBinaryPath() (string, error) { } return argv0, nil } + +func CurrentBinaryInvocation() (string, error) { + if len(os.Args) == 0 || os.Args[0] == "" { + return "", errors.New("no binary invokation found") + } + + return os.Args[0], nil +} + +func (c *Context) CurrentBinaryPath() string { + path, err := CurrentBinaryPath() + if err != nil { + panic(err) + } + + return path +} + +func (c *Context) CurrentBinaryInvocation() string { + invocation, err := CurrentBinaryInvocation() + if err != nil { + panic(err) + } + + return invocation +} diff --git a/command.go b/command.go index f68555b..20a4fd9 100644 --- a/command.go +++ b/command.go @@ -48,6 +48,8 @@ type Command struct { DescriptionFunc DescriptionFunc // The category the command is part of Category string + // The function to call when checking for shell command completions + ShellComplete ShellCompleteFunc // An action to execute before any sub-subcommands are run, but after the context is ready // If a non-nil error is returned, no sub-subcommands are run Before BeforeFunc diff --git a/completion.go b/completion.go new file mode 100644 index 0000000..9428d6b --- /dev/null +++ b/completion.go @@ -0,0 +1,124 @@ +//go:build darwin || linux || freebsd || openbsd + +package console + +import ( + "fmt" + "os" + "runtime/debug" + + "github.com/posener/complete/v2" +) + +func init() { + for _, key := range []string{"COMP_LINE", "COMP_POINT", "COMP_DEBUG"} { + if _, hasEnv := os.LookupEnv(key); hasEnv { + // Disable Garbage collection for faster autocompletion + debug.SetGCPercent(-1) + return + } + } +} + +var autoCompleteCommand = &Command{ + Category: "self", + Name: "autocomplete", + Description: "Internal command to provide shell completion suggestions", + Hidden: Hide, + FlagParsing: FlagParsingSkippedAfterFirstArg, + Args: ArgDefinition{ + &Arg{ + Slice: true, + Optional: true, + }, + }, + Action: AutocompleteAppAction, +} + +func registerAutocompleteCommands(a *Application) { + if IsGoRun() { + return + } + + a.Commands = append( + []*Command{shellAutoCompleteInstallCommand, autoCompleteCommand}, + a.Commands..., + ) +} + +func AutocompleteAppAction(c *Context) error { + cmd := complete.Command{ + Flags: map[string]complete.Predictor{}, + Sub: map[string]*complete.Command{}, + } + + // transpose registered commands and flags to posener/complete equivalence + for _, command := range c.App.VisibleCommands() { + subCmd := command.convertToPosenerCompleteCommand(c) + + for _, name := range command.Names() { + cmd.Sub[name] = &subCmd + } + } + + for _, f := range c.App.VisibleFlags() { + if vf, ok := f.(*verbosityFlag); ok { + vf.addToPosenerFlags(c, cmd.Flags) + continue + } + + predictor := ContextPredictor{f, c} + + for _, name := range f.Names() { + name = fmt.Sprintf("%s%s", prefixFor(name), name) + cmd.Flags[name] = predictor + } + } + + cmd.Complete(c.App.HelpName) + return nil +} + +func (c *Command) convertToPosenerCompleteCommand(ctx *Context) complete.Command { + command := complete.Command{ + Flags: map[string]complete.Predictor{}, + } + + for _, f := range c.VisibleFlags() { + for _, name := range f.Names() { + name = fmt.Sprintf("%s%s", prefixFor(name), name) + command.Flags[name] = ContextPredictor{f, ctx} + } + } + + if len(c.Args) > 0 || c.ShellComplete != nil { + command.Args = ContextPredictor{c, ctx} + } + + return command +} + +func (c *Command) PredictArgs(ctx *Context, prefix string) []string { + if c.ShellComplete != nil { + return c.ShellComplete(ctx, prefix) + } + + return nil +} + +type Predictor interface { + PredictArgs(*Context, string) []string +} + +// ContextPredictor determines what terms can follow a command or a flag +// It is used for autocompletion, given the last word in the already completed +// command line, what words can complete it. +type ContextPredictor struct { + predictor Predictor + ctx *Context +} + +// Predict invokes the predict function and implements the Predictor interface +func (p ContextPredictor) Predict(prefix string) []string { + return p.predictor.PredictArgs(p.ctx, prefix) +} diff --git a/completion_installer.go b/completion_installer.go new file mode 100644 index 0000000..d77f842 --- /dev/null +++ b/completion_installer.go @@ -0,0 +1,140 @@ +//go:build darwin || linux || freebsd || openbsd + +package console + +import ( + "bytes" + "embed" + "fmt" + "os" + "path" + "strings" + "text/template" + + "github.com/pkg/errors" + "github.com/symfony-cli/terminal" +) + +// completionTemplates holds our shell completions templates. +// +//go:embed resources/completion.* +var completionTemplates embed.FS + +var shellAutoCompleteInstallCommand = &Command{ + Category: "self", + Name: "completion", + Aliases: []*Alias{ + {Name: "completion"}, + }, + Usage: "Dumps the completion script for the current shell", + ShellComplete: func(*Context, string) []string { + return []string{"bash", "zsh", "fish"} + }, + Description: `The {{.HelpName}} command dumps the shell completion script required +to use shell autocompletion (currently, bash, zsh and fish completion are supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + {{.HelpName}} {{ call .Shell }} | sudo tee {{ call .CompletionFile }} + +Or dump the script to a local file and source it: + + {{.HelpName}} {{ call .Shell }} > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "{{ call .RcFile }}" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this to the end of your shell configuration file (e.g. "{{ call .RcFile }}"): + + eval "$({{.HelpName}} {{ call .Shell }})"`, + DescriptionFunc: func(command *Command, application *Application) string { + var buf bytes.Buffer + + tpl := template.Must(template.New("description").Parse(command.Description)) + + if err := tpl.Execute(&buf, struct { + // allows to directly access any field from the command inside the template + *Command + Shell func() string + RcFile func() string + CompletionFile func() string + }{ + Command: command, + Shell: guessShell, + RcFile: func() string { + switch guessShell() { + case "fish": + return "~/.config/fish/config.fish" + case "zsh": + return "~/.zshrc" + default: + return "~/.bashrc" + } + }, + CompletionFile: func() string { + switch guessShell() { + case "fish": + return fmt.Sprintf("/etc/fish/completions/%s.fish", application.HelpName) + case "zsh": + return fmt.Sprintf("$fpath[1]/_%s", application.HelpName) + default: + return fmt.Sprintf("/etc/bash_completion.d/%s", application.HelpName) + } + }, + }); err != nil { + panic(err) + } + + return buf.String() + }, + Args: []*Arg{ + { + Name: "shell", + Description: `The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given`, + Optional: true, + }, + }, + Action: func(c *Context) error { + shell := c.Args().Get("shell") + if shell == "" { + shell = guessShell() + } + + templates, err := template.ParseFS(completionTemplates, "resources/*") + if err != nil { + return errors.WithStack(err) + } + + if tpl := templates.Lookup(fmt.Sprintf("completion.%s", shell)); tpl != nil { + return errors.WithStack(tpl.Execute(terminal.Stdout, c)) + } + + var supportedShell []string + + for _, tmpl := range templates.Templates() { + if tmpl.Tree == nil || tmpl.Root == nil { + continue + } + supportedShell = append(supportedShell, strings.TrimLeft(path.Ext(tmpl.Name()), ".")) + } + + if shell == "" { + return errors.Errorf(`shell not detected, supported shells: "%s"`, strings.Join(supportedShell, ", ")) + } + + return errors.Errorf(`shell "%s" is not supported, supported shells: "%s"`, shell, strings.Join(supportedShell, ", ")) + }, +} + +func guessShell() string { + return path.Base(os.Getenv("SHELL")) +} diff --git a/completion_others.go b/completion_others.go new file mode 100644 index 0000000..2c83efc --- /dev/null +++ b/completion_others.go @@ -0,0 +1,10 @@ +//go:build !darwin && !linux && !freebsd && !openbsd +// +build !darwin,!linux,!freebsd,!openbsd + +package console + +const HasAutocompleteSupport = false + +func IsAutocomplete(c *Command) bool { + return false +} diff --git a/completion_unix.go b/completion_unix.go new file mode 100644 index 0000000..edc260d --- /dev/null +++ b/completion_unix.go @@ -0,0 +1,10 @@ +//go:build darwin || linux || freebsd || openbsd +// +build darwin linux freebsd openbsd + +package console + +const SupportsAutocomplete = true + +func IsAutocomplete(c *Command) bool { + return c == autoCompleteCommand +} diff --git a/flag.go b/flag.go index 95ec721..b4cf7cc 100644 --- a/flag.go +++ b/flag.go @@ -92,6 +92,8 @@ func (f FlagsByName) Swap(i, j int) { // this interface be implemented. type Flag interface { fmt.Stringer + + PredictArgs(*Context, string) []string Validate(*Context) error // Apply Flag settings to the given flag set Apply(*flag.FlagSet) diff --git a/flags.go b/flags.go index 0915055..90f72f8 100644 --- a/flags.go +++ b/flags.go @@ -27,16 +27,17 @@ import ( // BoolFlag is a flag with type bool type BoolFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue bool - DefaultText string - Required bool - Validator func(*Context, bool) error - Destination *bool + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, bool) error + Destination *bool } // String returns a readable representation of this value @@ -45,6 +46,13 @@ func (f *BoolFlag) String() string { return FlagStringer(f) } +func (f *BoolFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{"true", "false"} +} + func (f *BoolFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Bool(f.Name)) @@ -80,16 +88,17 @@ func lookupBool(name string, f *flag.Flag) bool { // DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) type DurationFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue time.Duration - DefaultText string - Required bool - Validator func(*Context, time.Duration) error - Destination *time.Duration + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue time.Duration + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, time.Duration) error + Destination *time.Duration } // String returns a readable representation of this value @@ -98,6 +107,13 @@ func (f *DurationFlag) String() string { return FlagStringer(f) } +func (f *DurationFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *DurationFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Duration(f.Name)) @@ -133,16 +149,17 @@ func lookupDuration(name string, f *flag.Flag) time.Duration { // Float64Flag is a flag with type float64 type Float64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue float64 - DefaultText string - Required bool - Validator func(*Context, float64) error - Destination *float64 + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue float64 + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, float64) error + Destination *float64 } // String returns a readable representation of this value @@ -151,6 +168,13 @@ func (f *Float64Flag) String() string { return FlagStringer(f) } +func (f *Float64Flag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *Float64Flag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Float64(f.Name)) @@ -186,15 +210,16 @@ func lookupFloat64(name string, f *flag.Flag) float64 { // GenericFlag is a flag with type Generic type GenericFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, interface{}) error - Destination Generic + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, interface{}) error + Destination Generic } // String returns a readable representation of this value @@ -203,6 +228,13 @@ func (f *GenericFlag) String() string { return FlagStringer(f) } +func (f *GenericFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *GenericFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Generic(f.Name)) @@ -238,16 +270,17 @@ func lookupGeneric(name string, f *flag.Flag) interface{} { // Int64Flag is a flag with type int64 type Int64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue int64 - DefaultText string - Required bool - Validator func(*Context, int64) error - Destination *int64 + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue int64 + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, int64) error + Destination *int64 } // String returns a readable representation of this value @@ -256,6 +289,13 @@ func (f *Int64Flag) String() string { return FlagStringer(f) } +func (f *Int64Flag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *Int64Flag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Int64(f.Name)) @@ -291,16 +331,17 @@ func lookupInt64(name string, f *flag.Flag) int64 { // IntFlag is a flag with type int type IntFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue int - DefaultText string - Required bool - Validator func(*Context, int) error - Destination *int + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue int + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, int) error + Destination *int } // String returns a readable representation of this value @@ -309,6 +350,13 @@ func (f *IntFlag) String() string { return FlagStringer(f) } +func (f *IntFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *IntFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Int(f.Name)) @@ -344,15 +392,16 @@ func lookupInt(name string, f *flag.Flag) int { // IntSliceFlag is a flag with type *IntSlice type IntSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, []int) error - Destination *IntSlice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, []int) error + Destination *IntSlice } // String returns a readable representation of this value @@ -361,6 +410,13 @@ func (f *IntSliceFlag) String() string { return FlagStringer(f) } +func (f *IntSliceFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *IntSliceFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.IntSlice(f.Name)) @@ -398,15 +454,16 @@ func lookupIntSlice(name string, f *flag.Flag) []int { // Int64SliceFlag is a flag with type *Int64Slice type Int64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, []int64) error - Destination *Int64Slice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, []int64) error + Destination *Int64Slice } // String returns a readable representation of this value @@ -415,6 +472,13 @@ func (f *Int64SliceFlag) String() string { return FlagStringer(f) } +func (f *Int64SliceFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *Int64SliceFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Int64Slice(f.Name)) @@ -452,15 +516,16 @@ func lookupInt64Slice(name string, f *flag.Flag) []int64 { // Float64SliceFlag is a flag with type *Float64Slice type Float64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, []float64) error - Destination *Float64Slice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, []float64) error + Destination *Float64Slice } // String returns a readable representation of this value @@ -469,6 +534,13 @@ func (f *Float64SliceFlag) String() string { return FlagStringer(f) } +func (f *Float64SliceFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *Float64SliceFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Float64Slice(f.Name)) @@ -506,16 +578,17 @@ func lookupFloat64Slice(name string, f *flag.Flag) []float64 { // StringFlag is a flag with type string type StringFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue string - DefaultText string - Required bool - Validator func(*Context, string) error - Destination *string + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue string + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, string) error + Destination *string } // String returns a readable representation of this value @@ -524,6 +597,13 @@ func (f *StringFlag) String() string { return FlagStringer(f) } +func (f *StringFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *StringFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.String(f.Name)) @@ -559,15 +639,16 @@ func lookupString(name string, f *flag.Flag) string { // StringSliceFlag is a flag with type *StringSlice type StringSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, []string) error - Destination *StringSlice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, []string) error + Destination *StringSlice } // String returns a readable representation of this value @@ -576,6 +657,13 @@ func (f *StringSliceFlag) String() string { return FlagStringer(f) } +func (f *StringSliceFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *StringSliceFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.StringSlice(f.Name)) @@ -613,15 +701,16 @@ func lookupStringSlice(name string, f *flag.Flag) []string { // StringMapFlag is a flag with type *StringMap type StringMapFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultText string - Required bool - Validator func(*Context, map[string]string) error - Destination *StringMap + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, map[string]string) error + Destination *StringMap } // String returns a readable representation of this value @@ -630,6 +719,13 @@ func (f *StringMapFlag) String() string { return FlagStringer(f) } +func (f *StringMapFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *StringMapFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.StringMap(f.Name)) @@ -667,16 +763,17 @@ func lookupStringMap(name string, f *flag.Flag) map[string]string { // Uint64Flag is a flag with type uint64 type Uint64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue uint64 - DefaultText string - Required bool - Validator func(*Context, uint64) error - Destination *uint64 + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue uint64 + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, uint64) error + Destination *uint64 } // String returns a readable representation of this value @@ -685,6 +782,13 @@ func (f *Uint64Flag) String() string { return FlagStringer(f) } +func (f *Uint64Flag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *Uint64Flag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Uint64(f.Name)) @@ -720,16 +824,17 @@ func lookupUint64(name string, f *flag.Flag) uint64 { // UintFlag is a flag with type uint type UintFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - DefaultValue uint - DefaultText string - Required bool - Validator func(*Context, uint) error - Destination *uint + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + DefaultValue uint + DefaultText string + Required bool + ArgsPredictor func(*Context, string) []string + Validator func(*Context, uint) error + Destination *uint } // String returns a readable representation of this value @@ -738,6 +843,13 @@ func (f *UintFlag) String() string { return FlagStringer(f) } +func (f *UintFlag) PredictArgs(c *Context, prefix string) []string { + if f.ArgsPredictor != nil { + return f.ArgsPredictor(c, prefix) + } + return []string{} +} + func (f *UintFlag) Validate(c *Context) error { if f.Validator != nil { return f.Validator(c, c.Uint(f.Name)) diff --git a/funcs.go b/funcs.go index ae8e818..e8b5672 100644 --- a/funcs.go +++ b/funcs.go @@ -19,6 +19,9 @@ package console +// ShellCompleteFunc is an action to execute when the shell completion flag is set +type ShellCompleteFunc func(*Context, string) []string + // BeforeFunc is an action to execute before any subcommands are run, but after // the context is ready if a non-nil error is returned, no subcommands are run type BeforeFunc func(*Context) error diff --git a/go.go b/go.go new file mode 100644 index 0000000..f3a1e88 --- /dev/null +++ b/go.go @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021-present Fabien Potencier + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package console + +import ( + "os" + "path/filepath" + "strings" +) + +func IsGoRun() bool { + // Unfortunately, Golang does not expose that we are currently using go run + // So we detect the main binary is (or used to be ;)) "go" and then the + // current binary is within a temp "go-build" directory. + _, exe := filepath.Split(os.Getenv("_")) + argv0, _ := os.Executable() + + return exe == "go" && strings.Contains(argv0, "go-build") +} diff --git a/go.mod b/go.mod index 7fee3d9..cc40aa1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/agext/levenshtein v1.2.3 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 + github.com/posener/complete/v2 v2.1.0 github.com/rs/zerolog v1.32.0 github.com/symfony-cli/terminal v1.0.7 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c @@ -16,6 +17,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/posener/script v1.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.19.0 // indirect diff --git a/go.sum b/go.sum index b63e641..56aac3a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -21,12 +23,20 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete/v2 v2.1.0 h1:IpAWxMyiJ6zDSoq+QmEBF0thpOramC0kYuEFBTcQeTI= +github.com/posener/complete/v2 v2.1.0/go.mod h1:AkzsSVGx4ysH/4OhZf57dr4yszGXgFmXsP/VNwlaW7U= +github.com/posener/script v1.2.0 h1:DrZz0qFT8lCLkYNi1PleLDANFnKxJ2VmlNPJbAkVLsE= +github.com/posener/script v1.2.0/go.mod h1:s4sVvRXtdc/1aK6otTSeW2BVXndO8MsoOVUwK74zcg4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/symfony-cli/terminal v1.0.7 h1:57L9PUTE2cHfQtP8Ti8dyiiPEYlQ1NBIDpMJ3RPEGPc= github.com/symfony-cli/terminal v1.0.7/go.mod h1:Etv22IyeGiMoIQPPj51hX31j7xuYl1njyuAFkrvybqU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -38,3 +48,5 @@ golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging_flags.go b/logging_flags.go index 1b084f2..441a107 100644 --- a/logging_flags.go +++ b/logging_flags.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/posener/complete/v2" "github.com/symfony-cli/terminal" ) @@ -99,6 +100,10 @@ func VerbosityFlag(name, alias, shortAlias string) *verbosityFlag { } } +func (f *verbosityFlag) PredictArgs(c *Context, prefix string) []string { + return []string{"1", "2", "3", "4"} +} + func (f *verbosityFlag) Validate(c *Context) error { return nil } @@ -161,3 +166,27 @@ func (f *verbosityFlag) String() string { return fmt.Sprintf("%s\t%s", names, strings.TrimSpace(usage)) } + +func (f *verbosityFlag) addToPosenerFlags(c *Context, flags map[string]complete.Predictor) { + for i, n := 1, len(terminal.LogLevels)-2; i <= n; i++ { + name := prefixFor(f.ShortAlias) + name += strings.Repeat(f.ShortAlias, i) + flags[name] = complete.PredictFunc(func(prefix string) []string { + return f.PredictArgs(c, prefix) + }) + } + + for _, alias := range f.Aliases { + if alias != "" { + flags[prefixFor(alias)+alias] = complete.PredictFunc(func(prefix string) []string { + return f.PredictArgs(c, prefix) + }) + } + } + + if f.Name != "" { + flags[prefixFor(f.Name)+f.Name] = complete.PredictFunc(func(prefix string) []string { + return f.PredictArgs(c, prefix) + }) + } +} diff --git a/output_flags.go b/output_flags.go index 6d1c073..67a61e3 100644 --- a/output_flags.go +++ b/output_flags.go @@ -95,6 +95,10 @@ func (f *quietFlag) ForApp(app *Application) *quietFlag { } } +func (f *quietFlag) PredictArgs(*Context, string) []string { + return []string{"true", "false", ""} +} + func (f *quietFlag) Validate(c *Context) error { return nil } @@ -129,6 +133,12 @@ var ( ) func (app *Application) configureIO(c *Context) { + if IsAutocomplete(c.Command) { + terminal.DefaultStdout.SetDecorated(false) + terminal.Stdin.SetInteractive(false) + return + } + if c.IsSet(AnsiFlag.Name) { terminal.DefaultStdout.SetDecorated(c.Bool(AnsiFlag.Name)) } else if c.IsSet(NoAnsiFlag.Name) { diff --git a/resources/completion.bash b/resources/completion.bash new file mode 100644 index 0000000..e1a17c2 --- /dev/null +++ b/resources/completion.bash @@ -0,0 +1,58 @@ +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Bash completions for the CLI binary +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.bash +# - https://github.com/posener/complete/blob/master/install/bash.go +# - https://github.com/scop/bash-completion/blob/master/completions/sudo +# + +# this wrapper function allows us to let Symfony knows how to call the +# `bin/console` using the Symfony CLI binary (to ensure the right env and PHP +# versions are used) +_{{ .App.HelpName }}_console() { + # shellcheck disable=SC2068 + {{ .CurrentBinaryInvocation }} console $@ +} + +_complete_{{ .App.HelpName }}() { + + # Use the default completion for shell redirect operators. + for w in '>' '>>' '&>' '<'; do + if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then + compopt -o filenames + COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}")) + return 0 + fi + done + + for (( i=1; i <= COMP_CWORD; i++ )); do + if [[ "${COMP_WORDS[i]}" != -* ]]; then + case "${COMP_WORDS[i]}" in + console|php|pecl|composer|run|local:run) + _SF_CMD="_{{ .App.HelpName }}_console" _command_offset $i + return + esac; + fi + done + + IFS=$'\n' COMPREPLY=( $(COMP_LINE="${COMP_LINE}" COMP_POINT="${COMP_POINT}" COMP_DEBUG="$COMP_DEBUG" {{ .CurrentBinaryPath }} self:autocomplete) ) +} + +complete -F _complete_{{ .App.HelpName }} {{ .App.HelpName }} diff --git a/resources/completion.fish b/resources/completion.fish new file mode 100644 index 0000000..c82fa9e --- /dev/null +++ b/resources/completion.fish @@ -0,0 +1,42 @@ +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Fish completions for the CLI binary +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.fish +# - https://github.com/posener/complete/blob/master/install/fish.go +# - https://github.com/fish-shell/fish-shell/blob/master/share/completions/sudo.fish +# + +function __complete_{{ .App.HelpName }} + set -lx COMP_LINE (commandline -cp) + test -z (commandline -ct) + and set COMP_LINE "$COMP_LINE " + {{ .CurrentBinaryInvocation }} self:autocomplete +end + +# this wrapper function allows us to call Symfony autocompletion letting it +# knows how to call the `bin/console` using the Symfony CLI binary (to ensure +# the right env and PHP versions are used) +function __complete_{{ .App.HelpName }}_console + set -x _SF_CMD "{{ .CurrentBinaryInvocation }}" "console" + _sf_console +end + +complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from console" -a '(__complete_{{ .App.HelpName }}_console)' -f +complete -f -c '{{ .App.HelpName }}' -n "not __fish_seen_subcommand_from console php pecl composer run local:run" -a '(__complete_{{ .App.HelpName }})' diff --git a/resources/completion.zsh b/resources/completion.zsh new file mode 100644 index 0000000..9abfeae --- /dev/null +++ b/resources/completion.zsh @@ -0,0 +1,80 @@ +#compdef {{ .App.HelpName }} + +# Copyright (c) 2021-present Fabien Potencier +# +# This file is part of Symfony CLI project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# +# zsh completions for {{ .App.HelpName }} +# +# References: +# - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Console/Resources/completion.zsh +# - https://github.com/posener/complete/blob/master/install/zsh.go +# - https://stackoverflow.com/a/13547531 +# + +# this wrapper function allows us to let Symfony knows how to call the +# `bin/console` using the Symfony CLI binary (to ensure the right env and PHP +# versions are used) +_{{ .App.HelpName }}_console() { + # shellcheck disable=SC2068 + {{ .CurrentBinaryInvocation }} console $@ +} + +_complete_{{ .App.HelpName }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ .App.HelpName }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # detect if we are in a wrapper command and need to "forward" completion to it + for ((i = 1; i <= $#words; i++)); do + if [[ "${words[i]}" != -* ]]; then + case "${words[i]}" in + console|php|pecl|composer|run|local:run) + shift words + (( CURRENT-- )) + _SF_CMD="_{{ .App.HelpName }}_console" _normal + return + esac; + fi + done + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + completions+=${comp} + fi + done < <(COMP_LINE="$words" ${words[0]} ${_SF_CMD:-${words[1]}} self:autocomplete) + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix +} + +compdef _complete_{{ .App.HelpName }} {{ .App.HelpName }}