mirror of
https://github.com/lucas-labs/claude-plugins.git
synced 2026-07-18 23:16:51 -03:00
550 lines
11 KiB
Markdown
550 lines
11 KiB
Markdown
# Settings File Parsing Techniques
|
||
|
||
Complete guide to parsing `.claude/plugin-name.local.md` files in bash scripts.
|
||
|
||
## File Structure
|
||
|
||
Settings files use markdown with YAML frontmatter:
|
||
|
||
```markdown
|
||
---
|
||
field1: value1
|
||
field2: "value with spaces"
|
||
numeric_field: 42
|
||
boolean_field: true
|
||
list_field: ["item1", "item2", "item3"]
|
||
---
|
||
|
||
# Markdown Content
|
||
|
||
This body content can be extracted separately.
|
||
It's useful for prompts, documentation, or additional context.
|
||
```
|
||
|
||
## Parsing Frontmatter
|
||
|
||
### Extract Frontmatter Block
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
FILE=".claude/my-plugin.local.md"
|
||
|
||
# Extract everything between --- markers (excluding the markers themselves)
|
||
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
|
||
```
|
||
|
||
**How it works:**
|
||
- `sed -n` - Suppress automatic printing
|
||
- `/^---$/,/^---$/` - Range from first `---` to second `---`
|
||
- `{ /^---$/d; p; }` - Delete the `---` lines, print everything else
|
||
|
||
### Extract Individual Fields
|
||
|
||
**String fields:**
|
||
```bash
|
||
# Simple value
|
||
VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//')
|
||
|
||
# Quoted value (removes surrounding quotes)
|
||
VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//' | sed 's/^"\(.*\)"$/\1/')
|
||
```
|
||
|
||
**Boolean fields:**
|
||
```bash
|
||
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
|
||
|
||
# Use in condition
|
||
if [[ "$ENABLED" == "true" ]]; then
|
||
# Enabled
|
||
fi
|
||
```
|
||
|
||
**Numeric fields:**
|
||
```bash
|
||
MAX=$(echo "$FRONTMATTER" | grep '^max_value:' | sed 's/max_value: *//')
|
||
|
||
# Validate it's a number
|
||
if [[ "$MAX" =~ ^[0-9]+$ ]]; then
|
||
# Use in numeric comparison
|
||
if [[ $MAX -gt 100 ]]; then
|
||
# Too large
|
||
fi
|
||
fi
|
||
```
|
||
|
||
**List fields (simple):**
|
||
```bash
|
||
# YAML: list: ["item1", "item2", "item3"]
|
||
LIST=$(echo "$FRONTMATTER" | grep '^list:' | sed 's/list: *//')
|
||
# Result: ["item1", "item2", "item3"]
|
||
|
||
# For simple checks:
|
||
if [[ "$LIST" == *"item1"* ]]; then
|
||
# List contains item1
|
||
fi
|
||
```
|
||
|
||
**List fields (proper parsing with jq):**
|
||
```bash
|
||
# For proper list handling, use yq or convert to JSON
|
||
# This requires yq to be installed (brew install yq)
|
||
|
||
# Extract list as JSON array
|
||
LIST=$(echo "$FRONTMATTER" | yq -o json '.list' 2>/dev/null)
|
||
|
||
# Iterate over items
|
||
echo "$LIST" | jq -r '.[]' | while read -r item; do
|
||
echo "Processing: $item"
|
||
done
|
||
```
|
||
|
||
## Parsing Markdown Body
|
||
|
||
### Extract Body Content
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
FILE=".claude/my-plugin.local.md"
|
||
|
||
# Extract everything after the closing ---
|
||
# Counts --- markers: first is opening, second is closing, everything after is body
|
||
BODY=$(awk '/^---$/{i++; next} i>=2' "$FILE")
|
||
```
|
||
|
||
**How it works:**
|
||
- `/^---$/` - Match `---` lines
|
||
- `{i++; next}` - Increment counter and skip the `---` line
|
||
- `i>=2` - Print all lines after second `---`
|
||
|
||
**Handles edge case:** If `---` appears in the markdown body, it still works because we only count the first two `---` at the start.
|
||
|
||
### Use Body as Prompt
|
||
|
||
```bash
|
||
# Extract body
|
||
PROMPT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
|
||
|
||
# Feed back to Claude
|
||
echo '{"decision": "block", "reason": "'"$PROMPT"'"}' | jq .
|
||
```
|
||
|
||
**Important:** Use `jq -n --arg` for safer JSON construction with user content:
|
||
|
||
```bash
|
||
PROMPT=$(awk '/^---$/{i++; next} i>=2' "$FILE")
|
||
|
||
# Safe JSON construction
|
||
jq -n --arg prompt "$PROMPT" '{
|
||
"decision": "block",
|
||
"reason": $prompt
|
||
}'
|
||
```
|
||
|
||
## Common Parsing Patterns
|
||
|
||
### Pattern: Field with Default
|
||
|
||
```bash
|
||
VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/')
|
||
|
||
# Use default if empty
|
||
if [[ -z "$VALUE" ]]; then
|
||
VALUE="default_value"
|
||
fi
|
||
```
|
||
|
||
### Pattern: Optional Field
|
||
|
||
```bash
|
||
OPTIONAL=$(echo "$FRONTMATTER" | grep '^optional_field:' | sed 's/optional_field: *//' | sed 's/^"\(.*\)"$/\1/')
|
||
|
||
# Only use if present
|
||
if [[ -n "$OPTIONAL" ]] && [[ "$OPTIONAL" != "null" ]]; then
|
||
# Field is set, use it
|
||
echo "Optional field: $OPTIONAL"
|
||
fi
|
||
```
|
||
|
||
### Pattern: Multiple Fields at Once
|
||
|
||
```bash
|
||
# Parse all fields in one pass
|
||
while IFS=': ' read -r key value; do
|
||
# Remove quotes if present
|
||
value=$(echo "$value" | sed 's/^"\(.*\)"$/\1/')
|
||
|
||
case "$key" in
|
||
enabled)
|
||
ENABLED="$value"
|
||
;;
|
||
mode)
|
||
MODE="$value"
|
||
;;
|
||
max_size)
|
||
MAX_SIZE="$value"
|
||
;;
|
||
esac
|
||
done <<< "$FRONTMATTER"
|
||
```
|
||
|
||
## Updating Settings Files
|
||
|
||
### Atomic Updates
|
||
|
||
Always use temp file + atomic move to prevent corruption:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
FILE=".claude/my-plugin.local.md"
|
||
NEW_VALUE="updated_value"
|
||
|
||
# Create temp file
|
||
TEMP_FILE="${FILE}.tmp.$$"
|
||
|
||
# Update field using sed
|
||
sed "s/^field_name: .*/field_name: $NEW_VALUE/" "$FILE" > "$TEMP_FILE"
|
||
|
||
# Atomic replace
|
||
mv "$TEMP_FILE" "$FILE"
|
||
```
|
||
|
||
### Update Single Field
|
||
|
||
```bash
|
||
# Increment iteration counter
|
||
CURRENT=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
|
||
NEXT=$((CURRENT + 1))
|
||
|
||
# Update file
|
||
TEMP_FILE="${FILE}.tmp.$$"
|
||
sed "s/^iteration: .*/iteration: $NEXT/" "$FILE" > "$TEMP_FILE"
|
||
mv "$TEMP_FILE" "$FILE"
|
||
```
|
||
|
||
### Update Multiple Fields
|
||
|
||
```bash
|
||
# Update several fields at once
|
||
TEMP_FILE="${FILE}.tmp.$$"
|
||
|
||
sed -e "s/^iteration: .*/iteration: $NEXT_ITERATION/" \
|
||
-e "s/^pr_number: .*/pr_number: $PR_NUMBER/" \
|
||
-e "s/^status: .*/status: $NEW_STATUS/" \
|
||
"$FILE" > "$TEMP_FILE"
|
||
|
||
mv "$TEMP_FILE" "$FILE"
|
||
```
|
||
|
||
## Validation Techniques
|
||
|
||
### Validate File Exists and Is Readable
|
||
|
||
```bash
|
||
FILE=".claude/my-plugin.local.md"
|
||
|
||
if [[ ! -f "$FILE" ]]; then
|
||
echo "Settings file not found" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -r "$FILE" ]]; then
|
||
echo "Settings file not readable" >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
### Validate Frontmatter Structure
|
||
|
||
```bash
|
||
# Count --- markers (should be exactly 2 at start)
|
||
MARKER_COUNT=$(grep -c '^---$' "$FILE" 2>/dev/null || echo "0")
|
||
|
||
if [[ $MARKER_COUNT -lt 2 ]]; then
|
||
echo "Invalid settings file: missing frontmatter markers" >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
### Validate Field Values
|
||
|
||
```bash
|
||
MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//')
|
||
|
||
case "$MODE" in
|
||
strict|standard|lenient)
|
||
# Valid mode
|
||
;;
|
||
*)
|
||
echo "Invalid mode: $MODE (must be strict, standard, or lenient)" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
```
|
||
|
||
### Validate Numeric Ranges
|
||
|
||
```bash
|
||
MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
|
||
|
||
if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
|
||
echo "max_size must be a number" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ $MAX_SIZE -lt 1 ]] || [[ $MAX_SIZE -gt 10000000 ]]; then
|
||
echo "max_size out of range (1-10000000)" >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
## Edge Cases and Gotchas
|
||
|
||
### Quotes in Values
|
||
|
||
YAML allows both quoted and unquoted strings:
|
||
|
||
```yaml
|
||
# These are equivalent:
|
||
field1: value
|
||
field2: "value"
|
||
field3: 'value'
|
||
```
|
||
|
||
**Handle both:**
|
||
```bash
|
||
# Remove surrounding quotes if present
|
||
VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\\(.*\\)'$/\\1/")
|
||
```
|
||
|
||
### --- in Markdown Body
|
||
|
||
If the markdown body contains `---`, the parsing still works because we only match the first two:
|
||
|
||
```markdown
|
||
---
|
||
field: value
|
||
---
|
||
|
||
# Body
|
||
|
||
Here's a separator:
|
||
---
|
||
|
||
More content after the separator.
|
||
```
|
||
|
||
The `awk '/^---$/{i++; next} i>=2'` pattern handles this correctly.
|
||
|
||
### Empty Values
|
||
|
||
Handle missing or empty fields:
|
||
|
||
```yaml
|
||
field1:
|
||
field2: ""
|
||
field3: null
|
||
```
|
||
|
||
**Parsing:**
|
||
```bash
|
||
VALUE=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
|
||
# VALUE will be empty string
|
||
|
||
# Check for empty/null
|
||
if [[ -z "$VALUE" ]] || [[ "$VALUE" == "null" ]]; then
|
||
VALUE="default"
|
||
fi
|
||
```
|
||
|
||
### Special Characters
|
||
|
||
Values with special characters need careful handling:
|
||
|
||
```yaml
|
||
message: "Error: Something went wrong!"
|
||
path: "/path/with spaces/file.txt"
|
||
regex: "^[a-zA-Z0-9_]+$"
|
||
```
|
||
|
||
**Safe parsing:**
|
||
```bash
|
||
# Always quote variables when using
|
||
MESSAGE=$(echo "$FRONTMATTER" | grep '^message:' | sed 's/message: *//' | sed 's/^"\(.*\)"$/\1/')
|
||
|
||
echo "Message: $MESSAGE" # Quoted!
|
||
```
|
||
|
||
## Performance Optimization
|
||
|
||
### Cache Parsed Values
|
||
|
||
If reading settings multiple times:
|
||
|
||
```bash
|
||
# Parse once
|
||
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
|
||
|
||
# Extract multiple fields from cached frontmatter
|
||
FIELD1=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
|
||
FIELD2=$(echo "$FRONTMATTER" | grep '^field2:' | sed 's/field2: *//')
|
||
FIELD3=$(echo "$FRONTMATTER" | grep '^field3:' | sed 's/field3: *//')
|
||
```
|
||
|
||
**Don't:** Re-parse file for each field.
|
||
|
||
### Lazy Loading
|
||
|
||
Only parse settings when needed:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
input=$(cat)
|
||
|
||
# Quick checks first (no file I/O)
|
||
tool_name=$(echo "$input" | jq -r '.tool_name')
|
||
if [[ "$tool_name" != "Write" ]]; then
|
||
exit 0 # Not a write operation, skip
|
||
fi
|
||
|
||
# Only now check settings file
|
||
if [[ -f ".claude/my-plugin.local.md" ]]; then
|
||
# Parse settings
|
||
# ...
|
||
fi
|
||
```
|
||
|
||
## Debugging
|
||
|
||
### Print Parsed Values
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
set -x # Enable debug tracing
|
||
|
||
FILE=".claude/my-plugin.local.md"
|
||
|
||
if [[ -f "$FILE" ]]; then
|
||
echo "Settings file found" >&2
|
||
|
||
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
|
||
echo "Frontmatter:" >&2
|
||
echo "$FRONTMATTER" >&2
|
||
|
||
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
|
||
echo "Enabled: $ENABLED" >&2
|
||
fi
|
||
```
|
||
|
||
### Validate Parsing
|
||
|
||
```bash
|
||
# Show what was parsed
|
||
echo "Parsed values:" >&2
|
||
echo " enabled: $ENABLED" >&2
|
||
echo " mode: $MODE" >&2
|
||
echo " max_size: $MAX_SIZE" >&2
|
||
|
||
# Verify expected values
|
||
if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
|
||
echo "⚠️ Unexpected enabled value: $ENABLED" >&2
|
||
fi
|
||
```
|
||
|
||
## Alternative: Using yq
|
||
|
||
For complex YAML, consider using `yq`:
|
||
|
||
```bash
|
||
# Install: brew install yq
|
||
|
||
# Parse YAML properly
|
||
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
|
||
|
||
# Extract fields with yq
|
||
ENABLED=$(echo "$FRONTMATTER" | yq '.enabled')
|
||
MODE=$(echo "$FRONTMATTER" | yq '.mode')
|
||
LIST=$(echo "$FRONTMATTER" | yq -o json '.list_field')
|
||
|
||
# Iterate list properly
|
||
echo "$LIST" | jq -r '.[]' | while read -r item; do
|
||
echo "Item: $item"
|
||
done
|
||
```
|
||
|
||
**Pros:**
|
||
- Proper YAML parsing
|
||
- Handles complex structures
|
||
- Better list/object support
|
||
|
||
**Cons:**
|
||
- Requires yq installation
|
||
- Additional dependency
|
||
- May not be available on all systems
|
||
|
||
**Recommendation:** Use sed/grep for simple fields, yq for complex structures.
|
||
|
||
## Complete Example
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# Configuration
|
||
SETTINGS_FILE=".claude/my-plugin.local.md"
|
||
|
||
# Quick exit if not configured
|
||
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
||
# Use defaults
|
||
ENABLED=true
|
||
MODE=standard
|
||
MAX_SIZE=1000000
|
||
else
|
||
# Parse frontmatter
|
||
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS_FILE")
|
||
|
||
# Extract fields with defaults
|
||
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
|
||
ENABLED=${ENABLED:-true}
|
||
|
||
MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//' | sed 's/^"\(.*\)"$/\1/')
|
||
MODE=${MODE:-standard}
|
||
|
||
MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
|
||
MAX_SIZE=${MAX_SIZE:-1000000}
|
||
|
||
# Validate values
|
||
if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
|
||
echo "⚠️ Invalid enabled value, using default" >&2
|
||
ENABLED=true
|
||
fi
|
||
|
||
if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
|
||
echo "⚠️ Invalid max_size, using default" >&2
|
||
MAX_SIZE=1000000
|
||
fi
|
||
fi
|
||
|
||
# Quick exit if disabled
|
||
if [[ "$ENABLED" != "true" ]]; then
|
||
exit 0
|
||
fi
|
||
|
||
# Use configuration
|
||
echo "Configuration loaded: mode=$MODE, max_size=$MAX_SIZE" >&2
|
||
|
||
# Apply logic based on settings
|
||
case "$MODE" in
|
||
strict)
|
||
# Strict validation
|
||
;;
|
||
standard)
|
||
# Standard validation
|
||
;;
|
||
lenient)
|
||
# Lenient validation
|
||
;;
|
||
esac
|
||
```
|
||
|
||
This provides robust settings handling with defaults, validation, and error recovery.
|