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
15 changes: 14 additions & 1 deletion completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,24 @@ func (rl *Shell) startMenuComplete(completer completion.Completer) {
}

// commandCompletion generates the completions for commands/args/flags.
func (rl *Shell) commandCompletion() completion.Values {
//
// The application-provided completer is user code that readline calls on nearly
// every keystroke (menu completion and as-you-type autocomplete both funnel
// through here). A panic in it — a bad completer or transient command-tree
// state — is recovered and surfaced as a completion message, so a single faulty
// completion degrades into a visible error instead of crashing the shell.
func (rl *Shell) commandCompletion() (values completion.Values) {
if rl.Completer == nil {
return completion.Values{}
}

defer func() {
if r := recover(); r != nil {
msg := CompleteMessage("completion error: %v", r)
values = msg.convert()
}
}()

line, cursor := rl.completer.Line()
comps := rl.Completer(*line, cursor.Pos())

Expand Down
54 changes: 54 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package readline

import (
"strings"
"testing"
)

// TestCommandCompletionRecoversFromPanic ensures a panic in the
// application-provided completer is recovered and surfaced as a completion
// message instead of propagating and crashing the shell. readline calls the
// completer on nearly every keystroke, so a single bad completer or transient
// state must not take down the process.
func TestCommandCompletionRecoversFromPanic(t *testing.T) {
rl := NewShell()
rl.Completer = func([]rune, int) Completions {
panic("boom")
}

defer func() {
if r := recover(); r != nil {
t.Fatalf("commandCompletion did not recover, panicked with: %v", r)
}
}()

values := rl.commandCompletion()

if values.Messages.IsEmpty() {
t.Fatal("recovered completion produced no message, want a completion error message")
}

var found bool
for _, msg := range values.Messages.Get() {
if strings.Contains(msg, "completion error") && strings.Contains(msg, "boom") {
found = true
}
}

if !found {
t.Fatalf("recovered messages = %v, want one containing the panic value", values.Messages.Get())
}
}

// TestCommandCompletionNilCompleter verifies the no-completer case stays a clean
// no-op (empty values, no message).
func TestCommandCompletionNilCompleter(t *testing.T) {
rl := NewShell()
rl.Completer = nil

values := rl.commandCompletion()

if !values.Messages.IsEmpty() {
t.Fatalf("nil completer produced messages %v, want none", values.Messages.Get())
}
}
Loading