mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-13 14:35:10 -05:00
Add shell completion for beacon-chain and validator CLI (#16245)
**What type of PR is this?**
Feature
**What does this PR do? Why is it needed?**
Introduces a `completion` subcommand to `beacon-chain` and `validator`
that outputs shell completion scripts. Supports Bash, Zsh, and Fish
shells.
```bash
# Load completions in current session
source <(beacon-chain completion bash)
# Persist for future sessions
beacon-chain completion zsh > "${fpath[1]}/_beacon-chain"
validator completion fish > ~/.config/fish/completions/validator.fish
```
Once loaded, users can press TAB to complete subcommands, nested
commands, and flags. Flag completion supports prefix matching (e.g.,
typing `--exec<TAB>` suggests `--execution-endpoint`,
`--execution-headers`).
**Which issues(s) does this PR fix?**
Fixes #16244
**Other notes for review**
The implementation adds three files to the existing `cmd` package:
- `completion.go` - Defines `CompletionCommand()` returning a
`*cli.Command` with `bash`, `zsh`, `fish` subcommands
- `completion_scripts.go` - Contains the shell script templates
- `completion_test.go` - Unit tests for command structure and script
content
Changes to `beacon-chain` and `validator`:
- Import `cmd.CompletionCommand("binary-name")` in the Commands slice
- Set `EnableBashCompletion: true` on the cli.App to activate
urfave/cli's `--generate-bash-completion` hidden flag
The shell scripts call the binary with `--generate-bash-completion`
appended to get context-aware suggestions. This means completions
automatically reflect the current binary's flags and commands.
**Acknowledgements**
- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
Signed-off-by: Willian Paixao <willian@ufpa.br>
This commit is contained in:
3
changelog/willianpaixao_shell-autocompletion.md
Normal file
3
changelog/willianpaixao_shell-autocompletion.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Added shell completion support for `beacon-chain` and `validator` CLI tools.
|
||||
@@ -3,6 +3,8 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"completion.go",
|
||||
"completion_scripts.go",
|
||||
"config.go",
|
||||
"defaults.go",
|
||||
"flags.go",
|
||||
@@ -28,6 +30,7 @@ go_test(
|
||||
name = "go_default_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"completion_test.go",
|
||||
"config_test.go",
|
||||
"flags_test.go",
|
||||
"helpers_test.go",
|
||||
|
||||
@@ -271,9 +271,11 @@ func main() {
|
||||
Commands: []*cli.Command{
|
||||
dbcommands.Commands,
|
||||
jwtcommands.Commands,
|
||||
cmd.CompletionCommand("beacon-chain"),
|
||||
},
|
||||
Flags: appFlags,
|
||||
Before: before,
|
||||
Flags: appFlags,
|
||||
Before: before,
|
||||
EnableBashCompletion: true,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
63
cmd/completion.go
Normal file
63
cmd/completion.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CompletionCommand returns the completion command for the given binary name.
|
||||
// The binaryName parameter should be "beacon-chain" or "validator".
|
||||
func CompletionCommand(binaryName string) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "completion",
|
||||
Category: "completion",
|
||||
Usage: "Generate shell completion scripts",
|
||||
Description: fmt.Sprintf(`Generate shell completion scripts for bash, zsh, or fish.
|
||||
|
||||
To load completions:
|
||||
|
||||
Bash:
|
||||
$ source <(%[1]s completion bash)
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
|
||||
Zsh:
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
# You may need to start a new shell for completions to take effect.
|
||||
|
||||
Fish:
|
||||
$ %[1]s completion fish | source
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
`, binaryName),
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "bash",
|
||||
Usage: "Generate bash completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(bashCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "zsh",
|
||||
Usage: "Generate zsh completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(zshCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fish",
|
||||
Usage: "Generate fish completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(fishCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
99
cmd/completion_scripts.go
Normal file
99
cmd/completion_scripts.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// bashCompletionScript returns the bash completion script for the given binary.
|
||||
func bashCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for bash function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`#!/bin/bash
|
||||
|
||||
_%[1]s_completions() {
|
||||
local cur prev words cword opts
|
||||
COMPREPLY=()
|
||||
|
||||
# Use bash-completion if available, otherwise set variables directly
|
||||
if declare -F _init_completion >/dev/null 2>&1; then
|
||||
_init_completion -n "=:" || return
|
||||
else
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
words=("${COMP_WORDS[@]}")
|
||||
cword=$COMP_CWORD
|
||||
fi
|
||||
|
||||
# Build command array for completion - flag must be at the END
|
||||
local -a requestComp
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
requestComp=("${COMP_WORDS[@]:0:COMP_CWORD}" "$cur" --generate-bash-completion)
|
||||
else
|
||||
requestComp=("${COMP_WORDS[@]:0:COMP_CWORD}" --generate-bash-completion)
|
||||
fi
|
||||
|
||||
opts=$("${requestComp[@]}" 2>/dev/null)
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "${cur}"))
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _%[1]s_completions %[2]s
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
|
||||
// zshCompletionScript returns the zsh completion script for the given binary.
|
||||
func zshCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for zsh function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`#compdef %[2]s
|
||||
|
||||
_%[1]s() {
|
||||
local curcontext="$curcontext" ret=1
|
||||
local -a completions
|
||||
|
||||
# Build command array with --generate-bash-completion at the END
|
||||
local -a requestComp
|
||||
if [[ "${words[CURRENT]}" == -* ]]; then
|
||||
requestComp=(${words[1,CURRENT]} --generate-bash-completion)
|
||||
else
|
||||
requestComp=(${words[1,CURRENT-1]} --generate-bash-completion)
|
||||
fi
|
||||
|
||||
completions=($("${requestComp[@]}" 2>/dev/null))
|
||||
|
||||
if [[ ${#completions[@]} -gt 0 ]]; then
|
||||
_describe -t commands '%[2]s' completions && ret=0
|
||||
fi
|
||||
|
||||
# Fallback to file completion
|
||||
_files && ret=0
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
compdef _%[1]s %[2]s
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
|
||||
// fishCompletionScript returns the fish completion script for the given binary.
|
||||
func fishCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for fish function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`# Fish completion for %[2]s
|
||||
|
||||
function __fish_%[1]s_complete
|
||||
set -l args (commandline -opc)
|
||||
set -l cur (commandline -ct)
|
||||
|
||||
# Build command with --generate-bash-completion at the END
|
||||
if string match -q -- '-*' "$cur"
|
||||
%[2]s $args $cur --generate-bash-completion 2>/dev/null
|
||||
else
|
||||
%[2]s $args --generate-bash-completion 2>/dev/null
|
||||
end
|
||||
end
|
||||
|
||||
complete -c %[2]s -f -a "(__fish_%[1]s_complete)"
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
105
cmd/completion_test.go
Normal file
105
cmd/completion_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCompletionCommand(t *testing.T) {
|
||||
t.Run("creates command with correct name", func(t *testing.T) {
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
require.Equal(t, "completion", cmd.Name)
|
||||
})
|
||||
|
||||
t.Run("has three subcommands", func(t *testing.T) {
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
require.Equal(t, 3, len(cmd.Subcommands))
|
||||
|
||||
names := make([]string, len(cmd.Subcommands))
|
||||
for i, sub := range cmd.Subcommands {
|
||||
names[i] = sub.Name
|
||||
}
|
||||
assert.DeepEqual(t, []string{"bash", "zsh", "fish"}, names)
|
||||
})
|
||||
|
||||
t.Run("description contains binary name", func(t *testing.T) {
|
||||
cmd := CompletionCommand("validator")
|
||||
assert.Equal(t, true, strings.Contains(cmd.Description, "validator"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBashCompletionScript(t *testing.T) {
|
||||
script := bashCompletionScript("beacon-chain")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "beacon-chain"), "script should contain binary name")
|
||||
assert.Equal(t, true, strings.Contains(script, "_beacon_chain_completions"), "script should contain function name with underscores")
|
||||
assert.Equal(t, true, strings.Contains(script, "complete -o bashdefault"), "script should contain complete command")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestZshCompletionScript(t *testing.T) {
|
||||
script := zshCompletionScript("validator")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "#compdef validator"), "script should contain compdef directive")
|
||||
assert.Equal(t, true, strings.Contains(script, "_validator"), "script should contain function name")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestFishCompletionScript(t *testing.T) {
|
||||
script := fishCompletionScript("beacon-chain")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "complete -c beacon-chain"), "script should contain complete command")
|
||||
assert.Equal(t, true, strings.Contains(script, "__fish_beacon_chain_complete"), "script should contain function name with underscores")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestScriptFunctionNames(t *testing.T) {
|
||||
// Test that hyphens are converted to underscores in function names
|
||||
bashScript := bashCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(bashScript, "_beacon_chain_completions"))
|
||||
assert.Equal(t, false, strings.Contains(bashScript, "_beacon-chain_completions"))
|
||||
|
||||
zshScript := zshCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(zshScript, "_beacon_chain"))
|
||||
|
||||
fishScript := fishCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(fishScript, "__fish_beacon_chain_complete"))
|
||||
}
|
||||
|
||||
func TestCompletionSubcommandActions(t *testing.T) {
|
||||
// Test that Action functions execute without errors
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subcommand string
|
||||
}{
|
||||
{"bash action executes", "bash"},
|
||||
{"zsh action executes", "zsh"},
|
||||
{"fish action executes", "fish"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var subCmd *cli.Command
|
||||
for _, sub := range cmd.Subcommands {
|
||||
if sub.Name == tt.subcommand {
|
||||
subCmd = sub
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, subCmd, "subcommand should exist")
|
||||
require.NotNil(t, subCmd.Action, "subcommand should have an action")
|
||||
|
||||
// Action should not return an error; use a real cli.Context
|
||||
app := &cli.App{}
|
||||
ctx := cli.NewContext(app, nil, nil)
|
||||
err := subCmd.Action(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,8 +140,10 @@ func main() {
|
||||
slashingprotectioncommands.Commands,
|
||||
dbcommands.Commands,
|
||||
web.Commands,
|
||||
cmd.CompletionCommand("validator"),
|
||||
},
|
||||
Flags: appFlags,
|
||||
Flags: appFlags,
|
||||
EnableBashCompletion: true,
|
||||
Before: func(ctx *cli.Context) error {
|
||||
// Load flags from config file, if specified.
|
||||
if err := cmd.LoadFlagsFromConfig(ctx, appFlags); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user