From 5dcc605b7f0c1de6d1790074f66a34610643be1a Mon Sep 17 00:00:00 2001 From: Marcus Ramberg Date: Wed, 4 Feb 2026 22:20:22 +0100 Subject: [PATCH] feat: ShellComplete for fish --- fish.go | 30 ++++++++++++++++++++++++++++++ fish_test.go | 21 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/fish.go b/fish.go index 1607f55b13..52b2f2db0c 100644 --- a/fish.go +++ b/fish.go @@ -34,6 +34,17 @@ func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error { // Add global flags completions := prepareFishFlags(cmd.Name, cmd) + if cmd.ShellComplete != nil { + var completion strings.Builder + fmt.Fprintf(&completion, + "complete -c %s -n '%s' -xa '(%s --generate-shell-completion 2>/dev/null)'", + cmd.Name, + fishFlagHelper(cmd.Name, cmd), + cmd.Name, + ) + completions = append(completions, completion.String()) + } + // Add commands and their flags completions = append( completions, @@ -72,6 +83,25 @@ func prepareFishCommands(binary string, parent *Command) []string { } completions = append(completions, completion.String()) } + + if command.ShellComplete != nil { + var completion strings.Builder + var path []string + lineage := command.Lineage() + for i := len(lineage) - 2; i >= 0; i-- { + path = append(path, lineage[i].Name) + } + + fmt.Fprintf(&completion, + "complete -c %s -n '%s' -xa '(%s %s --generate-shell-completion 2>/dev/null)'", + binary, + fishFlagHelper(binary, command), + binary, + strings.Join(path, " "), + ) + completions = append(completions, completion.String()) + } + completions = append( completions, prepareFishFlags(binary, command)..., diff --git a/fish_test.go b/fish_test.go index 492b516b75..ae946d8ce3 100644 --- a/fish_test.go +++ b/fish_test.go @@ -1,6 +1,7 @@ package cli import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -38,3 +39,23 @@ func TestFishCompletion(t *testing.T) { require.NoError(t, err) expectFileContent(t, "testdata/expected-fish-full.fish", res) } + +func TestFishCompletionShellComplete(t *testing.T) { + cmd := buildExtendedTestCommand() + cmd.ShellComplete = func(context.Context, *Command) {} + + configCmd := cmd.Command("config") + configCmd.ShellComplete = func(context.Context, *Command) {} + + subConfigCmd := configCmd.Command("sub-config") + subConfigCmd.ShellComplete = func(context.Context, *Command) {} + + cmd.setupCommandGraph() + + res, err := cmd.ToFishCompletion() + require.NoError(t, err) + + assert.Contains(t, res, "complete -c greet -n '__fish_greet_no_subcommand' -xa '(greet --generate-shell-completion 2>/dev/null)'") + assert.Contains(t, res, "complete -c greet -n '__fish_seen_subcommand_from config c' -xa '(greet config --generate-shell-completion 2>/dev/null)'") + assert.Contains(t, res, "complete -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss' -xa '(greet config sub-config --generate-shell-completion 2>/dev/null)'") +}