diff --git a/.gitignore b/.gitignore index a4aea7d25..592ae4f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ caraparse/caraparse docs/book example/cmd/_test_files/*.txt example/example +example-nonposix/example-nonposix integration.cov unit.cov .vscode diff --git a/carapace.go b/carapace.go index 736cfc42b..900691b89 100644 --- a/carapace.go +++ b/carapace.go @@ -57,7 +57,7 @@ func (c Carapace) PositionalCompletion(action ...Action) { // PositionalAnyCompletion defines completion for any positional arguments not already defined. func (c Carapace) PositionalAnyCompletion(action Action) { - storage.get(c.cmd).positionalAny = action + storage.get(c.cmd).positionalAny = &action } // DashCompletion defines completion for positional arguments after dash (`--`) using a list of Actions. @@ -67,7 +67,7 @@ func (c Carapace) DashCompletion(action ...Action) { // DashAnyCompletion defines completion for any positional arguments after dash (`--`) not already defined. func (c Carapace) DashAnyCompletion(action Action) { - storage.get(c.cmd).dashAny = action + storage.get(c.cmd).dashAny = &action } // FlagCompletion defines completion for flags using a map consisting of name and Action. diff --git a/compat.go b/compat.go index 800116f23..44eddcb09 100644 --- a/compat.go +++ b/compat.go @@ -2,6 +2,7 @@ package carapace import ( "fmt" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -10,7 +11,10 @@ import ( func registerValidArgsFunction(cmd *cobra.Command) { if cmd.ValidArgsFunction == nil { cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - action := storage.getPositional(cmd, len(args)).Invoke(Context{Args: args, Value: toComplete}) + action := Action{}.Invoke(Context{Args: args, Value: toComplete}) // TODO just IvokedAction{} ok? + if storage.hasPositional(cmd, len(args)) { + action = storage.getPositional(cmd, len(args)).Invoke(Context{Args: args, Value: toComplete}) + } return cobraValuesFor(action), cobraDirectiveFor(action) } } @@ -18,9 +22,16 @@ func registerValidArgsFunction(cmd *cobra.Command) { func registerFlagCompletion(cmd *cobra.Command) { cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if !storage.hasFlag(cmd, f.Name) { + return // skip if not defined in carapace + } + if _, ok := cmd.GetFlagCompletionFunc(f.Name); ok { + return // skip if already defined in cobra + } + err := cmd.RegisterFlagCompletionFunc(f.Name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { a := storage.getFlag(cmd, f.Name) - action := a.Invoke(Context{Args: args, Value: toComplete}) + action := a.Invoke(Context{Args: args, Value: toComplete}) // TODO cmd might differ for persistentflags and either way args or cmd will be wrong return cobraValuesFor(action), cobraDirectiveFor(action) }) if err != nil { @@ -51,3 +62,48 @@ func cobraDirectiveFor(action InvokedAction) cobra.ShellCompDirective { } return directive } + +type compDirective cobra.ShellCompDirective + +func (d compDirective) matches(cobraDirective cobra.ShellCompDirective) bool { + return d&compDirective(cobraDirective) != 0 +} + +func (d compDirective) ToA(values ...string) Action { + var action Action + switch { + case d.matches(cobra.ShellCompDirectiveError): + return ActionMessage("an error occured") + case d.matches(cobra.ShellCompDirectiveFilterDirs): + switch len(values) { + case 0: + action = ActionDirectories() + default: + action = ActionDirectories().Chdir(values[0]) + } + case d.matches(cobra.ShellCompDirectiveFilterFileExt): + extensions := make([]string, 0) + for _, v := range values { + extensions = append(extensions, "."+v) + } + return ActionFiles(extensions...) + case len(values) == 0 && !d.matches(cobra.ShellCompDirectiveNoFileComp): + action = ActionFiles() + default: + vals := make([]string, 0) + for _, v := range values { + if splitted := strings.SplitN(v, "\t", 2); len(splitted) == 2 { + vals = append(vals, splitted[0], splitted[1]) + } else { + vals = append(vals, splitted[0], "") + } + } + action = ActionValuesDescribed(vals...) + } + + if d.matches(cobra.ShellCompDirectiveNoSpace) { + action = action.NoSpace() + } + + return action +} diff --git a/compat_test.go b/compat_test.go index fa5590101..0e92024f0 100644 --- a/compat_test.go +++ b/compat_test.go @@ -1,7 +1,7 @@ package carapace import ( - "io/ioutil" + "io" "os" "strings" "testing" @@ -54,7 +54,7 @@ func TestRegisterFlagCompletion(t *testing.T) { _ = cmd.Execute() w.Close() - out, _ := ioutil.ReadAll(r) + out, _ := io.ReadAll(r) os.Stdout = rescueStdout if lines := strings.Split(string(out), "\n"); lines[0] != "1\tone" { diff --git a/context.go b/context.go index bdf02b4fa..2eed94a3a 100644 --- a/context.go +++ b/context.go @@ -12,6 +12,7 @@ import ( "github.com/rsteube/carapace/pkg/util" "github.com/rsteube/carapace/third_party/github.com/drone/envsubst" "github.com/rsteube/carapace/third_party/golang.org/x/sys/execabs" + "github.com/spf13/cobra" ) // Context provides information during completion. @@ -28,6 +29,7 @@ type Context struct { Dir string mockedReplies map[string]string + cmd *cobra.Command // needed for ActionCobra } // NewContext creates a new context for given arguments. diff --git a/defaultActions.go b/defaultActions.go index e2c9a45d4..2d1df6ad9 100644 --- a/defaultActions.go +++ b/defaultActions.go @@ -474,7 +474,11 @@ func ActionPositional(cmd *cobra.Command) Action { c.Args = cmd.Flags().Args() entry := storage.get(cmd) - a := entry.positionalAny + var a Action + if entry.positionalAny != nil { + a = *entry.positionalAny + } + if index := len(c.Args); index < len(entry.positional) { a = entry.positional[len(c.Args)] } @@ -514,3 +518,18 @@ func ActionCommands(cmd *cobra.Command) Action { return batch.ToA() }) } + +// ActionCora bridges given cobra completion function. +func ActionCobra(f func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)) Action { + return ActionCallback(func(c Context) Action { + switch { + case f == nil: + return ActionValues() + case c.cmd == nil: // ensure cmd is never nil even if context does not contain one + LOG.Print("cmd is nil [ActionCobra]") + c.cmd = &cobra.Command{Use: "_carapace_actioncobra", Hidden: true, Deprecated: "dummy command for ActionCobra"} + } + values, directive := f(c.cmd, c.cmd.Flags().Args(), c.Value) + return compDirective(directive).ToA(values...) + }) +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ae439ba81..6f943bfd9 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -55,6 +55,7 @@ - [ToMultiPartsA](./carapace/invokedAction/toMultiPartsA.md) - [DefaultActions](./carapace/defaultActions.md) - [ActionCallback](./carapace/defaultActions/actionCallback.md) + - [ActionCobra](./carapace/defaultActions/actionCobra.md) - [ActionCommands](./carapace/defaultActions/actionCommands.md) - [ActionDirectories](./carapace/defaultActions/actionDirectories.md) - [ActionExecCommand](./carapace/defaultActions/actionExecCommand.md) diff --git a/docs/src/carapace/defaultActions/actionCobra.cast b/docs/src/carapace/defaultActions/actionCobra.cast new file mode 100644 index 000000000..6450c4681 --- /dev/null +++ b/docs/src/carapace/defaultActions/actionCobra.cast @@ -0,0 +1,50 @@ +{"version": 2, "width": 108, "height": 24, "timestamp": 1701100000, "env": {"SHELL": "elvish", "TERM": "tmux-256color"}} +[0.122617, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h\u001b[?25l\r???> ???> \r\u001b[5C\u001b[?25h"] +[0.123322, "o", "\u001b[?25l\r\u001b[5C\u001b[K\r\u001b[5C\u001b[?25h"] +[0.139511, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] +[0.139635, "o", "\u001b[?25l\r\u001b[K\r\n\u001b[0;1;36mcarapace/example\u001b[0;m on \u001b[0;1;35m cobra-bridge\u001b[0;m \u001b[0;1;31m[$!?]\u001b[0;m via \u001b[0;1;36m🐹 v1.21.4 \r\n\u001b[0;1;37mesh\u001b[0;m \u001b[0;1;32m❯\u001b[0;m \r\u001b[6C\u001b[?25h"] +[0.456695, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[0;32me\u001b[0;m\r\u001b[7C\u001b[?25h"] +[0.457047, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"] +[0.457406, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"] +[0.476703, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"] +[0.476774, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"] +[0.661672, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;31mex\u001b[0;m\r\u001b[8C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"] +[0.756942, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexa\u001b[0;m\r\u001b[9C\u001b[?25h"] +[0.757869, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[9C\u001b[?25h"] +[0.758057, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[9C\u001b[?25h"] +[0.915507, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;31mexam\u001b[0;m\r\u001b[10C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[10C\u001b[?25h"] +[0.972819, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[10C\u001b[0;31mp\u001b[0;m\r\u001b[11C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[11C\u001b[?25h"] +[1.088957, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[11C\u001b[0;31ml\u001b[0;m\r\u001b[12C\u001b[?25h"] +[1.242635, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexample\u001b[0;m\r\u001b[13C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[13C\u001b[?25h"] +[1.318984, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[13C \r\u001b[14C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[14C\u001b[?25h"] +[1.421313, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[14Ca\r\u001b[15C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[15C\u001b[?25h"] +[1.565644, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[15Cc\r\u001b[16C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[16C\u001b[?25h"] +[1.789857, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[16Ct\r\u001b[17C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[17C\u001b[?25h"] +[1.861419, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[17Ci\r\u001b[18C\u001b[?25h"] +[1.861572, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[18C\u001b[?25h"] +[1.953279, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[18Co\r\u001b[19C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[19C\u001b[?25h"] +[2.001547, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[19Cn\r\u001b[20C\u001b[?25h"] +[2.001701, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[20C\u001b[?25h"] +[2.133981, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[20C \r\u001b[21C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[21C\u001b[?25h"] +[2.297596, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[21C-\r\u001b[22C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[22C\u001b[?25h"] +[2.449595, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[22C-\r\u001b[23C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[23C\u001b[?25h"] +[2.665291, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[23Cc\r\u001b[24C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[24C\u001b[?25h"] +[2.769879, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[24Co\r\u001b[25C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[25C\u001b[?25h"] +[2.920473, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[21C\u001b[K\u001b[0;4m--cobra \r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7;34m--cobra\u001b[0;2;7m (ActionCobra())\u001b[0;m \u001b[0;34m--commands\u001b[0;2m (ActionCommands())\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"] +[3.679034, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[21C\u001b[K--cobra \r\n\u001b[J\u001b[A\r\u001b[29C\u001b[?25h"] +[4.167667, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[29C\u001b[0;4mone\r\n\u001b[0;1;37;45m COMPLETING argument \u001b[0;m \r\n\u001b[0;7mone\u001b[0;m two\u001b[1A\r\u001b[22C\u001b[?25h"] +[5.054262, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[29C\u001b[K\u001b[0;4mtwo\r\n\r\n\u001b[0;m\u001b[Kone \u001b[0;7mtwo\u001b[0;m\u001b[1A\r\u001b[22C\u001b[?25h"] +[6.629692, "o", "\u001b[?25l\u001b[3A\r\r\n\r\n\u001b[29C\u001b[Ktwo\r\n\u001b[J\u001b[A\r\u001b[32C\u001b[?25h"] +[6.629826, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[32C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[32C\u001b[?25h"] +[7.230671, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\r\u001b[6C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"] +[7.231617, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"] +[7.25196, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[6C\u001b[?25h"] +[7.471051, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[0;32me\u001b[0;m\r\u001b[7C\u001b[?25h"] +[7.471209, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[7C\u001b[?25h"] +[7.684061, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;31mex\u001b[0;m\r\u001b[8C\u001b[?25h"] +[7.684762, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"] +[7.688565, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"] +[7.688621, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[8C\u001b[?25h"] +[7.816432, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[8C\u001b[0;31mi\u001b[0;m\r\u001b[9C\u001b[?25h"] +[7.907227, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\u001b[6C\u001b[K\u001b[0;32mexit\u001b[0;m\r\u001b[10C\u001b[?25h\u001b[?25l\u001b[2A\r\r\n\r\n\r\u001b[10C\u001b[?25h"] +[8.0253, "o", "\u001b[?25l\u001b[2A\r\r\n\r\n\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] diff --git a/docs/src/carapace/defaultActions/actionCobra.md b/docs/src/carapace/defaultActions/actionCobra.md new file mode 100644 index 000000000..ffed651d6 --- /dev/null +++ b/docs/src/carapace/defaultActions/actionCobra.md @@ -0,0 +1,13 @@ +# ActionCobra + +[`ActionCobra`] bridges given cobra completion function. + +```go +carapace.ActionCobra(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"one", "two"}, cobra.ShellCompDirectiveNoSpace +}) +``` + +![](./actionCobra.cast) + +[`ActionCobra`]:https://pkg.go.dev/github.com/rsteube/carapace#ActionCobra diff --git a/example/cmd/action.go b/example/cmd/action.go index 95684c846..eb70cc717 100644 --- a/example/cmd/action.go +++ b/example/cmd/action.go @@ -21,6 +21,7 @@ func init() { rootCmd.AddCommand(actionCmd) actionCmd.Flags().String("callback", "", "ActionCallback()") + actionCmd.Flags().String("cobra", "", "ActionCobra()") actionCmd.Flags().String("commands", "", "ActionCommands()") actionCmd.Flags().String("directories", "", "ActionDirectories()") actionCmd.Flags().String("execcommand", "", "ActionExecCommand()") @@ -49,6 +50,9 @@ func init() { } return carapace.ActionMessage("values flag is not set") }), + "cobra": carapace.ActionCobra(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"one", "two"}, cobra.ShellCompDirectiveNoSpace + }), "commands": carapace.ActionCommands(rootCmd).Split(), "directories": carapace.ActionDirectories(), "execcommand": carapace.ActionExecCommand("git", "remote")(func(output []byte) carapace.Action { diff --git a/example/cmd/action_test.go b/example/cmd/action_test.go index b280690f5..0d88dfd41 100644 --- a/example/cmd/action_test.go +++ b/example/cmd/action_test.go @@ -24,6 +24,13 @@ func TestAction(t *testing.T) { Expect(carapace.ActionMessage("values flag is not set"). Usage("ActionCallback()")) + s.Run("action", "--cobra", ""). + Expect(carapace.ActionValues( + "one", + "two", + ).NoSpace(). + Usage("ActionCobra()")) + s.Run("action", "--commands", "s"). Expect(carapace.ActionValuesDescribed( "special", "", diff --git a/example/cmd/compat.go b/example/cmd/compat.go new file mode 100644 index 000000000..704f3a2a3 --- /dev/null +++ b/example/cmd/compat.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + + "github.com/rsteube/carapace" + "github.com/spf13/cobra" +) + +var compatCmd = &cobra.Command{ + Use: "compat", + Short: "", + Run: func(cmd *cobra.Command, args []string) {}, +} + +func init() { + carapace.Gen(compatCmd).Standalone() + + compatCmd.Flags() + + compatCmd.Flags().String("error", "", "ShellCompDirectiveError") + compatCmd.Flags().String("nospace", "", "ShellCompDirectiveNoSpace") + compatCmd.Flags().String("nofilecomp", "", "ShellCompDirectiveNoFileComp") + compatCmd.Flags().String("filterfileext", "", "ShellCompDirectiveFilterFileExt") + compatCmd.Flags().String("filterdirs", "", "ShellCompDirectiveFilterDirs") + compatCmd.Flags().String("filterdirs-chdir", "", "ShellCompDirectiveFilterDirs") + compatCmd.Flags().String("keeporder", "", "ShellCompDirectiveKeepOrder") + compatCmd.Flags().String("default", "", "ShellCompDirectiveDefault") + + compatCmd.Flags().String("unset", "", "no completions defined") + compatCmd.PersistentFlags().String("persistent-compat", "", "persistent flag defined with cobra") + + rootCmd.AddCommand(compatCmd) + + _ = compatCmd.RegisterFlagCompletionFunc("error", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveError + }) + _ = compatCmd.RegisterFlagCompletionFunc("nospace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"one", "two"}, cobra.ShellCompDirectiveNoSpace + }) + _ = compatCmd.RegisterFlagCompletionFunc("nofilecomp", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + _ = compatCmd.RegisterFlagCompletionFunc("filterfileext", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"mod", "sum"}, cobra.ShellCompDirectiveFilterFileExt + }) + + _ = compatCmd.RegisterFlagCompletionFunc("filterdirs", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveFilterDirs + }) + + _ = compatCmd.RegisterFlagCompletionFunc("filterdirs-chdir", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"subdir"}, cobra.ShellCompDirectiveFilterDirs + }) + + _ = compatCmd.RegisterFlagCompletionFunc("keeporder", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"one", "three", "two"}, cobra.ShellCompDirectiveKeepOrder + }) + + _ = compatCmd.RegisterFlagCompletionFunc("default", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveDefault + }) + + _ = compatCmd.RegisterFlagCompletionFunc("persistent-compat", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{ + fmt.Sprintf("args: %#v toComplete: %#v", args, toComplete), + "alternative", + }, cobra.ShellCompDirectiveNoFileComp + }) + + compatCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + args = cmd.Flags().Args() + switch len(args) { + case 0: + return []string{"p1", "positional1"}, cobra.ShellCompDirectiveDefault + case 1: + return nil, cobra.ShellCompDirectiveDefault + case 2: + return []string{ + fmt.Sprintf("args: %#v toComplete: %#v", args, toComplete), + "alternative", + }, cobra.ShellCompDirectiveNoFileComp + default: + return nil, cobra.ShellCompDirectiveNoFileComp + } + } +} diff --git a/example/cmd/compat_sub.go b/example/cmd/compat_sub.go new file mode 100644 index 000000000..ac3d3ef1b --- /dev/null +++ b/example/cmd/compat_sub.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "github.com/rsteube/carapace" + "github.com/spf13/cobra" +) + +var compat_subCmd = &cobra.Command{ + Use: "sub", + Short: "", + Run: func(cmd *cobra.Command, args []string) {}, +} + +func init() { + carapace.Gen(compat_subCmd).Standalone() + + compatCmd.AddCommand(compat_subCmd) +} diff --git a/example/cmd/compat_sub_test.go b/example/cmd/compat_sub_test.go new file mode 100644 index 000000000..9fa70c27e --- /dev/null +++ b/example/cmd/compat_sub_test.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "testing" + + "github.com/rsteube/carapace" + "github.com/rsteube/carapace/pkg/sandbox" +) + +func TestCompatPersistent(t *testing.T) { + sandbox.Package(t, "github.com/rsteube/carapace/example")(func(s *sandbox.Sandbox) { + s.Run("compat", "sub", "--persistent-compat", ""). + Expect(carapace.ActionValues( + `args: []string(nil) toComplete: ""`, + "alternative", + ).Usage("persistent flag defined with cobra")) + + s.Run("compat", "sub", "one", "--persistent-compat", ""). + Expect(carapace.ActionValues( + `args: []string{"one"} toComplete: ""`, + "alternative", + ).Usage("persistent flag defined with cobra")) + + s.Run("compat", "sub", "one", "two", "--persistent-compat", "a"). + Expect(carapace.ActionValues( + `args: []string{"one", "two"} toComplete: "a"`, + "alternative", + ).Usage("persistent flag defined with cobra")) + }) +} diff --git a/example/cmd/compat_test.go b/example/cmd/compat_test.go new file mode 100644 index 000000000..8c5793658 --- /dev/null +++ b/example/cmd/compat_test.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "testing" + + "github.com/rsteube/carapace" + "github.com/rsteube/carapace/pkg/sandbox" + "github.com/rsteube/carapace/pkg/style" +) + +func TestCompat(t *testing.T) { + sandbox.Package(t, "github.com/rsteube/carapace/example")(func(s *sandbox.Sandbox) { + s.Files( + "subdir/file1.txt", "", + "subdir/subdir2/file2.txt", "", + "go.mod", "", + "go.sum", "", + "README.md", "", + ) + + s.Run("compat", "--error", ""). + Expect(carapace.ActionMessage("an error occured"). + Usage("ShellCompDirectiveError")) + + s.Run("compat", "--nospace", ""). + Expect(carapace.ActionValues( + "one", + "two", + ).NoSpace(). + Usage("ShellCompDirectiveNoSpace")) + + s.Run("compat", "--nofilecomp", ""). + Expect(carapace.ActionValues(). + Usage("ShellCompDirectiveNoFileComp")) + + s.Run("compat", "--filterfileext", ""). + Expect(carapace.ActionValues( + "subdir/", + "go.mod", + "go.sum", + ).NoSpace('/'). + Tag("files"). + StyleF(style.ForPath). + Usage("ShellCompDirectiveFilterFileExt")) + + s.Run("compat", "--filterdirs", ""). + Expect(carapace.ActionValues( + "subdir/", + ).NoSpace('/'). + Tag("directories"). + StyleF(style.ForPath). + Usage("ShellCompDirectiveFilterDirs")) + + s.Run("compat", "--filterdirs-chdir", ""). + Expect(carapace.ActionValues( + "subdir2/", + ).NoSpace('/'). + Tag("directories"). + StyleF(style.ForPathExt). + Usage("ShellCompDirectiveFilterDirs")) + + s.Run("compat", "--keeporder", ""). + Expect(carapace.ActionValues( + "one", + "two", + "three", + ).Usage("ShellCompDirectiveKeepOrder")) + + s.Run("compat", "--default", ""). + Expect(carapace.ActionValues( + "subdir/", + "go.mod", + "go.sum", + "README.md", + ).NoSpace('/'). + Tag("files"). + StyleF(style.ForPath). + Usage("ShellCompDirectiveDefault")) + + s.Run("compat", "--unset", ""). + Expect(carapace.ActionValues(). + Usage("no completions defined")) + + s.Run("compat", ""). + Expect(carapace.Batch( + carapace.ActionValues( + "p1", + "positional1", + ), + carapace.ActionValues( + "sub", + ).Tag("commands"), + ).ToA().Usage("")) + + s.Run("compat", "positional1", ""). + Expect(carapace.ActionValues( + "subdir/", + "go.mod", + "go.sum", + "README.md", + ).NoSpace('/'). + StyleF(style.ForPath). + Tag("files"). + Usage("")) + + s.Run("compat", "positional1", "main.go", ""). + Expect(carapace.ActionValues( + `args: []string{"positional1", "main.go"} toComplete: ""`, + "alternative", + ).Usage("")) + + s.Run("compat", "positional1", "main.go", "a"). + Expect(carapace.ActionValues( + `args: []string{"positional1", "main.go"} toComplete: "a"`, + "alternative", + ).Usage("")) + + s.Run("compat", "--nospace", "one", "positional1", "--", "main.go", "a"). + Expect(carapace.ActionValues( + `args: []string{"positional1", "main.go"} toComplete: "a"`, + "alternative", + ).Usage("")) + }) +} diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index 1233971f5..cafbe985b 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -73,6 +73,7 @@ func TestRoot(t *testing.T) { ).Style(style.Magenta).Tag("plugin commands"), carapace.ActionValuesDescribed( "chain", "shorthand chain", + "compat", "", "completion", "Generate the autocompletion script for the specified shell", "group", "group example", "help", "Help about any command", diff --git a/go.work.sum b/go.work.sum index 0eead8492..8464a6111 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,5 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/rsteube/carapace-pflag v0.0.4 h1:Onb0cLNLxg1xJr2EsMlBldAI5KkybrvZ89b5cRElZXI= github.com/rsteube/carapace-pflag v0.0.4/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/rsteube/carapace-pflag v0.0.5 h1:QQC0KnthHMayHsX7B7DxqOkr0B6JSIM0glB+KrSTruU= @@ -7,6 +8,6 @@ github.com/rsteube/carapace-pflag v0.1.0 h1:CPJRlj3jbyOnxuMf5pdrM76hEwdQ0STDDmkA github.com/rsteube/carapace-pflag v0.1.0/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/rsteube/carapace-shlex v0.0.1 h1:8uvsc+ISKw7uoITSp92nNisFUOulYMz+Uu7N5nbHTiM= github.com/rsteube/carapace-shlex v0.0.1/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= -github.com/rsteube/carapace-shlex v0.0.4 h1:3GVn8PaM2RCxPTAiwVy9vDQI8Mi7DqrbdpDgf5ZzQmY= -github.com/rsteube/carapace-shlex v0.0.4/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= diff --git a/storage.go b/storage.go index 082d986bf..b937885e2 100644 --- a/storage.go +++ b/storage.go @@ -17,9 +17,9 @@ type entry struct { flag ActionMap flagMutex sync.RWMutex positional []Action - positionalAny Action + positionalAny *Action dash []Action - dashAny Action + dashAny *Action preinvoke func(cmd *cobra.Command, flag *pflag.Flag, action Action) Action prerun func(cmd *cobra.Command, args []string) bridged bool @@ -72,6 +72,18 @@ func (s _storage) bridge(cmd *cobra.Command) { } } +func (s _storage) hasFlag(cmd *cobra.Command, name string) bool { + if flag := cmd.LocalFlags().Lookup(name); flag == nil && cmd.HasParent() { + return s.hasFlag(cmd.Parent(), name) + } else { + entry := s.get(cmd) + entry.flagMutex.RLock() + defer entry.flagMutex.RUnlock() + _, ok := entry.flag[name] + return ok + } +} + func (s _storage) getFlag(cmd *cobra.Command, name string) Action { if flag := cmd.LocalFlags().Lookup(name); flag == nil && cmd.HasParent() { return s.getFlag(cmd.Parent(), name) @@ -79,7 +91,15 @@ func (s _storage) getFlag(cmd *cobra.Command, name string) Action { entry := s.get(cmd) entry.flagMutex.RLock() defer entry.flagMutex.RUnlock() - a := s.preinvoke(cmd, flag, entry.flag[name]) + + flagAction, ok := entry.flag[name] + if !ok { + if f, ok := cmd.GetFlagCompletionFunc(name); ok { + flagAction = ActionCobra(f) + } + } + + a := s.preinvoke(cmd, flag, flagAction) return ActionCallback(func(c Context) Action { // TODO verify order of execution is correct invoked := a.Invoke(c) @@ -112,6 +132,24 @@ func (s _storage) preinvoke(cmd *cobra.Command, flag *pflag.Flag, action Action) return a } +func (s _storage) hasPositional(cmd *cobra.Command, index int) bool { + entry := s.get(cmd) + isDash := common.IsDash(cmd) + + // TODO fallback to cobra defined completion if exists + + switch { + case !isDash && len(entry.positional) > index: + return true + case !isDash: + return entry.positionalAny != nil + case len(entry.dash) > index: + return true + default: + return entry.dashAny != nil + } +} + func (s _storage) getPositional(cmd *cobra.Command, index int) Action { entry := s.get(cmd) isDash := common.IsDash(cmd) @@ -119,14 +157,23 @@ func (s _storage) getPositional(cmd *cobra.Command, index int) Action { var a Action switch { case !isDash && len(entry.positional) > index: - a = s.preinvoke(cmd, nil, entry.positional[index]) + a = entry.positional[index] case !isDash: - a = s.preinvoke(cmd, nil, entry.positionalAny) + if entry.positionalAny != nil { + a = *entry.positionalAny + } else { + a = ActionCobra(cmd.ValidArgsFunction) + } case len(entry.dash) > index: - a = s.preinvoke(cmd, nil, entry.dash[index]) + a = entry.dash[index] default: - a = s.preinvoke(cmd, nil, entry.dashAny) + if entry.dashAny != nil { + a = *entry.dashAny + } else { + a = ActionCobra(cmd.ValidArgsFunction) + } } + a = s.preinvoke(cmd, nil, a) return ActionCallback(func(c Context) Action { invoked := a.Invoke(c) diff --git a/traverse.go b/traverse.go index 2a5447718..eb8a747b9 100644 --- a/traverse.go +++ b/traverse.go @@ -26,6 +26,7 @@ func traverse(cmd *cobra.Command, args []string) (Action, Context) { fs := pflagfork.FlagSet{FlagSet: cmd.Flags()} context := NewContext(args...) + context.cmd = cmd loop: for i, arg := range context.Args { switch {