Bash · JSON · YAML · .env · jq · yq
JSON, YAML und .env-Dateien in Shell-Workflows kombinieren
jq, yq, dotenv und sichere Export-Patterns für Bash

Moderne Infrastruktur liefert Konfiguration in JSON, YAML und .env-Dateien gleichzeitig. Wer diese Formate in Shell-Workflows zusammenführt, braucht robuste Lade-, Validierungs- und Export-Patterns – sonst werden fehlende Felder, unsichere Variablen und Format-Inkonsistenzen zum stillen Risiko in Deployment-Pipelines.

15 Min. Lesezeit jq · yq · dotenv · Export · Validierung Bash 4.x · 5.x · Linux · CI/CD

1. Warum Konfigurationsformate im Shell-Workflow zusammenwachsen

In der modernen Infrastruktur existieren JSON, YAML und .env-Dateien selten allein. Eine typische Deployment-Pipeline liest Service-Konfiguration aus einer config.yaml, lädt Umgebungsvariablen aus einer .env-Datei und wertet API-Antworten als JSON aus – alles im selben Shell-Skript. Dieses Zusammenwachsen ist kein Zufall, sondern eine Konsequenz aus der Diversität moderner Toolketten: Terraform spricht HCL und JSON, Kubernetes YAML, Docker Compose YAML, die meisten APIs JSON, und Legacy-Deployments setzen auf .env-Dateien. Wer diese Formate in einem Shell-Workflow zusammenführen will, braucht verlässliche Werkzeuge und klare Muster.

Das grundlegende Problem ist nicht die Syntax, sondern die Sicherheit beim Laden: Eine fehlende Pflichtfeld-Prüfung in einem JSON-Objekt, ein unsicher geladenes .env-File mit Sonderzeichen in Werten, ein YAML-Alias, der unerwartet Werte überschreibt – all das sind Fehlerquellen, die in Produktionsumgebungen zu inkonsistentem Verhalten führen. Die Lösungsstrategien in diesem Artikel decken die wichtigsten Muster ab: sicheres Laden, Validierung, Kombination mit Prioritätslogik und sichere Behandlung von Secrets in allen drei Formaten.

2. JSON in Bash verarbeiten: jq als Shell-Werkzeug

jq ist der De-facto-Standard für JSON-Verarbeitung in Shell-Skripten. Es kombiniert einen Stream-Prozessor mit einer vollständigen Filtersprache und gibt transformierte JSON-Strukturen oder extrahierte Werte aus. Das Kernmuster für die Verwendung in Shell-Workflows ist das Extrahieren von Werten in Variablen: VAR=$(jq -r '.key' config.json) gibt den rohen String-Wert ohne JSON-Anführungszeichen aus. Das Flag -r (raw output) ist dabei entscheidend – ohne es enthält der zurückgegebene Wert JSON-Anführungszeichen, was bei weiterer Verarbeitung zu Fehlern führt.

Für den Shell-Workflow-Einsatz besonders wertvoll ist die Fähigkeit von jq, mehrere Werte in einem einzigen Aufruf zu extrahieren und daraus direkt Shell-Variablen-Zuweisungen zu generieren. Das Muster eval "$(jq -r 'to_entries[] | "export \(.key)=\(.value | @sh)"' config.json)" exportiert alle Felder eines JSON-Objekts als Shell-Variablen, wobei @sh die Werte korrekt für Shell-Quoting escaped. Dieses Muster vermeidet Code-Injection durch Sonderzeichen in Werten und ist damit deutlich sicherer als naive String-Konkatenation.


#!/usr/bin/env bash
# json-loader.sh — Safe JSON parsing and variable export in shell workflows
set -euo pipefail

CONFIG_FILE="${1:-config.json}"

# Guard: check if jq is available
command -v jq >/dev/null 2>&1 || { echo "[ERROR] jq is not installed" >&2; exit 1; }

# Guard: validate JSON syntax before processing
jq empty "$CONFIG_FILE" 2>/dev/null || { echo "[ERROR] Invalid JSON: $CONFIG_FILE" >&2; exit 1; }

# Extract single value (raw string, no JSON quotes)
DB_HOST=$(jq -r '.database.host // empty' "$CONFIG_FILE")
DB_PORT=$(jq -r '.database.port // 3306' "$CONFIG_FILE")

