feat: add help and exit commands

This commit is contained in:
Stevan Freeborn
2026-07-26 10:54:19 -05:00
parent b43c36ab4d
commit c78ff443a6
7 changed files with 301 additions and 2 deletions
+1
View File
@@ -1 +1,2 @@
bin/ bin/
repl.log
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"fmt"
"os"
"sort"
)
type context struct {
commands map[string]cliCommand
}
func (r *context) register(c cliCommand) {
if c.name != "" {
r.commands[c.name] = c
}
}
func NewContext() *context {
var exitCommand = cliCommand{
name: "exit",
description: "Exit the Pokedex",
callback: commandExit,
}
var helpCommand = cliCommand{
name: "help",
description: "Displays a help message",
callback: commandHelp,
}
c := &context{
commands: map[string]cliCommand{},
}
c.register(exitCommand)
c.register(helpCommand)
return c
}
type commandFunc func(c *context) error
type cliCommand struct {
name string
description string
callback commandFunc
}
func commandExit(c *context) error {
fmt.Println("Closing the Pokedex... Goodbye!")
os.Exit(0)
return nil
}
func commandHelp(c *context) error {
fmt.Println("Welcome to the Pokedex!")
fmt.Println("Usage:")
fmt.Println("")
sortedKeys := []string{}
for k := range c.commands {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
for _, key := range sortedKeys {
command := c.commands[key]
fmt.Printf("%s: %s\n", command.name, command.description)
}
return nil
}
+1
View File
@@ -0,0 +1 @@
package main
+5 -2
View File
@@ -1,7 +1,10 @@
package main package main
import "fmt" import (
"bufio"
"os"
)
func main() { func main() {
fmt.Println("Hello, World!") runREPL(bufio.NewScanner(os.Stdin), os.Stdout, cleanInput)
} }
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"fmt"
"io"
"regexp"
"strings"
)
func cleanInput(input string) []string {
normalized := strings.ToLower(input)
trimmed := strings.TrimSpace(normalized)
re := regexp.MustCompile(`[\s]+`)
words := re.Split(trimmed, -1)
return words
}
type inputScanner interface {
Scan() bool
Err() error
Text() string
}
func runREPL(s inputScanner, w io.Writer, sanitizer func(string) []string) {
context := NewContext()
for {
fmt.Fprint(w, "Pokedex > ")
s.Scan()
err := s.Err()
if err != nil {
fmt.Fprintf(w, "Pokedex > Sorry I didn't get that.\n")
continue
}
input := s.Text()
if input == "" {
break
}
cleanedInput := sanitizer(input)
if len(cleanedInput) < 1 {
fmt.Fprintf(w, "Pokedex > Sorry I couldn't understand that.\n")
continue
}
requestedCommand := cleanedInput[0]
command, exists := context.commands[requestedCommand]
if !exists {
fmt.Fprintln(w, "Unknown command")
continue
}
err = command.callback(context)
if err != nil {
fmt.Fprintf(w, "Pokedex > Error occurred executing command: %s", err)
}
}
}
+151
View File
@@ -0,0 +1,151 @@
package main
import (
"bufio"
"bytes"
"errors"
"slices"
"strings"
"testing"
)
func TestCleanInput(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
expected []string
}{
{
name: "it should split words at whitepspace",
input: "stevan reece freeborn",
expected: []string{"stevan", "reece", "freeborn"},
},
{
name: "it should trim leading and or training whitespace",
input: " hello world ",
expected: []string{"hello", "world"},
},
{
name: "it should normalize words to lowercase",
input: "got to catch them ALL",
expected: []string{"got", "to", "catch", "them", "all"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
result := cleanInput(c.input)
if slices.Equal(result, c.expected) {
return
}
t.Errorf("received %v but expected %v", result, c.expected)
})
}
}
type erroringInputScanner struct {
timesCalled int
}
func (m *erroringInputScanner) Err() error {
if m.timesCalled == 0 {
m.timesCalled += 1
return errors.New("oh no!")
}
return nil
}
func (m *erroringInputScanner) Text() string {
return ""
}
func (m *erroringInputScanner) Scan() bool {
return true
}
func TestRunRepl(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
expected string
getScanner func(*strings.Reader) inputScanner
sanitizer func(string) []string
}{
{
name: "it should print custom prompt",
input: "",
expected: "Pokedex > ",
getScanner: func(r *strings.Reader) inputScanner {
return bufio.NewScanner(r)
},
sanitizer: cleanInput,
},
{
name: "it should print appropriate error message when scan fails",
input: "",
expected: "Pokedex > Sorry I didn't get that.",
getScanner: func(r *strings.Reader) inputScanner {
return &erroringInputScanner{}
},
sanitizer: cleanInput,
},
{
name: "it should print appropriate error message when sanitizer returns no words",
input: "Hi! my command is hello\n",
expected: "Pokedex > Sorry I couldn't understand that.",
getScanner: func(r *strings.Reader) inputScanner {
return bufio.NewScanner(r)
},
sanitizer: func(s string) []string {
return []string{}
},
},
{
name: "it should print expected message for unknown command",
input: "STFU please\n",
expected: "Unknown command",
getScanner: func(r *strings.Reader) inputScanner {
return bufio.NewScanner(r)
},
sanitizer: cleanInput,
},
// TODO: Need to refactor after considering os.exit call
// {
// name: "it should execute command if found",
// input: "EXIT\n",
// expected: "Closing the Pokedex... Goodbye!\n",
// getScanner: func(r *strings.Reader) inputScanner {
// return bufio.NewScanner(r)
// },
// sanitizer: cleanInput,
// },
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
in := strings.NewReader(c.input)
out := new(bytes.Buffer)
runREPL(c.getScanner(in), out, c.sanitizer)
result := out.String()
if strings.Contains(result, c.expected) {
return
}
t.Errorf("expected %q to contain %q", result, c.expected)
})
}
}
Executable
+1
View File
@@ -0,0 +1 @@
go test ./... -v