Back to Articles

Building Interactive CLI Tools in Go with go-prompt: A Deep Dive

[ View on GitHub ]

Building Interactive CLI Tools in Go with go-prompt: A Deep Dive

Hook

While most Go CLI tools stop at flag parsing, production tools like Rancher CLI and docker-slim deliver rich, interactive experiences with auto-completion and command history—all powered by a single library inspired by Python’s best terminal UI toolkit.

Context

The Go ecosystem has long excelled at building command-line tools, but there’s been a persistent gap between simple utilities that parse flags and sophisticated interactive shells like those found in database clients or REPLs. Developers who wanted auto-completion, command history, or rich keyboard interactions typically had to either build terminal handling from scratch—a notoriously complex undertaking—or compromise on user experience.

This gap became particularly evident as cloud-native tools proliferated. Tools in the Kubernetes ecosystem set new UX expectations: users wanted real-time auto-completion, intuitive navigation, and familiar keyboard shortcuts. Meanwhile, Python developers had python-prompt-toolkit, an elegant library that made building these experiences straightforward. Go needed its own answer. That’s where go-prompt comes in—a library by Masashi Shibata that ports the core concepts of python-prompt-toolkit to Go, providing a battle-tested foundation for interactive CLI tools without requiring developers to become terminal emulation experts.

Technical Insight

Core Engine

User Logic

Keyboard Events

Current State

Buffer & Cursor

Suggestions

Execute Command

VT100/ANSI Codes

Visual Output

User Input

Main Event Loop

Document Model

Completer Callback

Terminal Renderer

Executor Callback

Terminal Display

System architecture — auto-generated

At its core, go-prompt appears to implement an architecture built around three key abstractions: the Document model (which tracks buffer state and cursor position), user-provided callbacks (for completion and execution logic), and a rendering layer that handles cross-platform terminal display. This separation of concerns means you focus on what your CLI does, not how terminals work.

The simplest way to understand go-prompt is through code. Here’s the example from the README that demonstrates the fundamental pattern:

package main

import (
    "fmt"
    "github.com/c-bata/go-prompt"
)

func completer(d prompt.Document) []prompt.Suggest {
    s := []prompt.Suggest{
        {Text: "users", Description: "Store the username and age"},
        {Text: "articles", Description: "Store the article text posted by user"},
        {Text: "comments", Description: "Store the text commented to articles"},
    }
    return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}

func main() {
    fmt.Println("Please select table.")
    t := prompt.Input("> ", completer)
    fmt.Println("You selected " + t)
}

This 20-line program gives you auto-completion with filtering, command history, and Emacs-style keyboard shortcuts. The completer function receives a Document object representing the current buffer state and returns a slice of Suggest structs. The library handles everything else: rendering the suggestion dropdown, filtering as the user types, managing cursor position, and capturing keyboard input.

The Document abstraction is particularly clever. Instead of exposing raw buffer manipulation, it provides methods like GetWordBeforeCursor() that extract meaningful context from the current cursor position. This makes implementing context-aware completion straightforward—you’re working with semantic concepts, not character indices. The built-in FilterHasPrefix function demonstrates this: it takes your suggestions and the current word fragment, returning only relevant matches. You can swap this for custom filtering logic like fuzzy matching without touching the rendering layer.

For more complex applications, go-prompt exposes extensive customization through functional options. The library offers configuration for colors, prefixes, history behavior, and keyboard shortcuts. The library ships with Emacs-style bindings by default (Ctrl+A for beginning-of-line, Ctrl+E for end, Ctrl+W to cut words, and others documented in the README), and these can be customized. This options pattern keeps the simple case simple while providing escape hatches for sophisticated UX requirements.

The cross-platform terminal abstraction deserves special attention. go-prompt handles the differences between Unix-like systems and Windows terminal environments, working consistently across iTerm2, Terminal.app, Command Prompt, and gnome-terminal (as confirmed in the README). This is non-trivial work—terminal emulators have decades of quirks and edge cases. By abstracting this complexity, the library lets you build once and deploy everywhere, which is particularly valuable for DevOps tooling that needs to run in diverse environments.

Production usage validates the approach. Projects like Rancher CLI use go-prompt to manage complex cloud infrastructure through interactive commands. The kube-prompt project (also by the same author) demonstrates how to build kubectl-style experiences with context-aware completion for Kubernetes resources. docker-slim, a tool for container image optimization, uses it for interactive configuration. These aren’t toy examples—they’re production systems handling real workloads, and they’ve collectively validated that go-prompt’s design works beyond simple demos.

Gotcha

The callback-based API, while simple for basic cases, can become awkward in complex applications. Your completer function is stateless by design—it receives a Document and returns suggestions. If you need to maintain application state, track async operations, or coordinate between multiple completion sources, you’ll need to manage that state externally. This can lead to global variables or closure patterns that might feel unidiomatic in modern Go applications. Additionally, while cross-platform support covers the major terminals listed in the README (iTerm2, Terminal.app, Command Prompt, gnome-terminal), exotic terminal emulators or older systems might expose edge cases. Test thoroughly in your target environments, especially if you’re deploying to heterogeneous infrastructure.

Verdict

Use go-prompt if you’re building interactive CLI tools in Go that need sophisticated auto-completion, command history, and rich keyboard shortcuts—particularly DevOps tools, REPLs, database clients, or anything requiring the kind of experience seen in kube-prompt. Its production usage in Rancher CLI, docker-slim, and numerous other projects demonstrates real-world reliability, and the straightforward API means you can have a working interactive prompt in under an hour. The cross-platform terminal abstraction alone saves significant low-level implementation work. Skip it if you’re building simple utilities where flag parsing suffices, or if your application has complex state management needs where the callback architecture might become awkward for applications with intricate internal state or async coordination requirements.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/c-bata-go-prompt.svg)](https://starlog.is/api/badge-click/developer-tools/c-bata-go-prompt)