From 9de83fa9fe6be9a9cac74f1f1e83fd92c010c514 Mon Sep 17 00:00:00 2001 From: Bittrance Date: Mon, 5 May 2025 20:42:39 +0200 Subject: [PATCH 1/2] Fish completion presents only the canonical name of suggested command. --- fish.go | 2 +- testdata/expected-fish-full.fish | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fish.go b/fish.go index 93d4d4aab1..dcfe2087b6 100644 --- a/fish.go +++ b/fish.go @@ -61,7 +61,7 @@ func (cmd *Command) prepareFishCommands(commands []*Command, previousCommands [] "complete -x -c %s -n '%s' -a '%s'", cmd.Name, cmd.fishSubcommandHelper(previousCommands, commands), - strings.Join(command.Names(), " "), + command.Name, ) if command.Usage != "" { diff --git a/testdata/expected-fish-full.fish b/testdata/expected-fish-full.fish index eb29d69b0d..2c07007b6c 100644 --- a/testdata/expected-fish-full.fish +++ b/testdata/expected-fish-full.fish @@ -14,17 +14,17 @@ complete -c greet -n '__fish_greet_no_subcommand' -f -l flag -s fl -s f -r complete -c greet -n '__fish_greet_no_subcommand' -f -l another-flag -s b -d 'another usage text' complete -c greet -n '__fish_greet_no_subcommand' -l logfile -r complete -c greet -n '__fish_greet_no_subcommand' -l foofile -r -complete -x -c greet -n '__fish_greet_no_subcommand' -a 'config c' -d 'another usage test' +complete -x -c greet -n '__fish_greet_no_subcommand' -a 'config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from config c' -l flag -s fl -s f -r complete -c greet -n '__fish_seen_subcommand_from config c' -f -l another-flag -s b -d 'another usage text' -complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss' -a 'sub-config s ss' -d 'another usage test' +complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss' -a 'sub-config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-flag -s sub-fl -s s -r complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-command-flag -s s -d 'some usage text' -complete -x -c greet -n '__fish_greet_no_subcommand' -a 'info i in' -d 'retrieve generic information' +complete -x -c greet -n '__fish_greet_no_subcommand' -a 'info' -d 'retrieve generic information' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'some-command' complete -c greet -n '__fish_seen_subcommand_from hidden-command' -f -l completable -complete -x -c greet -n '__fish_greet_no_subcommand' -a 'usage u' -d 'standard usage text' +complete -x -c greet -n '__fish_greet_no_subcommand' -a 'usage' -d 'standard usage text' complete -c greet -n '__fish_seen_subcommand_from usage u' -l flag -s fl -s f -r complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l another-flag -s b -d 'another usage text' -complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su' -a 'sub-usage su' -d 'standard usage text' +complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su' -a 'sub-usage' -d 'standard usage text' complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l sub-command-flag -s s -d 'some usage text' From 86cbfed30aac073b492f008c4234765cf3703f65 Mon Sep 17 00:00:00 2001 From: Bittrance Date: Mon, 5 May 2025 20:34:17 +0200 Subject: [PATCH 2/2] Fish completion tests now include command setup. This allows the completions to depend on parent-child relations, making several method signatures simpler. --- fish.go | 52 +++++++++++++++----------------- fish_test.go | 1 + testdata/expected-fish-full.fish | 18 +++++++++-- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/fish.go b/fish.go index dcfe2087b6..481b665e85 100644 --- a/fish.go +++ b/fish.go @@ -32,12 +32,12 @@ func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error { } // Add global flags - completions := cmd.prepareFishFlags(cmd.VisibleFlags(), []string{}) + completions := prepareFishFlags(cmd.Name, cmd) // Add commands and their flags completions = append( completions, - cmd.prepareFishCommands(cmd.Commands, []string{})..., + prepareFishCommands(cmd.Name, cmd)..., ) toplevelCommandNames := []string{} @@ -52,15 +52,16 @@ func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error { }) } -func (cmd *Command) prepareFishCommands(commands []*Command, previousCommands []string) []string { +func prepareFishCommands(binary string, parent *Command) []string { + commands := parent.Commands completions := []string{} for _, command := range commands { if !command.Hidden { var completion strings.Builder fmt.Fprintf(&completion, "complete -x -c %s -n '%s' -a '%s'", - cmd.Name, - cmd.fishSubcommandHelper(previousCommands, commands), + binary, + fishSubcommandHelper(binary, parent, commands), command.Name, ) @@ -73,31 +74,28 @@ func (cmd *Command) prepareFishCommands(commands []*Command, previousCommands [] } completions = append( completions, - cmd.prepareFishFlags(command.VisibleFlags(), command.Names())..., + prepareFishFlags(binary, command)..., ) // recursively iterate subcommands - if len(command.Commands) > 0 { - completions = append( - completions, - cmd.prepareFishCommands( - command.Commands, command.Names(), - )..., - ) - } + completions = append( + completions, + prepareFishCommands(binary, command)..., + ) } return completions } -func (cmd *Command) prepareFishFlags(flags []Flag, previousCommands []string) []string { +func prepareFishFlags(binary string, owner *Command) []string { + flags := owner.VisibleFlags() completions := []string{} for _, f := range flags { completion := &strings.Builder{} fmt.Fprintf(completion, "complete -c %s -n '%s'", - cmd.Name, - cmd.fishFlagHelper(previousCommands), + binary, + fishFlagHelper(binary, owner), ) fishAddFileFlag(f, completion) @@ -146,28 +144,28 @@ func fishAddFileFlag(flag Flag, completion *strings.Builder) { completion.WriteString(" -f") } -func (cmd *Command) fishSubcommandHelper(allCommands []string, siblings []*Command) string { - fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", cmd.Name) - if len(allCommands) > 0 { +func fishSubcommandHelper(binary string, command *Command, siblings []*Command) string { + fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary) + if len(command.Lineage()) > 1 { var siblingNames []string - for _, command := range siblings { - siblingNames = append(siblingNames, command.Names()...) + for _, sibling := range siblings { + siblingNames = append(siblingNames, sibling.Names()...) } fishHelper = fmt.Sprintf( "__fish_seen_subcommand_from %s; and not __fish_seen_subcommand_from %s", - strings.Join(allCommands, " "), + strings.Join(command.Names(), " "), strings.Join(siblingNames, " "), ) } return fishHelper } -func (cmd *Command) fishFlagHelper(allCommands []string) string { - fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", cmd.Name) - if len(allCommands) > 0 { +func fishFlagHelper(binary string, command *Command) string { + fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary) + if len(command.Lineage()) > 1 { fishHelper = fmt.Sprintf( "__fish_seen_subcommand_from %s", - strings.Join(allCommands, " "), + strings.Join(command.Names(), " "), ) } return fishHelper diff --git a/fish_test.go b/fish_test.go index 0a5dfb7f55..492b516b75 100644 --- a/fish_test.go +++ b/fish_test.go @@ -19,6 +19,7 @@ func TestFishCompletion(t *testing.T) { Name: "foofile", TakesFile: true, }) + cmd.setupCommandGraph() oldTemplate := FishCompletionTemplate defer func() { FishCompletionTemplate = oldTemplate }() diff --git a/testdata/expected-fish-full.fish b/testdata/expected-fish-full.fish index 2c07007b6c..e02a659efb 100644 --- a/testdata/expected-fish-full.fish +++ b/testdata/expected-fish-full.fish @@ -17,14 +17,28 @@ complete -c greet -n '__fish_greet_no_subcommand' -l foofile -r complete -x -c greet -n '__fish_greet_no_subcommand' -a 'config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from config c' -l flag -s fl -s f -r complete -c greet -n '__fish_seen_subcommand_from config c' -f -l another-flag -s b -d 'another usage text' -complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss' -a 'sub-config' -d 'another usage test' +complete -c greet -n '__fish_seen_subcommand_from config c' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss help h' -a 'sub-config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-flag -s sub-fl -s s -r complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-command-flag -s s -d 'some usage text' +complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from sub-config s ss; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' +complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'info' -d 'retrieve generic information' +complete -c greet -n '__fish_seen_subcommand_from info i in' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from info i in; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'some-command' +complete -c greet -n '__fish_seen_subcommand_from some-command' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from some-command; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -c greet -n '__fish_seen_subcommand_from hidden-command' -f -l completable +complete -c greet -n '__fish_seen_subcommand_from hidden-command' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from hidden-command; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'usage' -d 'standard usage text' complete -c greet -n '__fish_seen_subcommand_from usage u' -l flag -s fl -s f -r complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l another-flag -s b -d 'another usage text' -complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su' -a 'sub-usage' -d 'standard usage text' +complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su help h' -a 'sub-usage' -d 'standard usage text' complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l sub-command-flag -s s -d 'some usage text' +complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l help -s h -d 'show help' +complete -x -c greet -n '__fish_seen_subcommand_from sub-usage su; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' +complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su help h' -a 'help' -d 'Shows a list of commands or help for one command'