From 9ac5b79e9eee3a2555a91bb05a264a64badbbb1f Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 12 Oct 2026 10:50:37 +0200 Subject: [PATCH] refactor(completion): name path arguments explicitly and share firstArg The six commands that take a file or directory argument set their completer directly instead of a Use-line parser wrapping every command, the first-argument-only wrappers become one firstArg helper, and only the bash completion help keeps a custom text. Signed-off-by: bircni --- internal/app/action_jobs.go | 2 +- internal/app/action_logs.go | 2 +- internal/app/action_runs.go | 21 ++--- internal/app/action_values.go | 7 +- internal/app/actions.go | 17 +--- internal/app/alias_completion_test.go | 34 ++----- internal/app/artifacts.go | 4 +- internal/app/comments.go | 10 +-- internal/app/completion.go | 125 ++++++++------------------ internal/app/issue_attachments.go | 2 +- internal/app/issue_edit.go | 2 +- internal/app/issues.go | 16 +--- internal/app/labels.go | 2 +- internal/app/metadata.go | 18 +--- internal/app/milestone_issues.go | 2 +- internal/app/milestone_state.go | 2 +- internal/app/release_assets.go | 2 +- internal/app/releases.go | 2 +- internal/app/repo_mutations.go | 2 +- internal/app/ssh_keys.go | 2 +- internal/app/teams.go | 7 +- internal/app/time.go | 6 +- 22 files changed, 75 insertions(+), 212 deletions(-) diff --git a/internal/app/action_jobs.go b/internal/app/action_jobs.go index 368c846d..e4d8aef6 100644 --- a/internal/app/action_jobs.go +++ b/internal/app/action_jobs.go @@ -38,7 +38,7 @@ $ tea run jobs 34 --json id,conclusion --jq '.[] | select(.conclusion == "failur }, } pageFlags(cmd, &limit, &page) - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) return cmd } diff --git a/internal/app/action_logs.go b/internal/app/action_logs.go index 3c50cb27..3f9eff45 100644 --- a/internal/app/action_logs.go +++ b/internal/app/action_logs.go @@ -107,7 +107,7 @@ $ tea run logs 34 --output logs.zip`, cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Wait for jobs to start and print new lines until they complete") cmd.Flags().DurationVarP(&interval, "interval", "i", 5*time.Second, "Polling interval with --follow") cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to follow") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) _ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs()) return cmd } diff --git a/internal/app/action_runs.go b/internal/app/action_runs.go index 7afff430..e60a2623 100644 --- a/internal/app/action_runs.go +++ b/internal/app/action_runs.go @@ -228,7 +228,7 @@ $ tea run view 34 --json jobs --jq '.jobs[] | {name, conclusion}'`, cmd.Flags().BoolVar(&noJobs, "no-jobs", false, "Leave out the jobs") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the run in the browser") cmd.Flags().BoolVar(&log, "log", false, "Print the logs of every job instead, like tea run logs") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) return cmd } @@ -459,17 +459,6 @@ func (a *application) runPicker() (terminal.Chooser, bool) { return chooser, err == nil && a.policy(s).CanPrompt(a.deps.Streams) } -// completeRunArg completes the run argument of a run command with recent runs. -func (a *application) completeRunArg() cobra.CompletionFunc { - runs := a.completeRuns() - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return runs(cmd, args, toComplete) - } -} - // accountNote reminds people who pass the global --user, which selects the account tea acts as, // of the flag they may have meant, as gh's --user means something else. func (a *application) accountNote(cmd *cobra.Command, t target, hint string) { @@ -507,7 +496,7 @@ $ tea run watch 34 --timeout 10m && echo deployed`, } cmd.Flags().DurationVarP(&interval, "interval", "i", 3*time.Second, "Refresh interval") cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to wait") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) return cmd } @@ -622,7 +611,7 @@ $ tea run cancel 34 --force`, }, } cmd.Flags().BoolVar(&force, "force", false, "Cancel without waiting for the runner to acknowledge") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) return cmd } @@ -681,7 +670,7 @@ $ tea run rerun 34 --job build`, } cmd.Flags().BoolVar(&failed, "failed", false, "Rerun only the failed jobs") cmd.Flags().StringVarP(&job, "job", "j", "", "Rerun only this job, by `name or ID`") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) _ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs()) return cmd } @@ -736,7 +725,7 @@ $ tea run delete 34 --yes`, return nil }, } - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) return cmd } diff --git a/internal/app/action_values.go b/internal/app/action_values.go index 54fcb54e..b8763f7d 100644 --- a/internal/app/action_values.go +++ b/internal/app/action_values.go @@ -433,10 +433,5 @@ func (a *application) completeActionValues(secret bool, scope *actionScopeFlags) if secret { names = a.completeSecrets(scope.scope) } - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return names(cmd, args, toComplete) - } + return firstArg(names) } diff --git a/internal/app/actions.go b/internal/app/actions.go index b94a795e..7100c271 100644 --- a/internal/app/actions.go +++ b/internal/app/actions.go @@ -128,7 +128,7 @@ $ tea workflow view ci --web`, cmd.Flags().BoolVar(&showYAML, "yaml", false, "Print the workflow file") cmd.Flags().StringVarP(&ref, "ref", "r", "", "Branch, tag or commit of the workflow file that --yaml prints (default: the default branch)") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the workflow file in the browser") - cmd.ValidArgsFunction = a.completeWorkflowArg() + cmd.ValidArgsFunction = firstArg(a.completeWorkflows()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) return cmd } @@ -269,7 +269,7 @@ $ tea workflow run nightly --follow`, cmd.Flags().BoolVar(&follow, "follow", false, "Follow the logs of the run's jobs after dispatching it") cmd.Flags().DurationVar(&interval, "interval", 2*time.Second, "Polling interval with --follow") cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Minute, "Maximum time to follow") - cmd.ValidArgsFunction = a.completeWorkflowArg() + cmd.ValidArgsFunction = firstArg(a.completeWorkflows()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) _ = cmd.RegisterFlagCompletionFunc("field", a.completeWorkflowInputs()) _ = cmd.RegisterFlagCompletionFunc("input", a.completeWorkflowInputs()) @@ -445,7 +445,7 @@ func (a *application) workflowStateCommand(enable bool) *cobra.Command { $ tea workflow %s Nightly`, verb, verb), Args: argsExact(1), Annotations: map[string]string{"tea.arguments": workflowArguments}, - ValidArgsFunction: a.completeWorkflowArg(), + ValidArgsFunction: firstArg(a.completeWorkflows()), RunE: func(cmd *cobra.Command, args []string) error { t, err := a.resolve(cmd.Context(), true) if err != nil { @@ -472,17 +472,6 @@ $ tea workflow %s Nightly`, verb, verb), } } -// completeWorkflowArg completes the workflow argument with workflow files. -func (a *application) completeWorkflowArg() cobra.CompletionFunc { - workflows := a.completeWorkflows() - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return workflows(cmd, args, toComplete) - } -} - // completeWorkflowInputs completes the inputs of the workflow given as the first argument as // name=, then the values of a choice or boolean input. func (a *application) completeWorkflowInputs() cobra.CompletionFunc { diff --git a/internal/app/alias_completion_test.go b/internal/app/alias_completion_test.go index d64d1c2f..51fcd260 100644 --- a/internal/app/alias_completion_test.go +++ b/internal/app/alias_completion_test.go @@ -124,6 +124,8 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) { "release asset upload v1 a.txt ": cobra.ShellCompDirectiveDefault, "repo clone alice/project ": cobra.ShellCompDirectiveFilterDirs, "repo clone alice/project dir ": cobra.ShellCompDirectiveNoFileComp, + "issue attachment upload 5 ": cobra.ShellCompDirectiveDefault, + "release create v1 ": cobra.ShellCompDirectiveDefault, "--cwd ": cobra.ShellCompDirectiveFilterDirs, "--jq ": cobra.ShellCompDirectiveNoFileComp, } { @@ -136,9 +138,9 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) { return } path := strings.Fields(cmd.CommandPath())[1:] - slots := argSlots(cmd.Use) _, directive := complete(t, deps, out, append(path, "")...) - require.Equal(t, len(slots) > 0 && slots[0].kind == "file", directive == cobra.ShellCompDirectiveDefault, "%s : %d", cmd.CommandPath(), directive) + takesFile := cmd.CommandPath() == "tea label import" || cmd.CommandPath() == "tea ssh-key create" + require.Equal(t, takesFile, directive == cobra.ShellCompDirectiveDefault, "%s : %d", cmd.CommandPath(), directive) for _, flags := range []*pflag.FlagSet{cmd.LocalNonPersistentFlags(), cmd.PersistentFlags()} { flags.VisitAll(func(flag *pflag.Flag) { if flag.NoOptDefVal != "" || flag.Hidden { @@ -152,21 +154,6 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) { }) } -func TestArgSlots(t *testing.T) { - for use, want := range map[string][]argSlot{ - "list": {}, - "view ": {{}}, - "upload ...": {{}, {kind: "file", variadic: true}}, - "clone [directory]": {{}, {kind: "directory"}}, - "create ": {{kind: "file"}}, - "review [ | | ]": {{}}, - "man [command...]": {{variadic: true}}, - "set ": {{}, {}}, - } { - require.Equal(t, want, append([]argSlot{}, argSlots(use)...), use) - } -} - func TestOfflineCompletions(t *testing.T) { deps, out, _ := testDeps(t) deps.Git = &fakeGit{results: map[string]string{ @@ -338,15 +325,6 @@ func TestInstanceCompletions(t *testing.T) { func TestCompletionInstallHelp(t *testing.T) { deps, out, _ := testDeps(t) - for shell, want := range map[string]string{ - "bash": "$ eval \"$(tea completion bash)\"\n", - "zsh": "$ autoload -U compinit && compinit\n", - "fish": "$ tea completion fish > ~/.config/fish/completions/tea.fish\n", - "powershell": "$ tea completion powershell | Out-String | Invoke-Expression\n", - } { - out.Reset() - require.NoError(t, Execute(context.Background(), deps, []string{"completion", shell, "--help"})) - require.Contains(t, out.String(), want, shell) - require.NotContains(t, out.String(), "source <(tea completion bash)\n", shell) - } + require.NoError(t, Execute(context.Background(), deps, []string{"completion", "bash", "--help"})) + require.Contains(t, out.String(), "$ eval \"$(tea completion bash)\"\n") } diff --git a/internal/app/artifacts.go b/internal/app/artifacts.go index 20b55e28..8b89dbbb 100644 --- a/internal/app/artifacts.go +++ b/internal/app/artifacts.go @@ -50,7 +50,7 @@ $ tea run artifact list 34`, }, } pageFlags(list, &limit, &page) - list.ValidArgsFunction = a.completeRunArg() + list.ValidArgsFunction = firstArg(a.completeRuns()) group.AddCommand(list) return group @@ -148,7 +148,7 @@ $ tea run download 34 --zip`, destination.register(cmd) cmd.Flags().StringSliceVarP(&names, "name", "n", nil, "Only download the artifacts with these names") cmd.Flags().BoolVar(&asZip, "zip", false, "Save each artifact as a ZIP archive instead of extracting it") - cmd.ValidArgsFunction = a.completeRunArg() + cmd.ValidArgsFunction = firstArg(a.completeRuns()) _ = cmd.RegisterFlagCompletionFunc("name", commaSeparated(a.completeArtifactNames())) return cmd } diff --git a/internal/app/comments.go b/internal/app/comments.go index 275e3c32..a924df49 100644 --- a/internal/app/comments.go +++ b/internal/app/comments.go @@ -426,13 +426,7 @@ func article(noun string) string { // completeDiscussions completes the open issues or pull requests that a command of kind takes. func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc { if kind == pullRef { - complete := a.completePulls("open") - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return complete(cmd, args, toComplete) - } + return firstArg(a.completePulls("open")) } - return a.completeIssueArgs("open", false) + return firstArg(a.completeIssues("open")) } diff --git a/internal/app/completion.go b/internal/app/completion.go index f596b3f6..786fd8c5 100644 --- a/internal/app/completion.go +++ b/internal/app/completion.go @@ -519,9 +519,9 @@ func actionScope(scope func() services.ActionScope, t target) services.ActionSco // pathFlags take local file paths, like every flag whose name ends in -file. var pathFlags = []string{"asset", "attachment", "file", "input", "ssh-key"} -// completeDefaults completes paths only where a command takes them: path flags, flags marked with -// MarkFlagFilename, and positional arguments named file or directory in Use. Everything else without a -// completer completes nothing, by the root's default directive, rather than file names. +// completeDefaults completes paths only where a command takes them: path flags and flags marked +// with MarkFlagFilename. Everything else without a completer completes nothing, by the root's +// default directive, rather than file names. func completeDefaults(cmd *cobra.Command) { for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.PersistentFlags()} { flags.VisitAll(func(flag *pflag.Flag) { @@ -536,23 +536,6 @@ func completeDefaults(cmd *cobra.Command) { } }) } - if slots := argSlots(cmd.Use); len(slots) > 0 { - complete := cmd.ValidArgsFunction - cmd.ValidArgsFunction = func(c *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - slot := slots[min(len(args), len(slots)-1)] - switch { - case len(args) >= len(slots) && !slot.variadic: - return nil, cobra.ShellCompDirectiveNoFileComp - case slot.kind == "file": - return completeFiles(c, args, toComplete) - case slot.kind == "directory": - return completeDirectories(c, args, toComplete) - case complete != nil: - return complete(c, args, toComplete) - } - return nil, cobra.ShellCompDirectiveNoFileComp - } - } for _, child := range cmd.Commands() { completeDefaults(child) } @@ -568,89 +551,53 @@ func completeDirectories(*cobra.Command, []string, string) ([]cobra.Completion, return nil, cobra.ShellCompDirectiveFilterDirs } -// argSlot is one positional argument that a Use line declares, such as , [directory] or -// ...; kind is "file" or "directory" for path arguments. -type argSlot struct { - kind string - variadic bool -} - -// argSlots parses the positional arguments of a Use line. -func argSlots(use string) []argSlot { - _, rest, _ := strings.Cut(use, " ") - var slots []argSlot - depth, start := 0, 0 - for i, r := range rest { - switch r { - case '<', '[', '{': - if depth == 0 { - start = i - } - depth++ - case '>', ']', '}': - if depth--; depth != 0 { - continue - } - slot := argSlot{variadic: strings.HasPrefix(rest[i+1:], "...") || strings.HasSuffix(rest[start:i], "...")} - for name := range strings.FieldsFuncSeq(rest[start:i+1], func(r rune) bool { return strings.ContainsRune("<>[]|. ", r) }) { - switch { - case name == "file" || strings.HasSuffix(name, "-file"): - slot.kind = "file" - case name == "directory" || name == "dir": - slot.kind = "directory" - } - } - slots = append(slots, slot) +// firstArg completes only the first argument with complete. +func firstArg(complete cobra.CompletionFunc) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp } + return complete(cmd, args, toComplete) } - return slots } -// completionInstallHelp explains how to load the completion scripts, including with macOS's bash 3.2 -// and in zsh without compinit. +// pathArgs completes the arguments before position n with complete, and paths with path from +// position n on: every later argument when variadic, else only the one at n. +func pathArgs(n int, variadic bool, path, complete cobra.CompletionFunc) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + switch { + case len(args) < n: + return complete(cmd, args, toComplete) + case len(args) == n || variadic: + return path(cmd, args, toComplete) + } + return nil, cobra.ShellCompDirectiveNoFileComp + } +} + +// completionInstallHelp explains how to load the bash script with macOS's bash 3.2, which cannot +// source <(tea completion bash); cobra's help covers the other shells. func completionInstallHelp(root *cobra.Command) { completion, _, err := root.Find([]string{"completion"}) if err != nil || completion == root { return } completion.Short = "Generate shell completion scripts" - completion.Long = `Generate shell completion scripts for tea. - -Completion offers commands, flags, flag values such as --state or --json fields, and values -from the selected instance, such as repositories, issue numbers and labels. See -tea completion --help for how to load the script in your shell.` - help := map[string][2]string{ - "bash": {`Generate the completion script for bash. - -The script needs the bash-completion package. On macOS, install bash-completion with -Homebrew for the system bash 3.2, or bash-completion@2 for a Homebrew bash, and load it -as its install notes say. - -To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS, -~/.bash_profile). Use eval: bash 3.2, the default on macOS, cannot source <(tea completion bash).`, `# Load completions in this shell, or in every shell from ~/.bashrc -$ eval "$(tea completion bash)" -# Or install the script for every user (Linux, then macOS) -$ tea completion bash > /etc/bash_completion.d/tea -$ tea completion bash > "$(brew --prefix)/etc/bash_completion.d/tea"`}, - "zsh": {`Generate the completion script for zsh. - -Completion must be initialized before the script loads: add these lines to ~/.zshrc, -keeping an existing compinit call instead of the first line. Without compinit, zsh -reports "command not found: compdef".`, `$ autoload -U compinit && compinit -$ source <(tea completion zsh) -# Or install the script into a directory of your fpath once -$ tea completion zsh > "${fpath[1]}/_tea"`}, - "fish": {`Generate the completion script for fish. - -To load completions in every new shell, install the script once.`, `$ tea completion fish > ~/.config/fish/completions/tea.fish`}, - "powershell": {`Generate the completion script for PowerShell. - -To load completions in every new shell, add this line to your PowerShell profile.`, `$ tea completion powershell | Out-String | Invoke-Expression`}, + examples := map[string]string{ + "bash": `$ eval "$(tea completion bash)"`, + "zsh": `$ source <(tea completion zsh)`, + "fish": `$ tea completion fish > ~/.config/fish/completions/tea.fish`, + "powershell": `$ tea completion powershell | Out-String | Invoke-Expression`, } for _, shell := range completion.Commands() { - shell.Long, shell.Example = help[shell.Name()][0], help[shell.Name()][1] + shell.Example = examples[shell.Name()] if flag := shell.Flags().Lookup("no-descriptions"); flag != nil { flag.Usage = "Disable completion descriptions" } } + bash, _, _ := completion.Find([]string{"bash"}) + bash.Long = `Generate the completion script for bash. It needs the bash-completion package. + +To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS, +~/.bash_profile); bash 3.2, the default on macOS, cannot source <(tea completion bash).` } diff --git a/internal/app/issue_attachments.go b/internal/app/issue_attachments.go index dd9281f4..269e6aa1 100644 --- a/internal/app/issue_attachments.go +++ b/internal/app/issue_attachments.go @@ -90,7 +90,7 @@ are addressed by the %s number and the attachment ID that attachment list shows. var uploadName string upload := &cobra.Command{Use: "upload { | } ", Short: "Attach a file", Example: fmt.Sprintf(`$ tea %[1]s attachment upload %[2]s crash.log -$ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error { +$ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: pathArgs(1, false, completeFiles, completeFirst), RunE: func(cmd *cobra.Command, args []string) error { name := uploadName if cmd.Flags().Changed("name") && strings.TrimSpace(name) == "" { return clierr.Wrap(clierr.Usage, errors.New("--name cannot be empty")) diff --git a/internal/app/issue_edit.go b/internal/app/issue_edit.go index 8cddb57e..5eaae88a 100644 --- a/internal/app/issue_edit.go +++ b/internal/app/issue_edit.go @@ -124,7 +124,7 @@ $ tea issue edit 12 --body-file notes.md`, Annotations: map[string]string{"tea.a a.completeMetadataFlags(cmd) _ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) - cmd.ValidArgsFunction = a.completeIssueArgs("open", true) + cmd.ValidArgsFunction = a.completeIssues("open") return cmd } diff --git a/internal/app/issues.go b/internal/app/issues.go index 4b52bfd6..4e1295ac 100644 --- a/internal/app/issues.go +++ b/internal/app/issues.go @@ -187,22 +187,10 @@ $ tea issue view https://gitea.com/OWNER/REPO/issues/12 --web`, Annotations: map }} cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the issue in the browser") cmd.Flags().BoolVarP(&comments, "comments", "c", false, "View issue comments") - cmd.ValidArgsFunction = a.completeIssueArgs("open", false) + cmd.ValidArgsFunction = firstArg(a.completeIssues("open")) return cmd } -// completeIssueArgs completes issue numbers in state for the first argument, or for every -// argument when variadic. -func (a *application) completeIssueArgs(state string, variadic bool) cobra.CompletionFunc { - complete := a.completeIssues(state) - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 && !variadic { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return complete(cmd, args, toComplete) - } -} - func (a *application) issueCreateCommand() *cobra.Command { var title, body, bodyFile, deadline, templateName, ref, milestone string var useEditor, web bool @@ -446,7 +434,7 @@ $ tea issue %[1]s 12 13 --comment "Done in #14"`, verb), Annotations: map[string return errors.Join(append(failures, writeBatchJSON(a, cmd, len(numbers), results))...) }} cmd.Flags().StringVarP(&comment, "comment", "c", "", "Leave a comment before the issue is "+state) - cmd.ValidArgsFunction = a.completeIssueArgs(completeState, true) + cmd.ValidArgsFunction = a.completeIssues(completeState) return cmd } diff --git a/internal/app/labels.go b/internal/app/labels.go index b898d9d3..650b5ea3 100644 --- a/internal/app/labels.go +++ b/internal/app/labels.go @@ -302,7 +302,7 @@ func (a *application) labelImportCommand(scope labelScope) *cobra.Command { " ;description" and " ;exclusive", " ;archived" or both. tea label export writes this format, and Gitea's label templates use it. Existing labels are refused unless --force updates them.`, Example: `$ tea label import labels.txt -$ tea label export --repo alice/old | tea label import - --repo alice/new`, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error { +$ tea label export --repo alice/old | tea label import - --repo alice/new`, Args: argsExact(1), ValidArgsFunction: completeFiles, RunE: func(cmd *cobra.Command, args []string) error { return a.importLabels(cmd, scope, args[0], force) }} cmd.Flags().BoolVarP(&force, "force", "f", false, "Update labels that exist already") diff --git a/internal/app/metadata.go b/internal/app/metadata.go index d457e2b0..9b7334fe 100644 --- a/internal/app/metadata.go +++ b/internal/app/metadata.go @@ -161,7 +161,7 @@ $ tea milestone view 3 --web`, Annotations: map[string]string{"tea.arguments": m }} cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the milestone in the browser") - cmd.ValidArgsFunction = a.completeMilestoneArgs(false) + cmd.ValidArgsFunction = firstArg(a.completeMilestones()) return cmd } @@ -340,7 +340,7 @@ $ tea milestone edit 3 --editor`, Annotations: map[string]string{"tea.arguments" }} fields.register(cmd) cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") - cmd.ValidArgsFunction = a.completeMilestoneArgs(false) + cmd.ValidArgsFunction = firstArg(a.completeMilestones()) return cmd } @@ -367,7 +367,7 @@ $ tea milestone delete 7 --yes`, Annotations: map[string]string{"tea.confirm": " return a.jsonResult(cmd, item) }} cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") - cmd.ValidArgsFunction = a.completeMilestoneArgs(false) + cmd.ValidArgsFunction = firstArg(a.completeMilestones()) return cmd } @@ -378,15 +378,3 @@ func findMilestone(cmd *cobra.Command, s services.Milestones, raw string, byName } return s.FindOne(cmd.Context(), raw, byName) } - -// completeMilestoneArgs completes milestone titles for the first argument, or for every argument -// when variadic. -func (a *application) completeMilestoneArgs(variadic bool) cobra.CompletionFunc { - complete := a.completeMilestones() - return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 && !variadic { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return complete(cmd, args, toComplete) - } -} diff --git a/internal/app/milestone_issues.go b/internal/app/milestone_issues.go index cff6f190..ae1550c4 100644 --- a/internal/app/milestone_issues.go +++ b/internal/app/milestone_issues.go @@ -40,7 +40,7 @@ $ tea milestone issue list v1.0 --state all --kind issues`, Annotations: map[str enumFlag(list, &opts.State, "state", "s", "open", "Filter by state", "open", "closed", "all") enumFlag(list, &opts.Kind, "kind", "", "all", "List issues, pull requests or both", "issues", "pulls", "all") list.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") - list.ValidArgsFunction = a.completeMilestoneArgs(false) + list.ValidArgsFunction = firstArg(a.completeMilestones()) group.AddCommand(list) for _, verb := range []string{"add", "remove"} { var byName bool diff --git a/internal/app/milestone_state.go b/internal/app/milestone_state.go index f3a6e700..48a009cd 100644 --- a/internal/app/milestone_state.go +++ b/internal/app/milestone_state.go @@ -54,6 +54,6 @@ $ tea milestone %[1]s v1.0 v1.1`, verb), Annotations: map[string]string{"tea.arg return errors.Join(append(failures, writeBatchJSON(a, cmd, len(selected), results))...) }} cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs") - cmd.ValidArgsFunction = a.completeMilestoneArgs(true) + cmd.ValidArgsFunction = a.completeMilestones() return cmd } diff --git a/internal/app/release_assets.go b/internal/app/release_assets.go index 37a1c88b..d90ad47c 100644 --- a/internal/app/release_assets.go +++ b/internal/app/release_assets.go @@ -154,7 +154,7 @@ $ tea release upload v1.0.0 dist/tea-linux --clobber`, } command.Flags().StringVar(&name, "name", "", "Asset name (default: the file's name)") command.Flags().BoolVar(&clobber, "clobber", false, "Replace assets with the same name") - command.ValidArgsFunction = a.completeReleases() + command.ValidArgsFunction = pathArgs(1, true, completeFiles, a.completeReleases()) return command } diff --git a/internal/app/releases.go b/internal/app/releases.go index 2a7817ca..44160d2b 100644 --- a/internal/app/releases.go +++ b/internal/app/releases.go @@ -307,7 +307,7 @@ $ tea release create v1.1.0-rc1 --target release/1.1 --draft --prerelease --titl command.Flags().StringVar(&fields.tag, "tag", "", "Release tag") _ = command.Flags().MarkHidden("tag") command.Flags().StringSliceVar(&assets, "asset", nil, "File to upload as an asset (repeatable)") - command.ValidArgsFunction = a.completeTags() + command.ValidArgsFunction = pathArgs(1, true, completeFiles, a.completeTags()) return command } diff --git a/internal/app/repo_mutations.go b/internal/app/repo_mutations.go index 48eae56e..83d1dc81 100644 --- a/internal/app/repo_mutations.go +++ b/internal/app/repo_mutations.go @@ -284,7 +284,7 @@ $ tea repo clone alice/project -- --branch develop --recurse-submodules`, command.Flags().IntVarP(&opts.depth, "depth", "d", 0, "Number of commits to fetch (0: full history)") command.Flags().StringVarP(&opts.upstream, "upstream-remote-name", "u", "upstream", "Remote name for the parent of a fork") command.Flags().BoolVar(&noUpstream, "no-upstream", false, "Do not add a remote for the parent of a fork") - command.ValidArgsFunction = a.completeRepos() + command.ValidArgsFunction = pathArgs(1, false, completeDirectories, a.completeRepos()) return command } diff --git a/internal/app/ssh_keys.go b/internal/app/ssh_keys.go index e39cca5d..6e4113f2 100644 --- a/internal/app/ssh_keys.go +++ b/internal/app/ssh_keys.go @@ -135,7 +135,7 @@ func sshKeySource(source string) string { func (a *application) sshKeyCreateCommand() *cobra.Command { var title string cmd := &cobra.Command{ - Use: "create []", Aliases: []string{"add"}, Short: "Add an SSH public key from a file or stdin", Args: usageArgs(cobra.MaximumNArgs(1)), + Use: "create []", Aliases: []string{"add"}, Short: "Add an SSH public key from a file or stdin", Args: usageArgs(cobra.MaximumNArgs(1)), ValidArgsFunction: completeFiles, Long: `Add an SSH public key to your account. Without a file, or with -, the key is read from stdin. The title defaults to the key's comment, such as alice@laptop, or else to this computer's name.`, diff --git a/internal/app/teams.go b/internal/app/teams.go index 6bfd7e3d..9a6b40ef 100644 --- a/internal/app/teams.go +++ b/internal/app/teams.go @@ -339,12 +339,7 @@ to set single units instead, or to override some of them.`, $ tea team create acme/reviewers --unit repo.code=read,repo.pulls=write # give the team access to every repository of the organization $ tea team create acme ops --permission admin --all-repositories`, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return a.completeOrgs()(cmd, args, toComplete) - }, + ValidArgsFunction: firstArg(a.completeOrgs()), RunE: func(cmd *cobra.Command, args []string) error { org, name, scoped := strings.Cut(args[0], "/") names := 0 diff --git a/internal/app/time.go b/internal/app/time.go index eab87222..259f6990 100644 --- a/internal/app/time.go +++ b/internal/app/time.go @@ -161,7 +161,7 @@ func (a *application) timeCreateCommand() *cobra.Command { return &cobra.Command{Use: "create { | } ", Aliases: []string{"add", "log"}, Short: "Log time spent on an issue", Long: `Log time spent on an issue or pull request. The duration uses h, m and s units, such as 1h25m or "1h 25m".`, Example: `$ tea time create 12 1h25m $ tea time add 12 "1h 30m" -$ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: a.completeIssueArgs("open", false), RunE: func(cmd *cobra.Command, args []string) error { +$ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: firstArg(a.completeIssues("open")), RunE: func(cmd *cobra.Command, args []string) error { seconds, err := parseSpent(args[1]) if err != nil { return err @@ -181,7 +181,7 @@ $ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[str func (a *application) timeDeleteCommand() *cobra.Command { return &cobra.Command{Use: "delete { | } ", Short: "Delete a time record", Example: `# Find the record with tea time list --issue 12 -$ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: a.completeIssueArgs("all", false), RunE: func(cmd *cobra.Command, args []string) error { +$ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: firstArg(a.completeIssues("all")), RunE: func(cmd *cobra.Command, args []string) error { id, err := number(args[1]) if err != nil { return err @@ -201,7 +201,7 @@ $ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete } func (a *application) timeResetCommand() *cobra.Command { - return &cobra.Command{Use: "reset { | }", Short: "Delete all time logged on an issue", Example: "$ tea time reset 12 --yes", Annotations: map[string]string{"tea.text": "time reset does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(1), ValidArgsFunction: a.completeIssueArgs("all", false), RunE: func(cmd *cobra.Command, args []string) error { + return &cobra.Command{Use: "reset { | }", Short: "Delete all time logged on an issue", Example: "$ tea time reset 12 --yes", Annotations: map[string]string{"tea.text": "time reset does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(1), ValidArgsFunction: firstArg(a.completeIssues("all")), RunE: func(cmd *cobra.Command, args []string) error { target, n, err := a.resolveRef(cmd, args[0], issueRef) if err != nil { return err