DBChat: Getting a Toy REPL Going in Golang (Part 2)

Hi there! I'm Shrijith Venkatrama, the founder of Hexmos. Right now, I’m building LiveAPI, a super-convenient tool that simplifies engineering workflows by generating awesome API docs from your code in minutes. In this tutorial series, I am on a journey to build for myself DBChat - a simple tool for using AI chat to explore and evolve databases. See previous posts to get more context: Building DBChat - Explore and Evolve Your DB with Simple Chat (Part 1) Kicking Off The DBChat Project The first step is to start a GoLang project: go mod init dbchat mkdir -p cmd/dbchat pkg touch cmd/dbchat/main.go Then let's create a skeleton cmd/dbchat/main.go like this: package main import ( "fmt" "log" ) func main() { log.Println("Starting dbchat application...") fmt.Println("Welcome to dbchat!") } Also, let's create a convenience Makefile at the project root to build, run and clean go artifacts out of our source: # Binary name BINARY_NAME=dbchat # Build directory BUILD_DIR=build # Go build flags BUILD_FLAGS=-v .PHONY: build run clean # Build the application build: @echo "Building ${BINARY_NAME}..." @mkdir -p ${BUILD_DIR} @go build ${BUILD_FLAGS} -o ${BUILD_DIR}/${BINARY_NAME} ./cmd/dbchat # Run the application run: @go run ./cmd/dbchat # Clean build artifacts clean: @echo "Cleaning..." @rm -rf ${BUILD_DIR} Running dbchat for Testing Purposes make run Building dbchat to Get a Binary Output make build cd build ./dbchat And we get this nice output: 2025/01/10 22:51:37 Starting dbchat... Welcome to dbchat! Implementing a Basic REPL Loop The next thing we want to do is - get a basic REPL going. In simpler words - you can call it an "interactive shell". Usually, in C, et-cetera, the readline library within a loop is how it's achieved. In Golang, I was shopping around for any helper libraries that can save our time from implementing our own REPL from scratch. go-repl Something that looks sort of good for our purposes is go-repl As you can see - it gives us many "goodies" which we are used to in a typical shell environment. Let's plug it in for now, if necessary we will change later. Getting Started with go-repl Turns out the original repo is at: https://github.com/OpenEngineer/go-repl And we find an example in the the README in that repo. So I make a adaption, and turn my main.go to look something like this: package main import ( "fmt" "log" "strings" repl "github.com/openengineer/go-repl" ) const banner = ` ██████╗ ██████╗ ██████╗██╗ ██╗ █████╗ ████████╗ ██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗╚══██╔══╝ ██║ ██║██████╔╝██║ ███████║███████║ ██║ ██║ ██║██╔══██╗██║ ██╔══██║██╔══██║ ██║ ██████╔╝██████╔╝╚██████╗██║ ██║██║ ██║ ██║ ╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ` var helpMessage = `Available commands: help display this message hello get a friendly greeting quit quit DBChat` // implements repl.Handler interface type DBChatHandler struct { r *repl.Repl } func main() { log.Println("Starting dbchat...") fmt.Println(banner) fmt.Println("Welcome to DBChat! Type 'help' for available commands.") h := &DBChatHandler{} h.r = repl.NewRepl(h) // start the terminal loop if err := h.r.Loop(); err != nil { log.Fatal(err) } } func (h *DBChatHandler) Prompt() string { return "dbchat> " } func (h *DBChatHandler) Tab(buffer string) string { return "" // do nothing for now } func (h *DBChatHandler) Eval(line string) string { fields := strings.Fields(line) if len(fields) == 0 { return "" } cmd, _ := fields[0], fields[1:] switch cmd { case "help": return helpMessage case "hello": return "Hello! Welcome to DBChat!

Jan 10, 2025 - 19:22
 0
DBChat: Getting a Toy REPL Going in Golang (Part 2)

Hi there! I'm Shrijith Venkatrama, the founder of Hexmos. Right now, I’m building LiveAPI, a super-convenient tool that simplifies engineering workflows by generating awesome API docs from your code in minutes.

