spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / plugin-dev / skills / plugin-settings / references / parsing-techniques.md
549 lines · 11.2 KBPreviewCodeRaw
1# Settings File Parsing Techniques
2
3Complete guide to parsing `.claude/plugin-name.local.md` files in bash scripts.
4
5## File Structure
6
7Settings files use markdown with YAML frontmatter:
8
9```markdown
10---
11field1: value1
12field2: "value with spaces"
13numeric_field: 42
14boolean_field: true
15list_field: ["item1", "item2", "item3"]
16---
17
18# Markdown Content
19
20This body content can be extracted separately.
21It's useful for prompts, documentation, or additional context.
22```
23
24## Parsing Frontmatter
25
26### Extract Frontmatter Block
27
28```bash
29#!/bin/bash
30FILE=".claude/my-plugin.local.md"
31
32# Extract everything between --- markers (excluding the markers themselves)
33FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
34```
35
36**How it works:**
37- `sed -n` - Suppress automatic printing
38- `/^---$/,/^---$/` - Range from first `---` to second `---`
39- `{ /^---$/d; p; }` - Delete the `---` lines, print everything else
40
41### Extract Individual Fields
42
43**String fields:**
44```bash
45# Simple value
46VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//')
47
48# Quoted value (removes surrounding quotes)
49VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//' | sed 's/^"\(.*\)"$/\1/')
50```
51
52**Boolean fields:**
53```bash
54ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
55
56# Use in condition
57if [[ "$ENABLED" == "true" ]]; then
58 # Enabled
59fi
60```
61
62**Numeric fields:**
63```bash
64MAX=$(echo "$FRONTMATTER" | grep '^max_value:' | sed 's/max_value: *//')
65
66# Validate it's a number
67if [[ "$MAX" =~ ^[0-9]+$ ]]; then
68 # Use in numeric comparison
69 if [[ $MAX -gt 100 ]]; then
70 # Too large
71 fi
72fi
73```
74
75**List fields (simple):**
76```bash
77# YAML: list: ["item1", "item2", "item3"]
78LIST=$(echo "$FRONTMATTER" | grep '^list:' | sed 's/list: *//')
79# Result: ["item1", "item2", "item3"]
80
81# For simple checks:
82if [[ "$LIST" == *"item1"* ]]; then
83 # List contains item1
84fi
85```
86
87**List fields (proper parsing with jq):**
88```bash
89# For proper list handling, use yq or convert to JSON
90# This requires yq to be installed (brew install yq)
91
92# Extract list as JSON array
93LIST=$(echo "$FRONTMATTER" | yq -o json '.list' 2>/dev/null)
94
95# Iterate over items
96echo "$LIST" | jq -r '.[]' | while read -r item; do
97 echo "Processing: $item"
98done
99```
100
101## Parsing Markdown Body
102
103### Extract Body Content
104
105```bash
106#!/bin/bash
107FILE=".claude/my-plugin.local.md"
108
109# Extract everything after the closing ---
110# Counts --- markers: first is opening, second is closing, everything after is body
111BODY=$(awk '/^---$/{i++; next} i>=2' "$FILE")
112```
113
114**How it works:**
115- `/^---$/` - Match `---` lines
116- `{i++; next}` - Increment counter and skip the `---` line
117- `i>=2` - Print all lines after second `---`
118
119**Handles edge case:** If `---` appears in the markdown body, it still works because we only count the first two `---` at the start.
120
121### Use Body as Prompt
122
123```bash
124# Extract body
125PROMPT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
126
127# Feed back to Claude
128echo '{"decision": "block", "reason": "'"$PROMPT"'"}' | jq .
129```
130
131**Important:** Use `jq -n --arg` for safer JSON construction with user content:
132
133```bash
134PROMPT=$(awk '/^---$/{i++; next} i>=2' "$FILE")
135
136# Safe JSON construction
137jq -n --arg prompt "$PROMPT" '{
138 "decision": "block",
139 "reason": $prompt
140}'
141```
142
143## Common Parsing Patterns
144
145### Pattern: Field with Default
146
147```bash
148VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/')
149
150# Use default if empty
151if [[ -z "$VALUE" ]]; then
152 VALUE="default_value"
153fi
154```
155
156### Pattern: Optional Field
157
158```bash
159OPTIONAL=$(echo "$FRONTMATTER" | grep '^optional_field:' | sed 's/optional_field: *//' | sed 's/^"\(.*\)"$/\1/')
160
161# Only use if present
162if [[ -n "$OPTIONAL" ]] && [[ "$OPTIONAL" != "null" ]]; then
163 # Field is set, use it
164 echo "Optional field: $OPTIONAL"
165fi
166```
167
168### Pattern: Multiple Fields at Once
169
170```bash
171# Parse all fields in one pass
172while IFS=': ' read -r key value; do
173 # Remove quotes if present
174 value=$(echo "$value" | sed 's/^"\(.*\)"$/\1/')
175
176 case "$key" in
177 enabled)
178 ENABLED="$value"
179 ;;
180 mode)
181 MODE="$value"
182 ;;
183 max_size)
184 MAX_SIZE="$value"
185 ;;
186 esac
187done <<< "$FRONTMATTER"
188```
189
190## Updating Settings Files
191
192### Atomic Updates
193
194Always use temp file + atomic move to prevent corruption:
195
196```bash
197#!/bin/bash
198FILE=".claude/my-plugin.local.md"
199NEW_VALUE="updated_value"
200
201# Create temp file
202TEMP_FILE="${FILE}.tmp.$$"
203
204# Update field using sed
205sed "s/^field_name: .*/field_name: $NEW_VALUE/" "$FILE" > "$TEMP_FILE"
206
207# Atomic replace
208mv "$TEMP_FILE" "$FILE"
209```
210
211### Update Single Field
212
213```bash
214# Increment iteration counter
215CURRENT=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
216NEXT=$((CURRENT + 1))
217
218# Update file
219TEMP_FILE="${FILE}.tmp.$$"
220sed "s/^iteration: .*/iteration: $NEXT/" "$FILE" > "$TEMP_FILE"
221mv "$TEMP_FILE" "$FILE"
222```
223
224### Update Multiple Fields
225
226```bash
227# Update several fields at once
228TEMP_FILE="${FILE}.tmp.$$"
229
230sed -e "s/^iteration: .*/iteration: $NEXT_ITERATION/" \
231 -e "s/^pr_number: .*/pr_number: $PR_NUMBER/" \
232 -e "s/^status: .*/status: $NEW_STATUS/" \
233 "$FILE" > "$TEMP_FILE"
234
235mv "$TEMP_FILE" "$FILE"
236```
237
238## Validation Techniques
239
240### Validate File Exists and Is Readable
241
242```bash
243FILE=".claude/my-plugin.local.md"
244
245if [[ ! -f "$FILE" ]]; then
246 echo "Settings file not found" >&2
247 exit 1
248fi
249
250if [[ ! -r "$FILE" ]]; then
251 echo "Settings file not readable" >&2
252 exit 1
253fi
254```
255
256### Validate Frontmatter Structure
257
258```bash
259# Count --- markers (should be exactly 2 at start)
260MARKER_COUNT=$(grep -c '^---$' "$FILE" 2>/dev/null || echo "0")
261
262if [[ $MARKER_COUNT -lt 2 ]]; then
263 echo "Invalid settings file: missing frontmatter markers" >&2
264 exit 1
265fi
266```
267
268### Validate Field Values
269
270```bash
271MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//')
272
273case "$MODE" in
274 strict|standard|lenient)
275 # Valid mode
276 ;;
277 *)
278 echo "Invalid mode: $MODE (must be strict, standard, or lenient)" >&2
279 exit 1
280 ;;
281esac
282```
283
284### Validate Numeric Ranges
285
286```bash
287MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
288
289if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
290 echo "max_size must be a number" >&2
291 exit 1
292fi
293
294if [[ $MAX_SIZE -lt 1 ]] || [[ $MAX_SIZE -gt 10000000 ]]; then
295 echo "max_size out of range (1-10000000)" >&2
296 exit 1
297fi
298```
299
300## Edge Cases and Gotchas
301
302### Quotes in Values
303
304YAML allows both quoted and unquoted strings:
305
306```yaml
307# These are equivalent:
308field1: value
309field2: "value"
310field3: 'value'
311```
312
313**Handle both:**
314```bash
315# Remove surrounding quotes if present
316VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\\(.*\\)'$/\\1/")
317```
318
319### --- in Markdown Body
320
321If the markdown body contains `---`, the parsing still works because we only match the first two:
322
323```markdown
324---
325field: value
326---
327
328# Body
329
330Here's a separator:
331---
332
333More content after the separator.
334```
335
336The `awk '/^---$/{i++; next} i>=2'` pattern handles this correctly.
337
338### Empty Values
339
340Handle missing or empty fields:
341
342```yaml
343field1:
344field2: ""
345field3: null
346```
347
348**Parsing:**
349```bash
350VALUE=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
351# VALUE will be empty string
352
353# Check for empty/null
354if [[ -z "$VALUE" ]] || [[ "$VALUE" == "null" ]]; then
355 VALUE="default"
356fi
357```
358
359### Special Characters
360
361Values with special characters need careful handling:
362
363```yaml
364message: "Error: Something went wrong!"
365path: "/path/with spaces/file.txt"
366regex: "^[a-zA-Z0-9_]+$"
367```
368
369**Safe parsing:**
370```bash
371# Always quote variables when using
372MESSAGE=$(echo "$FRONTMATTER" | grep '^message:' | sed 's/message: *//' | sed 's/^"\(.*\)"$/\1/')
373
374echo "Message: $MESSAGE" # Quoted!
375```
376
377## Performance Optimization
378
379### Cache Parsed Values
380
381If reading settings multiple times:
382
383```bash
384# Parse once
385FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
386
387# Extract multiple fields from cached frontmatter
388FIELD1=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
389FIELD2=$(echo "$FRONTMATTER" | grep '^field2:' | sed 's/field2: *//')
390FIELD3=$(echo "$FRONTMATTER" | grep '^field3:' | sed 's/field3: *//')
391```
392
393**Don't:** Re-parse file for each field.
394
395### Lazy Loading
396
397Only parse settings when needed:
398
399```bash
400#!/bin/bash
401input=$(cat)
402
403# Quick checks first (no file I/O)
404tool_name=$(echo "$input" | jq -r '.tool_name')
405if [[ "$tool_name" != "Write" ]]; then
406 exit 0 # Not a write operation, skip
407fi
408
409# Only now check settings file
410if [[ -f ".claude/my-plugin.local.md" ]]; then
411 # Parse settings
412 # ...
413fi
414```
415
416## Debugging
417
418### Print Parsed Values
419
420```bash
421#!/bin/bash
422set -x # Enable debug tracing
423
424FILE=".claude/my-plugin.local.md"
425
426if [[ -f "$FILE" ]]; then
427 echo "Settings file found" >&2
428
429 FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
430 echo "Frontmatter:" >&2
431 echo "$FRONTMATTER" >&2
432
433 ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
434 echo "Enabled: $ENABLED" >&2
435fi
436```
437
438### Validate Parsing
439
440```bash
441# Show what was parsed
442echo "Parsed values:" >&2
443echo " enabled: $ENABLED" >&2
444echo " mode: $MODE" >&2
445echo " max_size: $MAX_SIZE" >&2
446
447# Verify expected values
448if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
449 echo "⚠️ Unexpected enabled value: $ENABLED" >&2
450fi
451```
452
453## Alternative: Using yq
454
455For complex YAML, consider using `yq`:
456
457```bash
458# Install: brew install yq
459
460# Parse YAML properly
461FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
462
463# Extract fields with yq
464ENABLED=$(echo "$FRONTMATTER" | yq '.enabled')
465MODE=$(echo "$FRONTMATTER" | yq '.mode')
466LIST=$(echo "$FRONTMATTER" | yq -o json '.list_field')
467
468# Iterate list properly
469echo "$LIST" | jq -r '.[]' | while read -r item; do
470 echo "Item: $item"
471done
472```
473
474**Pros:**
475- Proper YAML parsing
476- Handles complex structures
477- Better list/object support
478
479**Cons:**
480- Requires yq installation
481- Additional dependency
482- May not be available on all systems
483
484**Recommendation:** Use sed/grep for simple fields, yq for complex structures.
485
486## Complete Example
487
488```bash
489#!/bin/bash
490set -euo pipefail
491
492# Configuration
493SETTINGS_FILE=".claude/my-plugin.local.md"
494
495# Quick exit if not configured
496if [[ ! -f "$SETTINGS_FILE" ]]; then
497 # Use defaults
498 ENABLED=true
499 MODE=standard
500 MAX_SIZE=1000000
501else
502 # Parse frontmatter
503 FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS_FILE")
504
505 # Extract fields with defaults
506 ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
507 ENABLED=${ENABLED:-true}
508
509 MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//' | sed 's/^"\(.*\)"$/\1/')
510 MODE=${MODE:-standard}
511
512 MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
513 MAX_SIZE=${MAX_SIZE:-1000000}
514
515 # Validate values
516 if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
517 echo "⚠️ Invalid enabled value, using default" >&2
518 ENABLED=true
519 fi
520
521 if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
522 echo "⚠️ Invalid max_size, using default" >&2
523 MAX_SIZE=1000000
524 fi
525fi
526
527# Quick exit if disabled
528if [[ "$ENABLED" != "true" ]]; then
529 exit 0
530fi
531
532# Use configuration
533echo "Configuration loaded: mode=$MODE, max_size=$MAX_SIZE" >&2
534
535# Apply logic based on settings
536case "$MODE" in
537 strict)
538 # Strict validation
539 ;;
540 standard)
541 # Standard validation
542 ;;
543 lenient)
544 # Lenient validation
545 ;;
546esac
547```
548
549This provides robust settings handling with defaults, validation, and error recovery.