Skip to content

Commit 7dcbcd6

Browse files
authored
Merge pull request #81 from ethpandaops/feat/shell-completion
feat: add shell completion for bash, zsh, fish, and powershell
2 parents f350082 + 8803b64 commit 7dcbcd6

File tree

11 files changed

+206
-12
lines changed

11 files changed

+206
-12
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ make build # Build from source
2626
make install # Install globally (optional)
2727
```
2828

29+
## Shell Completion
30+
31+
Enable tab completion for commands, services, and arguments:
32+
33+
```bash
34+
# Bash
35+
source <(xcli completion bash)
36+
37+
# To load completions for each session (Linux):
38+
xcli completion bash > /etc/bash_completion.d/xcli
39+
40+
# To load completions for each session (macOS):
41+
xcli completion bash > $(brew --prefix)/etc/bash_completion.d/xcli
42+
43+
# Zsh
44+
echo "autoload -U compinit; compinit" >> ~/.zshrc # Enable completion (once)
45+
xcli completion zsh > "${fpath[1]}/_xcli"
46+
47+
# Fish
48+
xcli completion fish | source
49+
50+
# To load completions for each session:
51+
xcli completion fish > ~/.config/fish/completions/xcli.fish
52+
```
53+
2954
## Quick Start
3055

3156
```bash

cmd/xcli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func main() {
9898
// Add root-level commands
9999
rootCmd.AddCommand(commands.NewInitCommand(log, configPath))
100100
rootCmd.AddCommand(commands.NewConfigCommand(log, configPath))
101+
rootCmd.AddCommand(commands.NewCompletionCommand())
101102

102103
// Add stack commands
103104
rootCmd.AddCommand(commands.NewLabCommand(log, configPath))

pkg/commands/completion.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// NewCompletionCommand creates the completion command for generating shell completion scripts.
11+
func NewCompletionCommand() *cobra.Command {
12+
return &cobra.Command{
13+
Use: "completion [bash|zsh|fish|powershell]",
14+
Short: "Generate shell completion scripts",
15+
Long: fmt.Sprintf(`Generate shell completion scripts for xcli.
16+
17+
To load completions:
18+
19+
Bash:
20+
$ source <(%[1]s completion bash)
21+
22+
# To load completions for each session, execute once:
23+
# Linux:
24+
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
25+
# macOS:
26+
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
27+
28+
Zsh:
29+
# If shell completion is not already enabled in your environment,
30+
# you will need to enable it. You can execute the following once:
31+
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
32+
33+
# To load completions for each session, execute once:
34+
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
35+
36+
# You will need to start a new shell for this setup to take effect.
37+
38+
Fish:
39+
$ %[1]s completion fish | source
40+
41+
# To load completions for each session, execute once:
42+
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
43+
44+
PowerShell:
45+
PS> %[1]s completion powershell | Out-String | Invoke-Expression
46+
47+
# To load completions for every new session, run:
48+
PS> %[1]s completion powershell > %[1]s.ps1
49+
# and source this file from your PowerShell profile.
50+
`, "xcli"),
51+
DisableFlagsInUseLine: true,
52+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
53+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
54+
RunE: func(cmd *cobra.Command, args []string) error {
55+
switch args[0] {
56+
case "bash":
57+
return cmd.Root().GenBashCompletion(os.Stdout)
58+
case "zsh":
59+
return cmd.Root().GenZshCompletion(os.Stdout)
60+
case "fish":
61+
return cmd.Root().GenFishCompletion(os.Stdout, true)
62+
case "powershell":
63+
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
64+
default:
65+
return fmt.Errorf("unsupported shell: %s", args[0])
66+
}
67+
},
68+
}
69+
}

pkg/commands/completion_helpers.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package commands
2+
3+
import (
4+
"io"
5+
6+
"github.com/ethpandaops/xcli/pkg/config"
7+
"github.com/ethpandaops/xcli/pkg/constants"
8+
"github.com/ethpandaops/xcli/pkg/orchestrator"
9+
"github.com/sirupsen/logrus"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// completeServices returns a ValidArgsFunction that completes service names.
14+
// It loads the config and creates an orchestrator to get the dynamic service list.
15+
func completeServices(configPath string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
16+
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
17+
// Don't complete if we already have an argument (for single-arg commands)
18+
if len(args) > 0 {
19+
return nil, cobra.ShellCompDirectiveNoFileComp
20+
}
21+
22+
// Create a silent logger for completion (no output)
23+
log := logrus.New()
24+
log.SetOutput(io.Discard)
25+
26+
// Try to load config - fail gracefully
27+
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
28+
if err != nil {
29+
return nil, cobra.ShellCompDirectiveNoFileComp
30+
}
31+
32+
// Try to create orchestrator - fail gracefully
33+
orch, err := orchestrator.NewOrchestrator(log, labCfg, cfgPath)
34+
if err != nil {
35+
return nil, cobra.ShellCompDirectiveNoFileComp
36+
}
37+
38+
return orch.GetValidServices(), cobra.ShellCompDirectiveNoFileComp
39+
}
40+
}
41+
42+
// completeModes returns a ValidArgsFunction that completes mode values.
43+
func completeModes() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
44+
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
45+
if len(args) > 0 {
46+
return nil, cobra.ShellCompDirectiveNoFileComp
47+
}
48+
49+
return []string{constants.ModeLocal, constants.ModeHybrid}, cobra.ShellCompDirectiveNoFileComp
50+
}
51+
}
52+
53+
// completeRebuildProjects returns a ValidArgsFunction that completes rebuild project names.
54+
func completeRebuildProjects() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
55+
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
56+
if len(args) > 0 {
57+
return nil, cobra.ShellCompDirectiveNoFileComp
58+
}
59+
60+
return []string{
61+
"xatu-cbt",
62+
"all",
63+
"cbt",
64+
"cbt-api",
65+
"lab-backend",
66+
"lab-frontend",
67+
"prometheus",
68+
"grafana",
69+
}, cobra.ShellCompDirectiveNoFileComp
70+
}
71+
}
72+
73+
// completeReleasableProjects returns a ValidArgsFunction that completes releasable project names.
74+
// Supports multiple arguments since release accepts multiple projects.
75+
func completeReleasableProjects() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
76+
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
77+
// Filter out already-provided arguments
78+
alreadyUsed := make(map[string]bool, len(args))
79+
for _, arg := range args {
80+
alreadyUsed[arg] = true
81+
}
82+
83+
completions := make([]string, 0, len(constants.ReleasableProjects))
84+
for _, project := range constants.ReleasableProjects {
85+
if !alreadyUsed[project] {
86+
completions = append(completions, project)
87+
}
88+
}
89+
90+
return completions, cobra.ShellCompDirectiveNoFileComp
91+
}
92+
}

