state-directory-manager

State Directory Manager

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "state-directory-manager" with this command: npx skills add vamseeachanta/workspace-hub/vamseeachanta-workspace-hub-state-directory-manager

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:

#!/bin/bash

ABOUTME: XDG Base Directory compliant state management

ABOUTME: Cross-platform directory locations

XDG Base Directories with fallbacks

XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"

Application-specific directories

APP_NAME="my-tool" CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME" DATA_DIR="$XDG_DATA_HOME/$APP_NAME" STATE_DIR="$XDG_STATE_HOME/$APP_NAME" CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME" LOG_DIR="$STATE_DIR/logs"

Initialize directories

init_directories() { mkdir -p "$CONFIG_DIR" mkdir -p "$DATA_DIR" mkdir -p "$STATE_DIR" mkdir -p "$CACHE_DIR" mkdir -p "$LOG_DIR" }

  1. Workspace-Hub Pattern

Alternative using home directory (from workspace-hub scripts):

#!/bin/bash

ABOUTME: Workspace-hub style state directory management

ABOUTME: Simple $HOME/.app-name pattern

APP_NAME="workspace-hub" APP_DIR="${HOME}/.${APP_NAME}"

Directory structure

CONFIG_DIR="$APP_DIR/config" DATA_DIR="$APP_DIR/data" LOGS_DIR="$APP_DIR/logs" CACHE_DIR="$APP_DIR/cache" TEMP_DIR="$APP_DIR/tmp"

Initialize with proper permissions

init_app_dirs() { local dirs=("$CONFIG_DIR" "$DATA_DIR" "$LOGS_DIR" "$CACHE_DIR" "$TEMP_DIR")

for dir in "${dirs[@]}"; do
    if [[ ! -d "$dir" ]]; then
        mkdir -p "$dir"
        chmod 700 "$dir"  # Private by default
    fi
done

}

Clean old temp files

