Writing a simple REPL in Go

In this post I wanted to share a simple read-eval-print-loop (REPL) that I wrote as part of following cstacks’ clone of sqlite. I’d done this once in C++, but this time I was writing it in Go. What inspired me to write this post was my amazement at how effortless the whole process was. In less than an hour, we had a simple REPL set up, and as Go code tends to be, the code itself is highly readable.


First, some backstory: this past semester, I spent many hours working in Go to write distributed systems such a distributed file system, an implementation of Raft (a consensus algorithm), and Tapestry (a distributed hash table). What I’ve found is that Go is just unreasonably fun to work with. It’s normal to come up against some pain points when learning a new language, and while there were some of these, the overall process was smooth and I found myself looking forward to working in Go every day.

So at the end of the semester, I bought a copy of The Go Programming Language and decided that the planned revamp of CS127 Database Management Systems would be done in Go instead of Java.


Let’s start off by taking a look at the standard library packages we’ll be importing.

package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

There’s nothing very fancy we need here. In order, we have a package for buffered input/output, one for formatted input/output, two packages to interface with the operating system, and a string manipulation package.

We then write some functions to print common prompts. One for the regular prompt, and one in response to unknown commands.

// cliName is the name used in the repl prompts
var cliName string = "simpleREPL"

// printPrompt displays the repl prompt at the start of each loop
func printPrompt() {
	fmt.Print(cliName, "> ")
}

// printUnkown informs the user about invalid commands
func printUnknown(text string) {
	fmt.Println(text, ": command not found")
}

We can add in some hard-coded meta functions as well such as help and clear.

// displayHelp informs the user about our hardcoded functions
func displayHelp() {
	fmt.Printf(
		"Welcome to %v! These are the available commands: \n",
		cliName,
	)
	fmt.Println(".help    - Show available commands")
	fmt.Println(".clear   - Clear the terminal screen")
	fmt.Println(".exit    - Closes your connection to ", dbName)
}

// clearScreen clears the terminal screen
func clearScreen() {
	cmd := exec.Command("clear")
	cmd.Stdout = os.Stdout
	cmd.Run()
}

Normally a REPL might have to do some parsing, so we set up some helper functions to handle well-formed commands, and invalid commands. There’s nothing much here because this is a simple REPL, so we don’t actually support anything besides the hard-coded meta-commands for now.

// handleInvalidCmd attempts to recover from a bad command
func handleInvalidCmd(text string) {
	defer printUnknown(text)
}

// handleCmd parses the given commands
func handleCmd(text string) {
	handleInvalidCmd(text)
}

We might consider writing a helper function to clean up input. This removes all leading and trailing whitespace from the input, and converts everything to lowercase.

// cleanInput preprocesses input to the db repl
func cleanInput(text string) string {
	output := strings.TrimSpace(text)
	output = strings.ToLower(output)
	return output
}

And finally, we get to the main function where we set up a map from meta-commands to their respective helper functions, start scanning for standard input, and create the read-eval-print-loop.

func main() {
	// Hardcoded repl commands
	commands := map[string]interface{}{
		".help":  displayHelp,
		".clear": clearScreen,
	}
	// Begin the repl loop
	reader := bufio.NewScanner(os.Stdin)
	printPrompt()
	for reader.Scan() {
		text := cleanInput(reader.Text())
		if command, exists := commands[text]; exists {
			// Call a hardcoded function
			command.(func())()
		} else if strings.EqualFold(".exit", text) {
			// Close the program on the exit command
			return
		} else {
			// Pass the command to the parser
			handleCmd(text)
		}
		printPrompt()
	}
	// Print an additional line if we encountered an EOF character
	fmt.Println()
}

And that’s it, that’s all there is to it. No head-scratching about setting up input/output buffers, no suffering needed to map commands to helper functions. Just straight-forward no-nonsense code. Refreshing, isn’t it?

Here it is in its 84-line entirety:

package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

// dbName is the name used in the repl prompts
var cliName string = "simpleREPL"

// printPrompt displays the repl prompt at the start of each loop
func printPrompt() {
	fmt.Print(cliName, "> ")
}

// printUnkown informs the user about invalid commands
func printUnknown(text string) {
	fmt.Println(text, ": command not found")
}

// displayHelp informs the user about our hardcoded functions
func displayHelp() {
	fmt.Printf(
		"Welcome to %v! These are the available commands: \n",
		cliName,
	)
	fmt.Println(".help    - Show available commands")
	fmt.Println(".clear   - Clear the terminal screen")
	fmt.Println(".exit    - Closes your connection to ", dbName)
}

// clearScreen clears the terminal screen
func clearScreen() {
	cmd := exec.Command("clear")
	cmd.Stdout = os.Stdout
	cmd.Run()
}

// handleInvalidCmd attempts to recover from a bad command
func handleInvalidCmd(text string) {
	defer printUnknown(text)
}

// handleCmd parses the given commands
func handleCmd(text string) {
	handleInvalidCmd(text)
}

// cleanInput preprocesses input to the db repl
func cleanInput(text string) string {
	output := strings.TrimSpace(text)
	output = strings.ToLower(output)
	return output
}

func main() {
	// Hardcoded repl commands
	commands := map[string]interface{}{
		".help":  displayHelp,
		".clear": clearScreen,
	}
	// Begin the repl loop
	reader := bufio.NewScanner(os.Stdin)
	printPrompt()
	for reader.Scan() {
		text := cleanInput(reader.Text())
		if command, exists := commands[text]; exists {
			// Call a hardcoded function
			command.(func())()
		} else if strings.EqualFold(".exit", text) {
			// Close the program on the exit command
			return
		} else {
			// Pass the command to the parser
			handleCmd(text)
		}
		printPrompt()
	}
	// Print an additional line if we encountered an EOF character
	fmt.Println()
}

Leave a Reply

Your email address will not be published. Required fields are marked *