pkg/commands/lab_logs.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ func NewLabLogsCommand(log logrus.FieldLogger, configPath string) *cobra.Command
1414
var follow bool
1515

1616
cmd := &cobra.Command{
17-
Use: "logs [service]",
18-
Short: "Show lab service logs",
19-
Long: `Show logs for all lab services or a specific service.`,
17+
Use: "logs [service]",
18+
Short: "Show lab service logs",
19+
Long: `Show logs for all lab services or a specific service.`,
20+
ValidArgsFunction: completeServices(configPath),
2021
RunE: func(cmd *cobra.Command, args []string) error {
2122
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
2223
if err != nil {

pkg/commands/lab_mode.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ After switching modes, restart the stack for changes to take effect:
3939
Examples:
4040
xcli lab mode local # Switch to fully local mode
4141
xcli lab mode hybrid # Switch to hybrid mode (requires external ClickHouse config)`,
42-
Args: cobra.ExactArgs(1),
42+
Args: cobra.ExactArgs(1),
43+
ValidArgsFunction: completeModes(),
4344
RunE: func(cmd *cobra.Command, args []string) error {
4445
mode := args[0]
4546
if mode != constants.ModeLocal && mode != constants.ModeHybrid {

pkg/commands/lab_rebuild.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ Examples:
7070
xcli lab rebuild grafana # Reload custom dashboards
7171
7272
Note: All rebuild commands automatically restart their respective services if running.`,
73-
Args: cobra.ExactArgs(1),
73+
Args: cobra.ExactArgs(1),
74+
ValidArgsFunction: completeRebuildProjects(),
7475
RunE: func(cmd *cobra.Command, args []string) error {
7576
project := args[0]
7677

pkg/commands/lab_release.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ Options:
6565
xcli lab release xatu-cbt --no-deps # Skip dependency prompts
6666
6767
Note: Requires GitHub CLI (gh) to be installed and authenticated.`,
68-
Args: cobra.ArbitraryArgs,
68+
Args: cobra.ArbitraryArgs,
69+
ValidArgsFunction: completeReleasableProjects(),
6970
RunE: func(cmd *cobra.Command, args []string) error {
7071
return runRelease(cmd.Context(), log, args, stackFlag, bumpFlag,
7172
yesFlag, !noWatchFlag, noDepsFlag, timeoutFlag)

pkg/commands/lab_restart.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313
// NewLabRestartCommand creates the lab restart command.
1414
func NewLabRestartCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
1515
return &cobra.Command{
16-
Use: "restart <service>",
17-
Short: "Restart a lab service",
18-
Long: `Restart a specific lab service.`,
19-
Args: cobra.ExactArgs(1),
16+
Use: "restart <service>",
17+
Short: "Restart a lab service",
18+
Long: `Restart a specific lab service.`,
19+
Args: cobra.ExactArgs(1),
20+
ValidArgsFunction: completeServices(configPath),
2021
RunE: func(cmd *cobra.Command, args []string) error {
2122
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
2223
if err != nil {

pkg/commands/lab_start.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Available services:
2626
Example:
2727
xcli lab start lab-backend
2828
xcli lab start cbt-mainnet`,
29-
Args: cobra.ExactArgs(1),
29+
Args: cobra.ExactArgs(1),
30+
ValidArgsFunction: completeServices(configPath),
3031
RunE: func(cmd *cobra.Command, args []string) error {
3132
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
3233
if err != nil {

0 commit comments

Comments
 (0)