yq — Gotchas
Dotted Keys
This is the most common yq trap. yq interprets dots as path separators by default.
cat <<'YAML' > /tmp/hosts.yml
hosts:
gitlab.com:
token: ghp_xxxx
protocol: https
YAML
# WRONG — yq traverses hosts → gitlab → com
yq '.hosts.gitlab.com.token' /tmp/hosts.yml
# Output: null (wrong path)
# CORRECT — bracket notation preserves the literal key
yq '.hosts["gitlab.com"].token' /tmp/hosts.yml
# Output: ghp_xxxx
Rule: any key containing ., /, -, or special characters needs bracket notation.
Hyphens in antora.yml attributes: ["ise-ip"] not .ise-ip.
yq '.asciidoc.attributes["attribute-missing"]' docs/antora.yml
# Output: warn@
# Confirm it works before running on credential files
!!null Tag
YAML’s !!null tag explicitly sets a value to null.
It is NOT the same as a missing key.
cat <<'YAML' > /tmp/test.yml
active: true
deprecated: !!null
name: test
version: ~
YAML
yq -r 'to_entries[] | select(.value == null) | .key' /tmp/test.yml
# Output:
# deprecated
# version
Both !!null and ~ produce null.
has("deprecated") returns true — the key exists, it just holds null.
cat <<'YAML' > /tmp/config.yml
hosts:
github.com:
oauth_token: !!null
git_protocol: ssh
YAML
yq '.hosts["github.com"].oauth_token' /tmp/config.yml
# Output: null
# Check if it is explicitly null vs missing
yq '.hosts["github.com"] | has("oauth_token")' /tmp/config.yml
# Output: true (key exists but is null — this is intentional)
If oauth_token shows !!null, it was deliberately cleared.
Remove the key entirely if it is truly unwanted: yq -i 'del(.hosts["github.com"].oauth_token)' config.yml.
String vs Number
YAML autodetects types. Unquoted numbers become integers; quoted ones stay strings.
cat <<'YAML' > /tmp/types.yml
port_number: 443
port_string: "443"
version: 1.0
flag: true
YAML
yq '.port_number | type' /tmp/types.yml
# Output: !!int
yq '.port_string | type' /tmp/types.yml
# Output: !!str
yq '.version | type' /tmp/types.yml
# Output: !!float
yq '.flag | type' /tmp/types.yml
# Output: !!bool
yq '.asciidoc.attributes["port-https"] | type' docs/antora.yml
# Output: !!str
# Antora attributes are ALWAYS strings — quoting ports prevents
# YAML from parsing "443" as integer 443, which Antora would reject.
cat <<'YAML' > /tmp/norway.yml
countries:
- name: Norway
code: NO
- name: Sweden
code: SE
YAML
yq '.countries[0].code' /tmp/norway.yml
# Output: false (YAML 1.1 interprets NO as boolean false!)
# Fix: quote it
cat <<'YAML' > /tmp/norway-fixed.yml
countries:
- name: Norway
code: "NO"
- name: Sweden
code: "SE"
YAML
yq '.countries[0].code' /tmp/norway-fixed.yml
# Output: "NO"
YAML 1.1 treats yes, no, on, off, y, n as booleans.
Always quote values that might collide.
yq vs jq Differences
# yq handles both
yq '.name' docs/antora.yml # YAML — works
echo '{"name":"test"}' | yq '.name' # JSON — works
# jq handles only JSON
echo '{"name":"test"}' | jq '.name' # JSON — works
jq '.name' docs/antora.yml # YAML — parse error
yq -o json '.' docs/antora.yml | jq '.asciidoc.attributes | keys | length'
# Use yq to convert YAML→JSON, then jq for complex JSON processing
# yq uses the same expression language as jq, but:
# 1. yq preserves YAML comments; jq does not handle comments
# 2. yq supports YAML tags (!!str, !!int); jq has no equivalent
# 3. yq's -i flag edits YAML in place; jq has no -i flag (use sponge or temp file)
# 4. yq can output YAML, JSON, XML, CSV, TSV, props; jq outputs only JSON
Credential Safety
# WRONG — exposes all tokens
yq '.' ~/.config/gh/hosts.yml
# CORRECT — inspect keys only
yq 'keys' ~/.config/gh/hosts.yml 2>/dev/null
# CORRECT — test path syntax on safe fields
yq -r '.hosts["github.com"].git_protocol' ~/.config/gh/hosts.yml 2>/dev/null
# 1. List top-level structure
yq 'keys' file.yml
# 2. Check specific non-secret fields
yq '.field_name | type' file.yml
# 3. Never pipe credential files to other commands
# 4. Never redirect credential file output to disk
YAML Anchors and Aliases
cat <<'YAML' > /tmp/anchors.yml
defaults: &defaults
adapter: postgres
host: localhost
port: 5432
development:
<<: *defaults
database: dev_db
production:
<<: *defaults
database: prod_db
host: db.prod.internal
YAML
yq '.production' /tmp/anchors.yml
# Output:
# adapter: postgres
# host: db.prod.internal
# port: 5432
# database: prod_db
yq resolves anchors on read. The <<: *defaults merge key injects the anchor’s content.
Production overrides host — the last definition wins.
cat <<'YAML' > /tmp/anchors.yml
defaults: &defaults
timeout: 30
service_a:
<<: *defaults
name: alpha
service_b:
<<: *defaults
name: beta
YAML
yq '.. | select(anchor != "")' /tmp/anchors.yml 2>/dev/null
# Inspects anchor metadata — useful for auditing shared config
Multiline Strings
cat <<'YAML' > /tmp/multi.yml
script: |
#!/bin/bash
echo "line 1"
echo "line 2"
YAML
yq -r '.script' /tmp/multi.yml
# Output:
# #!/bin/bash
# echo "line 1"
# echo "line 2"
cat <<'YAML' > /tmp/multi.yml
description: >
This is a long
description that
gets folded into
one line.
YAML
yq -r '.description' /tmp/multi.yml
# Output: This is a long description that gets folded into one line.
The | and > indicators control how yq interprets whitespace.
Be aware of trailing newlines: | preserves them, |- strips the trailing newline.