Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ type Parser struct {
SortFlagsReverse bool // when true with SortFlags, sort order is reversed (Z..A)
}

// supportedCompletionShells lists every shell that can receive generated completion output.
var supportedCompletionShells = []string{"bash", "zsh", "fish", "powershell", "nushell"}

// completionShellList joins the supported completion shell names into a space separated string.
func completionShellList() string {
return strings.Join(supportedCompletionShells, " ")
}

// isSupportedCompletionShell reports whether the provided shell is eligible for generated completions.
func isSupportedCompletionShell(shell string) bool {
for _, supported := range supportedCompletionShells {
if shell == supported {
return true
}
}
return false
}

// TrailingSubcommand returns the last and most specific subcommand invoked.
func (p *Parser) TrailingSubcommand() *Subcommand {
return p.subcommandContext
Expand Down Expand Up @@ -98,19 +116,17 @@ func (p *Parser) ParseArgs(args []string) error {
if len(args) >= 1 && strings.EqualFold(args[0], "completion") {
// no shell provided
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Please specify a shell for completion. Supported shells: bash zsh")
fmt.Fprintf(os.Stderr, "Please specify a shell for completion. Supported shells: %s\n", completionShellList())
exitOrPanic(2)
}

shell := strings.ToLower(args[1])
switch shell {
case "bash", "zsh":
if isSupportedCompletionShell(shell) {
p.Completion(shell)
exitOrPanic(0)
default:
fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: bash zsh\n", args[1])
exitOrPanic(2)
}
fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", args[1], completionShellList())
exitOrPanic(2)
}
}

Expand Down Expand Up @@ -141,13 +157,19 @@ func (p *Parser) ParseArgs(args []string) error {
// Completion takes in a shell type and outputs the completion script for
// that shell.
func (p *Parser) Completion(completionType string) {
switch {
case strings.ToLower(completionType) == "bash":
switch strings.ToLower(completionType) {
case "bash":
fmt.Print(GenerateBashCompletion(p))
case strings.ToLower(completionType) == "zsh":
case "zsh":
fmt.Print(GenerateZshCompletion(p))
case "fish":
fmt.Print(GenerateFishCompletion(p))
case "powershell":
fmt.Print(GeneratePowerShellCompletion(p))
case "nushell":
fmt.Print(GenerateNushellCompletion(p))
default:
fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\n", completionType)
fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", completionType, completionShellList())
}
}

Expand Down
123 changes: 123 additions & 0 deletions parser_completion_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package flaggy_test

import (
"io"
"os"
"strings"
"testing"

"github.com/integrii/flaggy"
)

// runParserWithArgs executes a new parser using the provided os.Args tail while capturing
// stdout, stderr, and the panic payload triggered by exitOrPanic so tests can assert behavior.
func runParserWithArgs(t *testing.T, args []string) (string, string, any) {
t.Helper()

originalArgs := os.Args
os.Args = append([]string{"starfleet"}, args...)
defer func() {
os.Args = originalArgs
}()

parser := flaggy.NewParser("starfleet")

originalStdout := os.Stdout
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stdout pipe: %v", err)
}

originalStderr := os.Stderr
stderrReader, stderrWriter, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stderr pipe: %v", err)
}

os.Stdout = stdoutWriter
os.Stderr = stderrWriter
defer func() {
os.Stdout = originalStdout
os.Stderr = originalStderr
}()

originalPanicSetting := flaggy.PanicInsteadOfExit
flaggy.PanicInsteadOfExit = true
defer func() {
flaggy.PanicInsteadOfExit = originalPanicSetting
}()

var recovered any
func() {
defer func() {
recovered = recover()
}()
_ = parser.Parse()
}()

stdoutWriter.Close()
stderrWriter.Close()

stdoutBytes, err := io.ReadAll(stdoutReader)
if err != nil {
t.Fatalf("failed to read stdout pipe: %v", err)
}
stderrBytes, err := io.ReadAll(stderrReader)
if err != nil {
t.Fatalf("failed to read stderr pipe: %v", err)
}

stdoutReader.Close()
stderrReader.Close()

return string(stdoutBytes), string(stderrBytes), recovered
}

// TestParseCompletionRequiresShell ensures that invoking the completion command without a shell
// prints guidance that lists every supported shell so users know their options.
func TestParseCompletionRequiresShell(t *testing.T) {
_, stderr, recovered := runParserWithArgs(t, []string{"completion"})
if recovered == nil {
t.Fatalf("expected parser to exit when no shell provided")
}
expectedExit := "Panic instead of exit with code: 2"
if recovered != expectedExit {
t.Fatalf("expected exit code 2 when shell missing: %v", recovered)
}
want := "Supported shells: bash zsh fish powershell nushell"
if !strings.Contains(stderr, want) {
t.Fatalf("expected stderr to describe all supported shells: %s", stderr)
}
}

// TestParseCompletionSupportsAllShells confirms that every advertised completion shell runs
// through the parser and emits shell-specific output when provided on the command line.
func TestParseCompletionSupportsAllShells(t *testing.T) {
cases := []struct {
shell string
expected string
}{
{shell: "bash", expected: "# bash completion"},
{shell: "zsh", expected: "#compdef"},
{shell: "fish", expected: "# fish completion"},
{shell: "powershell", expected: "# PowerShell completion"},
{shell: "nushell", expected: "# nushell completion"},
}

for _, tc := range cases {
stdout, stderr, recovered := runParserWithArgs(t, []string{"completion", tc.shell})
if recovered == nil {
t.Fatalf("expected parser to exit after writing %s completion", tc.shell)
}
expectedExit := "Panic instead of exit with code: 0"
if recovered != expectedExit {
t.Fatalf("expected exit code 0 for %s completion: %v", tc.shell, recovered)
}
if len(stderr) > 0 {
t.Fatalf("expected stderr to be empty for %s completion: %s", tc.shell, stderr)
}
if !strings.Contains(stdout, tc.expected) {
t.Fatalf("expected stdout for %s completion to include %q: %s", tc.shell, tc.expected, stdout)
}
}
}