diff --git a/cmd/molecule/molecule_test.go b/cmd/molecule/molecule_test.go index 9015df9..d51afb8 100644 --- a/cmd/molecule/molecule_test.go +++ b/cmd/molecule/molecule_test.go @@ -793,3 +793,71 @@ func TestCLI_Init_AlreadyExists(t *testing.T) { t.Errorf("expected non-zero exit code when molecule.yaml exists, got 0") } } + +func TestCLI_Completion_Help(t *testing.T) { + exe := mol(t) + root := repoRoot() + for _, shell := range []string{"bash", "zsh", "fish", "powershell"} { + t.Run(shell+"-help", func(t *testing.T) { + cmd := exec.Command(exe, "completion", shell, "--help") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Dir = root + err := cmd.Run() + if err != nil { + t.Fatalf("molecule completion %s --help: %v\nstderr: %s", shell, err, stderr.String()) + } + out := stdout.String() + if out == "" { + t.Errorf("empty stdout for molecule completion %s --help", shell) + } + }) + } +} + +func TestCLI_Completion_GeneratesScript(t *testing.T) { + exe := mol(t) + root := repoRoot() + for _, shell := range []string{"bash", "zsh", "fish", "powershell"} { + t.Run(shell, func(t *testing.T) { + cmd := exec.Command(exe, "completion", shell) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Dir = root + err := cmd.Run() + if err != nil { + t.Fatalf("molecule completion %s: %v\nstderr: %s", shell, err, stderr.String()) + } + out := stdout.String() + if out == "" { + t.Errorf("empty completion script for %s", shell) + } + // The script should mention molecule or contain a directive/completion call + if !strings.Contains(out, "molecule") && !strings.Contains(out, "_molecule") { + t.Errorf("completion script for %s does not mention molecule:\n%s", shell, out) + } + }) + } +} + +func TestCLI_Completion_InvalidShell(t *testing.T) { + exe := mol(t) + root := repoRoot() + cmd := exec.Command(exe, "completion", "unsupported-shell") + var stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Dir = root + err := cmd.Run() + if err == nil { + t.Fatalf("expected error for unsupported shell, got none") + } + exitErr, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("expected *exec.ExitError, got %T", err) + } + if exitErr.ExitCode() == 0 { + t.Errorf("expected non-zero exit code for unsupported shell, got 0") + } +} diff --git a/internal/cmd/completion.go b/internal/cmd/completion.go new file mode 100644 index 0000000..c2ce13d --- /dev/null +++ b/internal/cmd/completion.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// completionCmd represents the shell completion subcommand. +// Cobra v1.10+ generates completions for bash, zsh, fish, and PowerShell. +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion scripts", + Long: `Generate shell completion scripts for molecule. + +Cobra automatically generates completions for: + + Bash — completions for bash (~/.bashrc or ~/.bash_completion) + Zsh — completions for zsh (usually ~/.zshrc) + Fish — completions for fish (~/.config/fish/completions) + PowerShell — completions for PowerShell (profile) + +Examples: + + # Bash (add to ~/.bashrc or ~/.bash_completion) + source <(molecule completion bash) + + # Zsh (add to ~/.zshrc) + autoload -U compinit && compinit + autoload -Uz bashcompinit && bashcompinit + source <(molecule completion zsh) + compdef _molecule molecule + + # Fish + molecule completion fish | source + + # PowerShell (add to $PROFILE) + molecule completion powershell | Out-String | Invoke-Expression +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactValidArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return rootCmd.GenBashCompletion(os.Stdout) + case "zsh": + return rootCmd.GenZshCompletion(os.Stdout) + case "fish": + return rootCmd.GenFishCompletion(os.Stdout, true) + case "powershell": + return rootCmd.GenPowerShellCompletionWithDesc(os.Stdout) + } + return nil // unreachable + }, +} + +func init() { + rootCmd.AddCommand(completionCmd) +}