# 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.