diff --git a/cmd/completion.go b/cmd/completion.go index 8f850c1..1e5f0f3 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -2,6 +2,9 @@ package cmd import ( "fmt" + "io" + "os" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -10,13 +13,21 @@ import ( const shellPowerShell = "powershell" func newCompletionCmd() *cobra.Command { - return &cobra.Command{ + var output string + + cmd := &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate shell completion scripts", Args: cobra.ExactArgs(1), ValidArgs: []string{"bash", "zsh", "fish", shellPowerShell}, RunE: func(cmd *cobra.Command, args []string) error { - out := cmd.OutOrStdout() + out, closeOut, err := completionOutput(cmd, output) + if err != nil { + return err + } + if closeOut != nil { + defer func() { _ = closeOut() }() + } root := cmd.Root() switch shell := strings.ToLower(args[0]); shell { @@ -33,4 +44,23 @@ func newCompletionCmd() *cobra.Command { } }, } + cmd.Flags().StringVarP(&output, "output", "o", "", "Write the completion script to a file") + return cmd +} + +func completionOutput(cmd *cobra.Command, output string) (io.Writer, func() error, error) { + if strings.TrimSpace(output) == "" { + return cmd.OutOrStdout(), nil, nil + } + dir := filepath.Dir(output) + if dir != "." && dir != "" { + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, nil, fmt.Errorf("create completion output directory: %w", err) + } + } + f, err := os.Create(output) + if err != nil { + return nil, nil, fmt.Errorf("create completion output file: %w", err) + } + return f, f.Close, nil } diff --git a/cmd/completion_test.go b/cmd/completion_test.go index e01613b..adea330 100644 --- a/cmd/completion_test.go +++ b/cmd/completion_test.go @@ -2,6 +2,8 @@ package cmd import ( "bytes" + "os" + "path/filepath" "testing" "github.com/spf13/cobra" @@ -24,6 +26,25 @@ func TestCompletionCmd_GeneratesPowerShell(t *testing.T) { assert.Contains(t, out.String(), "Register-ArgumentCompleter") } +func TestCompletionCmd_WritesOutputFile(t *testing.T) { + root := &cobra.Command{Use: "app"} + root.AddCommand(newCompletionCmd()) + outPath := filepath.Join(t.TempDir(), "nested", "app.ps1") + + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"completion", "powershell", "--output", outPath}) + + err := root.Execute() + + require.NoError(t, err) + assert.Empty(t, out.String()) + data, err := os.ReadFile(outPath) + require.NoError(t, err) + assert.Contains(t, string(data), "Register-ArgumentCompleter") +} + func TestCompletionCmd_RejectsUnsupportedShell(t *testing.T) { root := &cobra.Command{Use: "app"} root.AddCommand(newCompletionCmd())