State Directory Manager
Patterns for managing persistent state, configuration, and cache directories in bash scripts following XDG Base Directory specification.
When to Use This Skill
✅ Use when:
- Scripts need to persist data between runs
- Storing user preferences or configuration
- Caching results for performance
- Managing log files with rotation
- Creating portable CLI tools
❌ Avoid when:
- One-time scripts that don't need state
- Scripts that should be purely stateless
- When environment variables are sufficient
Core Capabilities
1. XDG Base Directory Standard
Follow the XDG specification for directory locations:
bash1#!/bin/bash 2# ABOUTME: XDG Base Directory compliant state management 3# ABOUTME: Cross-platform directory locations 4 5# XDG Base Directories with fallbacks 6XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" 7XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" 8XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" 9XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" 10 11# Application-specific directories 12APP_NAME="my-tool" 13CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME" 14DATA_DIR="$XDG_DATA_HOME/$APP_NAME" 15STATE_DIR="$XDG_STATE_HOME/$APP_NAME" 16CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME" 17LOG_DIR="$STATE_DIR/logs" 18 19# Initialize directories 20init_directories() { 21 mkdir -p "$CONFIG_DIR" 22 mkdir -p "$DATA_DIR" 23 mkdir -p "$STATE_DIR" 24 mkdir -p "$CACHE_DIR" 25 mkdir -p "$LOG_DIR" 26}
2. Workspace-Hub Pattern
Alternative using home directory (from workspace-hub scripts):
bash1#!/bin/bash 2# ABOUTME: Workspace-hub style state directory management 3# ABOUTME: Simple $HOME/.app-name pattern 4 5APP_NAME="workspace-hub" 6APP_DIR="${HOME}/.${APP_NAME}" 7 8# Directory structure 9CONFIG_DIR="$APP_DIR/config" 10DATA_DIR="$APP_DIR/data" 11LOGS_DIR="$APP_DIR/logs" 12CACHE_DIR="$APP_DIR/cache" 13TEMP_DIR="$APP_DIR/tmp" 14 15# Initialize with proper permissions 16init_app_dirs() { 17 local dirs=("$CONFIG_DIR" "$DATA_DIR" "$LOGS_DIR" "$CACHE_DIR" "$TEMP_DIR") 18 19 for dir in "${dirs[@]}"; do 20 if [[ ! -d "$dir" ]]; then 21 mkdir -p "$dir" 22 chmod 700 "$dir" # Private by default 23 fi 24 done 25} 26 27# Clean old temp files 28clean_temp() { 29 find "$TEMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true 30}
3. Configuration File Management
Read and write configuration files:
bash1#!/bin/bash 2# ABOUTME: Configuration file management 3# ABOUTME: Key-value pairs with defaults 4 5CONFIG_FILE="$CONFIG_DIR/config" 6 7# Default configuration 8declare -A DEFAULT_CONFIG=( 9 ["parallel_workers"]="5" 10 ["log_level"]="INFO" 11 ["auto_sync"]="true" 12 ["timeout"]="30" 13) 14 15# Initialize config with defaults 16init_config() { 17 if [[ ! -f "$CONFIG_FILE" ]]; then 18 { 19 echo "# Configuration for $APP_NAME" 20 echo "# Generated: $(date)" 21 echo "" 22 for key in "${!DEFAULT_CONFIG[@]}"; do 23 echo "${key}=${DEFAULT_CONFIG[$key]}" 24 done 25 } > "$CONFIG_FILE" 26 fi 27} 28 29# Read config value 30get_config() { 31 local key="$1" 32 local default="${2:-${DEFAULT_CONFIG[$key]:-}}" 33 34 if [[ -f "$CONFIG_FILE" ]]; then 35 local value 36 value=$(grep "^${key}=" "$CONFIG_FILE" 2>/dev/null | cut -d'=' -f2-) 37 echo "${value:-$default}" 38 else 39 echo "$default" 40 fi 41} 42 43# Write config value 44set_config() { 45 local key="$1" 46 local value="$2" 47 48 init_config 49 50 if grep -q "^${key}=" "$CONFIG_FILE" 2>/dev/null; then 51 # Update existing 52 sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG_FILE" 53 else 54 # Add new 55 echo "${key}=${value}" >> "$CONFIG_FILE" 56 fi 57} 58 59# Load all config into associative array 60load_config() { 61 declare -gA CONFIG 62 63 # Start with defaults 64 for key in "${!DEFAULT_CONFIG[@]}"; do 65 CONFIG[$key]="${DEFAULT_CONFIG[$key]}" 66 done 67 68 # Override with file values 69 if [[ -f "$CONFIG_FILE" ]]; then 70 while IFS='=' read -r key value; do 71 [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue 72 CONFIG[$key]="$value" 73 done < "$CONFIG_FILE" 74 fi 75} 76 77# Usage 78init_config 79load_config 80echo "Parallel workers: ${CONFIG[parallel_workers]}" 81set_config "parallel_workers" "10"
4. State File Operations
Track persistent state between runs:
bash1#!/bin/bash 2# ABOUTME: State file operations 3# ABOUTME: Track last run, progress, etc. 4 5STATE_FILE="$STATE_DIR/state.json" 6 7# Initialize state 8init_state() { 9 if [[ ! -f "$STATE_FILE" ]]; then 10 cat > "$STATE_FILE" << EOF 11{ 12 "version": "1.0.0", 13 "created": "$(date -Iseconds)", 14 "last_run": null, 15 "run_count": 0, 16 "last_status": null 17} 18EOF 19 fi 20} 21 22# Get state value (requires jq) 23get_state() { 24 local key="$1" 25 local default="${2:-null}" 26 27 if [[ -f "$STATE_FILE" ]] && command -v jq &>/dev/null; then 28 jq -r ".$key // $default" "$STATE_FILE" 29 else 30 echo "$default" 31 fi 32} 33 34# Update state value (requires jq) 35set_state() { 36 local key="$1" 37 local value="$2" 38 39 init_state 40 41 if command -v jq &>/dev/null; then 42 local temp=$(mktemp) 43 jq ".$key = $value" "$STATE_FILE" > "$temp" && mv "$temp" "$STATE_FILE" 44 fi 45} 46 47# Record run 48record_run() { 49 local status="$1" 50 51 set_state "last_run" "\"$(date -Iseconds)\"" 52 set_state "last_status" "\"$status\"" 53 set_state "run_count" "$(($(get_state run_count 0) + 1))" 54} 55 56# Simple key-value state (no jq required) 57STATE_KV_FILE="$STATE_DIR/state.kv" 58 59get_state_kv() { 60 local key="$1" 61 local default="$2" 62 63 if [[ -f "$STATE_KV_FILE" ]]; then 64 grep "^${key}=" "$STATE_KV_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" 65 else 66 echo "$default" 67 fi 68} 69 70set_state_kv() { 71 local key="$1" 72 local value="$2" 73 74 mkdir -p "$(dirname "$STATE_KV_FILE")" 75 76 if [[ -f "$STATE_KV_FILE" ]] && grep -q "^${key}=" "$STATE_KV_FILE"; then 77 sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_KV_FILE" 78 else 79 echo "${key}=${value}" >> "$STATE_KV_FILE" 80 fi 81}
5. Cache Management
Implement caching with expiration:
bash1#!/bin/bash 2# ABOUTME: Cache management with TTL 3# ABOUTME: Store and retrieve cached data 4 5CACHE_TTL="${CACHE_TTL:-3600}" # 1 hour default 6 7# Get cache file path 8cache_path() { 9 local key="$1" 10 local hash=$(echo -n "$key" | md5sum | cut -c1-16) 11 echo "$CACHE_DIR/${hash}" 12} 13 14# Check if cache is valid 15cache_valid() { 16 local key="$1" 17 local ttl="${2:-$CACHE_TTL}" 18 local path=$(cache_path "$key") 19 20 if [[ -f "$path" ]]; then 21 local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path"))) 22 [[ $age -lt $ttl ]] 23 else 24 return 1 25 fi 26} 27 28# Get from cache 29cache_get() { 30 local key="$1" 31 local ttl="${2:-$CACHE_TTL}" 32 local path=$(cache_path "$key") 33 34 if cache_valid "$key" "$ttl"; then 35 cat "$path" 36 return 0 37 fi 38 return 1 39} 40 41# Set cache 42cache_set() { 43 local key="$1" 44 local value="$2" 45 local path=$(cache_path "$key") 46 47 mkdir -p "$CACHE_DIR" 48 echo "$value" > "$path" 49} 50 51# Delete cache 52cache_delete() { 53 local key="$1" 54 local path=$(cache_path "$key") 55 rm -f "$path" 56} 57 58# Clear all cache 59cache_clear() { 60 rm -rf "$CACHE_DIR"/* 61} 62 63# Clean expired cache entries 64cache_clean() { 65 local ttl="${1:-$CACHE_TTL}" 66 find "$CACHE_DIR" -type f -mmin "+$((ttl / 60))" -delete 2>/dev/null || true 67} 68 69# Usage with automatic caching 70get_with_cache() { 71 local key="$1" 72 local command="$2" 73 local ttl="${3:-$CACHE_TTL}" 74 75 if cache_valid "$key" "$ttl"; then 76 cache_get "$key" 77 else 78 local result 79 result=$(eval "$command") 80 cache_set "$key" "$result" 81 echo "$result" 82 fi 83} 84 85# Example 86result=$(get_with_cache "api_response" "curl -s https://api.example.com/data" 300)
6. Log File Management
Manage logs with rotation:
bash1#!/bin/bash 2# ABOUTME: Log file management with rotation 3# ABOUTME: Automatic cleanup of old logs 4 5LOG_FILE="$LOG_DIR/app.log" 6LOG_MAX_SIZE=$((10 * 1024 * 1024)) # 10MB 7LOG_MAX_FILES=5 8 9# Initialize logging 10init_logging() { 11 mkdir -p "$LOG_DIR" 12 touch "$LOG_FILE" 13} 14 15# Write to log 16log_to_file() { 17 local level="$1" 18 shift 19 local message="$*" 20 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') 21 22 echo "[$timestamp] $level: $message" >> "$LOG_FILE" 23 24 # Check if rotation needed 25 maybe_rotate_logs 26} 27 28# Rotate logs if needed 29maybe_rotate_logs() { 30 if [[ -f "$LOG_FILE" ]]; then 31 local size=$(stat -c %s "$LOG_FILE" 2>/dev/null || stat -f %z "$LOG_FILE") 32 33 if [[ $size -gt $LOG_MAX_SIZE ]]; then 34 rotate_logs 35 fi 36 fi 37} 38 39# Perform log rotation 40rotate_logs() { 41 # Remove oldest 42 rm -f "${LOG_FILE}.${LOG_MAX_FILES}" 43 44 # Shift existing 45 for ((i=LOG_MAX_FILES-1; i>=1; i--)); do 46 if [[ -f "${LOG_FILE}.$i" ]]; then 47 mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))" 48 fi 49 done 50 51 # Rotate current 52 if [[ -f "$LOG_FILE" ]]; then 53 mv "$LOG_FILE" "${LOG_FILE}.1" 54 touch "$LOG_FILE" 55 fi 56} 57 58# Clean old logs 59clean_old_logs() { 60 local days="${1:-30}" 61 find "$LOG_DIR" -name "*.log*" -mtime "+$days" -delete 2>/dev/null || true 62} 63 64# View recent logs 65tail_logs() { 66 local lines="${1:-50}" 67 tail -n "$lines" "$LOG_FILE" 68} 69 70# Search logs 71search_logs() { 72 local pattern="$1" 73 grep -h "$pattern" "$LOG_DIR"/*.log* 2>/dev/null | tail -100 74}
Complete Example: State Manager Module
bash1#!/bin/bash 2# ABOUTME: Complete state directory manager 3# ABOUTME: Reusable module for bash scripts 4 5# ───────────────────────────────────────────────────────────────── 6# State Directory Manager v1.0.0 7# ───────────────────────────────────────────────────────────────── 8 9# Application identity (override in your script) 10: "${STATE_APP_NAME:=my-app}" 11 12# Directory setup 13STATE_BASE_DIR="${HOME}/.${STATE_APP_NAME}" 14STATE_CONFIG_DIR="$STATE_BASE_DIR/config" 15STATE_DATA_DIR="$STATE_BASE_DIR/data" 16STATE_CACHE_DIR="$STATE_BASE_DIR/cache" 17STATE_LOG_DIR="$STATE_BASE_DIR/logs" 18STATE_TMP_DIR="$STATE_BASE_DIR/tmp" 19 20# File paths 21STATE_CONFIG_FILE="$STATE_CONFIG_DIR/config" 22STATE_STATE_FILE="$STATE_DATA_DIR/state" 23STATE_LOG_FILE="$STATE_LOG_DIR/app.log" 24 25# Settings 26STATE_CACHE_TTL="${STATE_CACHE_TTL:-3600}" 27STATE_LOG_MAX_SIZE="${STATE_LOG_MAX_SIZE:-10485760}" 28STATE_LOG_MAX_FILES="${STATE_LOG_MAX_FILES:-5}" 29 30# ───────────────────────────────────────────────────────────────── 31# Initialization 32# ───────────────────────────────────────────────────────────────── 33 34state_init() { 35 local dirs=( 36 "$STATE_CONFIG_DIR" 37 "$STATE_DATA_DIR" 38 "$STATE_CACHE_DIR" 39 "$STATE_LOG_DIR" 40 "$STATE_TMP_DIR" 41 ) 42 43 for dir in "${dirs[@]}"; do 44 if [[ ! -d "$dir" ]]; then 45 mkdir -p "$dir" 46 chmod 700 "$dir" 47 fi 48 done 49 50 # Initialize files 51 [[ -f "$STATE_CONFIG_FILE" ]] || touch "$STATE_CONFIG_FILE" 52 [[ -f "$STATE_STATE_FILE" ]] || touch "$STATE_STATE_FILE" 53 [[ -f "$STATE_LOG_FILE" ]] || touch "$STATE_LOG_FILE" 54} 55 56# ───────────────────────────────────────────────────────────────── 57# Config Functions 58# ───────────────────────────────────────────────────────────────── 59 60state_config_get() { 61 local key="$1" 62 local default="$2" 63 grep "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" 64} 65 66state_config_set() { 67 local key="$1" 68 local value="$2" 69 70 if grep -q "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null; then 71 sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_CONFIG_FILE" 72 else 73 echo "${key}=${value}" >> "$STATE_CONFIG_FILE" 74 fi 75} 76 77state_config_list() { 78 cat "$STATE_CONFIG_FILE" 2>/dev/null | grep -v '^#' | grep -v '^$' 79} 80 81# ───────────────────────────────────────────────────────────────── 82# State Functions 83# ───────────────────────────────────────────────────────────────── 84 85state_get() { 86 local key="$1" 87 local default="$2" 88 grep "^${key}=" "$STATE_STATE_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" 89} 90 91state_set() { 92 local key="$1" 93 local value="$2" 94 95 if grep -q "^${key}=" "$STATE_STATE_FILE" 2>/dev/null; then 96 sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_STATE_FILE" 97 else 98 echo "${key}=${value}" >> "$STATE_STATE_FILE" 99 fi 100} 101 102# ───────────────────────────────────────────────────────────────── 103# Cache Functions 104# ───────────────────────────────────────────────────────────────── 105 106state_cache_key() { 107 echo -n "$1" | md5sum | cut -c1-16 108} 109 110state_cache_get() { 111 local key="$1" 112 local ttl="${2:-$STATE_CACHE_TTL}" 113 local path="$STATE_CACHE_DIR/$(state_cache_key "$key")" 114 115 if [[ -f "$path" ]]; then 116 local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path"))) 117 if [[ $age -lt $ttl ]]; then 118 cat "$path" 119 return 0 120 fi 121 fi 122 return 1 123} 124 125state_cache_set() { 126 local key="$1" 127 local value="$2" 128 local path="$STATE_CACHE_DIR/$(state_cache_key "$key")" 129 echo "$value" > "$path" 130} 131 132state_cache_clear() { 133 rm -rf "$STATE_CACHE_DIR"/* 134} 135 136# ───────────────────────────────────────────────────────────────── 137# Log Functions 138# ───────────────────────────────────────────────────────────────── 139 140state_log() { 141 local level="$1" 142 shift 143 local message="$*" 144 echo "[$(date '+%Y-%m-%d %H:%M:%S')] $level: $message" >> "$STATE_LOG_FILE" 145 146 # Auto-rotate 147 local size=$(stat -c %s "$STATE_LOG_FILE" 2>/dev/null || echo 0) 148 if [[ $size -gt $STATE_LOG_MAX_SIZE ]]; then 149 state_log_rotate 150 fi 151} 152 153state_log_rotate() { 154 rm -f "${STATE_LOG_FILE}.${STATE_LOG_MAX_FILES}" 155 for ((i=STATE_LOG_MAX_FILES-1; i>=1; i--)); do 156 [[ -f "${STATE_LOG_FILE}.$i" ]] && mv "${STATE_LOG_FILE}.$i" "${STATE_LOG_FILE}.$((i+1))" 157 done 158 mv "$STATE_LOG_FILE" "${STATE_LOG_FILE}.1" 159 touch "$STATE_LOG_FILE" 160} 161 162state_log_tail() { 163 tail -n "${1:-50}" "$STATE_LOG_FILE" 164} 165 166# ───────────────────────────────────────────────────────────────── 167# Cleanup Functions 168# ───────────────────────────────────────────────────────────────── 169 170state_cleanup() { 171 # Clean temp files older than 1 day 172 find "$STATE_TMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true 173 174 # Clean expired cache 175 find "$STATE_CACHE_DIR" -type f -mmin "+$((STATE_CACHE_TTL / 60))" -delete 2>/dev/null || true 176 177 # Clean old logs 178 find "$STATE_LOG_DIR" -name "*.log.*" -mtime +30 -delete 2>/dev/null || true 179} 180 181state_reset() { 182 rm -rf "$STATE_BASE_DIR" 183 state_init 184} 185 186# ───────────────────────────────────────────────────────────────── 187# Auto-initialize 188# ───────────────────────────────────────────────────────────────── 189 190state_init
Usage in Scripts
bash1#!/bin/bash 2# Your script that uses the state manager 3 4# Set app name before sourcing 5STATE_APP_NAME="my-tool" 6 7# Source the state manager 8source /path/to/state-manager.sh 9 10# Now use it 11state_config_set "api_key" "abc123" 12api_key=$(state_config_get "api_key") 13 14state_set "last_run" "$(date -Iseconds)" 15state_log "INFO" "Script started" 16 17# Use cache 18if ! result=$(state_cache_get "api_response"); then 19 result=$(curl -s https://api.example.com/data) 20 state_cache_set "api_response" "$result" 21fi
Best Practices
- Use Standard Locations - Follow XDG or
$HOME/.app-name - Initialize Early - Call init before any operations
- Handle Permissions - Use 700 for private data
- Clean Up Regularly - Remove old temp/cache files
- Rotate Logs - Prevent unbounded growth
Resources
Version History
- 1.0.0 (2026-01-14): Initial release - extracted from workspace-hub patterns