In this tutorial series, I am on a journey to build for myself DBChat - a simple tool for using AI chat to explore and evolve databases.

See previous posts to get more context:

  1. Building DBChat - Explore and Evolve Your DB with Simple Chat (Part 1)

Kicking Off The DBChat Project

The first step is to start a GoLang project:

go mod init dbchat
mkdir -p cmd/dbchat pkg
touch cmd/dbchat/main.go

Then let's create a skeleton cmd/dbchat/main.go like this:

package main

import (
    "fmt"
    "log"
)

func main() {
    log.Println("Starting dbchat application...")
    fmt.Println("Welcome to dbchat!")
}

Also, let's create a convenience Makefile at the project root to build, run and clean go artifacts out of our source:

# Binary name
BINARY_NAME=dbchat

# Build directory
BUILD_DIR=build

# Go build flags
BUILD_FLAGS=-v

.PHONY: build run clean

# Build the application
build:
    @echo "Building ${BINARY_NAME}..."
    @mkdir -p ${BUILD_DIR}
    @go build ${BUILD_FLAGS} -o ${BUILD_DIR}/${BINARY_NAME} ./cmd/dbchat

# Run the application
run:
    @go run ./cmd/dbchat

# Clean build artifacts
clean:
    @echo "Cleaning..."
    @rm -rf ${BUILD_DIR} 

Running dbchat for Testing Purposes

make run

Building dbchat to Get a Binary Output

make build
cd build
./dbchat

And we get this nice output:

2025/01/10 22:51:37 Starting dbchat...
Welcome to dbchat!

Implementing a Basic REPL Loop

The next thing we want to do is - get a basic REPL going. In simpler words - you can call it an "interactive shell".

Usually, in C, et-cetera, the readline library within a loop is how it's achieved.

In Golang, I was shopping around for any helper libraries that can save our time from implementing our own

REPL from scratch.

go-repl

Something that looks sort of good for our purposes is go-repl

bc83f4cf-766b-46a8-a616-1dff76ba91bc

As you can see - it gives us many "goodies" which we are used to in a typical shell environment. Let's plug it in for now,

if necessary we will change later.

Getting Started with go-repl

Turns out the original repo is at: https://github.com/OpenEngineer/go-repl

And we find an example in the the README in that repo.

So I make a adaption, and turn my main.go to look something like this:

package main

import (
    "fmt"
    "log"
    "strings"

    repl "github.com/openengineer/go-repl"
)

const banner = `
██████╗ ██████╗  ██████╗██╗  ██╗ █████╗ ████████╗
██╔══██╗██╔══██╗██╔════╝██║  ██║██╔══██╗╚══██╔══╝
██║  ██║██████╔╝██║     ███████║███████║   ██║   
██║  ██║██╔══██╗██║     ██╔══██║██╔══██║   ██║   
██████╔╝██████╔╝╚██████╗██║  ██║██║  ██║   ██║   
╚═════╝ ╚═════╝  ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   
`

var helpMessage = `Available commands:
help     display this message
hello    get a friendly greeting
quit     quit DBChat`

// implements repl.Handler interface
type DBChatHandler struct {
    r *repl.Repl
}

func main() {
    log.Println("Starting dbchat...")
    fmt.Println(banner)
    fmt.Println("Welcome to DBChat! Type 'help' for available commands.")

    h := &DBChatHandler{}
    h.r = repl.NewRepl(h)

    // start the terminal loop
    if err := h.r.Loop(); err != nil {
        log.Fatal(err)
    }
}

func (h *DBChatHandler) Prompt() string {
    return "dbchat> "
}

func (h *DBChatHandler) Tab(buffer string) string {
    return "" // do nothing for now
}

func (h *DBChatHandler) Eval(line string) string {
    fields := strings.Fields(line)

    if len(fields) == 0 {
        return ""
    }

    cmd, _ := fields[0], fields[1:]
    switch cmd {
    case "help":
        return helpMessage
    case "hello":
        return "Hello! Welcome to DBChat!