Repository: mikefarah/yq
Pull request #1099 (2022-02-09 02:18:26)

Decoder Properties


Patch:
diff --git a/cmd/root.go b/cmd/root.go
index 6d4758da..d53ed809 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -63,7 +63,7 @@ yq -i '.stuff = "foo"' myfile.yml # update myfile.yml inplace
 	}
 
 	rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "yaml", "[yaml|y|json|j|props|p|xml|x] output format type.")
-	rootCmd.PersistentFlags().StringVarP(&inputFormat, "input-format", "p", "yaml", "[yaml|y|xml|x] parse format for input. Note that json is a subset of yaml.")
+	rootCmd.PersistentFlags().StringVarP(&inputFormat, "input-format", "p", "yaml", "[yaml|y|props|p|xml|x] parse format for input. Note that json is a subset of yaml.")
 
 	rootCmd.PersistentFlags().StringVar(&xmlAttributePrefix, "xml-attribute-prefix", "+", "prefix for xml attributes")
 	rootCmd.PersistentFlags().StringVar(&xmlContentName, "xml-content-name", "+content", "name for xml content (if no attribute name is present).")
diff --git a/cmd/utils.go b/cmd/utils.go
index 7381b956..ef5de85c 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -54,7 +54,10 @@ func configureDecoder() (yqlib.Decoder, error) {
 	switch yqlibInputFormat {
 	case yqlib.XMLInputFormat:
 		return yqlib.NewXMLDecoder(xmlAttributePrefix, xmlContentName), nil
+	case yqlib.PropertiesInputFormat:
+		return yqlib.NewPropertiesDecoder(), nil
 	}
+
 	return yqlib.NewYamlDecoder(), nil
 }
 
