Skip to content

Project 06 — Menu-Driven CLI App

Build an interactive script with a main menu, multiple subcommands, a persistent config file, and a --help flag — the kind of tool you actually run every day.

What You Will Build

An interactive task manager (or pick your own domain) with: - A numbered main menu built with select or a while loop - Subcommands: add, list, done, delete, config - Persistent data storage in a plain text file - A config file for user preferences - Non-interactive mode: app.sh add "Buy milk" (works in scripts too) - --help with command-specific help: app.sh help add - Color output using ANSI escape codes

Pattern 1: select Statement

#!/usr/bin/env bash
PS3="Choose an option: "
options=("Add task" "List tasks" "Mark done" "Quit")

select choice in "${options[@]}"; do
    case "$REPLY" in
        1) cmd_add ;;
        2) cmd_list ;;
        3) cmd_done ;;
        4) break ;;
        *) echo "Invalid choice" ;;
    esac
done

Pattern 2: Manual while Loop (more control)

show_menu() {
    echo ""
    echo "  TASK MANAGER"
    echo "  ─────────────"
    echo "  1. Add task"
    echo "  2. List tasks"
    echo "  3. Mark done"
    echo "  4. Config"
    echo "  5. Quit"
    echo ""
}

while true; do
    show_menu
    read -rp "  > " choice
    case "$choice" in
        1) cmd_add ;;
        2) cmd_list ;;
        3) cmd_done ;;
        4) cmd_config ;;
        5|q|quit|exit) echo "Bye."; break ;;
        *) echo "Unknown option: $choice" ;;
    esac
done

Persistent Storage

Store tasks in a plain text file — one task per line:

1|pending|2024-01-15|Buy groceries
2|done|2024-01-14|Write report
3|pending|2024-01-15|Call dentist
DATA_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/taskman/tasks.tsv"
mkdir -p "$(dirname "$DATA_FILE")"

cmd_add() {
    read -rp "Task description: " desc
    [[ -n "$desc" ]] || { echo "Empty task ignored"; return; }
    local id=$(( $(wc -l < "$DATA_FILE" 2>/dev/null || echo 0) + 1 ))
    echo "$id|pending|$(date +%Y-%m-%d)|$desc" >> "$DATA_FILE"
    echo "Added: $desc"
}

cmd_list() {
    [[ -f "$DATA_FILE" ]] || { echo "No tasks yet."; return; }
    echo ""
    printf "%-4s %-10s %-12s %s\n" "ID" "STATUS" "DATE" "DESCRIPTION"
    printf "%-4s %-10s %-12s %s\n" "──" "──────" "────" "───────────"
    while IFS='|' read -r id status date desc; do
        printf "%-4s %-10s %-12s %s\n" "$id" "$status" "$date" "$desc"
    done < "$DATA_FILE"
    echo ""
}

Color Output

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RESET='\033[0m'

echo -e "${GREEN}Task added successfully${RESET}"
echo -e "${RED}Error: task not found${RESET}"

Check terminal support

Always check [[ -t 1 ]] (stdout is a terminal) before using color codes. Disable colors when output is piped or redirected: [[ -t 1 ]] && GREEN='\033[0;32m' || GREEN=''

Config File

CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/taskman/config"

# Read config
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"

# Defaults
COLOR_OUTPUT="${COLOR_OUTPUT:-true}"
DATE_FORMAT="${DATE_FORMAT:-%Y-%m-%d}"

cmd_config() {
    echo "Current config: $CONFIG_FILE"
    cat "$CONFIG_FILE" 2>/dev/null || echo "(no config file — using defaults)"
}

Stretch Goals

  • Add priority levels and sort tasks by priority
  • Add due dates and highlight overdue tasks in red
  • Add export to CSV or JSON
  • Add --format json flag for machine-readable output
  • Write bats tests for cmd_add and cmd_list

[[05-git-hooks]] | index