|
Download plugins/plugin-dev/skills/plugin-settings/references/parsing-techniques.md from SaylorTwift/claude-code: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/SaylorTwift/claude-code/resolve/main/plugins/plugin-dev/skills/plugin-settings/references/parsing-techniques.md
- Command line
-
hf download hf://SaylorTwift/claude-code/plugins/plugin-dev/skills/plugin-settings/references/parsing-techniques.md
-
curl -L -o parsing-techniques.md https://huggingface.co/SaylorTwift/claude-code/resolve/main/plugins/plugin-dev/skills/plugin-settings/references/parsing-techniques.md
11.5 kB
| # 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. | |