Building Interactive CLI Applications in Go
Introduction
Interactive CLI applications provide better user experience through prompts, menus, and real-time feedback. This guide covers building interactive CLI applications in Go using various approaches.
Interactive CLIs make applications more user-friendly and reduce the need for complex flag combinations.
Basic User Input
Simple Input Handling
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// ReadInput reads a line from user input
func ReadInput(prompt string) string {
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
// ReadPassword reads password without echoing
func ReadPassword(prompt string) string {
fmt.Print(prompt)
// Note: This is simplified; use golang.org/x/term for production
reader := bufio.NewReader(os.Stdin)
password, _ := reader.ReadString('\n')
return strings.TrimSpace(password)
}
// ReadConfirmation reads yes/no confirmation
func ReadConfirmation(prompt string) bool {
response := ReadInput(prompt + " (y/n): ")
return strings.ToLower(response) == "y" || strings.ToLower(response) == "yes"
}
// Example usage
func BasicInputExample() {
name := ReadInput("Enter your name: ")
fmt.Printf("Hello, %s!\n", name)
if ReadConfirmation("Do you want to continue?") {
fmt.Println("Continuing...")
}
}
Good: Proper Interactive CLI Implementation
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// InputValidator validates user input
type InputValidator func(string) error
// InteractivePrompt handles interactive user prompts
type InteractivePrompt struct {
reader *bufio.Reader
}
// NewInteractivePrompt creates a new interactive prompt
func NewInteractivePrompt() *InteractivePrompt {
return &InteractivePrompt{
reader: bufio.NewReader(os.Stdin),
}
}
// String prompts for a string value
func (ip *InteractivePrompt) String(prompt string, validators ...InputValidator) (string, error) {
for {
fmt.Print(prompt)
input, err := ip.reader.ReadString('\n')
if err != nil {
return "", err
}
input = strings.TrimSpace(input)
// Validate input
for _, validator := range validators {
if err := validator(input); err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
}
return input, nil
}
}
// Integer prompts for an integer value
func (ip *InteractivePrompt) Integer(prompt string) (int, error) {
for {
fmt.Print(prompt)
input, err := ip.reader.ReadString('\n')
if err != nil {
return 0, err
}
input = strings.TrimSpace(input)
value, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Error: Please enter a valid integer")
continue
}
return value, nil
}
}
// Choice prompts for a choice from options
func (ip *InteractivePrompt) Choice(prompt string, options []string) (string, error) {
fmt.Println(prompt)
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
}
for {
fmt.Print("Enter your choice (1-" + strconv.Itoa(len(options)) + "): ")
input, err := ip.reader.ReadString('\n')
if err != nil {
return "", err
}
input = strings.TrimSpace(input)
choice, err := strconv.Atoi(input)
if err != nil || choice < 1 || choice > len(options) {
fmt.Println("Error: Invalid choice")
continue
}
return options[choice-1], nil
}
}
// Confirmation prompts for yes/no confirmation
func (ip *InteractivePrompt) Confirmation(prompt string) (bool, error) {
for {
fmt.Print(prompt + " (y/n): ")
input, err := ip.reader.ReadString('\n')
if err != nil {
return false, err
}
input = strings.ToLower(strings.TrimSpace(input))
if input == "y" || input == "yes" {
return true, nil
} else if input == "n" || input == "no" {
return false, nil
}
fmt.Println("Error: Please enter 'y' or 'n'")
}
}
// Menu displays an interactive menu
type Menu struct {
title string
options map[string]func() error
prompt *InteractivePrompt
}
// NewMenu creates a new menu
func NewMenu(title string) *Menu {
return &Menu{
title: title,
options: make(map[string]func() error),
prompt: NewInteractivePrompt(),
}
}
// AddOption adds a menu option
func (m *Menu) AddOption(label string, handler func() error) {
m.options[label] = handler
}
// Display displays the menu
func (m *Menu) Display() error {
for {
fmt.Println("\n" + strings.Repeat("=", 40))
fmt.Println(m.title)
fmt.Println(strings.Repeat("=", 40))
options := make([]string, 0, len(m.options)+1)
for label := range m.options {
options = append(options, label)
}
options = append(options, "Exit")
choice, err := m.prompt.Choice("Select an option:", options)
if err != nil {
return err
}
if choice == "Exit" {
break
}
if handler, exists := m.options[choice]; exists {
if err := handler(); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
}
return nil
}
// Example usage
func InteractiveMenuExample() {
menu := NewMenu("Main Menu")
menu.AddOption("Create User", func() error {
prompt := NewInteractivePrompt()
name, _ := prompt.String("Enter name: ")
email, _ := prompt.String("Enter email: ")
fmt.Printf("Created user: %s (%s)\n", name, email)
return nil
})
menu.AddOption("List Users", func() error {
fmt.Println("Users: user1, user2, user3")
return nil
})
menu.AddOption("Delete User", func() error {
prompt := NewInteractivePrompt()
name, _ := prompt.String("Enter user name: ")
fmt.Printf("Deleted user: %s\n", name)
return nil
})
menu.Display()
}
Bad: Improper Interactive CLI
package main
import (
"fmt"
"os"
)
// BAD: No input validation
func BadReadInput() string {
var input string
fmt.Scanln(&input)
return input // No validation
}
// BAD: No error handling
func BadMenu() {
fmt.Println("1. Option 1")
fmt.Println("2. Option 2")
var choice int
fmt.Scanln(&choice)
// No error handling
// No validation
}
Problems:
- No input validation
- No error handling
- No user feedback
- Poor UX
Terminal UI Libraries
Using Bubble Tea
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// Model represents the application state
type Model struct {
choices []string
cursor int
selected map[int]struct{}
}
// Init initializes the model
func (m Model) Init() tea.Cmd {
return nil
}
// Update handles messages
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
// View renders the UI
func (m Model) View() string {
s := "What should we buy?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
// BubbleTeaExample demonstrates Bubble Tea usage
func BubbleTeaExample() {
m := Model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
fmt.Println("Error:", err)
}
}
Using Promptui
package main
import (
"fmt"
"github.com/manifoldco/promptui"
)
// PromptUIExample demonstrates promptui usage
func PromptUIExample() {
// Text prompt
prompt := promptui.Prompt{
Label: "Enter your name",
}
name, _ := prompt.Run()
fmt.Printf("Hello, %s!\n", name)
// Select prompt
items := []string{"Option 1", "Option 2", "Option 3"}
selectPrompt := promptui.Select{
Label: "Select an option",
Items: items,
}
_, result, _ := selectPrompt.Run()
fmt.Printf("You selected: %s\n", result)
// Confirm prompt
confirmPrompt := promptui.Prompt{
Label: "Continue",
IsConfirm: true,
}
confirmPrompt.Run()
}
Input Validation
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// ValidateEmail validates email format
func ValidateEmail(email string) error {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
if !matched {
return fmt.Errorf("invalid email format")
}
return nil
}
// ValidateNotEmpty validates non-empty input
func ValidateNotEmpty(input string) error {
if strings.TrimSpace(input) == "" {
return fmt.Errorf("input cannot be empty")
}
return nil
}
// ValidateInteger validates integer input
func ValidateInteger(input string) error {
_, err := strconv.Atoi(input)
return err
}
// ValidateRange validates integer range
func ValidateRange(min, max int) InputValidator {
return func(input string) error {
value, err := strconv.Atoi(input)
if err != nil {
return err
}
if value < min || value > max {
return fmt.Errorf("value must be between %d and %d", min, max)
}
return nil
}
}
Best Practices
1. Provide Clear Prompts
fmt.Print("Enter your name: ")
2. Validate Input
if err := ValidateEmail(email); err != nil {
fmt.Printf("Error: %v\n", err)
}
3. Handle Errors
if err != nil {
return err
}
4. Provide Feedback
fmt.Println("User created successfully!")
Common Pitfalls
1. No Input Validation
Always validate user input.
2. Poor Error Messages
Provide clear error messages.
3. No Confirmation
Ask for confirmation for destructive operations.
4. Unclear Prompts
Make prompts clear and concise.
Resources
Summary
Interactive CLIs improve user experience. Key takeaways:
- Provide clear prompts
- Validate all input
- Handle errors gracefully
- Provide feedback
- Use libraries for advanced UI
- Ask for confirmation for destructive operations
- Test with various inputs
By building interactive CLIs, you create better user experiences.
Comments