124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
|
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
|
)
|
|
|
|
// errQuit is a sentinel returned by runQuit to exit the REPL.
|
|
var errQuit = errors.New("quit")
|
|
|
|
// runShell runs the interactive REPL until the user types quit/exit or EOF.
|
|
func runShell(ctx context.Context, client grpcapi.MaglevClient) error {
|
|
root := buildTree()
|
|
|
|
comp := &Completer{root: root, client: client}
|
|
ql := &questionListener{root: root, client: client}
|
|
|
|
cfg := &readline.Config{
|
|
Prompt: "maglev> ",
|
|
AutoComplete: comp,
|
|
InterruptPrompt: "^C",
|
|
EOFPrompt: "exit",
|
|
Listener: ql,
|
|
}
|
|
rl, err := readline.NewEx(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("readline init: %w", err)
|
|
}
|
|
ql.rl = rl
|
|
defer func() { _ = rl.Close() }()
|
|
|
|
for {
|
|
line, err := rl.Readline()
|
|
if err == readline.ErrInterrupt {
|
|
continue
|
|
}
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokens := splitTokens(line)
|
|
if len(tokens) == 0 {
|
|
continue
|
|
}
|
|
|
|
if err := dispatch(ctx, root, client, tokens); err != nil {
|
|
if errors.Is(err, errQuit) {
|
|
return nil
|
|
}
|
|
_, _ = fmt.Fprintf(rl.Stderr(), "%s\n", formatError(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// dispatch walks the tree and executes the matched command.
|
|
func dispatch(ctx context.Context, root *Node, client grpcapi.MaglevClient, tokens []string) error {
|
|
node, args, remaining := Walk(root, tokens)
|
|
|
|
if len(remaining) > 0 {
|
|
// One or more tokens couldn't be matched. Report the first
|
|
// offending token with the consumed prefix for context; don't
|
|
// dump the full command tree prefixed with garbage, which is
|
|
// what the previous code did and what prompted this fix.
|
|
consumed := tokens[:len(tokens)-len(remaining)]
|
|
return unknownCommandError(consumed, remaining[0])
|
|
}
|
|
|
|
if node.Run == nil {
|
|
showHelpAt(node, strings.Join(tokens, " "))
|
|
return nil
|
|
}
|
|
|
|
return node.Run(ctx, client, args)
|
|
}
|
|
|
|
// unknownCommandError builds the error returned by dispatch when the
|
|
// tree walk couldn't consume the full token list. The format differs
|
|
// slightly depending on whether any tokens were consumed, so the
|
|
// message always points at the first unknown token and its context.
|
|
func unknownCommandError(consumed []string, bad string) error {
|
|
if len(consumed) == 0 {
|
|
return fmt.Errorf("unknown command: %s", bad)
|
|
}
|
|
return fmt.Errorf("unknown subcommand %q after %q", bad, strings.Join(consumed, " "))
|
|
}
|
|
|
|
// showHelpAt prints the reachable leaves below node, each displayed
|
|
// with the given prefix. Split from dispatch so the caller can decide
|
|
// which node to anchor the help at without re-walking the tree.
|
|
func showHelpAt(node *Node, prefix string) {
|
|
lines := expandPaths(node, prefix, make(map[*Node]bool))
|
|
|
|
maxLen := 0
|
|
for _, l := range lines {
|
|
if len(l.path) > maxLen {
|
|
maxLen = len(l.path)
|
|
}
|
|
}
|
|
|
|
if len(lines) == 0 {
|
|
fmt.Println(" <no completions>")
|
|
return
|
|
}
|
|
for _, l := range lines {
|
|
if l.help != "" {
|
|
fmt.Printf("%-*s %s\n", maxLen+2, l.path, l.help)
|
|
} else {
|
|
fmt.Printf("%s\n", l.path)
|
|
}
|
|
}
|
|
}
|