From ce30e98be22bd54f259e680dface9fa511ddc5d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ingo=20B=C3=BCrk?= <ingo+github@buerk.io>
Date: Tue, 15 Nov 2022 16:21:11 +0100
Subject: [PATCH] complete aliases for subcommands

When completing a subcommand, also take its aliases into consideration
instead of only its name.

fixes #1852
---
 completions.go      | 12 ++++++++-
 completions_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 74 insertions(+), 1 deletion(-)

diff --git a/completions.go b/completions.go
index e8a0206d..4e2d77b5 100644
--- a/completions.go
+++ b/completions.go
@@ -424,10 +424,20 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 				// - there are no local, non-persistent flags on the command-line or TraverseChildren is true
 				for _, subCmd := range finalCmd.Commands() {
 					if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
+						directive = ShellCompDirectiveNoFileComp
+
+						// Only ever complete the name OR one of the aliases, no need to offer multiple matching ones
+						// for the same command.
 						if strings.HasPrefix(subCmd.Name(), toComplete) {
 							completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
+						} else {
+							for _, alias := range subCmd.Aliases {
+								if strings.HasPrefix(alias, toComplete) {
+									completions = append(completions, fmt.Sprintf("%s\t%s", alias, subCmd.Short))
+									break
+								}
+							}
 						}
-						directive = ShellCompDirectiveNoFileComp
 					}
 				}
 			}
diff --git a/completions_test.go b/completions_test.go
index abac12e4..af54fac3 100644
--- a/completions_test.go
+++ b/completions_test.go
@@ -2176,6 +2176,69 @@ func TestValidArgsNotValidArgsFunc(t *testing.T) {
 	}
 }
 
+func TestCommandAliasesCompletionInGo(t *testing.T) {
+	rootCmd := &Command{
+		Use: "root",
+		Run: emptyRun,
+	}
+
+	subCmd := &Command{
+		Use:     "sandstone",
+		Aliases: []string{"slate", "pumice", "pegmatite"},
+		Run:     emptyRun,
+	}
+
+	rootCmd.AddCommand(subCmd)
+
+	testcases := []struct {
+		desc               string
+		toComplete         string
+		expectedCompletion string
+	}{
+		{
+			desc:               "command name",
+			toComplete:         "sand",
+			expectedCompletion: "sandstone",
+		},
+		{
+			desc:               "command name if an alias also matches",
+			toComplete:         "s",
+			expectedCompletion: "sandstone",
+		},
+		{
+			desc:               "alias if command name does not match",
+			toComplete:         "sla",
+			expectedCompletion: "slate",
+		},
+		{
+			desc:               "only one alias if multiple match",
+			toComplete:         "p",
+			expectedCompletion: "pumice",
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.desc, func(t *testing.T) {
+			args := append([]string{ShellCompRequestCmd}, tc.toComplete)
+			output, err := executeCommand(rootCmd, args...)
+
+			expectedCompletion := strings.Join([]string{
+				tc.expectedCompletion,
+				":4",
+				"Completion ended with directive: ShellCompDirectiveNoFileComp",
+				"",
+			}, "\n")
+
+			switch {
+			case err == nil && output != expectedCompletion:
+				t.Errorf("expected: %q, got: %q", expectedCompletion, output)
+			case err != nil:
+				t.Errorf("Unexpected error %q", err)
+			}
+		})
+	}
+}
+
 func TestArgAliasesCompletionInGo(t *testing.T) {
 	rootCmd := &Command{
 		Use:        "root",