diff --git a/commands/bridge_auth_addtoken.go b/commands/bridge_auth_addtoken.go index c0458fda..dfdc66b6 100644 --- a/commands/bridge_auth_addtoken.go +++ b/commands/bridge_auth_addtoken.go @@ -41,10 +41,12 @@ func newBridgeAuthAddTokenCommand() *cobra.Command { flags.StringVarP(&options.target, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) + cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets())) flags.StringVarP(&options.login, "login", "l", "", "The login in the remote bug-tracker") flags.StringVarP(&options.user, "user", "u", "", "The user to add the token to. Default is the current user") + cmd.RegisterFlagCompletionFunc("user", completeUser(env)) return cmd } diff --git a/commands/bridge_auth_rm.go b/commands/bridge_auth_rm.go index fa73ad11..a28057de 100644 --- a/commands/bridge_auth_rm.go +++ b/commands/bridge_auth_rm.go @@ -16,7 +16,8 @@ func newBridgeAuthRm() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runBridgeAuthRm(env, args) }, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeBridgeAuth(env), } return cmd diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go index f174cdb7..7233bb51 100644 --- a/commands/bridge_auth_show.go +++ b/commands/bridge_auth_show.go @@ -21,7 +21,8 @@ func newBridgeAuthShow() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgeAuthShow(env, args) }), - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeBridgeAuth(env), } return cmd diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go index 6533e497..bbfc13be 100644 --- a/commands/bridge_configure.go +++ b/commands/bridge_configure.go @@ -97,6 +97,7 @@ git bug bridge configure \ flags.StringVarP(&options.name, "name", "n", "", "A distinctive name to identify the bridge") flags.StringVarP(&options.target, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) + cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets())) flags.StringVarP(&options.params.URL, "url", "u", "", "The URL of the remote repository") flags.StringVarP(&options.params.BaseURL, "base-url", "b", "", "The base URL of your remote issue tracker") flags.StringVarP(&options.params.Login, "login", "l", "", "The login on your remote issue tracker") diff --git a/commands/bridge_pull.go b/commands/bridge_pull.go index 3155ebf4..9370e088 100644 --- a/commands/bridge_pull.go +++ b/commands/bridge_pull.go @@ -32,7 +32,8 @@ func newBridgePullCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgePull(env, options, args) }), - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), + ValidArgsFunction: completeBridge(env), } flags := cmd.Flags() diff --git a/commands/bridge_push.go b/commands/bridge_push.go index a232f0f0..ef1f2d3e 100644 --- a/commands/bridge_push.go +++ b/commands/bridge_push.go @@ -23,7 +23,8 @@ func newBridgePushCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgePush(env, args) }), - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), + ValidArgsFunction: completeBridge(env), } return cmd diff --git a/commands/bridge_rm.go b/commands/bridge_rm.go index 121a35ad..0306944e 100644 --- a/commands/bridge_rm.go +++ b/commands/bridge_rm.go @@ -16,7 +16,8 @@ func newBridgeRm() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgeRm(env, args) }), - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeBridge(env), } return cmd diff --git a/commands/comment.go b/commands/comment.go index 90657e4a..b4b4628b 100644 --- a/commands/comment.go +++ b/commands/comment.go @@ -18,6 +18,7 @@ func newCommentCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runComment(env, args) }), + ValidArgsFunction: completeBug(env), } cmd.AddCommand(newCommentAddCommand()) diff --git a/commands/comment_add.go b/commands/comment_add.go index 11ba5ad3..f308428c 100644 --- a/commands/comment_add.go +++ b/commands/comment_add.go @@ -25,6 +25,7 @@ func newCommentAddCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runCommentAdd(env, options, args) }), + ValidArgsFunction: completeBug(env), } flags := cmd.Flags() diff --git a/commands/helper_completion.go b/commands/helper_completion.go new file mode 100644 index 00000000..3a089e35 --- /dev/null +++ b/commands/helper_completion.go @@ -0,0 +1,342 @@ +package commands + +import ( + "fmt" + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge" + "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/cache" + _select "github.com/MichaelMure/git-bug/commands/select" +) + +type validArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) + +func completionHandlerError(err error) (completions []string, directives cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveError +} + +func completeBridge(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + bridges, err := bridge.ConfiguredBridges(env.backend) + if err != nil { + return completionHandlerError(err) + } + + completions = make([]string, len(bridges)) + for i, bridge := range bridges { + completions[i] = bridge + "\t" + "Bridge" + } + + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeBridgeAuth(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + creds, err := auth.List(env.backend) + if err != nil { + return completionHandlerError(err) + } + + completions = make([]string, len(creds)) + for i, cred := range creds { + meta := make([]string, 0, len(cred.Metadata())) + for k, v := range cred.Metadata() { + meta = append(meta, k+":"+v) + } + sort.Strings(meta) + metaFmt := strings.Join(meta, ",") + + completions[i] = cred.ID().Human() + "\t" + cred.Target() + " " + string(cred.Kind()) + " " + metaFmt + } + + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeBug(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + return completeBugWithBackend(env.backend, toComplete) + } +} + +func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + allIds := backend.AllBugsIds() + bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) + for i, id := range allIds { + var err error + bugExcerpt[i], err = backend.ResolveBugExcerpt(id) + if err != nil { + return completionHandlerError(err) + } + } + + for i, id := range allIds { + if strings.Contains(id.String(), strings.TrimSpace(toComplete)) { + completions = append(completions, id.Human()+"\t"+bugExcerpt[i].Title) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp +} + +func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + b, args, err := _select.ResolveBug(env.backend, args) + if err == _select.ErrNoValidId { + // we need a bug first to complete labels + return completeBugWithBackend(env.backend, toComplete) + } + if err != nil { + return completionHandlerError(err) + } + + snap := b.Snapshot() + + seenLabels := map[bug.Label]bool{} + for _, label := range args { + seenLabels[bug.Label(label)] = addOrRemove + } + + var labels []bug.Label + if addOrRemove { + for _, label := range snap.Labels { + seenLabels[label] = true + } + + allLabels := env.backend.ValidLabels() + labels = make([]bug.Label, 0, len(allLabels)) + for _, label := range allLabels { + if !seenLabels[label] { + labels = append(labels, label) + } + } + } else { + labels = make([]bug.Label, 0, len(snap.Labels)) + for _, label := range snap.Labels { + if seenLabels[label] { + labels = append(labels, label) + } + } + } + + completions = make([]string, len(labels)) + for i, label := range labels { + completions[i] = string(label) + "\t" + "Label" + } + + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeFrom(choices []string) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return choices, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeGitRemote(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + remoteMap, err := env.backend.GetRemotes() + if err != nil { + return completionHandlerError(err) + } + completions = make([]string, 0, len(remoteMap)) + for remote, url := range remoteMap { + completions = append(completions, remote+"\t"+"Remote: "+url) + } + sort.Strings(completions) + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeLabel(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + labels := env.backend.ValidLabels() + completions = make([]string, len(labels)) + for i, label := range labels { + if strings.Contains(label.String(), " ") { + completions[i] = fmt.Sprintf("\"%s\"\tLabel", label.String()) + } else { + completions[i] = fmt.Sprintf("%s\tLabel", label.String()) + } + } + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeLs(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if strings.HasPrefix(toComplete, "status:") { + completions = append(completions, "status:open\tOpen bugs") + completions = append(completions, "status:closed\tClosed bugs") + return completions, cobra.ShellCompDirectiveDefault + } + + byPerson := []string{"author:", "participant:", "actor:"} + byLabel := []string{"label:", "no:"} + needBackend := false + for _, key := range append(byPerson, byLabel...) { + if strings.HasPrefix(toComplete, key) { + needBackend = true + } + } + + if needBackend { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + } + + for _, key := range byPerson { + if !strings.HasPrefix(toComplete, key) { + continue + } + ids := env.backend.AllIdentityIds() + completions = make([]string, len(ids)) + for i, id := range ids { + user, err := env.backend.ResolveIdentityExcerpt(id) + if err != nil { + return completionHandlerError(err) + } + var handle string + if user.Login != "" { + handle = user.Login + } else { + // "author:John Doe" does not work yet, so use the first name. + handle = strings.Split(user.Name, " ")[0] + } + completions[i] = key + handle + "\t" + user.DisplayName() + } + return completions, cobra.ShellCompDirectiveNoFileComp + } + + for _, key := range byLabel { + if !strings.HasPrefix(toComplete, key) { + continue + } + labels := env.backend.ValidLabels() + completions = make([]string, len(labels)) + for i, label := range labels { + if strings.Contains(label.String(), " ") { + completions[i] = key + "\"" + string(label) + "\"" + } else { + completions[i] = key + string(label) + } + } + return completions, cobra.ShellCompDirectiveNoFileComp + } + + completions = []string{ + "actor:\tFilter by actor", + "author:\tFilter by author", + "label:\tFilter by label", + "no:\tExclude bugs by label", + "participant:\tFilter by participant", + "status:\tFilter by open/close status", + "title:\tFilter by title", + } + return completions, cobra.ShellCompDirectiveNoSpace + } +} + +func completeUser(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + ids := env.backend.AllIdentityIds() + completions = make([]string, len(ids)) + for i, id := range ids { + user, err := env.backend.ResolveIdentityExcerpt(id) + if err != nil { + return completionHandlerError(err) + } + completions[i] = user.Id.Human() + "\t" + user.DisplayName() + } + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func completeUserForQuery(env *Env) validArgsFunction { + return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + if err := loadBackend(env)(cmd, args); err != nil { + return completionHandlerError(err) + } + defer func() { + _ = env.backend.Close() + }() + + ids := env.backend.AllIdentityIds() + completions = make([]string, len(ids)) + for i, id := range ids { + user, err := env.backend.ResolveIdentityExcerpt(id) + if err != nil { + return completionHandlerError(err) + } + var handle string + if user.Login != "" { + handle = user.Login + } else { + // "author:John Doe" does not work yet, so use the first name. + handle = strings.Split(user.Name, " ")[0] + } + completions[i] = handle + "\t" + user.DisplayName() + } + return completions, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/commands/label.go b/commands/label.go index d108b089..906974a5 100644 --- a/commands/label.go +++ b/commands/label.go @@ -16,6 +16,7 @@ func newLabelCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runLabel(env, args) }), + ValidArgsFunction: completeBug(env), } cmd.AddCommand(newLabelAddCommand()) diff --git a/commands/label_add.go b/commands/label_add.go index c60ecfeb..65439a4a 100644 --- a/commands/label_add.go +++ b/commands/label_add.go @@ -17,6 +17,7 @@ func newLabelAddCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runLabelAdd(env, args) }), + ValidArgsFunction: completeBugAndLabels(env, true), } return cmd diff --git a/commands/label_rm.go b/commands/label_rm.go index 1cdcd248..3f4e1958 100644 --- a/commands/label_rm.go +++ b/commands/label_rm.go @@ -17,6 +17,7 @@ func newLabelRmCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runLabelRm(env, args) }), + ValidArgsFunction: completeBugAndLabels(env, false), } return cmd diff --git a/commands/ls.go b/commands/ls.go index db4145d0..da5ea8ce 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -56,6 +56,7 @@ git bug ls status:open --by creation "foo bar" baz RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runLs(env, options, args) }), + ValidArgsFunction: completeLs(env), } flags := cmd.Flags() @@ -63,26 +64,36 @@ git bug ls status:open --by creation "foo bar" baz flags.StringSliceVarP(&options.statusQuery, "status", "s", nil, "Filter by status. Valid values are [open,closed]") + cmd.RegisterFlagCompletionFunc("status", completeFrom([]string{"open", "closed"})) flags.StringSliceVarP(&options.authorQuery, "author", "a", nil, "Filter by author") flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil, "Filter by metadata. Example: github-url=URL") + cmd.RegisterFlagCompletionFunc("author", completeUserForQuery(env)) flags.StringSliceVarP(&options.participantQuery, "participant", "p", nil, "Filter by participant") + cmd.RegisterFlagCompletionFunc("participant", completeUserForQuery(env)) flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil, "Filter by actor") + cmd.RegisterFlagCompletionFunc("actor", completeUserForQuery(env)) flags.StringSliceVarP(&options.labelQuery, "label", "l", nil, "Filter by label") + cmd.RegisterFlagCompletionFunc("label", completeLabel(env)) flags.StringSliceVarP(&options.titleQuery, "title", "t", nil, "Filter by title") flags.StringSliceVarP(&options.noQuery, "no", "n", nil, "Filter by absence of something. Valid values are [label]") + cmd.RegisterFlagCompletionFunc("no", completeLabel(env)) flags.StringVarP(&options.sortBy, "by", "b", "creation", "Sort the results by a characteristic. Valid values are [id,creation,edit]") + cmd.RegisterFlagCompletionFunc("by", completeFrom([]string{"id", "creation", "edit"})) flags.StringVarP(&options.sortDirection, "direction", "d", "asc", "Select the sorting direction. Valid values are [asc,desc]") + cmd.RegisterFlagCompletionFunc("direction", completeFrom([]string{"asc", "desc"})) flags.StringVarP(&options.outputFormat, "format", "f", "default", "Select the output formatting style. Valid values are [default,plain,json,org-mode]") + cmd.RegisterFlagCompletionFunc("format", + completeFrom([]string{"default", "plain", "json", "org-mode"})) return cmd } @@ -92,13 +103,9 @@ func runLs(env *Env, opts lsOptions, args []string) error { var err error if len(args) >= 1 { - // either the shell or cobra remove the quotes, we need them back for the parsing - for i, arg := range args { - if strings.Contains(arg, " ") { - args[i] = fmt.Sprintf("\"%s\"", arg) - } - } - assembled := strings.Join(args, " ") + // either the shell or cobra remove the quotes, we need them back for the query parsing + assembled := repairQuery(args) + q, err = query.Parse(assembled) if err != nil { return err @@ -142,6 +149,19 @@ func runLs(env *Env, opts lsOptions, args []string) error { } } +func repairQuery(args []string) string { + for i, arg := range args { + split := strings.Split(arg, ":") + for j, s := range split { + if strings.Contains(s, " ") { + split[j] = fmt.Sprintf("\"%s\"", s) + } + } + args[i] = strings.Join(split, ":") + } + return strings.Join(args, " ") +} + type JSONBugExcerpt struct { Id string `json:"id"` HumanId string `json:"human_id"` diff --git a/commands/ls_test.go b/commands/ls_test.go new file mode 100644 index 00000000..aff94e03 --- /dev/null +++ b/commands/ls_test.go @@ -0,0 +1,43 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_repairQuery(t *testing.T) { + cases := []struct { + args []string + output string + }{ + { + []string{""}, + "", + }, + { + []string{"foo"}, + "foo", + }, + { + []string{"foo", "bar"}, + "foo bar", + }, + { + []string{"foo bar", "baz"}, + "\"foo bar\" baz", + }, + { + []string{"foo:bar", "baz"}, + "foo:bar baz", + }, + { + []string{"foo:bar boo", "baz"}, + "foo:\"bar boo\" baz", + }, + } + + for _, tc := range cases { + require.Equal(t, tc.output, repairQuery(tc.args)) + } +} diff --git a/commands/pull.go b/commands/pull.go index f3a31414..29c9f034 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -18,6 +18,7 @@ func newPullCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runPull(env, args) }), + ValidArgsFunction: completeGitRemote(env), } return cmd diff --git a/commands/push.go b/commands/push.go index 9d6ca7df..adba6bef 100644 --- a/commands/push.go +++ b/commands/push.go @@ -16,6 +16,7 @@ func newPushCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runPush(env, args) }), + ValidArgsFunction: completeGitRemote(env), } return cmd diff --git a/commands/rm.go b/commands/rm.go index 8205c128..2e1d924d 100644 --- a/commands/rm.go +++ b/commands/rm.go @@ -17,6 +17,7 @@ func newRmCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runRm(env, args) }), + ValidArgsFunction: completeBug(env), } flags := cmd.Flags() diff --git a/commands/root.go b/commands/root.go index e7848363..e012bd83 100644 --- a/commands/root.go +++ b/commands/root.go @@ -50,14 +50,6 @@ the same git remote you are already using to collaborate with other people. SilenceUsage: true, DisableAutoGenTag: true, - - // Custom bash code to connect the git completion for "git bug" to the - // git-bug completion for "git-bug" - BashCompletionFunction: ` -_git_bug() { - __start_git-bug "$@" -} -`, } cmd.AddCommand(newAddCommand()) diff --git a/commands/select.go b/commands/select.go index 34d00a32..f9e6ece7 100644 --- a/commands/select.go +++ b/commands/select.go @@ -31,6 +31,7 @@ The complementary command is "git bug deselect" performing the opposite operatio RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runSelect(env, args) }), + ValidArgsFunction: completeBug(env), } return cmd diff --git a/commands/show.go b/commands/show.go index 55140357..16747214 100644 --- a/commands/show.go +++ b/commands/show.go @@ -29,13 +29,17 @@ func newShowCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runShow(env, options, args) }), + ValidArgsFunction: completeBug(env), } flags := cmd.Flags() flags.SortFlags = false + fields := []string{"author", "authorEmail", "createTime", "lastEdit", "humanId", + "id", "labels", "shortId", "status", "title", "actors", "participants"} flags.StringVarP(&options.fields, "field", "", "", - "Select field to display. Valid values are [author,authorEmail,createTime,lastEdit,humanId,id,labels,shortId,status,title,actors,participants]") + "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]") + cmd.RegisterFlagCompletionFunc("by", completeFrom(fields)) flags.StringVarP(&options.format, "format", "f", "default", "Select the output formatting style. Valid values are [default,json,org-mode]") diff --git a/commands/status.go b/commands/status.go index c1e45c5f..c3e860b6 100644 --- a/commands/status.go +++ b/commands/status.go @@ -15,6 +15,7 @@ func newStatusCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runStatus(env, args) }), + ValidArgsFunction: completeBug(env), } cmd.AddCommand(newStatusCloseCommand()) diff --git a/commands/title.go b/commands/title.go index c4293530..f99c6eff 100644 --- a/commands/title.go +++ b/commands/title.go @@ -15,6 +15,7 @@ func newTitleCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runTitle(env, args) }), + ValidArgsFunction: completeBug(env), } cmd.AddCommand(newTitleEditCommand()) diff --git a/commands/title_edit.go b/commands/title_edit.go index 810c5e62..a9e7fe4b 100644 --- a/commands/title_edit.go +++ b/commands/title_edit.go @@ -24,6 +24,7 @@ func newTitleEditCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runTitleEdit(env, options, args) }), + ValidArgsFunction: completeBug(env), } flags := cmd.Flags() diff --git a/commands/user.go b/commands/user.go index b6a2e485..0fe3be4d 100644 --- a/commands/user.go +++ b/commands/user.go @@ -3,6 +3,7 @@ package commands import ( "errors" "fmt" + "strings" "github.com/spf13/cobra" @@ -24,6 +25,7 @@ func newUserCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runUser(env, options, args) }), + ValidArgsFunction: completeUser(env), } cmd.AddCommand(newUserAdoptCommand()) @@ -33,8 +35,10 @@ func newUserCommand() *cobra.Command { flags := cmd.Flags() flags.SortFlags = false + fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"} flags.StringVarP(&options.fields, "field", "f", "", - "Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]") + "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]") + cmd.RegisterFlagCompletionFunc("field", completeFrom(fields)) return cmd } diff --git a/commands/user_adopt.go b/commands/user_adopt.go index 166063ae..afef94ea 100644 --- a/commands/user_adopt.go +++ b/commands/user_adopt.go @@ -15,6 +15,7 @@ func newUserAdoptCommand() *cobra.Command { RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { return runUserAdopt(env, args) }), + ValidArgsFunction: completeUser(env), } return cmd diff --git a/commands/user_ls.go b/commands/user_ls.go index 98800b87..341f0dc1 100644 --- a/commands/user_ls.go +++ b/commands/user_ls.go @@ -32,6 +32,7 @@ func newUserLsCommand() *cobra.Command { flags.StringVarP(&options.format, "format", "f", "default", "Select the output formatting style. Valid values are [default,json]") + cmd.RegisterFlagCompletionFunc("format", completeFrom([]string{"default", "json"})) return cmd } diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 2320c1c0..85d2a15b 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -1,4 +1,4 @@ -# bash completion for git-bug -*- shell-script -*- +# bash completion V2 for git-bug -*- shell-script -*- __git-bug_debug() { @@ -7,64 +7,43 @@ __git-bug_debug() fi } -# Homebrew on Macs have version 1.3 of bash-completion which doesn't include -# _init_completion. This is a very minimal version of that function. +# Macs have bash3 for which the bash-completion package doesn't include +# _init_completion. This is a minimal version of that function. __git-bug_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } -__git-bug_index_of_word() -{ - local w word=$1 - shift - index=0 - for w in "$@"; do - [[ $w = "$word" ]] && return - index=$((index+1)) - done - index=-1 -} - -__git-bug_contains_word() -{ - local w word=$1; shift - for w in "$@"; do - [[ $w = "$word" ]] && return - done - return 1 -} - -__git-bug_handle_go_custom_completion() -{ - __git-bug_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" - - local shellCompDirectiveError=1 - local shellCompDirectiveNoSpace=2 - local shellCompDirectiveNoFileComp=4 - local shellCompDirectiveFilterFileExt=8 - local shellCompDirectiveFilterDirs=16 - - local out requestComp lastParam lastChar comp directive args +# This function calls the git-bug program to obtain the completion +# results and the directive. It fills the 'out' and 'directive' vars. +__git-bug_get_completion_results() { + local requestComp lastParam lastChar args # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly git-bug allows to handle aliases args=("${words[@]:1}") - requestComp="${words[0]} __completeNoDesc ${args[*]}" + requestComp="${words[0]} __complete ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} - __git-bug_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" + __git-bug_debug "lastParam ${lastParam}, lastChar ${lastChar}" if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go method. - __git-bug_debug "${FUNCNAME[0]}: Adding extra empty parameter" - requestComp="${requestComp} \"\"" + __git-bug_debug "Adding extra empty parameter" + requestComp="${requestComp} ''" fi - __git-bug_debug "${FUNCNAME[0]}: calling ${requestComp}" + # When completing a flag with an = (e.g., git-bug -n=) + # bash focuses on the part after the =, so we need to remove + # the flag part from $cur + if [[ "${cur}" == -*=* ]]; then + cur="${cur#*=}" + fi + + __git-bug_debug "Calling ${requestComp}" # Use eval to handle any environment variables and such out=$(eval "${requestComp}" 2>/dev/null) @@ -76,24 +55,36 @@ __git-bug_handle_go_custom_completion() # There is not directive specified directive=0 fi - __git-bug_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" - __git-bug_debug "${FUNCNAME[0]}: the completions are: ${out[*]}" + __git-bug_debug "The completion directive is: ${directive}" + __git-bug_debug "The completions are: ${out[*]}" +} + +__git-bug_process_completion_results() { + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then # Error code. No completion. - __git-bug_debug "${FUNCNAME[0]}: received error from custom completion go code" + __git-bug_debug "Received error from custom completion go code" return else if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then - __git-bug_debug "${FUNCNAME[0]}: activating no space" + __git-bug_debug "Activating no space" compopt -o nospace + else + __git-bug_debug "No space directive not supported in this version of bash" fi fi if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then - __git-bug_debug "${FUNCNAME[0]}: activating no file completion" + __git-bug_debug "Activating no file completion" compopt +o default + else + __git-bug_debug "No file completion directive not supported in this version of bash" fi fi fi @@ -101,6 +92,7 @@ __git-bug_handle_go_custom_completion() if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local fullFilter filter filteringCmd + # Do not use quotes around the $out variable or else newline # characters will be kept. for filter in ${out[*]}; do @@ -112,1394 +104,179 @@ __git-bug_handle_go_custom_completion() $filteringCmd elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then # File completion for directories only - local subdir + # Use printf to strip any trailing newline + local subdir subdir=$(printf "%s" "${out[0]}") if [ -n "$subdir" ]; then __git-bug_debug "Listing directories in $subdir" - __git-bug_handle_subdirs_in_dir_flag "$subdir" + pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return else __git-bug_debug "Listing directories in ." _filedir -d fi else - while IFS='' read -r comp; do - COMPREPLY+=("$comp") - done < <(compgen -W "${out[*]}" -- "$cur") + __git-bug_handle_completion_types fi + + __git-bug_handle_special_char "$cur" : + __git-bug_handle_special_char "$cur" = } -__git-bug_handle_reply() -{ - __git-bug_debug "${FUNCNAME[0]}" - local comp - case $cur in - -*) - if [[ $(type -t compopt) = "builtin" ]]; then - compopt -o nospace - fi - local allflags - if [ ${#must_have_one_flag[@]} -ne 0 ]; then - allflags=("${must_have_one_flag[@]}") - else - allflags=("${flags[*]} ${two_word_flags[*]}") - fi - while IFS='' read -r comp; do +__git-bug_handle_completion_types() { + __git-bug_debug "__git-bug_handle_completion_types: COMP_TYPE is $COMP_TYPE" + + case $COMP_TYPE in + 37|42) + # Type: menu-complete/menu-complete-backward and insert-completions + # If the user requested inserting one completion at a time, or all + # completions at once on the command-line we must remove the descriptions. + # https://github.com/spf13/cobra/issues/1508 + local tab comp + tab=$(printf '\t') + while IFS='' read -r comp; do + # Strip any description + comp=${comp%%$tab*} + # Only consider the completions that match + comp=$(compgen -W "$comp" -- "$cur") + if [ -n "$comp" ]; then COMPREPLY+=("$comp") - done < <(compgen -W "${allflags[*]}" -- "$cur") - if [[ $(type -t compopt) = "builtin" ]]; then - [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace fi + done < <(printf "%s\n" "${out[@]}") + ;; - # complete after --flag=abc - if [[ $cur == *=* ]]; then - if [[ $(type -t compopt) = "builtin" ]]; then - compopt +o nospace - fi - - local index flag - flag="${cur%=*}" - __git-bug_index_of_word "${flag}" "${flags_with_completion[@]}" - COMPREPLY=() - if [[ ${index} -ge 0 ]]; then - PREFIX="" - cur="${cur#*=}" - ${flags_completion[${index}]} - if [ -n "${ZSH_VERSION:-}" ]; then - # zsh completion needs --flag= prefix - eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" - fi - fi - fi - - if [[ -z "${flag_parsing_disabled}" ]]; then - # If flag parsing is enabled, we have completed the flags and can return. - # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough - # to possibly call handle_go_custom_completion. - return 0; - fi - ;; + *) + # Type: complete (normal completion) + __git-bug_handle_standard_completion_case + ;; esac +} - # check if we are handling a flag with special work handling - local index - __git-bug_index_of_word "${prev}" "${flags_with_completion[@]}" - if [[ ${index} -ge 0 ]]; then - ${flags_completion[${index}]} - return - fi +__git-bug_handle_standard_completion_case() { + local tab comp + tab=$(printf '\t') - # we are parsing a flag and don't have a special handler, no completion - if [[ ${cur} != "${words[cword]}" ]]; then - return - fi + local longest=0 + # Look for the longest completion so that we can format things nicely + while IFS='' read -r comp; do + # Strip any description before checking the length + comp=${comp%%$tab*} + # Only consider the completions that match + comp=$(compgen -W "$comp" -- "$cur") + if ((${#comp}>longest)); then + longest=${#comp} + fi + done < <(printf "%s\n" "${out[@]}") + + local completions=() + while IFS='' read -r comp; do + if [ -z "$comp" ]; then + continue + fi + + __git-bug_debug "Original comp: $comp" + comp="$(__git-bug_format_comp_descriptions "$comp" "$longest")" + __git-bug_debug "Final comp: $comp" + completions+=("$comp") + done < <(printf "%s\n" "${out[@]}") - local completions - completions=("${commands[@]}") - if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then - completions+=("${must_have_one_noun[@]}") - elif [[ -n "${has_completion_function}" ]]; then - # if a go completion function is provided, defer to that function - __git-bug_handle_go_custom_completion - fi - if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then - completions+=("${must_have_one_flag[@]}") - fi while IFS='' read -r comp; do COMPREPLY+=("$comp") done < <(compgen -W "${completions[*]}" -- "$cur") - if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then - while IFS='' read -r comp; do - COMPREPLY+=("$comp") - done < <(compgen -W "${noun_aliases[*]}" -- "$cur") + # If there is a single completion left, remove the description text + if [ ${#COMPREPLY[*]} -eq 1 ]; then + __git-bug_debug "COMPREPLY[0]: ${COMPREPLY[0]}" + comp="${COMPREPLY[0]%% *}" + __git-bug_debug "Removed description from single completion, which is now: ${comp}" + COMPREPLY=() + COMPREPLY+=("$comp") fi +} - if [[ ${#COMPREPLY[@]} -eq 0 ]]; then - if declare -F __git-bug_custom_func >/dev/null; then - # try command name qualified custom func - __git-bug_custom_func +__git-bug_handle_special_char() +{ + local comp="$1" + local char=$2 + if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then + local word=${comp%"${comp##*${char}}"} + local idx=${#COMPREPLY[*]} + while [[ $((--idx)) -ge 0 ]]; do + COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} + done + fi +} + +__git-bug_format_comp_descriptions() +{ + local tab + tab=$(printf '\t') + local comp="$1" + local longest=$2 + + # Properly format the description string which follows a tab character if there is one + if [[ "$comp" == *$tab* ]]; then + desc=${comp#*$tab} + comp=${comp%%$tab*} + + # $COLUMNS stores the current shell width. + # Remove an extra 4 because we add 2 spaces and 2 parentheses. + maxdesclength=$(( COLUMNS - longest - 4 )) + + # Make sure we can fit a description of at least 8 characters + # if we are to align the descriptions. + if [[ $maxdesclength -gt 8 ]]; then + # Add the proper number of spaces to align the descriptions + for ((i = ${#comp} ; i < longest ; i++)); do + comp+=" " + done else - # otherwise fall back to unqualified for compatibility - declare -F __custom_func >/dev/null && __custom_func + # Don't pad the descriptions so we can fit more text after the completion + maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + fi + + # If there is enough space for any description text, + # truncate the descriptions that are too long for the shell width + if [ $maxdesclength -gt 0 ]; then + if [ ${#desc} -gt $maxdesclength ]; then + desc=${desc:0:$(( maxdesclength - 1 ))} + desc+="…" + fi + comp+=" ($desc)" fi fi - # available in bash-completion >= 2, not always present on macOS - if declare -F __ltrim_colon_completions >/dev/null; then - __ltrim_colon_completions "$cur" - fi - - # If there is only 1 completion and it is a flag with an = it will be completed - # but we don't want a space after the = - if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then - compopt -o nospace - fi -} - -# The arguments should be in the form "ext1|ext2|extn" -__git-bug_handle_filename_extension_flag() -{ - local ext="$1" - _filedir "@(${ext})" -} - -__git-bug_handle_subdirs_in_dir_flag() -{ - local dir="$1" - pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return -} - -__git-bug_handle_flag() -{ - __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - # if a command required a flag, and we found it, unset must_have_one_flag() - local flagname=${words[c]} - local flagvalue="" - # if the word contained an = - if [[ ${words[c]} == *"="* ]]; then - flagvalue=${flagname#*=} # take in as flagvalue after the = - flagname=${flagname%=*} # strip everything after the = - flagname="${flagname}=" # but put the = back - fi - __git-bug_debug "${FUNCNAME[0]}: looking for ${flagname}" - if __git-bug_contains_word "${flagname}" "${must_have_one_flag[@]}"; then - must_have_one_flag=() - fi - - # if you set a flag which only applies to this command, don't show subcommands - if __git-bug_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then - commands=() - fi - - # keep flag value with flagname as flaghash - # flaghash variable is an associative array which is only supported in bash > 3. - if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then - if [ -n "${flagvalue}" ] ; then - flaghash[${flagname}]=${flagvalue} - elif [ -n "${words[ $((c+1)) ]}" ] ; then - flaghash[${flagname}]=${words[ $((c+1)) ]} - else - flaghash[${flagname}]="true" # pad "true" for bool flag - fi - fi - - # skip the argument to a two word flag - if [[ ${words[c]} != *"="* ]] && __git-bug_contains_word "${words[c]}" "${two_word_flags[@]}"; then - __git-bug_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" - c=$((c+1)) - # if we are looking for a flags value, don't show commands - if [[ $c -eq $cword ]]; then - commands=() - fi - fi - - c=$((c+1)) - -} - -__git-bug_handle_noun() -{ - __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - if __git-bug_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then - must_have_one_noun=() - elif __git-bug_contains_word "${words[c]}" "${noun_aliases[@]}"; then - must_have_one_noun=() - fi - - nouns+=("${words[c]}") - c=$((c+1)) -} - -__git-bug_handle_command() -{ - __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - local next_command - if [[ -n ${last_command} ]]; then - next_command="_${last_command}_${words[c]//:/__}" - else - if [[ $c -eq 0 ]]; then - next_command="_git-bug_root_command" - else - next_command="_${words[c]//:/__}" - fi - fi - c=$((c+1)) - __git-bug_debug "${FUNCNAME[0]}: looking for ${next_command}" - declare -F "$next_command" >/dev/null && $next_command -} - -__git-bug_handle_word() -{ - if [[ $c -ge $cword ]]; then - __git-bug_handle_reply - return - fi - __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - if [[ "${words[c]}" == -* ]]; then - __git-bug_handle_flag - elif __git-bug_contains_word "${words[c]}" "${commands[@]}"; then - __git-bug_handle_command - elif [[ $c -eq 0 ]]; then - __git-bug_handle_command - elif __git-bug_contains_word "${words[c]}" "${command_aliases[@]}"; then - # aliashash variable is an associative array which is only supported in bash > 3. - if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then - words[c]=${aliashash[${words[c]}]} - __git-bug_handle_command - else - __git-bug_handle_noun - fi - else - __git-bug_handle_noun - fi - __git-bug_handle_word -} - - -_git_bug() { - __start_git-bug "$@" -} - -_git-bug_add() -{ - last_command="git-bug_add" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--title=") - two_word_flags+=("--title") - two_word_flags+=("-t") - local_nonpersistent_flags+=("--title") - local_nonpersistent_flags+=("--title=") - local_nonpersistent_flags+=("-t") - flags+=("--message=") - two_word_flags+=("--message") - two_word_flags+=("-m") - local_nonpersistent_flags+=("--message") - local_nonpersistent_flags+=("--message=") - local_nonpersistent_flags+=("-m") - flags+=("--file=") - two_word_flags+=("--file") - two_word_flags+=("-F") - local_nonpersistent_flags+=("--file") - local_nonpersistent_flags+=("--file=") - local_nonpersistent_flags+=("-F") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_auth_add-token() -{ - last_command="git-bug_bridge_auth_add-token" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--target=") - two_word_flags+=("--target") - two_word_flags+=("-t") - local_nonpersistent_flags+=("--target") - local_nonpersistent_flags+=("--target=") - local_nonpersistent_flags+=("-t") - flags+=("--login=") - two_word_flags+=("--login") - two_word_flags+=("-l") - local_nonpersistent_flags+=("--login") - local_nonpersistent_flags+=("--login=") - local_nonpersistent_flags+=("-l") - flags+=("--user=") - two_word_flags+=("--user") - two_word_flags+=("-u") - local_nonpersistent_flags+=("--user") - local_nonpersistent_flags+=("--user=") - local_nonpersistent_flags+=("-u") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_auth_rm() -{ - last_command="git-bug_bridge_auth_rm" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_auth_show() -{ - last_command="git-bug_bridge_auth_show" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_auth() -{ - last_command="git-bug_bridge_auth" - - command_aliases=() - - commands=() - commands+=("add-token") - commands+=("rm") - commands+=("show") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_configure() -{ - last_command="git-bug_bridge_configure" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--name=") - two_word_flags+=("--name") - two_word_flags+=("-n") - local_nonpersistent_flags+=("--name") - local_nonpersistent_flags+=("--name=") - local_nonpersistent_flags+=("-n") - flags+=("--target=") - two_word_flags+=("--target") - two_word_flags+=("-t") - local_nonpersistent_flags+=("--target") - local_nonpersistent_flags+=("--target=") - local_nonpersistent_flags+=("-t") - flags+=("--url=") - two_word_flags+=("--url") - two_word_flags+=("-u") - local_nonpersistent_flags+=("--url") - local_nonpersistent_flags+=("--url=") - local_nonpersistent_flags+=("-u") - flags+=("--base-url=") - two_word_flags+=("--base-url") - two_word_flags+=("-b") - local_nonpersistent_flags+=("--base-url") - local_nonpersistent_flags+=("--base-url=") - local_nonpersistent_flags+=("-b") - flags+=("--login=") - two_word_flags+=("--login") - two_word_flags+=("-l") - local_nonpersistent_flags+=("--login") - local_nonpersistent_flags+=("--login=") - local_nonpersistent_flags+=("-l") - flags+=("--credential=") - two_word_flags+=("--credential") - two_word_flags+=("-c") - local_nonpersistent_flags+=("--credential") - local_nonpersistent_flags+=("--credential=") - local_nonpersistent_flags+=("-c") - flags+=("--token=") - two_word_flags+=("--token") - local_nonpersistent_flags+=("--token") - local_nonpersistent_flags+=("--token=") - flags+=("--token-stdin") - local_nonpersistent_flags+=("--token-stdin") - flags+=("--owner=") - two_word_flags+=("--owner") - two_word_flags+=("-o") - local_nonpersistent_flags+=("--owner") - local_nonpersistent_flags+=("--owner=") - local_nonpersistent_flags+=("-o") - flags+=("--project=") - two_word_flags+=("--project") - two_word_flags+=("-p") - local_nonpersistent_flags+=("--project") - local_nonpersistent_flags+=("--project=") - local_nonpersistent_flags+=("-p") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_pull() -{ - last_command="git-bug_bridge_pull" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--no-resume") - flags+=("-n") - local_nonpersistent_flags+=("--no-resume") - local_nonpersistent_flags+=("-n") - flags+=("--since=") - two_word_flags+=("--since") - two_word_flags+=("-s") - local_nonpersistent_flags+=("--since") - local_nonpersistent_flags+=("--since=") - local_nonpersistent_flags+=("-s") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_push() -{ - last_command="git-bug_bridge_push" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge_rm() -{ - last_command="git-bug_bridge_rm" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_bridge() -{ - last_command="git-bug_bridge" - - command_aliases=() - - commands=() - commands+=("auth") - commands+=("configure") - commands+=("pull") - commands+=("push") - commands+=("rm") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_commands() -{ - last_command="git-bug_commands" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--pretty") - flags+=("-p") - local_nonpersistent_flags+=("--pretty") - local_nonpersistent_flags+=("-p") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_comment_add() -{ - last_command="git-bug_comment_add" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--file=") - two_word_flags+=("--file") - two_word_flags+=("-F") - local_nonpersistent_flags+=("--file") - local_nonpersistent_flags+=("--file=") - local_nonpersistent_flags+=("-F") - flags+=("--message=") - two_word_flags+=("--message") - two_word_flags+=("-m") - local_nonpersistent_flags+=("--message") - local_nonpersistent_flags+=("--message=") - local_nonpersistent_flags+=("-m") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_comment_edit() -{ - last_command="git-bug_comment_edit" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--file=") - two_word_flags+=("--file") - two_word_flags+=("-F") - local_nonpersistent_flags+=("--file") - local_nonpersistent_flags+=("--file=") - local_nonpersistent_flags+=("-F") - flags+=("--message=") - two_word_flags+=("--message") - two_word_flags+=("-m") - local_nonpersistent_flags+=("--message") - local_nonpersistent_flags+=("--message=") - local_nonpersistent_flags+=("-m") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_comment() -{ - last_command="git-bug_comment" - - command_aliases=() - - commands=() - commands+=("add") - commands+=("edit") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_deselect() -{ - last_command="git-bug_deselect" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_label_add() -{ - last_command="git-bug_label_add" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_label_rm() -{ - last_command="git-bug_label_rm" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_label() -{ - last_command="git-bug_label" - - command_aliases=() - - commands=() - commands+=("add") - commands+=("rm") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_ls() -{ - last_command="git-bug_ls" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--status=") - two_word_flags+=("--status") - two_word_flags+=("-s") - local_nonpersistent_flags+=("--status") - local_nonpersistent_flags+=("--status=") - local_nonpersistent_flags+=("-s") - flags+=("--author=") - two_word_flags+=("--author") - two_word_flags+=("-a") - local_nonpersistent_flags+=("--author") - local_nonpersistent_flags+=("--author=") - local_nonpersistent_flags+=("-a") - flags+=("--metadata=") - two_word_flags+=("--metadata") - two_word_flags+=("-m") - local_nonpersistent_flags+=("--metadata") - local_nonpersistent_flags+=("--metadata=") - local_nonpersistent_flags+=("-m") - flags+=("--participant=") - two_word_flags+=("--participant") - two_word_flags+=("-p") - local_nonpersistent_flags+=("--participant") - local_nonpersistent_flags+=("--participant=") - local_nonpersistent_flags+=("-p") - flags+=("--actor=") - two_word_flags+=("--actor") - two_word_flags+=("-A") - local_nonpersistent_flags+=("--actor") - local_nonpersistent_flags+=("--actor=") - local_nonpersistent_flags+=("-A") - flags+=("--label=") - two_word_flags+=("--label") - two_word_flags+=("-l") - local_nonpersistent_flags+=("--label") - local_nonpersistent_flags+=("--label=") - local_nonpersistent_flags+=("-l") - flags+=("--title=") - two_word_flags+=("--title") - two_word_flags+=("-t") - local_nonpersistent_flags+=("--title") - local_nonpersistent_flags+=("--title=") - local_nonpersistent_flags+=("-t") - flags+=("--no=") - two_word_flags+=("--no") - two_word_flags+=("-n") - local_nonpersistent_flags+=("--no") - local_nonpersistent_flags+=("--no=") - local_nonpersistent_flags+=("-n") - flags+=("--by=") - two_word_flags+=("--by") - two_word_flags+=("-b") - local_nonpersistent_flags+=("--by") - local_nonpersistent_flags+=("--by=") - local_nonpersistent_flags+=("-b") - flags+=("--direction=") - two_word_flags+=("--direction") - two_word_flags+=("-d") - local_nonpersistent_flags+=("--direction") - local_nonpersistent_flags+=("--direction=") - local_nonpersistent_flags+=("-d") - flags+=("--format=") - two_word_flags+=("--format") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--format") - local_nonpersistent_flags+=("--format=") - local_nonpersistent_flags+=("-f") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_ls-id() -{ - last_command="git-bug_ls-id" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_ls-label() -{ - last_command="git-bug_ls-label" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_pull() -{ - last_command="git-bug_pull" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_push() -{ - last_command="git-bug_push" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_rm() -{ - last_command="git-bug_rm" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_select() -{ - last_command="git-bug_select" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_show() -{ - last_command="git-bug_show" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--field=") - two_word_flags+=("--field") - local_nonpersistent_flags+=("--field") - local_nonpersistent_flags+=("--field=") - flags+=("--format=") - two_word_flags+=("--format") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--format") - local_nonpersistent_flags+=("--format=") - local_nonpersistent_flags+=("-f") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_status_close() -{ - last_command="git-bug_status_close" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_status_open() -{ - last_command="git-bug_status_open" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_status() -{ - last_command="git-bug_status" - - command_aliases=() - - commands=() - commands+=("close") - commands+=("open") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_termui() -{ - last_command="git-bug_termui" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_title_edit() -{ - last_command="git-bug_title_edit" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--title=") - two_word_flags+=("--title") - two_word_flags+=("-t") - local_nonpersistent_flags+=("--title") - local_nonpersistent_flags+=("--title=") - local_nonpersistent_flags+=("-t") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_title() -{ - last_command="git-bug_title" - - command_aliases=() - - commands=() - commands+=("edit") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_user_adopt() -{ - last_command="git-bug_user_adopt" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_user_create() -{ - last_command="git-bug_user_create" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--avatar=") - two_word_flags+=("--avatar") - two_word_flags+=("-a") - local_nonpersistent_flags+=("--avatar") - local_nonpersistent_flags+=("--avatar=") - local_nonpersistent_flags+=("-a") - flags+=("--email=") - two_word_flags+=("--email") - two_word_flags+=("-e") - local_nonpersistent_flags+=("--email") - local_nonpersistent_flags+=("--email=") - local_nonpersistent_flags+=("-e") - flags+=("--name=") - two_word_flags+=("--name") - two_word_flags+=("-n") - local_nonpersistent_flags+=("--name") - local_nonpersistent_flags+=("--name=") - local_nonpersistent_flags+=("-n") - flags+=("--non-interactive") - local_nonpersistent_flags+=("--non-interactive") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_user_ls() -{ - last_command="git-bug_user_ls" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--format=") - two_word_flags+=("--format") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--format") - local_nonpersistent_flags+=("--format=") - local_nonpersistent_flags+=("-f") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_user() -{ - last_command="git-bug_user" - - command_aliases=() - - commands=() - commands+=("adopt") - commands+=("create") - commands+=("ls") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--field=") - two_word_flags+=("--field") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--field") - local_nonpersistent_flags+=("--field=") - local_nonpersistent_flags+=("-f") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_version() -{ - last_command="git-bug_version" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--number") - flags+=("-n") - local_nonpersistent_flags+=("--number") - local_nonpersistent_flags+=("-n") - flags+=("--commit") - flags+=("-c") - local_nonpersistent_flags+=("--commit") - local_nonpersistent_flags+=("-c") - flags+=("--all") - flags+=("-a") - local_nonpersistent_flags+=("--all") - local_nonpersistent_flags+=("-a") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_webui() -{ - last_command="git-bug_webui" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--host=") - two_word_flags+=("--host") - local_nonpersistent_flags+=("--host") - local_nonpersistent_flags+=("--host=") - flags+=("--open") - local_nonpersistent_flags+=("--open") - flags+=("--no-open") - local_nonpersistent_flags+=("--no-open") - flags+=("--port=") - two_word_flags+=("--port") - two_word_flags+=("-p") - local_nonpersistent_flags+=("--port") - local_nonpersistent_flags+=("--port=") - local_nonpersistent_flags+=("-p") - flags+=("--read-only") - local_nonpersistent_flags+=("--read-only") - flags+=("--query=") - two_word_flags+=("--query") - two_word_flags+=("-q") - local_nonpersistent_flags+=("--query") - local_nonpersistent_flags+=("--query=") - local_nonpersistent_flags+=("-q") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_git-bug_root_command() -{ - last_command="git-bug" - - command_aliases=() - - commands=() - commands+=("add") - commands+=("bridge") - commands+=("commands") - commands+=("comment") - commands+=("deselect") - commands+=("label") - commands+=("ls") - commands+=("ls-id") - commands+=("ls-label") - commands+=("pull") - commands+=("push") - commands+=("rm") - commands+=("select") - commands+=("show") - commands+=("status") - commands+=("termui") - if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then - command_aliases+=("tui") - aliashash["tui"]="termui" - fi - commands+=("title") - commands+=("user") - commands+=("version") - commands+=("webui") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() + # Must use printf to escape all special characters + printf "%q" "${comp}" } __start_git-bug() { local cur prev words cword split - declare -A flaghash 2>/dev/null || : - declare -A aliashash 2>/dev/null || : + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly if declare -F _init_completion >/dev/null 2>&1; then - _init_completion -s || return + _init_completion -n "=:" || return else - __git-bug_init_completion -n "=" || return + __git-bug_init_completion -n "=:" || return fi - local c=0 - local flag_parsing_disabled= - local flags=() - local two_word_flags=() - local local_nonpersistent_flags=() - local flags_with_completion=() - local flags_completion=() - local commands=("git-bug") - local command_aliases=() - local must_have_one_flag=() - local must_have_one_noun=() - local has_completion_function="" - local last_command="" - local nouns=() - local noun_aliases=() + __git-bug_debug + __git-bug_debug "========= starting completion logic ==========" + __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" - __git-bug_handle_word + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __git-bug_debug "Truncated words[*]: ${words[*]}," + + local out directive + __git-bug_get_completion_results + __git-bug_process_completion_results } if [[ $(type -t compopt) = "builtin" ]]; then @@ -1509,3 +286,37 @@ else fi # ex: ts=4 sw=4 et filetype=sh + +_git_bug() { + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n "=:" || return + else + __git-bug_init_completion -n "=:" || return + fi + + # START PATCH + # replace in the array ("git","bug", ...) to ("git-bug", ...) and adjust the index in cword + words=("git-bug" "${words[@]:2}") + cword=$(($cword-1)) + # END PATCH + + __git-bug_debug + __git-bug_debug "========= starting completion logic ==========" + __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __git-bug_debug "Truncated words[*]: ${words[*]}," + + local out directive + __git-bug_get_completion_results + __git-bug_process_completion_results +} diff --git a/misc/gen_completion.go b/misc/gen_completion.go index c073e67e..1f86124d 100644 --- a/misc/gen_completion.go +++ b/misc/gen_completion.go @@ -40,25 +40,86 @@ func main() { } func genBash(root *cobra.Command) error { - cwd, _ := os.Getwd() - dir := filepath.Join(cwd, "misc", "bash_completion", "git-bug") - return root.GenBashCompletionFile(dir) + cwd, err := os.Getwd() + if err != nil { + return err + } + f, err := os.Create(filepath.Join(cwd, "misc", "bash_completion", "git-bug")) + if err != nil { + return err + } + defer f.Close() + + const patch = ` +_git_bug() { + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n "=:" || return + else + __git-bug_init_completion -n "=:" || return + fi + + # START PATCH + # replace in the array ("git","bug", ...) to ("git-bug", ...) and adjust the index in cword + words=("git-bug" "${words[@]:2}") + cword=$(($cword-1)) + # END PATCH + + __git-bug_debug + __git-bug_debug "========= starting completion logic ==========" + __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __git-bug_debug "Truncated words[*]: ${words[*]}," + + local out directive + __git-bug_get_completion_results + __git-bug_process_completion_results +} +` + err = root.GenBashCompletionV2(f, true) + if err != nil { + return err + } + + // Custom bash code to connect the git completion for "git bug" to the + // git-bug completion for "git-bug" + _, err = f.WriteString(patch) + + return err } func genFish(root *cobra.Command) error { - cwd, _ := os.Getwd() + cwd, err := os.Getwd() + if err != nil { + return err + } dir := filepath.Join(cwd, "misc", "fish_completion", "git-bug") return root.GenFishCompletionFile(dir, true) } func genPowerShell(root *cobra.Command) error { - cwd, _ := os.Getwd() + cwd, err := os.Getwd() + if err != nil { + return err + } path := filepath.Join(cwd, "misc", "powershell_completion", "git-bug") return root.GenPowerShellCompletionFile(path) } func genZsh(root *cobra.Command) error { - cwd, _ := os.Getwd() + cwd, err := os.Getwd() + if err != nil { + return err + } path := filepath.Join(cwd, "misc", "zsh_completion", "git-bug") return root.GenZshCompletionFile(path) }