Files
vpp-maglev/cmd/client/shell.go
Pim van Pelt bc6ccaa844 v1.0.0 — first release
Bump VERSION to 1.0.0 and cut the first tagged release of vpp-maglev.

Also in this commit:

- maglevc: MAGLEV_SERVER env var as an alternative to the --server
  flag, matching the MAGLEV_CONFIG / MAGLEV_GRPC_ADDR convention on
  the other binaries. The flag takes precedence when both are set.
- Rename cmd/maglevd -> cmd/server and cmd/maglevc -> cmd/client so
  the source directory names are decoupled from binary names (the
  frontend and tester commands already followed this convention).
  Build outputs and the Debian packages are unchanged.
2026-04-15 15:29:31 +02:00

124 lines
3.1 KiB
Go

// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
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)
}
}
}