Lesson 12: Building JSON by String Concatenation¶
Files affected: scripts/workspace-presets.sh, scripts/ocws-state.sh
Severity: Medium — produces malformed JSON that silently breaks any consumer
What Happened¶
workspace-presets.sh — heredoc with array-as-JSON¶
cat > "$preset_file" << EOF
{
"keybindings": {
"common": [
"A-r": "Reconfigure", # BUG: arrays use [], not {}
"A-q": "Close", # BUG: this is object syntax inside an array
...
],
}
}
EOF
This produces invalid JSON. Arrays ([...]) hold values; objects ({...}) hold
key-value pairs. Mixing the two breaks JSON parsers. Additionally, trailing commas
after the last element ("A-a": "Execute fuzzel" followed by ]) are illegal in
JSON (valid in JavaScript but not JSON).
ocws-state.sh — manual JSON assembly via string concatenation¶
local json_pairs=""
for key in "${!pairs[@]}"; do
json_pairs="$json_pairs, \"$key\": \"${pairs[$key]}\""
done
json_pairs="{${json_pairs#\", }}" # strip leading ", " then wrap in {}
echo "$json_pairs" > "$state_file"
Problems:
1. If a value contains ", \, or newlines, the resulting JSON is malformed.
2. The #", " strip only removes the first ", — if the loop starts with an
empty json_pairs the result is {, "key": "val"} which is invalid.
3. No escaping means injection: a value of bad"value produces "key": "bad"value".
The Fix¶
Always use jq to produce JSON. It handles all escaping automatically:
# From an associative array
declare -A pairs=([artist]='AC/DC' [title]='Back "In" Black')
# Build args dynamically
jq_args=()
for key in "${!pairs[@]}"; do
jq_args+=(--arg "$key" "${pairs[$key]}")
done
# Build the jq expression
jq_expr=$(printf '"%s": $%s, ' "${!pairs[@]}" | sed 's/, $//')
jq -n "${jq_args[@]}" "{$jq_expr}" > "$state_file"
Or for fixed-structure JSON in a heredoc, use jq as the template engine:
jq -n \
--arg name "$name" \
--arg desc "$description" \
--argjson ws_num "${ws_number:-1}" \
'{
name: $name,
description: $desc,
workspace: { number: $ws_num }
}' > "$preset_file"
For the workspace keybindings, use a proper JSON object instead of an array:
The General Rule¶
Never build JSON by concatenating strings. Use
jq -nwith--arg/--argjsonfor all JSON generation. This guarantees: - Proper quoting of strings - Escaping of special characters (",\, newlines, tabs) - Structurally valid output every time
| Pattern | Problem | Fix |
|---|---|---|
echo "{\"k\": \"$v\"}" |
No escaping of $v |
jq -n --arg k "$v" '{"k": $k}' |
heredoc with $vars |
Values with " break structure |
jq -n --arg v "$var" '...' |
| String concat loop | Leading/trailing comma bugs | jq -n with dynamic args |