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:
bash
1#!/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):
bash
1#!/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:
bash
1#!/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:
bash
1#!/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:
bash
1#!/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:
bash
1#!/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
bash
1#!/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
bash
1#!/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