# Guard: required field must not be empty
[[ -n "$DB_HOST" ]] || { echo "[ERROR] database.host is required in $CONFIG_FILE" >&2; exit 1; }

# Export entire object as shell variables safely (values are @sh-escaped)
eval "$(jq -r '
  .environment // {} |
  to_entries[] |
  "export \(.key)=\(.value | @sh)"
' "$CONFIG_FILE")"

# Iterate over JSON array
declare -a services=()
while IFS= read -r svc; do
  services+=("$svc")
done < <(jq -r '.services[]?.name // empty' "$CONFIG_FILE")

echo "Loaded ${#services[@]} services from $CONFIG_FILE"
echo "DB: ${DB_HOST}:${DB_PORT}"

3. YAML mit yq lesen, transformieren und exportieren

yq ist das YAML-Äquivalent zu jq und unterstützt in der modernen Version (Mike Farah's Go-Implementierung, v4+) eine weitgehend kompatible Syntax. Wichtig für den Shell-Workflow-Einsatz: Es gibt zwei konkurrierende yq-Implementierungen – die Python-basierte (pip install yq, verwendet intern jq) und die Go-basierte (snap install yq oder Binary-Download). Beide haben unterschiedliche Syntax, was in CI-Pipelines zu Überraschungen führt. Das erste Muster für jeden YAML-Shell-Workflow ist daher eine Versions-Prüfung zu Beginn.

YAML hat gegenüber JSON den Vorteil menschenlesbarer Konfiguration, bringt aber Tücken mit: Implizite Typen (der String yes wird in einigen Parsern als Boolean interpretiert), Multiline-Strings und Anker mit Aliases können unerwartet Werte zusammenführen. Beim Exportieren von YAML-Werten in Shell-Variablen gilt dasselbe Prinzip wie bei JSON: Werte müssen korrekt für die Shell escaped werden. Das yq -r-Flag gibt Raw-Output aus, und für die Massenexport-Variante empfiehlt sich der gleiche eval-Ansatz mit explizitem Shell-Escaping der Werte.


#!/usr/bin/env bash
# yaml-loader.sh — YAML parsing with yq in shell workflows
set -euo pipefail

YAML_FILE="${1:-config.yaml}"

# Check yq version: Go-based v4 vs Python-based
YQ_VERSION=$(yq --version 2>&1 | grep -oP 'version v?\K[0-9]+' | head -1)
if [[ "${YQ_VERSION:-0}" -lt 4 ]]; then
  echo "[WARN] yq v4+ (Go) recommended for this script" >&2
fi

# Validate YAML before processing
yq eval 'true' "$YAML_FILE" >/dev/null 2>&1 || {
  echo "[ERROR] Invalid YAML: $YAML_FILE" >&2; exit 1
}

# Read scalar values
APP_NAME=$(yq eval '.app.name // ""' "$YAML_FILE")
APP_ENV=$(yq eval '.app.environment // "production"' "$YAML_FILE")

# Export all keys from a YAML map as shell variables (Go yq v4 syntax)
eval "$(yq eval '.config | to_entries | .[] | "export " + .key + "=" + (.value | @sh)' "$YAML_FILE" 2>/dev/null || true)"

# Iterate over YAML sequence
declare -a hosts=()
while IFS= read -r host; do
  [[ -n "$host" ]] && hosts+=("$host")
done < <(yq eval '.servers[].host' "$YAML_FILE" 2>/dev/null)

# Convert YAML to JSON for jq post-processing
yq eval -o=json "$YAML_FILE" | jq -r '.deploy.steps[]?.name // empty'

echo "App: $APP_NAME ($APP_ENV), ${#hosts[@]} hosts"

4. .env-Dateien sicher laden und exportieren

.env-Dateien sind das älteste der drei Formate und gleichzeitig das fehleranfälligste beim Laden in Shell-Skripten. Das naive Muster source .env führt die Datei als Shell-Code aus – was funktioniert, solange alle Werte einfache Strings ohne Sonderzeichen sind, aber bei einem Wert wie PASSWORD=pa$$w0rd!&echo hacked sofort zu Code-Execution führt. Das sichere .env-Lade-Muster liest die Datei Zeile für Zeile, filtert Kommentare und leere Zeilen, und verwendet declare oder sichere Zuweisungsmuster statt source.

Ein weiteres häufiges Problem: .env-Werte können Anführungszeichen enthalten oder nicht – DB_PASS="my secret" und DB_PASS=my secret sind beide valide Schreibweisen in bestimmten dotenv-Dialekten, aber die Shell behandelt sie unterschiedlich. Das robuste Muster entfernt umgebende einfache und doppelte Anführungszeichen aus dem gelesenen Wert, bevor er exportiert wird. Zusätzlich müssen Variablennamen gegen Code-Injection geprüft werden – nur alphanumerische Zeichen und Unterstriche sind für Variablennamen zulässig.


#!/usr/bin/env bash
# dotenv-loader.sh — Safe .env file loading without source
set -euo pipefail

load_dotenv() {
  local env_file="$1"
  local export_vars="${2:-false}"

  [[ -f "$env_file" ]] || { echo "[ERROR] .env file not found: $env_file" >&2; return 1; }

  local line key raw_val val
  while IFS= read -r line || [[ -n "$line" ]]; do
    # Skip comments and empty lines
    [[ "$line" =~ ^[[:space:]]*# ]] && continue
    [[ "$line" =~ ^[[:space:]]*$ ]] && continue

    # Split at first = only
    key="${line%%=*}"
    raw_val="${line#*=}"

    # Validate key: only [A-Za-z_][A-Za-z0-9_]* allowed
    [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || {
      echo "[WARN] Skipping invalid key: $key" >&2; continue
    }

    # Strip surrounding quotes (single or double)
    val="${raw_val}"
    if [[ "$val" =~ ^\"(.*)\"$ ]]; then
      val="${BASH_REMATCH[1]}"
    elif [[ "$val" =~ ^\'(.*)\'$ ]]; then
      val="${BASH_REMATCH[1]}"
    fi

    if [[ "$export_vars" == "true" ]]; then
      export "$key"="$val"
    else
      declare -g "$key"="$val"
    fi
  done < "$env_file"
}

# Load and export .env file
load_dotenv ".env" true

# Override with environment-specific .env (higher priority)
[[ -f ".env.${APP_ENV:-production}" ]] && load_dotenv ".env.${APP_ENV:-production}" true

echo "DB_HOST=${DB_HOST:-not set}"

5. Drei Formate kombinieren: Prioritätslogik und Überschreibregeln

Wenn JSON, YAML und .env-Dateien im selben Shell-Workflow zusammenkommen, braucht man eine klare Prioritätslogik: Welches Format überschreibt welches? Die gängige Konvention in modernen Deployment-Systemen ist: Umgebungsvariablen aus der Shell haben höchste Priorität, gefolgt von .env-Dateien, dann YAML-Konfiguration und schließlich JSON-Defaults. Diese Reihenfolge spiegelt das Prinzip wider, dass spezifischere Quellen allgemeinere überschreiben – eine CI-Variable soll immer eine Datei-Konfiguration gewinnen.

Das Implementierungsmuster für diese Prioritätslogik nutzt Bash-Parameter-Expansion: Variablen werden mit dem ${VAR:-}-Muster gesetzt – wenn eine Variable bereits in der Umgebung gesetzt ist, bleibt sie unverändert. Der Trick liegt darin, die Quellen in der richtigen Reihenfolge zu laden: Zuerst JSON-Defaults in temporäre Variablen, dann YAML überschreibend, dann .env überschreibend, und schließlich echte Umgebungsvariablen, die nie überschrieben werden. Dieses Muster macht die Konfigurationsquelle in Debugging-Situationen transparent: Ein CONFIG_SOURCE=debug ./deploy.sh kann alle geladenen Werte mit ihrer Herkunft ausgeben.


#!/usr/bin/env bash
# config-merger.sh — Combine JSON defaults, YAML config and .env with priority
set -euo pipefail

# Priority order: env vars > .env > YAML > JSON defaults
# Load in REVERSE order: lowest priority first

# 1. JSON defaults (lowest priority)
if [[ -f "config.defaults.json" ]]; then
  while IFS='=' read -r k v; do
    # Only set if not already in environment
    [[ -z "${!k+x}" ]] && declare -g "$k"="$v"
  done < <(jq -r 'to_entries[] | "\(.key)=\(.value | @sh | gsub("^'"'"'|'"'"'$";""))"' config.defaults.json)
fi

# 2. YAML config (overrides JSON defaults)
if [[ -f "config.yaml" ]] && command -v yq >/dev/null 2>&1; then
  while IFS='=' read -r k v; do
    [[ -z "${!k+x}" ]] && declare -g "$k"="$v"
  done < <(yq eval '.config | to_entries[] | .key + "=" + (.value | tostring)' config.yaml 2>/dev/null)
fi

# 3. .env file (overrides YAML)
load_dotenv_safe() {
  local file="$1"
  while IFS= read -r line || [[ -n "$line" ]]; do
    [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
    local k="${line%%=*}" v="${line#*=}"
    [[ "$k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
    # .env wins over YAML/JSON but NOT over real env vars
    [[ -n "${!k+x}" ]] || export "$k"="${v//\"/}"
  done < "$file"
}
[[ -f ".env" ]] && load_dotenv_safe ".env"

# 4. Real environment variables already set — highest priority, nothing to do

# Debug output
if [[ "${CONFIG_DEBUG:-0}" == "1" ]]; then
  echo "=== Effective configuration ==="
  echo "APP_ENV=${APP_ENV:-production}"
  echo "DB_HOST=${DB_HOST:-localhost}"
  echo "DB_PORT=${DB_PORT:-3306}"
fi

6. Validierung: Pflichtfelder, Typen und Wertebereiche prüfen

Das Laden von Konfiguration ohne nachfolgende Validierung ist eines der häufigsten Muster, das zu schwer debugbaren Fehlern in Produktionssystemen führt. Ein leer geladenes DB_HOST führt nicht sofort zu einem Fehler – erst wenn der Datenbankbefehl ausgeführt wird, erscheint eine kryptische Fehlermeldung, die nichts mehr mit der eigentlichen Ursache zu tun hat. Das Validierungsmuster für Shell-Workflows prüft alle geladenen Konfigurationswerte direkt nach dem Laden, bevor das Skript weitere Schritte ausführt. So erscheinen Konfigurationsfehler frühzeitig mit klaren Meldungen.

Für JSON-basierte Validierung bietet jq mit JSON-Schema-artigen Filtern eine elegante Möglichkeit. Das Muster prüft, ob erforderliche Felder vorhanden sind, ob Ports im gültigen Bereich liegen, ob URLs das korrekte Schema haben und ob Enum-Werte aus einer definierten Menge stammen. Für YAML gilt dasselbe Prinzip: yq kann nach dem Laden prüfen, ob Strukturen wie erwartet vorhanden sind. Die Kombination aus früher Validierung, klaren Fehlermeldungen und Exit-Code-1 bei Fehlern macht Shell-Workflows deutlich robuster gegenüber Konfigurationsfehlern.

7. Secrets aus JSON und YAML sicher handhaben

Secrets in JSON- oder YAML-Dateien sind ein häufiges Anti-Pattern – aber in der Praxis nicht immer vollständig vermeidbar. Wenn Passwörter, API-Keys oder Zertifikate aus diesen Dateien geladen werden müssen, gibt es klare Sicherheitsregeln für den Shell-Workflow: Secrets niemals in Umgebungsvariablen exportieren, die länger als nötig sichtbar sind, niemals in Logdateien schreiben und niemals als Kommandozeilenargumente übergeben. Das sichere Muster ist: Secret-Wert lesen, sofort verwenden, Variable danach löschen (unset SECRET_VAR).

Eine weitere kritische Regel: Secrets aus JSON oder YAML nie mit set -x aktiv laden – set -x gibt jeden Befehl inklusive aller Variablenwerte aus, was Secrets in Logdateien oder CI-Ausgaben exponiert. Das Muster { set +x; load_secrets; set -x; } 2>/dev/null deaktiviert das Tracing temporär für den Secret-Lade-Block. Außerdem gilt für .env-Dateien mit Secrets: niemals per source laden, immer das zeilenweise Lese-Muster verwenden, das Werte nie als Shell-Code ausführt.

8. Integration in CI/CD-Pipelines

In CI/CD-Umgebungen kommt das volle Spektrum der Konfigurationsformate zusammen: Pipeline-Variablen aus dem CI-System (effektiv Umgebungsvariablen), .env-Dateien aus dem Repository, YAML-Konfiguration aus Helm-Charts oder Kubernetes-Manifesten und JSON-Antworten aus Vault oder Cloud-Metadaten-APIs. Der Shell-Workflow in einer Pipeline muss mit allen diesen Quellen umgehen können, ohne dass die Ausführungsreihenfolge oder fehlende Tools zu stillen Fehlern führen.

Das wichtigste Muster für CI-Pipelines: Alle Tools (jq, yq) zu Beginn des Skripts prüfen und mit klarer Fehlermeldung abbrechen, wenn eines fehlt. Viele CI-Images enthalten jq, aber nicht yq. Ein Fallback-Muster konvertiert YAML zu JSON mit Python oder Ruby, falls yq nicht verfügbar ist: python3 -c "import sys,yaml,json; json.dump(yaml.safe_load(sys.stdin), sys.stdout)" < config.yaml. Dieses Konvertierungsmuster ist in den meisten Linux-Umgebungen ohne zusätzliche Installation verfügbar und macht den Shell-Workflow portabler.

9. Konfigurationsformate im direkten Vergleich

Die Wahl des richtigen Konfigurationsformats für einen Shell-Workflow hängt von mehreren Faktoren ab: Lesbarkeit, Tool-Verfügbarkeit, Typsystem und Sicherheit beim Laden. Die folgende Tabelle vergleicht die drei Formate anhand dieser Kriterien und gibt Empfehlungen für typische Anwendungsfälle.

Kriterium JSON YAML .env
Shell-Tool jq (weit verbreitet) yq (2 Varianten!) Bash-Builtin möglich
Typsystem Explizit (String, Number, Bool, null) Implizit (Ambiguität bei yes/no) Nur Strings
Sicheres Laden jq -r mit @sh-Escaping yq eval mit Escaping Niemals source verwenden
Kommentare Nicht unterstützt Vollständig unterstützt Mit # unterstützt
Secrets-Eignung Nur mit Vault/Encryption Nur mit SOPS/Encryption Niemals für Secrets in Repos

Für komplexe Konfigurationsstrukturen mit Verschachtelung und Kommentaren ist YAML die erste Wahl. Für maschinengenerierte Ausgaben (API-Antworten, Terraform-Output, State-Dateien) ist JSON besser geeignet, weil es präziser im Typsystem ist und keine Parser-Ambiguitäten hat. .env-Dateien eignen sich ausschließlich für flache Key-Value-Konfigurationen ohne Verschachtelung und ohne Secrets in Repositories. In Shell-Workflows, die alle drei Formate verarbeiten, lohnt es sich, eine gemeinsame Lade-Bibliothek zu erstellen, die alle Formate unter einem einheitlichen Interface abstrahiert.

Mironsoft

Shell-Automatisierung, Konfigurationsmanagement und Deployment-Infrastruktur

Konfigurationsformate sicher in Shell-Workflows integrieren?

Wir analysieren bestehende Deployment-Skripte, erkennen unsichere Lade-Muster für JSON, YAML und .env-Dateien und ersetzen sie durch robuste, validierte Konfigurationsworkflows für euren Stack.

Konfigurationsanalyse

Bestehende JSON/YAML/.env-Lade-Patterns auf Sicherheit und Robustheit prüfen

Validierungs-Layer

Pflichtfeld-Prüfung, Typ-Validierung und frühe Fehlermeldungen implementieren

CI/CD-Integration

Konfigurationsworkflows in Pipeline-Stages integrieren und absichern

10. Zusammenfassung

Das sichere Kombinieren von JSON, YAML und .env-Dateien in Shell-Workflows erfordert drei Kernkompetenzen: sichere Tool-gestützte Verarbeitung mit jq und yq, ein klares Prioritätssystem für überschreibende Konfigurationsquellen und konsequente Validierung direkt nach dem Laden. jq -r mit @sh-Escaping ist das sichere Muster für JSON-zu-Shell-Variablen. Das zeilenweise Lesen von .env-Dateien mit Schlüssel-Validierung verhindert Code-Injection durch manipulierte Werte. Die Prioritätsreihenfolge Umgebungsvariablen > .env > YAML > JSON-Defaults spiegelt das Prinzip wider, dass spezifischere Konfiguration allgemeinere überschreibt.

Für den produktiven Einsatz in Shell-Workflows gilt: Tools zu Beginn prüfen, JSON/YAML vor dem Verarbeiten auf Syntax-Validität testen, Pflichtfelder sofort nach dem Laden prüfen und Secrets niemals länger als nötig in Variablen halten. Diese Muster machen Deployment-Skripte, die JSON, YAML und .env kombinieren, zu einem zuverlässigen, transparenten Glied in der Infrastrukturkette – statt zur unsichtbaren Fehlerquelle kurz vor dem Produktiv-Deployment.

JSON, YAML und .env in Shell-Workflows — Das Wichtigste auf einen Blick

JSON mit jq

jq -r '.key // empty' extrahiert Werte sicher. @sh-Escaping verhindert Code-Injection beim Massenexport in Shell-Variablen.

YAML mit yq

yq eval liest YAML-Felder. yq v4 (Go) und yq (Python) haben unterschiedliche Syntax – Versions-Check am Skriptanfang ist Pflicht.

.env sicher laden

Niemals source .env. Zeilenweises Lesen mit Schlüssel-Validierung (nur [A-Za-z_][A-Za-z0-9_]*) und Anführungszeichen-Stripping.

Priorität & Validierung

Reihenfolge: Env-Vars > .env > YAML > JSON-Defaults. Pflichtfelder sofort nach dem Laden prüfen. Secrets nie in Logs oder als CLI-Argumente.

11. FAQ: JSON, YAML und .env in Shell-Workflows

1Warum ist 'source .env' unsicher?
source führt die Datei als Shell-Code aus. Sonderzeichen in Werten werden als Befehle interpretiert. Immer zeilenweise lesen, Schlüssel validieren und Wert als String setzen.
2Was macht jq -r?
Raw-Output: ohne -r gibt jq Strings mit JSON-Anführungszeichen aus. Mit -r nur den reinen Wert – für Shell-Variablen immer -r verwenden.
3Welche yq-Version empfehlen?
Go-basiertes yq v4+ (Mike Farah). Python-Variante hat andere Syntax. Version am Skriptanfang prüfen, in CI explizit als Dependency definieren.
4JSON-Felder als Shell-Variablen exportieren?
eval "$(jq -r 'to_entries[] | "export \(.key)=\(.value | @sh)"' config.json)" – @sh escaped Sonderzeichen sicher für die Shell.
5YAML ohne yq verarbeiten?
Python-Fallback: python3 -c "import sys,yaml,json; json.dump(yaml.safe_load(sys.stdin),sys.stdout)" konvertiert YAML zu JSON für jq-Weiterverarbeitung.
6Pflichtfelder nach dem Laden prüfen?
DB_HOST="${DB_HOST:?Variable fehlt}" bricht mit klarer Meldung ab. Für JSON: jq -e '.field // error("missing")' config.json.
7Bestes Format für CI/CD?
YAML für menschenlesbare Konfiguration, JSON für API-Antworten, .env für simple Overrides. Umgebungsvariablen gewinnen immer gegen alle Datei-Quellen.
8Secrets aus JSON/YAML sicher?
Secret lesen, sofort verwenden, unset danach. Niemals mit set -x aktiv. Niemals als CLI-Argument. Für persistente Secrets Vault oder SOPS nutzen.
9JSON-Syntax in Shell prüfen?
jq empty datei.json – Exit-Code 0 bei gültig, 1 bei Fehler. Immer vor der Verarbeitung ausführen: jq empty config.json || { echo 'Invalid'; exit 1; }
10Mehrere .env-Dateien mit Priorität kombinieren?
In aufsteigender Priorität laden: erst .env, dann .env.production. Beim Laden nur setzen, wenn Variable noch nicht gesetzt – CI-Variablen gewinnen immer.