diff --git a/examples/example.properties b/examples/example.properties
new file mode 100644
index 00000000..71af9cdf
--- /dev/null
+++ b/examples/example.properties
@@ -0,0 +1,6 @@
+# comments on values appear
+person.name = Mike
+
+# comments on array values appear
+person.pets.0 = cat
+person.food.0 = pizza
\ No newline at end of file
diff --git a/pkg/yqlib/decoder.go b/pkg/yqlib/decoder.go
new file mode 100644
index 00000000..f8797607
--- /dev/null
+++ b/pkg/yqlib/decoder.go
@@ -0,0 +1,34 @@
+package yqlib
+
+import (
+	"fmt"
+	"io"
+
+	yaml "gopkg.in/yaml.v3"
+)
+
+type InputFormat uint
+
+const (
+	YamlInputFormat = 1 << iota
+	XMLInputFormat
+	PropertiesInputFormat
+)
+
+type Decoder interface {
+	Init(reader io.Reader)
+	Decode(node *yaml.Node) error
+}
+
+func InputFormatFromString(format string) (InputFormat, error) {
+	switch format {
+	case "yaml", "y":
+		return YamlInputFormat, nil
+	case "xml", "x":
+		return XMLInputFormat, nil
+	case "props", "p":
+		return PropertiesInputFormat, nil
+	default:
+		return 0, fmt.Errorf("unknown format '%v' please use [yaml|xml|props]", format)
+	}
+}
diff --git a/pkg/yqlib/decoder_properties.go b/pkg/yqlib/decoder_properties.go
new file mode 100644
index 00000000..85e0349d
--- /dev/null
+++ b/pkg/yqlib/decoder_properties.go
@@ -0,0 +1,121 @@
+package yqlib
+
+import (
+	"bytes"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/magiconair/properties"
+	"gopkg.in/yaml.v3"
+)
+
+type propertiesDecoder struct {
+	reader   io.Reader
+	finished bool
+	d        DataTreeNavigator
+}
+
+func NewPropertiesDecoder() Decoder {
+	return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
+}
+
+func (dec *propertiesDecoder) Init(reader io.Reader) {
+	dec.reader = reader
+	dec.finished = false
+}
+
+func parsePropKey(key string) []interface{} {
+	pathStrArray := strings.Split(key, ".")
+	path := make([]interface{}, len(pathStrArray))
+	for i, pathStr := range pathStrArray {
+		num, err := strconv.ParseInt(pathStr, 10, 32)
+		if err == nil {
+			path[i] = num
+		} else {
+			path[i] = pathStr
+		}
+	}
+	return path
+}
+
+func (dec *propertiesDecoder) processComment(c string) string {
+	if c == "" {
+		return ""
+	}
+	return "# " + c
+}
+
+func (dec *propertiesDecoder) applyProperty(properties *properties.Properties, context Context, key string) error {
+	value, _ := properties.Get(key)
+	path := parsePropKey(key)
+
+	rhsNode := &yaml.Node{
+		Value:       value,
+		Tag:         "!!str",
+		Kind:        yaml.ScalarNode,
+		LineComment: dec.processComment(properties.GetComment(key)),
+	}
+
+	rhsNode.Tag = guessTagFromCustomType(rhsNode)
+
+	rhsCandidateNode := &CandidateNode{
+		Path: path,
+		Node: rhsNode,
+	}
+
+	assignmentOp := &Operation{OperationType: assignOpType, Preferences: assignPreferences{}}
+
+	rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhsCandidateNode}
+
+	assignmentOpNode := &ExpressionNode{
+		Operation: assignmentOp,
+		LHS:       createTraversalTree(path, traversePreferences{}, false),
+		RHS:       &ExpressionNode{Operation: rhsOp},
+	}
+
+	_, err := dec.d.GetMatchingNodes(context, assignmentOpNode)
+	return err
+}
+
+func (dec *propertiesDecoder) Decode(rootYamlNode *yaml.Node) error {
+	if dec.finished {
+		return io.EOF
+	}
+	buf := new(bytes.Buffer)
+
+	if _, err := buf.ReadFrom(dec.reader); err != nil {
+		return err
+	}
+	if buf.Len() == 0 {
+		dec.finished = true
+		return io.EOF
+	}
+	properties, err := properties.LoadString(buf.String())
+	if err != nil {
+		return err
+	}
+
+	rootMap := &CandidateNode{
+		Node: &yaml.Node{
+			Kind: yaml.MappingNode,
+			Tag:  "!!map",
+		},
+	}
+
+	context := Context{}
+	context = context.SingleChildContext(rootMap)
+
+	for _, key := range properties.Keys() {
+		if err := dec.applyProperty(properties, context, key); err != nil {
+			return err
+		}
+
+	}
+
+	rootYamlNode.Kind = yaml.DocumentNode
+	rootYamlNode.Content = []*yaml.Node{rootMap.Node}
+	dec.finished = true
+	return nil
+
+}
diff --git a/pkg/yqlib/decoder_xml.go b/pkg/yqlib/decoder_xml.go
index 7db03ffa..7fa7ea27 100644
--- a/pkg/yqlib/decoder_xml.go
+++ b/pkg/yqlib/decoder_xml.go
@@ -2,7 +2,6 @@ package yqlib
 
 import (
 	"encoding/xml"
-	"fmt"
 	"io"
 	"strings"
 	"unicode"
@@ -11,24 +10,6 @@ import (
 	yaml "gopkg.in/yaml.v3"
 )
 
-type InputFormat uint
-
-const (
-	YamlInputFormat = 1 << iota
-	XMLInputFormat
-)
-
-func InputFormatFromString(format string) (InputFormat, error) {
-	switch format {
-	case "yaml", "y":
-		return YamlInputFormat, nil
-	case "xml", "x":
-		return XMLInputFormat, nil
-	default:
-		return 0, fmt.Errorf("unknown format '%v' please use [yaml|xml]", format)
-	}
-}
-
 type xmlDecoder struct {
 	reader          io.Reader
 	attributePrefix string
@@ -153,6 +134,7 @@ func (dec *xmlDecoder) Decode(rootYamlNode *yaml.Node) error {
 	if err != nil {
 		return err
 	} else if firstNode.Tag == "!!null" {
+		dec.finished = true
 		return io.EOF
 	}
 	rootYamlNode.Kind = yaml.DocumentNode
diff --git a/pkg/yqlib/decoder_yaml.go b/pkg/yqlib/decoder_yaml.go
index 58f1c520..37c4ad31 100644
--- a/pkg/yqlib/decoder_yaml.go
+++ b/pkg/yqlib/decoder_yaml.go
@@ -6,11 +6,6 @@ import (
 	yaml "gopkg.in/yaml.v3"
 )
 
-type Decoder interface {
-	Init(reader io.Reader)
-	Decode(node *yaml.Node) error
-}
-
 type yamlDecoder struct {
 	decoder yaml.Decoder
 }
diff --git a/pkg/yqlib/doc/usage/properties.md b/pkg/yqlib/doc/usage/properties.md
index 8f7e7a08..d5477434 100644
--- a/pkg/yqlib/doc/usage/properties.md
+++ b/pkg/yqlib/doc/usage/properties.md
@@ -27,7 +27,7 @@ emptyMap: []
 ```
 then
 ```bash
-yq -o=props -I=0 '.' sample.yml
+yq -o=props sample.yml
 ```
 will output
 ```properties
@@ -54,7 +54,7 @@ emptyMap: []
 ```
 then
 ```bash
-yq -o=props -I=0 '... comments = ""' sample.yml
+yq -o=props '... comments = ""' sample.yml
 ```
 will output
 ```properties
@@ -80,7 +80,7 @@ emptyMap: []
 ```
 then
 ```bash
-yq -o=props -I=0 '(.. | select( (tag == "!!map" or tag =="!!seq") and length == 0)) = ""' sample.yml
+yq -o=props '(.. | select( (tag == "!!map" or tag =="!!seq") and length == 0)) = ""' sample.yml
 ```
 will output
 ```properties
@@ -94,3 +94,66 @@ emptyArray =
 emptyMap = 
 ```
 
+## Decode properties
+Given a sample.properties file of:
+```properties
+# comments on values appear
+person.name = Mike
+
+# comments on array values appear
+person.pets.0 = cat
+person.food.0 = pizza
+
+```
+then
+```bash
+yq -p=props sample.properties
+```
+will output
+```yaml
+person:
+    name: Mike # comments on values appear
+    pets:
+        - cat # comments on array values appear
+    food:
+        - pizza
+```
+
+## Roundtrip
+Given a sample.properties file of:
+```properties
+# comments on values appear
+person.name = Mike
+
+# comments on array values appear
+person.pets.0 = cat
+person.food.0 = pizza
+
+```
+then
+```bash
+yq -p=props -o=props '.person.pets.0 = "dog"' sample.properties
+```
+will output
+```properties
+# comments on values appear
+person.name = Mike
+
+# comments on array values appear
+person.pets.0 = dog
+person.food.0 = pizza
+```
+
+## Empty doc
+Given a sample.properties file of:
+```properties
+
+```
+then
+```bash
+yq -p=props sample.properties
+```
+will output
+```yaml
+```
+


Test patch:
diff --git a/acceptance_tests/basic.sh b/acceptance_tests/basic.sh
index be9d8f6e..fbbd52cd 100755
--- a/acceptance_tests/basic.sh
+++ b/acceptance_tests/basic.sh
@@ -1,8 +1,8 @@
 #!/bin/bash
 
 setUp() {
-  rm test*.yml || true
-  rm .xyz -f || true
+  rm test*.yml 2>/dev/null || true
+  rm .xyz 2>/dev/null || true
 }
 
 testBasicEvalRoundTrip() {
diff --git a/acceptance_tests/inputs-format.sh b/acceptance_tests/inputs-format.sh
index 4c76cd2e..021b58a5 100755
--- a/acceptance_tests/inputs-format.sh
+++ b/acceptance_tests/inputs-format.sh
@@ -1,7 +1,43 @@
 #!/bin/bash
 
 setUp() {
-  rm test*.yml || true
+  rm test*.yml 2>/dev/null || true
+  rm test*.properties 2>/dev/null || true
+  rm test*.xml 2>/dev/null || true
+}
+
+testInputProperties() {
+  cat >test.properties <<EOL
+mike.things = hello
+EOL
+
+  read -r -d '' expected << EOM
+mike:
+  things: hello
+EOM
+
+  X=$(./yq e -p=props test.properties)
+  assertEquals "$expected" "$X"
+
+  X=$(./yq ea -p=props test.properties)
+  assertEquals "$expected" "$X"
+}
+
+testInputPropertiesGitHubAction() {
+  cat >test.properties <<EOL
+mike.things = hello
+EOL
+
+  read -r -d '' expected << EOM
+mike:
+  things: hello
+EOM
+
+  X=$(cat /dev/null | ./yq e -p=props test.properties)
+  assertEquals "$expected" "$X"
+
+  X=$(cat /dev/null | ./yq ea -p=props test.properties)
+  assertEquals "$expected" "$X"
 }
 
 testInputXml() {
diff --git a/pkg/yqlib/decoder_test.go b/pkg/yqlib/decoder_test.go
new file mode 100644
index 00000000..462eb4d1
--- /dev/null
+++ b/pkg/yqlib/decoder_test.go
@@ -0,0 +1,60 @@
+package yqlib
+
+import (
+	"bufio"
+	"bytes"
+	"strings"
+)
+
+type formatScenario struct {
+	input          string
+	indent         int
+	expression     string
+	expected       string
+	description    string
+	subdescription string
+	skipDoc        bool
+	scenarioType   string
+}
+
+func processFormatScenario(s formatScenario, decoder Decoder, encoder Encoder) string {
+
+	var output bytes.Buffer
+	writer := bufio.NewWriter(&output)
+
+	if decoder == nil {
+		decoder = NewYamlDecoder()
+	}
+
+	inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder)
+	if err != nil {
+		panic(err)
+	}
+
+	expression := s.expression
+	if expression == "" {
+		expression = "."
+	}
+
+	exp, err := getExpressionParser().ParseExpression(expression)
+
+	if err != nil {
+		panic(err)
+	}
+
+	context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, exp)
+
+	if err != nil {
+		panic(err)
+	}
+
+	printer := NewPrinter(encoder, NewSinglePrinterWriter(writer))
+	err = printer.PrintResults(context.MatchingNodes)
+	if err != nil {
+		panic(err)
+	}
+	writer.Flush()
+
+	return output.String()
+
+}
diff --git a/pkg/yqlib/json_test.go b/pkg/yqlib/json_test.go
index 2a3cd65a..9e1ac66e 100644
--- a/pkg/yqlib/json_test.go
+++ b/pkg/yqlib/json_test.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"bytes"
 	"fmt"
-	"strings"
 	"testing"
 
 	"github.com/mikefarah/yq/v4/test"
@@ -98,53 +97,13 @@ func decodeJSON(t *testing.T, jsonString string) *CandidateNode {
 
 func testJSONScenario(t *testing.T, s formatScenario) {
 	if s.scenarioType == "encode" || s.scenarioType == "roundtrip" {
-		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJONEncoder(s.indent)), s.description)
+		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewJONEncoder(s.indent)), s.description)
 	} else {
 		var actual = resultToString(t, decodeJSON(t, s.input))
 		test.AssertResultWithContext(t, s.expected, actual, s.description)
 	}
 }
 
-func processFormatScenario(s formatScenario, encoder Encoder) string {
-
-	var output bytes.Buffer
-	writer := bufio.NewWriter(&output)
-
-	var decoder = NewYamlDecoder()
-
-	inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder)
-	if err != nil {
-		panic(err)
-	}
-
-	expression := s.expression
-	if expression == "" {
-		expression = "."
-	}
-
-	exp, err := getExpressionParser().ParseExpression(expression)
-
-	if err != nil {
-		panic(err)
-	}
-
-	context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, exp)
-
-	if err != nil {
-		panic(err)
-	}
-
-	printer := NewPrinter(encoder, NewSinglePrinterWriter(writer))
-	err = printer.PrintResults(context.MatchingNodes)
-	if err != nil {
-		panic(err)
-	}
-	writer.Flush()
-
-	return output.String()
-
-}
-
 func documentJSONDecodeScenario(t *testing.T, w *bufio.Writer, s formatScenario) {
 	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
 
@@ -212,7 +171,7 @@ func documentJSONEncodeScenario(w *bufio.Writer, s formatScenario) {
 	}
 	writeOrPanic(w, "will output\n")
 
-	writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", processFormatScenario(s, NewJONEncoder(s.indent))))
+	writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewJONEncoder(s.indent))))
 }
 
 func TestJSONScenarios(t *testing.T) {
diff --git a/pkg/yqlib/properties_test.go b/pkg/yqlib/properties_test.go
index d136b008..f271dd20 100644
--- a/pkg/yqlib/properties_test.go
+++ b/pkg/yqlib/properties_test.go
@@ -26,6 +26,22 @@ person.pets.0 = cat
 person.food.0 = pizza
 `
 
+var expectedUpdatedProperties = `# comments on values appear
+person.name = Mike
+
+# comments on array values appear
+person.pets.0 = dog
+person.food.0 = pizza
+`
+
+var expectedDecodedYaml = `person:
+  name: Mike # comments on values appear
+  pets:
+    - cat # comments on array values appear
+  food:
+    - pizza
+`
+
 var expectedPropertiesNoComments = `person.name = Mike
 person.pets.0 = cat
 person.food.0 = pizza
@@ -61,10 +77,29 @@ var propertyScenarios = []formatScenario{
 		input:          samplePropertiesYaml,
 		expected:       expectedPropertiesWithEmptyMapsAndArrays,
 	},
+	{
+		description:  "Decode properties",
+		input:        expectedProperties,
+		expected:     expectedDecodedYaml,
+		scenarioType: "decode",
+	},
+	{
+		description:  "Roundtrip",
+		input:        expectedProperties,
+		expression:   `.person.pets.0 = "dog"`,
+		expected:     expectedUpdatedProperties,
+		scenarioType: "roundtrip",
+	},
+	{
+		description:  "Empty doc",
+		skipDoc:      true,
+		input:        "",
+		expected:     "",
+		scenarioType: "decode",
+	},
 }
 
-func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
-	s := i.(formatScenario)
+func documentEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
 	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
 
 	if s.subdescription != "" {
@@ -78,23 +113,88 @@ func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
 	writeOrPanic(w, "then\n")
 
 	expression := s.expression
-	if expression == "" {
-		expression = "."
-	}
 
-	if s.indent == 2 {
+	if expression != "" {
 		writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=props '%v' sample.yml\n```\n", expression))
 	} else {
-		writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=props -I=%v '%v' sample.yml\n```\n", s.indent, expression))
+		writeOrPanic(w, "```bash\nyq -o=props sample.yml\n```\n")
 	}
 	writeOrPanic(w, "will output\n")
 
-	writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewPropertiesEncoder())))
+	writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder())))
+}
+
+func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) {
+	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
+
+	if s.subdescription != "" {
+		writeOrPanic(w, s.subdescription)
+		writeOrPanic(w, "\n\n")
+	}
+
+	writeOrPanic(w, "Given a sample.properties file of:\n")
+	writeOrPanic(w, fmt.Sprintf("```properties\n%v\n```\n", s.input))
+
+	writeOrPanic(w, "then\n")
+
+	expression := s.expression
+	if expression != "" {
+		writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=props '%v' sample.properties\n```\n", expression))
+	} else {
+		writeOrPanic(w, "```bash\nyq -p=props sample.properties\n```\n")
+	}
+
+	writeOrPanic(w, "will output\n")
+
+	writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(s.indent, false, true, true))))
+}
+
+func documentRoundTripPropertyScenario(w *bufio.Writer, s formatScenario) {
+	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
+
+	if s.subdescription != "" {
+		writeOrPanic(w, s.subdescription)
+		writeOrPanic(w, "\n\n")
+	}
+
+	writeOrPanic(w, "Given a sample.properties file of:\n")
+	writeOrPanic(w, fmt.Sprintf("```properties\n%v\n```\n", s.input))
+
+	writeOrPanic(w, "then\n")
+
+	expression := s.expression
+	if expression != "" {
+		writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=props -o=props '%v' sample.properties\n```\n", expression))
+	} else {
+		writeOrPanic(w, "```bash\nyq -p=props -o=props sample.properties\n```\n")
+	}
+
+	writeOrPanic(w, "will output\n")
+
+	writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder())))
+}
+
+func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
+	s := i.(formatScenario)
+	if s.scenarioType == "decode" {
+		documentDecodePropertyScenario(w, s)
+	} else if s.scenarioType == "roundtrip" {
+		documentRoundTripPropertyScenario(w, s)
+	} else {
+		documentEncodePropertyScenario(w, s)
+	}
+
 }
 
 func TestPropertyScenarios(t *testing.T) {
 	for _, s := range propertyScenarios {
-		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesEncoder()), s.description)
+		if s.scenarioType == "decode" {
+			test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(2, false, true, true)), s.description)
+		} else if s.scenarioType == "roundtrip" {
+			test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder()), s.description)
+		} else {
+			test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder()), s.description)
+		}
 	}
 	genericScenarios := make([]interface{}, len(propertyScenarios))
 	for i, s := range propertyScenarios {
diff --git a/pkg/yqlib/xml_test.go b/pkg/yqlib/xml_test.go
index 9ff6a3f9..adf09374 100644
--- a/pkg/yqlib/xml_test.go
+++ b/pkg/yqlib/xml_test.go
@@ -2,88 +2,12 @@ package yqlib
 
 import (
 	"bufio"
-	"bytes"
-	"errors"
 	"fmt"
-	"io"
-	"strings"
 	"testing"
 
 	"github.com/mikefarah/yq/v4/test"
-	yaml "gopkg.in/yaml.v3"
 )
 
-func decodeXML(t *testing.T, s formatScenario) *CandidateNode {
-	decoder := NewXMLDecoder("+", "+content")
-
-	decoder.Init(strings.NewReader(s.input))
-
-	node := &yaml.Node{}
-	err := decoder.Decode(node)
-	if err != nil && !errors.Is(err, io.EOF) {
-		t.Error(err, "fail to decode", s.input)
-	}
-
-	expression := s.expression
-	if expression == "" {
-		expression = "."
-	}
-
-	exp, err := getExpressionParser().ParseExpression(expression)
-
-	if err != nil {
-		t.Error(err)
-		return nil
-	}
-
-	candidateNode := CandidateNode{Node: node}
-
-	context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: candidateNode.AsList()}, exp)
-
-	if err != nil {
-		t.Error(err)
-		return nil
-	}
-
-	return context.MatchingNodes.Front().Value.(*CandidateNode)
-}
-
-func processXMLScenario(s formatScenario) string {
-	var output bytes.Buffer
-	writer := bufio.NewWriter(&output)
-
-	var encoder = NewXMLEncoder(2, "+", "+content")
-
-	var decoder = NewYamlDecoder()
-	if s.scenarioType == "roundtrip" {
-		decoder = NewXMLDecoder("+", "+content")
-	}
-
-	inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder)
-	if err != nil {
-		panic(err)
-	}
-	node := inputs.Front().Value.(*CandidateNode).Node
-	err = encoder.Encode(writer, node)
-	if err != nil {
-		panic(err)
-	}
-	writer.Flush()
-
-	return output.String()
-}
-
-type formatScenario struct {
-	input          string
-	indent         int
-	expression     string
-	expected       string
-	description    string
-	subdescription string
-	skipDoc        bool
-	scenarioType   string
-}
-
 var inputXMLWithComments = `
 <!-- before cat -->
 <cat>
@@ -121,7 +45,7 @@ for x --></x>
 <!-- after cat -->
 `
 
-var expectedDecodeYamlWithSubChild = `D0, P[], (doc)::# before cat
+var expectedDecodeYamlWithSubChild = `# before cat
 cat:
     # in cat before
     x: "3" # multi
@@ -161,7 +85,7 @@ for x --></x>
 <!-- after cat -->
 `
 
-var expectedDecodeYamlWithArray = `D0, P[], (doc)::# before cat
+var expectedDecodeYamlWithArray = `# before cat
 cat:
     # in cat before
     x: "3" # multi
@@ -185,7 +109,7 @@ cat:
 # after cat
 `
 
-var expectedDecodeYamlWithComments = `D0, P[], (doc)::# before cat
+var expectedDecodeYamlWithComments = `# before cat
 cat:
     # in cat before
     x: "3" # multi
@@ -234,32 +158,32 @@ var xmlScenarios = []formatScenario{
 		description:    "Parse xml: simple",
 		subdescription: "Notice how all the values are strings, see the next example on how you can fix that.",
 		input:          "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat>\n  <says>meow</says>\n  <legs>4</legs>\n  <cute>true</cute>\n</cat>",
-		expected:       "D0, P[], (doc)::cat:\n    says: meow\n    legs: \"4\"\n    cute: \"true\"\n",
+		expected:       "cat:\n    says: meow\n    legs: \"4\"\n    cute: \"true\"\n",
 	},
 	{
 		description:    "Parse xml: number",
 		subdescription: "All values are assumed to be strings when parsing XML, but you can use the `from_yaml` operator on all the strings values to autoparse into the correct type.",
 		input:          "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat>\n  <says>meow</says>\n  <legs>4</legs>\n  <cute>true</cute>\n</cat>",
 		expression:     " (.. | select(tag == \"!!str\")) |= from_yaml",
-		expected:       "D0, P[], ()::cat:\n    says: meow\n    legs: 4\n    cute: true\n",
+		expected:       "cat:\n    says: meow\n    legs: 4\n    cute: true\n",
 	},
 	{
 		description:    "Parse xml: array",
 		subdescription: "Consecutive nodes with identical xml names are assumed to be arrays.",
 		input:          "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<animal>cat</animal>\n<animal>goat</animal>",
-		expected:       "D0, P[], (doc)::animal:\n    - cat\n    - goat\n",
+		expected:       "animal:\n    - cat\n    - goat\n",
 	},
 	{
 		description:    "Parse xml: attributes",
 		subdescription: "Attributes are converted to fields, with the default attribute prefix '+'. Use '--xml-attribute-prefix` to set your own.",
 		input:          "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat legs=\"4\">\n  <legs>7</legs>\n</cat>",
-		expected:       "D0, P[], (doc)::cat:\n    +legs: \"4\"\n    legs: \"7\"\n",
+		expected:       "cat:\n    +legs: \"4\"\n    legs: \"7\"\n",
 	},
 	{
 		description:    "Parse xml: attributes with content",
 		subdescription: "Content is added as a field, using the default content name of `+content`. Use `--xml-content-name` to set your own.",
 		input:          "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat legs=\"4\">meow</cat>",
-		expected:       "D0, P[], (doc)::cat:\n    +content: meow\n    +legs: \"4\"\n",
+		expected:       "cat:\n    +content: meow\n    +legs: \"4\"\n",
 	},
 	{
 		description:    "Parse xml: with comments",
@@ -272,28 +196,28 @@ var xmlScenarios = []formatScenario{
 		description:  "Empty doc",
 		skipDoc:      true,
 		input:        "",
-		expected:     "D0, P[], ()::null\n",
+		expected:     "",
 		scenarioType: "decode",
 	},
 	{
 		description:  "Empty single node",
 		skipDoc:      true,
 		input:        "<a/>",
-		expected:     "D0, P[], (doc)::a:\n",
+		expected:     "a:\n",
 		scenarioType: "decode",
 	},
 	{
 		description:  "Empty close node",
 		skipDoc:      true,
 		input:        "<a></a>",
-		expected:     "D0, P[], (doc)::a:\n",
+		expected:     "a:\n",
 		scenarioType: "decode",
 	},
 	{
 		description:  "Nested empty",
 		skipDoc:      true,
 		input:        "<a><b/></a>",
-		expected:     "D0, P[], (doc)::a:\n    b:\n",
+		expected:     "a:\n    b:\n",
 		scenarioType: "decode",
 	},
 	{
@@ -359,11 +283,12 @@ var xmlScenarios = []formatScenario{
 }
 
 func testXMLScenario(t *testing.T, s formatScenario) {
-	if s.scenarioType == "encode" || s.scenarioType == "roundtrip" {
-		test.AssertResultWithContext(t, s.expected, processXMLScenario(s), s.description)
+	if s.scenarioType == "encode" {
+		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewXMLEncoder(2, "+", "+content")), s.description)
+	} else if s.scenarioType == "roundtrip" {
+		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder("+", "+content"), NewXMLEncoder(2, "+", "+content")), s.description)
 	} else {
-		var actual = resultToString(t, decodeXML(t, s))
-		test.AssertResultWithContext(t, s.expected, actual, s.description)
+		test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder("+", "+content"), NewYamlEncoder(4, false, true, true)), s.description)
 	}
 }
 
