| name | state-directory-manager |
| version | 1.0.0 |
| description | Manage persistent state directories for bash scripts |
| author | workspace-hub |
| category | bash |
| tags | ["bash","state","persistence","config","directory","xdg"] |
| platforms | ["linux","macos"] |
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
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}"
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"
init_directories() {
mkdir -p "$CONFIG_DIR"
mkdir -p "$DATA_DIR"
mkdir -p "$STATE_DIR"
mkdir -p "$CACHE_DIR"
mkdir -p "$LOG_DIR"
}
2. Workspace-Hub Pattern
Alternative using home directory (from workspace-hub scripts):
#!/bin/bash
APP_NAME="workspace-hub"
APP_DIR="${HOME}/.${APP_NAME}"
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"
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"
fi
done
}
() {
find - f -mtime +1 -delete 2>/dev/null ||
}
3. Configuration File Management
Read and write configuration files:
#!/bin/bash
CONFIG_FILE="$CONFIG_DIR/config"
declare -A DEFAULT_CONFIG=(
["parallel_workers"]="5"
["log_level"]="INFO"
["auto_sync"]="true"
["timeout"]="30"
)
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
}
get_config() {
local key="$1"
local default="${2:-}"
[[ -f ]];
value
value=$(grep 2>/dev/null | -d -f2-)
}
() {
key=
value=
init_config
grep -q 2>/dev/null;
sed -i
>>
}
() {
-gA CONFIG
key ;
CONFIG[]=
[[ -f ]];
IFS= -r key value;
[[ =~ ^#.*$ || -z ]] &&
CONFIG[]=
<
}
init_config
load_config
set_config
4. State File Operations
Track persistent state between runs:
#!/bin/bash
STATE_FILE="$STATE_DIR/state.json"
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() {
local key="$1"
local default="${2:-null}"
if [[ -f "$STATE_FILE" ]] && command -v jq &>/dev/null; then
jq -r ".$key // $default" "$STATE_FILE"
else
echo "$default"
fi
}
set_state() {
local key="$1"
local value="$2"
init_state
if -v jq &>/dev/null;
temp=$()
jq > &&
}
() {
status=
set_state
set_state
set_state
}
STATE_KV_FILE=
() {
key=
default=
[[ -f ]];
grep 2>/dev/null | -d -f2- ||
}
() {
key=
value=
-p
[[ -f ]] && grep -q ;
sed -i
>>
}
5. Cache Management
Implement caching with expiration:
#!/bin/bash
CACHE_TTL="${CACHE_TTL:-3600}"
cache_path() {
local key="$1"
local hash=$(echo -n "$key" | md5sum | cut -c1-16)
echo "$CACHE_DIR/${hash}"
}
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
}
cache_get() {
local key=
ttl=
path=$(cache_path )
cache_valid ;
0
1
}
() {
key=
value=
path=$(cache_path )
-p
>
}
() {
key=
path=$(cache_path )
-f
}
() {
-rf /*
}
() {
ttl=
find - f -mmin -delete 2>/dev/null ||
}
() {
key=
=
ttl=
cache_valid ;
cache_get
result
result=$( )
cache_set
}
result=$(get_with_cache 300)
6. Log File Management
Manage logs with rotation:
#!/bin/bash
LOG_FILE="$LOG_DIR/app.log"
LOG_MAX_SIZE=$((10 * 1024 * 1024))
LOG_MAX_FILES=5
init_logging() {
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
}
log_to_file() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $level: $message" >> "$LOG_FILE"
maybe_rotate_logs
}
maybe_rotate_logs() {
if [[ -f "$LOG_FILE" ]]; then
local size=$(stat -c %s "$LOG_FILE" 2>/dev/null || stat -f %z "$LOG_FILE")
[[ -gt ]];
rotate_logs
}
() {
-f
((i=LOG_MAX_FILES-; i>=; i--));
[[ -f ]];
[[ -f ]];
}
() {
days=
find -name -mtime -delete 2>/dev/null ||
}
() {
lines=
-n
}
() {
pattern=
grep -h /*.* 2>/dev/null | -100
}
Complete Example: State Manager Module
#!/bin/bash
: "${STATE_APP_NAME:=my-app}"
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"
STATE_CONFIG_FILE="$STATE_CONFIG_DIR/config"
STATE_STATE_FILE="$STATE_DATA_DIR/state"
STATE_LOG_FILE="$STATE_LOG_DIR/app.log"
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}"
state_init() {
local dirs=(
)
;
[[ ! -d ]];
-p
700
[[ -f ]] ||
[[ -f ]] ||
[[ -f ]] ||
}
() {
key=
default=
grep 2>/dev/null | -d -f2- ||
}
() {
key=
value=
grep -q 2>/dev/null;
sed -i
>>
}
() {
2>/dev/null | grep -v | grep -v
}
() {
key=
default=
grep 2>/dev/null | -d -f2- ||
}
() {
key=
value=
grep -q 2>/dev/null;
sed -i
>>
}
() {
-n | | -c1-16
}
() {
key=
ttl=
path=
[[ -f ]];
age=$(($(date +%s) - $(stat -c %Y "" >/dev/null || stat -f %m "")))
[[ -lt ]];
0
1
}
() {
key=
value=
path=
>
}
() {
-rf /*
}
() {
level=
message=
>>
size=$( -c %s 2>/dev/null || 0)
[[ -gt ]];
state_log_rotate
}
() {
-f
((i=STATE_LOG_MAX_FILES-; i>=; i--));
[[ -f ]] &&
}
() {
-n
}
() {
find - f -mtime +1 -delete 2>/dev/null ||
find - f -mmin -delete 2>/dev/null ||
find -name -mtime +30 -delete 2>/dev/null ||
}
() {
-rf
state_init
}
state_init
Usage in Scripts
#!/bin/bash
STATE_APP_NAME="my-tool"
source /path/to/state-manager.sh
state_config_set "api_key" "abc123"
api_key=$(state_config_get "api_key")
state_set "last_run" "$(date -Iseconds)"
state_log "INFO" "Script started"
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
Version History
- 1.0.0 (2026-01-14): Initial release - extracted from workspace-hub patterns