clean_temp() { find "$TEMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true }

  1. Configuration File Management

Read and write configuration files:

#!/bin/bash

ABOUTME: Configuration file management

ABOUTME: Key-value pairs with defaults

CONFIG_FILE="$CONFIG_DIR/config"

Default configuration

declare -A DEFAULT_CONFIG=( ["parallel_workers"]="5" ["log_level"]="INFO" ["auto_sync"]="true" ["timeout"]="30" )

Initialize config with defaults

init_config() { if [[ ! -f "$CONFIG_FILE" ]]; then { echo "# Configuration for $APP_NAME" echo "# Generated: $(date)" echo "" for key in "${!DEFAULT_CONFIG[@]}"; do echo "${key}=${DEFAULT_CONFIG[$key]}" done } > "$CONFIG_FILE" fi }

Read config value

get_config() { local key="$1" local default="${2:-${DEFAULT_CONFIG[$key]:-}}"

if [[ -f "$CONFIG_FILE" ]]; then
    local value
    value=$(grep "^${key}=" "$CONFIG_FILE" 2>/dev/null | cut -d'=' -f2-)
    echo "${value:-$default}"
else
    echo "$default"
fi

}

Write config value

set_config() { local key="$1" local value="$2"

init_config

if grep -q "^${key}=" "$CONFIG_FILE" 2>/dev/null; then
    # Update existing
    sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG_FILE"
else
    # Add new
    echo "${key}=${value}" >> "$CONFIG_FILE"
fi

}

Load all config into associative array

load_config() { declare -gA CONFIG

# Start with defaults
for key in "${!DEFAULT_CONFIG[@]}"; do
    CONFIG[$key]="${DEFAULT_CONFIG[$key]}"
done

# Override with file values
if [[ -f "$CONFIG_FILE" ]]; then
    while IFS='=' read -r key value; do
        [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
        CONFIG[$key]="$value"
    done < "$CONFIG_FILE"
fi

}

Usage

init_config load_config echo "Parallel workers: ${CONFIG[parallel_workers]}" set_config "parallel_workers" "10"

  1. State File Operations

Track persistent state between runs:

#!/bin/bash

ABOUTME: State file operations

ABOUTME: Track last run, progress, etc.

STATE_FILE="$STATE_DIR/state.json"

Initialize state

init_state() { if [[ ! -f "$STATE_FILE" ]]; then cat > "$STATE_FILE" << EOF { "version": "1.0.0", "created": "$(date -Iseconds)", "last_run": null, "run_count": 0, "last_status": null } EOF fi }

Get state value (requires jq)

get_state() { local key="$1" local default="${2:-null}"

if [[ -f "$STATE_FILE" ]] &#x26;&#x26; command -v jq &#x26;>/dev/null; then
    jq -r ".$key // $default" "$STATE_FILE"
else
    echo "$default"
fi

}

Update state value (requires jq)

set_state() { local key="$1" local value="$2"

init_state

if command -v jq &#x26;>/dev/null; then
    local temp=$(mktemp)
    jq ".$key = $value" "$STATE_FILE" > "$temp" &#x26;&#x26; mv "$temp" "$STATE_FILE"
fi

}

Record run

record_run() { local status="$1"

set_state "last_run" "\"$(date -Iseconds)\""
set_state "last_status" "\"$status\""
set_state "run_count" "$(($(get_state run_count 0) + 1))"

}

Simple key-value state (no jq required)

STATE_KV_FILE="$STATE_DIR/state.kv"

get_state_kv() { local key="$1" local default="$2"

if [[ -f "$STATE_KV_FILE" ]]; then
    grep "^${key}=" "$STATE_KV_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default"
else
    echo "$default"
fi

}

set_state_kv() { local key="$1" local value="$2"

mkdir -p "$(dirname "$STATE_KV_FILE")"

if [[ -f "$STATE_KV_FILE" ]] &#x26;&#x26; grep -q "^${key}=" "$STATE_KV_FILE"; then
    sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_KV_FILE"
else
    echo "${key}=${value}" >> "$STATE_KV_FILE"
fi

}

  1. Cache Management

Implement caching with expiration:

#!/bin/bash

ABOUTME: Cache management with TTL

ABOUTME: Store and retrieve cached data

CACHE_TTL="${CACHE_TTL:-3600}" # 1 hour default

Get cache file path

cache_path() { local key="$1" local hash=$(echo -n "$key" | md5sum | cut -c1-16) echo "$CACHE_DIR/${hash}" }

Check if cache is valid

cache_valid() { local key="$1" local ttl="${2:-$CACHE_TTL}" local path=$(cache_path "$key")

if [[ -f "$path" ]]; then
    local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path")))
    [[ $age -lt $ttl ]]
else
    return 1
fi

}

Get from cache

cache_get() { local key="$1" local ttl="${2:-$CACHE_TTL}" local path=$(cache_path "$key")

if cache_valid "$key" "$ttl"; then
    cat "$path"
    return 0
fi
return 1

}

Set cache

cache_set() { local key="$1" local value="$2" local path=$(cache_path "$key")

mkdir -p "$CACHE_DIR"
echo "$value" > "$path"

}

Delete cache

cache_delete() { local key="$1" local path=$(cache_path "$key") rm -f "$path" }

Clear all cache

cache_clear() { rm -rf "$CACHE_DIR"/* }

Clean expired cache entries

cache_clean() { local ttl="${1:-$CACHE_TTL}" find "$CACHE_DIR" -type f -mmin "+$((ttl / 60))" -delete 2>/dev/null || true }

Usage with automatic caching

get_with_cache() { local key="$1" local command="$2" local ttl="${3:-$CACHE_TTL}"

if cache_valid "$key" "$ttl"; then
    cache_get "$key"
else
    local result
    result=$(eval "$command")
    cache_set "$key" "$result"
    echo "$result"
fi

}

Example

result=$(get_with_cache "api_response" "curl -s https://api.example.com/data" 300)

  1. Log File Management

Manage logs with rotation:

#!/bin/bash

ABOUTME: Log file management with rotation

ABOUTME: Automatic cleanup of old logs

LOG_FILE="$LOG_DIR/app.log" LOG_MAX_SIZE=$((10 * 1024 * 1024)) # 10MB LOG_MAX_FILES=5

Initialize logging

init_logging() { mkdir -p "$LOG_DIR" touch "$LOG_FILE" }

Write to log

log_to_file() { local level="$1" shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$timestamp] $level: $message" >> "$LOG_FILE"

# Check if rotation needed
maybe_rotate_logs

}

Rotate logs if needed

maybe_rotate_logs() { if [[ -f "$LOG_FILE" ]]; then local size=$(stat -c %s "$LOG_FILE" 2>/dev/null || stat -f %z "$LOG_FILE")

    if [[ $size -gt $LOG_MAX_SIZE ]]; then
        rotate_logs
    fi
fi

}

Perform log rotation

rotate_logs() { # Remove oldest rm -f "${LOG_FILE}.${LOG_MAX_FILES}"

# Shift existing
for ((i=LOG_MAX_FILES-1; i>=1; i--)); do
    if [[ -f "${LOG_FILE}.$i" ]]; then
        mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))"
    fi
done

# Rotate current
if [[ -f "$LOG_FILE" ]]; then
    mv "$LOG_FILE" "${LOG_FILE}.1"
    touch "$LOG_FILE"
fi

}

Clean old logs

clean_old_logs() { local days="${1:-30}" find "$LOG_DIR" -name ".log" -mtime "+$days" -delete 2>/dev/null || true }

View recent logs

tail_logs() { local lines="${1:-50}" tail -n "$lines" "$LOG_FILE" }

Search logs

search_logs() { local pattern="$1" grep -h "$pattern" "$LOG_DIR"/.log 2>/dev/null | tail -100 }

Complete Example: State Manager Module

#!/bin/bash

ABOUTME: Complete state directory manager

ABOUTME: Reusable module for bash scripts

─────────────────────────────────────────────────────────────────

State Directory Manager v1.0.0

─────────────────────────────────────────────────────────────────

Application identity (override in your script)

: "${STATE_APP_NAME:=my-app}"

Directory setup

STATE_BASE_DIR="${HOME}/.${STATE_APP_NAME}" STATE_CONFIG_DIR="$STATE_BASE_DIR/config" STATE_DATA_DIR="$STATE_BASE_DIR/data" STATE_CACHE_DIR="$STATE_BASE_DIR/cache" STATE_LOG_DIR="$STATE_BASE_DIR/logs" STATE_TMP_DIR="$STATE_BASE_DIR/tmp"

File paths

STATE_CONFIG_FILE="$STATE_CONFIG_DIR/config" STATE_STATE_FILE="$STATE_DATA_DIR/state" STATE_LOG_FILE="$STATE_LOG_DIR/app.log"

Settings

STATE_CACHE_TTL="${STATE_CACHE_TTL:-3600}" STATE_LOG_MAX_SIZE="${STATE_LOG_MAX_SIZE:-10485760}" STATE_LOG_MAX_FILES="${STATE_LOG_MAX_FILES:-5}"

─────────────────────────────────────────────────────────────────

Initialization

─────────────────────────────────────────────────────────────────

state_init() { local dirs=( "$STATE_CONFIG_DIR" "$STATE_DATA_DIR" "$STATE_CACHE_DIR" "$STATE_LOG_DIR" "$STATE_TMP_DIR" )

for dir in "${dirs[@]}"; do
    if [[ ! -d "$dir" ]]; then
        mkdir -p "$dir"
        chmod 700 "$dir"
    fi
done

# Initialize files
[[ -f "$STATE_CONFIG_FILE" ]] || touch "$STATE_CONFIG_FILE"
[[ -f "$STATE_STATE_FILE" ]] || touch "$STATE_STATE_FILE"
[[ -f "$STATE_LOG_FILE" ]] || touch "$STATE_LOG_FILE"

}

─────────────────────────────────────────────────────────────────

Config Functions

─────────────────────────────────────────────────────────────────

state_config_get() { local key="$1" local default="$2" grep "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" }

state_config_set() { local key="$1" local value="$2"

if grep -q "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null; then
    sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_CONFIG_FILE"
else
    echo "${key}=${value}" >> "$STATE_CONFIG_FILE"
fi

}

state_config_list() { cat "$STATE_CONFIG_FILE" 2>/dev/null | grep -v '^#' | grep -v '^$' }

─────────────────────────────────────────────────────────────────

State Functions

─────────────────────────────────────────────────────────────────

state_get() { local key="$1" local default="$2" grep "^${key}=" "$STATE_STATE_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" }

state_set() { local key="$1" local value="$2"

if grep -q "^${key}=" "$STATE_STATE_FILE" 2>/dev/null; then
    sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_STATE_FILE"
else
    echo "${key}=${value}" >> "$STATE_STATE_FILE"
fi

}

─────────────────────────────────────────────────────────────────

Cache Functions

─────────────────────────────────────────────────────────────────

state_cache_key() { echo -n "$1" | md5sum | cut -c1-16 }

state_cache_get() { local key="$1" local ttl="${2:-$STATE_CACHE_TTL}" local path="$STATE_CACHE_DIR/$(state_cache_key "$key")"

if [[ -f "$path" ]]; then
    local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path")))
    if [[ $age -lt $ttl ]]; then
        cat "$path"
        return 0
    fi
fi
return 1

}

state_cache_set() { local key="$1" local value="$2" local path="$STATE_CACHE_DIR/$(state_cache_key "$key")" echo "$value" > "$path" }

state_cache_clear() { rm -rf "$STATE_CACHE_DIR"/* }

─────────────────────────────────────────────────────────────────

Log Functions

─────────────────────────────────────────────────────────────────

state_log() { local level="$1" shift local message="$*" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $level: $message" >> "$STATE_LOG_FILE"

# Auto-rotate
local size=$(stat -c %s "$STATE_LOG_FILE" 2>/dev/null || echo 0)
if [[ $size -gt $STATE_LOG_MAX_SIZE ]]; then
    state_log_rotate
fi

}

state_log_rotate() { rm -f "${STATE_LOG_FILE}.${STATE_LOG_MAX_FILES}" for ((i=STATE_LOG_MAX_FILES-1; i>=1; i--)); do [[ -f "${STATE_LOG_FILE}.$i" ]] && mv "${STATE_LOG_FILE}.$i" "${STATE_LOG_FILE}.$((i+1))" done mv "$STATE_LOG_FILE" "${STATE_LOG_FILE}.1" touch "$STATE_LOG_FILE" }

state_log_tail() { tail -n "${1:-50}" "$STATE_LOG_FILE" }

─────────────────────────────────────────────────────────────────

Cleanup Functions

─────────────────────────────────────────────────────────────────

state_cleanup() { # Clean temp files older than 1 day find "$STATE_TMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true

# Clean expired cache
find "$STATE_CACHE_DIR" -type f -mmin "+$((STATE_CACHE_TTL / 60))" -delete 2>/dev/null || true

# Clean old logs
find "$STATE_LOG_DIR" -name "*.log.*" -mtime +30 -delete 2>/dev/null || true

}

state_reset() { rm -rf "$STATE_BASE_DIR" state_init }

─────────────────────────────────────────────────────────────────

Auto-initialize

─────────────────────────────────────────────────────────────────

state_init

Usage in Scripts

#!/bin/bash

Your script that uses the state manager

Set app name before sourcing

STATE_APP_NAME="my-tool"

Source the state manager

source /path/to/state-manager.sh

Now use it

state_config_set "api_key" "abc123" api_key=$(state_config_get "api_key")

state_set "last_run" "$(date -Iseconds)" state_log "INFO" "Script started"

Use cache

if ! result=$(state_cache_get "api_response"); then result=$(curl -s https://api.example.com/data) state_cache_set "api_response" "$result" fi

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

  • XDG Base Directory Spec

  • File Hierarchy Standard

Version History

  • 1.0.0 (2026-01-14): Initial release - extracted from workspace-hub patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

echarts

No summary provided by upstream source.

Repository SourceNeeds Review
General

pandoc

No summary provided by upstream source.

Repository SourceNeeds Review
General

mkdocs

No summary provided by upstream source.

Repository SourceNeeds Review
General

gis

No summary provided by upstream source.

Repository SourceNeeds Review