repl.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package cmd
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "github.com/urfave/cli/v3"
  9. )
  10. // REPL represents a Read-Eval-Print Loop for interactive CLI usage
  11. type REPL struct {
  12. app *cli.Command
  13. prompt string
  14. history []string
  15. }
  16. // NewREPL creates a new REPL instance
  17. func NewREPL(app *cli.Command) *REPL {
  18. return &REPL{
  19. app: app,
  20. prompt: "arp> ",
  21. }
  22. }
  23. // Run starts the REPL loop
  24. func (r *REPL) Run(ctx context.Context) error {
  25. scanner := bufio.NewScanner(os.Stdin)
  26. fmt.Println("ARP CLI - Interactive Mode")
  27. fmt.Println("Type 'help' for available commands, 'exit' or 'quit' to leave.")
  28. fmt.Println()
  29. for {
  30. // Show prompt
  31. fmt.Print(r.prompt)
  32. // Read line
  33. if !scanner.Scan() {
  34. // EOF (Ctrl+D)
  35. fmt.Println()
  36. break
  37. }
  38. line := strings.TrimSpace(scanner.Text())
  39. if line == "" {
  40. continue
  41. }
  42. // Check for exit commands
  43. if line == "exit" || line == "quit" || line == "q" {
  44. fmt.Println("Goodbye!")
  45. break
  46. }
  47. // Parse the line into args
  48. args, err := parseArgs(line)
  49. if err != nil {
  50. fmt.Fprintf(os.Stderr, "Error parsing command: %v\n", err)
  51. continue
  52. }
  53. // Prepend the app name to match CLI expectations
  54. args = append([]string{"arp_cli"}, args...)
  55. // Execute the command
  56. if err := r.app.Run(ctx, args); err != nil {
  57. fmt.Fprintf(os.Stderr, "Error: %v\n", err)
  58. }
  59. // Store in history
  60. r.history = append(r.history, line)
  61. }
  62. if err := scanner.Err(); err != nil {
  63. return fmt.Errorf("reading input: %w", err)
  64. }
  65. return nil
  66. }
  67. // parseArgs parses a command line string into arguments
  68. // Handles quoted strings and basic escaping
  69. func parseArgs(line string) ([]string, error) {
  70. var args []string
  71. var current strings.Builder
  72. inQuote := false
  73. quoteChar := rune(0)
  74. for i, ch := range line {
  75. switch {
  76. case ch == '\\' && i+1 < len(line):
  77. // Handle escape sequences
  78. next := rune(line[i+1])
  79. if next == '"' || next == '\'' || next == '\\' || next == ' ' {
  80. current.WriteRune(next)
  81. // Skip the next character
  82. line = line[:i] + line[i+1:]
  83. } else {
  84. current.WriteRune(ch)
  85. }
  86. case (ch == '"' || ch == '\'') && !inQuote:
  87. inQuote = true
  88. quoteChar = ch
  89. case ch == quoteChar && inQuote:
  90. inQuote = false
  91. quoteChar = 0
  92. case ch == ' ' && !inQuote:
  93. if current.Len() > 0 {
  94. args = append(args, current.String())
  95. current.Reset()
  96. }
  97. default:
  98. current.WriteRune(ch)
  99. }
  100. }
  101. // Add the last argument
  102. if current.Len() > 0 {
  103. args = append(args, current.String())
  104. }
  105. return args, nil
  106. }