@@ -378,12 +303,12 @@ func documentXMLScenario(t *testing.T, w *bufio.Writer, i interface{}) {
 	} else if s.scenarioType == "roundtrip" {
 		documentXMLRoundTripScenario(w, s)
 	} else {
-		documentXMLDecodeScenario(t, w, s)
+		documentXMLDecodeScenario(w, s)
 	}
 
 }
 
-func documentXMLDecodeScenario(t *testing.T, w *bufio.Writer, s formatScenario) {
+func documentXMLDecodeScenario(w *bufio.Writer, s formatScenario) {
 	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
 
 	if s.subdescription != "" {
@@ -402,18 +327,7 @@ func documentXMLDecodeScenario(t *testing.T, w *bufio.Writer, s formatScenario)
 	writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=xml '%v' sample.xml\n```\n", expression))
 	writeOrPanic(w, "will output\n")
 
-	var output bytes.Buffer
-	printer := NewSimpleYamlPrinter(bufio.NewWriter(&output), YamlOutputFormat, true, false, 2, true)
-
-	node := decodeXML(t, s)
-
-	err := printer.PrintResults(node.AsList())
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", output.String()))
+	writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder("+", "+content"), NewYamlEncoder(2, false, true, true))))
 }
 
 func documentXMLEncodeScenario(w *bufio.Writer, s formatScenario) {
@@ -431,7 +345,7 @@ func documentXMLEncodeScenario(w *bufio.Writer, s formatScenario) {
 	writeOrPanic(w, "```bash\nyq -o=xml '.' sample.yml\n```\n")
 	writeOrPanic(w, "will output\n")
 
-	writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processXMLScenario(s)))
+	writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewXMLEncoder(2, "+", "+content"))))
 }
 
 func documentXMLRoundTripScenario(w *bufio.Writer, s formatScenario) {
@@ -449,7 +363,7 @@ func documentXMLRoundTripScenario(w *bufio.Writer, s formatScenario) {
 	writeOrPanic(w, "```bash\nyq -p=xml -o=xml '.' sample.xml\n```\n")
 	writeOrPanic(w, "will output\n")
 
-	writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processXMLScenario(s)))
+	writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder("+", "+content"), NewXMLEncoder(2, "+", "+content"))))
 }
 
 func TestXMLScenarios(t *testing.T) {