yq-keep: yq-Edits ohne Neuformatierung der Datei

Ein kleines Werkzeug, das einen yq-Ausdruck direkt auf YAML-Dateien anwendet und nur die Zeilen ändert, die der Edit betrifft. Leerzeilen, Einrückung und Kommentarabstände bleiben erhalten.

Das Problem

yq liest YAML in einen Knotenbaum ein und schreibt den Baum wieder aus. Der Parser behält weder Leerzeilen noch die ursprüngliche Einrückung, deshalb formatiert yq -i die ganze Datei neu. Bei von Hand gepflegten Dateien wie Kubernetes-Manifesten oder Helm-Values erzeugt eine einzeilige Änderung einen großen Diff.

Die Anfragen sind seit Jahren offen:

Der Maintainer hat die Ursache auf die YAML-Bibliothek zurückgeführt (go-yaml/yaml#627); dort ist eine Korrektur in Arbeit, aber nicht veröffentlicht. Die meistbewertete Umgehung in #515 (61 Reaktionen) leitet die Ausgabe von yq durch diff | patch. Spätere Kommentare im Thread berichten, dass sie in manchen Fällen Kommentare verliert oder Listeneinträge zusammenführt. Formaterhaltende Editoren wie yamlpath gibt es, sie verwenden aber eine eigene Abfragesyntax, sodass vorhandene yq-Ausdrücke umgeschrieben werden müssen.

Wie es funktioniert

yq-keep behält yq als Engine und nimmt jeden Ausdruck an, der mit yq -i funktioniert. Für jede Datei erzeugt yq '.' FILE die yq-Darstellung der unveränderten Datei und yq EXPRESSION FILE die Darstellung nach dem Edit. Beide Ausgaben haben die Formatierung von yq, also enthält der Zeilen-Diff zwischen ihnen nur den Edit. Die Zeilen der ersten Darstellung werden der Originaldatei zugeordnet, wobei Leerzeilen und Unterschiede im Leerraum ignoriert werden, und jeder Hunk des Diffs wird an der zugeordneten Stelle im Original angewendet. Eingefügte Zeilen werden so eingerückt wie ihr nächster Nachbar auf derselben Ebene im Original.

Das Ergebnis wird erneut mit yq eingelesen und als JSON-Daten mit der Ausgabe von yq EXPRESSION FILE verglichen. Weichen beide ab, bleibt die Datei unverändert, und yq-keep endet mit Exit-Code 2. --diff zeigt die Änderung, ohne sie zu schreiben.

Das ist die Ausgabe für eine kleine Kustomization-Datei mit einer Leerzeile zwischen den Abschnitten, einer mit vier Leerzeichen eingerückten Liste und drei Leerzeichen vor einem Kommentar:

$ yq-keep --diff '.resources += ["rg.yaml"] | .images[0].newTag = "1.3.0"' kustomization.yaml
--- a/kustomization.yaml
+++ b/kustomization.yaml
@@ -4,7 +4,8 @@
 resources:
 - deployment.yaml   # main
 - service.yaml
+- rg.yaml
 
 images:
     - name: app
-      newTag: "1.2.3"
+      newTag: "1.3.0"

Derselbe Edit mit yq -i ändert 11 Zeilen: Er entfernt zusätzlich beide Leerzeilen, rückt die Listen unter resources und images neu ein und verkürzt die Leerzeichen vor # main.

Tests

Die Unit-Tests rufen das echte yq auf. Sie decken den Leerzeilen-Fall aus #515 ab, das Anhängen an eine Liste, Kommentare und Abstände in nicht berührten Zeilen, den Einrückungsstil eingefügter Zeilen, das Löschen eines Blocks, Dateien mit mehreren Dokumenten, CRLF-Zeilenenden und einen fehlenden Zeilenumbruch am Dateiende. Ein parametrisierter Test wendet sechs Ausdrücke, darunter sort_keys(..), eine Umbenennung der Schlüssel mit with_entries und Edits an Ankern und Merge-Keys, auf eine Datei mit Block-Skalaren und Flow-Listen an und prüft, dass das Ergebnis dieselben Daten enthält wie die Ausgabe von yq. Das CI führt die Tests auf Python 3.10 bis 3.13 mit yq v4.54.1 aus.

Außerdem lief yq-keep über 361 YAML-Dateien (Kubernetes-Manifeste, CI-Workflows, Compose- und pre-commit-Dateien) mit je fünf Edits. 1.621 von 1.636 Edits wurden geschrieben. Von den 15 Ablehnungen betrafen 11 Dateien, bei denen schon der eigene Roundtrip von yq die Daten verändert. Für die 1.088 Edits außer sort_keys, die eine Datei veränderten, änderte yq-keep insgesamt 2.819 Zeilen und yq -i 26.634.

Grenzen

  • Eine geänderte Zeile stammt aus der Ausgabe von yq und hat deren Abstände, zum Beispiel ein Leerzeichen vor dem Kommentar am Zeilenende.
  • Edits, die den Großteil einer Datei umordnen oder umschreiben (sort_keys(..), Umbenennen aller Schlüssel), ergeben korrekte Daten, aber die Leerzeilen bleiben an ihren alten Positionen.
  • Wo yq in einem geänderten Bereich einen Kommentar verliert, verliert yq-keep ihn auch.
  • Nur YAML wird unterstützt. yq läuft viermal pro Datei, daher ist yq-keep bei sehr großen Dateien langsamer als yq -i.

Code und Installationsanleitung liegen unter github.com/Jwrede/yq-keep.