Repository: mikefarah/yq
Pull request #1044 (2021-12-21 03:59:27)

Added XML decoder


Patch:
diff --git a/LICENSE b/LICENSE
index e2f1fc03..dfbcc195 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,5 @@
+The MIT License (MIT)
+
 Copyright (c) 2017 Mike Farah
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
diff --git a/cmd/constant.go b/cmd/constant.go
index 0b921410..5cca3c5c 100644
--- a/cmd/constant.go
+++ b/cmd/constant.go
@@ -6,6 +6,10 @@ var unwrapScalar = true
 var writeInplace = false
 var outputToJSON = false
 var outputFormat = "yaml"
+var inputFormat = "yaml"
+
+var xmlAttributePrefix = "+"
+var xmlContentName = "+content"
 
 var exitStatus = false
 var forceColor = false
diff --git a/cmd/evaluate_all_command.go b/cmd/evaluate_all_command.go
index af3c6ec7..c52e2526 100644
--- a/cmd/evaluate_all_command.go
+++ b/cmd/evaluate_all_command.go
@@ -75,6 +75,11 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	decoder, err := configureDecoder()
+	if err != nil {
+		return err
+	}
+
 	printerWriter := configurePrinterWriter(format, out)
 
 	printer := yqlib.NewPrinter(printerWriter, format, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
@@ -99,7 +104,7 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
 	switch len(args) {
 	case 0:
 		if pipingStdIn {
-			err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing)
+			err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing, decoder)
 		} else {
 			cmd.Println(cmd.UsageString())
 			return nil
@@ -108,10 +113,10 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
 		if nullInput {
 			err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(args[0]), printer, "")
 		} else {
-			err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing)
+			err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing, decoder)
 		}
 	default:
-		err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing)
+		err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing, decoder)
 	}
 
 	completedSuccessfully = err == nil
diff --git a/cmd/evalute_sequence_command.go b/cmd/evalute_sequence_command.go
index e875c4bd..2a371ed4 100644
--- a/cmd/evalute_sequence_command.go
+++ b/cmd/evalute_sequence_command.go
@@ -92,6 +92,11 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
 
 	printer := yqlib.NewPrinter(printerWriter, format, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
 
+	decoder, err := configureDecoder()
+	if err != nil {
+		return err
+	}
+
 	streamEvaluator := yqlib.NewStreamEvaluator()
 
 	if frontMatter != "" {
@@ -113,7 +118,7 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
 	switch len(args) {
 	case 0:
 		if pipingStdIn {
-			err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing)
+			err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing, decoder)
 		} else {
 			cmd.Println(cmd.UsageString())
 			return nil
@@ -122,10 +127,10 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
 		if nullInput {
 			err = streamEvaluator.EvaluateNew(processExpression(args[0]), printer, "")
 		} else {
-			err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing)
+			err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing, decoder)
 		}
 	default:
-		err = streamEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing)
+		err = streamEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing, decoder)
 	}
 	completedSuccessfully = err == nil
 
diff --git a/cmd/root.go b/cmd/root.go
index 11471d27..28e22648 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -49,6 +49,11 @@ See https://mikefarah.gitbook.io/yq/ for detailed documentation and examples.`,
 	}
 
 	rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "yaml", "[yaml|y|json|j|props|p] output format type.")
+	rootCmd.PersistentFlags().StringVarP(&inputFormat, "input-format", "p", "yaml", "[yaml|y|xml|x] input format type.")
+
+	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).")
+
 	rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating yaml docs from scratch.")
 	rootCmd.PersistentFlags().BoolVarP(&noDocSeparators, "no-doc", "N", false, "Don't print document separators (---)")
 
diff --git a/cmd/utils.go b/cmd/utils.go
index 483ea426..7ab518a4 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -45,6 +45,18 @@ func initCommand(cmd *cobra.Command, args []string) (firstFileIndex int, err err
 	return firstFileIndex, nil
 }
 
+func configureDecoder() (yqlib.Decoder, error) {
+	yqlibInputFormat, err := yqlib.InputFormatFromString(inputFormat)
+	if err != nil {
+		return nil, err
+	}
+	switch yqlibInputFormat {
+	case yqlib.XmlInputFormat:
+		return yqlib.NewXmlDecoder(xmlAttributePrefix, xmlContentName), nil
+	}
+	return yqlib.NewYamlDecoder(), nil
+}
+
 func configurePrinterWriter(format yqlib.PrinterOutputFormat, out io.Writer) yqlib.PrinterWriter {
 
 	var printerWriter yqlib.PrinterWriter
diff --git a/examples/mike.xml b/examples/mike.xml
new file mode 100644
index 00000000..e64b598b
--- /dev/null
+++ b/examples/mike.xml
@@ -0,0 +1,4 @@
+  <?xml version="1.0" encoding="UTF-8"?>
+  <cat>3f</cat>
+  <dog>meow:as</dog>
+  <dog3>true</dog3>
\ No newline at end of file
diff --git a/examples/mike2.xml b/examples/mike2.xml
new file mode 100644
index 00000000..0ad80955
--- /dev/null
+++ b/examples/mike2.xml
@@ -0,0 +1,11 @@
+  <?xml version="1.0" encoding="UTF-8"?>
+  <!-- osm-->
+  <osm version="0.6" generator="CGImap 0.0.2">
+  <!-- bounds-->
+   <bounds minlat="54.0889580" minlon="12.2487570" maxlat="54.0913900" maxlon="12.2524800">
+   <!-- great -->
+   cool
+   </bounds>
+   <foo>ba2234r</foo>
+   <foo>bar2234233</foo>
+  </osm>
\ No newline at end of file
diff --git a/go.mod b/go.mod
index ac72186a..be0a0302 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/magiconair/properties v1.8.5
 	github.com/spf13/cobra v1.3.0
 	github.com/timtadh/lexmachine v0.2.2
+	golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )
@@ -19,6 +20,7 @@ require (
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/timtadh/data-structures v0.5.3 // indirect
 	golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
+	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 )
 
diff --git a/go.sum b/go.sum
index 1cdc7256..aad032da 100644
--- a/go.sum
+++ b/go.sum
@@ -449,6 +449,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -553,6 +554,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/yqlib/all_at_once_evaluator.go b/pkg/yqlib/all_at_once_evaluator.go
index d23c56c6..371bc48f 100644
--- a/pkg/yqlib/all_at_once_evaluator.go
+++ b/pkg/yqlib/all_at_once_evaluator.go
@@ -8,7 +8,7 @@ import (
 
 // A yaml expression evaluator that runs the expression once against all files/nodes in memory.
 type Evaluator interface {
-	EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error
+	EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error
 
 	// EvaluateNodes takes an expression and one or more yaml nodes, returning a list of matching candidate nodes
 	EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error)
@@ -46,7 +46,7 @@ func (e *allAtOnceEvaluator) EvaluateCandidateNodes(expression string, inputCand
 	return context.MatchingNodes, nil
 }
 
-func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error {
+func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error {
 	fileIndex := 0
 	firstFileLeadingContent := ""
 
@@ -61,7 +61,7 @@ func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string
 			firstFileLeadingContent = leadingContent
 		}
 
-		fileDocuments, err := readDocuments(reader, filename, fileIndex)
+		fileDocuments, err := readDocuments(reader, filename, fileIndex, decoder)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/yqlib/decoder_xml.go b/pkg/yqlib/decoder_xml.go
new file mode 100644
index 00000000..ada214ca
--- /dev/null
+++ b/pkg/yqlib/decoder_xml.go
@@ -0,0 +1,249 @@
+package yqlib
+
+import (
+	"encoding/xml"
+	"fmt"
+	"io"
+	"unicode"
+
+	"golang.org/x/net/html/charset"
+	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
+	contentPrefix   string
+	finished        bool
+}
+
+func NewXmlDecoder(attributePrefix string, contentPrefix string) Decoder {
+	if contentPrefix == "" {
+		contentPrefix = "content"
+	}
+	return &xmlDecoder{attributePrefix: attributePrefix, contentPrefix: contentPrefix, finished: false}
+}
+
+func (dec *xmlDecoder) Init(reader io.Reader) {
+	dec.reader = reader
+	dec.finished = false
+}
+
+func (dec *xmlDecoder) createSequence(nodes []*xmlNode) (*yaml.Node, error) {
+	yamlNode := &yaml.Node{Kind: yaml.SequenceNode}
+	for _, child := range nodes {
+		yamlChild, err := dec.convertToYamlNode(child)
+		if err != nil {
+			return nil, err
+		}
+		yamlNode.Content = append(yamlNode.Content, yamlChild)
+	}
+
+	return yamlNode, nil
+}
+
+func (dec *xmlDecoder) createMap(n *xmlNode) (*yaml.Node, error) {
+	yamlNode := &yaml.Node{Kind: yaml.MappingNode, HeadComment: n.Comment}
+
+	if len(n.Data) > 0 {
+		label := dec.contentPrefix
+		yamlNode.Content = append(yamlNode.Content, createScalarNode(label, label), createScalarNode(n.Data, n.Data))
+	}
+
+	for _, keyValuePair := range n.Children {
+		label := keyValuePair.K
+		children := keyValuePair.V
+		labelNode := createScalarNode(label, label)
+		var valueNode *yaml.Node
+		var err error
+		log.Debug("len of children in %v is %v", label, len(children))
+		if len(children) > 1 {
+			valueNode, err = dec.createSequence(children)
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			valueNode, err = dec.convertToYamlNode(children[0])
+			if err != nil {
+				return nil, err
+			}
+		}
+		yamlNode.Content = append(yamlNode.Content, labelNode, valueNode)
+	}
+
+	return yamlNode, nil
+}
+
+func (dec *xmlDecoder) convertToYamlNode(n *xmlNode) (*yaml.Node, error) {
+	if len(n.Children) > 0 {
+		return dec.createMap(n)
+	}
+	scalar := createScalarNode(n.Data, n.Data)
+	scalar.HeadComment = n.Comment
+	return scalar, nil
+}
+
+func (dec *xmlDecoder) Decode(rootYamlNode *yaml.Node) error {
+	if dec.finished {
+		return io.EOF
+	}
+	root := &xmlNode{}
+	// cant use xj - it doesn't keep map order.
+	err := dec.decodeXml(root)
+
+	if err != nil {
+		return err
+	}
+	firstNode, err := dec.convertToYamlNode(root)
+
+	if err != nil {
+		return err
+	}
+	rootYamlNode.Kind = yaml.DocumentNode
+	rootYamlNode.Content = []*yaml.Node{firstNode}
+	dec.finished = true
+	return nil
+}
+
+type xmlNode struct {
+	Children []*xmlChildrenKv
+	Comment  string
+	Data     string
+}
+
+type xmlChildrenKv struct {
+	K string
+	V []*xmlNode
+}
+
+// AddChild appends a node to the list of children
+func (n *xmlNode) AddChild(s string, c *xmlNode) {
+
+	if n.Children == nil {
+		n.Children = make([]*xmlChildrenKv, 0)
+	}
+	log.Debug("looking for %s", s)
+	// see if we can find an existing entry to add to
+	for _, childEntry := range n.Children {
+		if childEntry.K == s {
+			log.Debug("found it, appending an entry%s", s)
+			childEntry.V = append(childEntry.V, c)
+			log.Debug("yay len of children in %v is %v", s, len(childEntry.V))
+			return
+		}
+	}
+	log.Debug("not there, making a new one %s", s)
+	n.Children = append(n.Children, &xmlChildrenKv{K: s, V: []*xmlNode{c}})
+}
+
+type element struct {
+	parent *element
+	n      *xmlNode
+	label  string
+}
+
+// this code is heavily based on https://github.com/basgys/goxml2json
+// main changes are to decode into a structure that preserves the original order
+// of the map keys.
+func (dec *xmlDecoder) decodeXml(root *xmlNode) error {
+	xmlDec := xml.NewDecoder(dec.reader)
+
+	// That will convert the charset if the provided XML is non-UTF-8
+	xmlDec.CharsetReader = charset.NewReaderLabel
+
+	// Create first element from the root node
+	elem := &element{
+		parent: nil,
+		n:      root,
+	}
+
+	for {
+		t, _ := xmlDec.Token()
+		if t == nil {
+			break
+		}
+
+		switch se := t.(type) {
+		case xml.StartElement:
+			// Build new a new current element and link it to its parent
+			elem = &element{
+				parent: elem,
+				n:      &xmlNode{},
+				label:  se.Name.Local,
+			}
+
+			// Extract attributes as children
+			for _, a := range se.Attr {
+				elem.n.AddChild(dec.attributePrefix+a.Name.Local, &xmlNode{Data: a.Value})
+			}
+		case xml.CharData:
+			// Extract XML data (if any)
+			elem.n.Data = trimNonGraphic(string(se))
+		case xml.EndElement:
+			// And add it to its parent list
+			if elem.parent != nil {
+				elem.parent.n.AddChild(elem.label, elem.n)
+			}
+
+			// Then change the current element to its parent
+			elem = elem.parent
+		case xml.Comment:
+			elem.n.Comment = trimNonGraphic(string(xml.CharData(se)))
+		}
+	}
+
+	return nil
+}
+
+// trimNonGraphic returns a slice of the string s, with all leading and trailing
+// non graphic characters and spaces removed.
+//
+// Graphic characters include letters, marks, numbers, punctuation, symbols,
+// and spaces, from categories L, M, N, P, S, Zs.
+// Spacing characters are set by category Z and property Pattern_White_Space.
+func trimNonGraphic(s string) string {
+	if s == "" {
+		return s
+	}
+
+	var first *int
+	var last int
+	for i, r := range []rune(s) {
+		if !unicode.IsGraphic(r) || unicode.IsSpace(r) {
+			continue
+		}
+
+		if first == nil {
+			f := i // copy i
+			first = &f
+			last = i
+		} else {
+			last = i
+		}
+	}
+
+	// If first is nil, it means there are no graphic characters
+	if first == nil {
+		return ""
+	}
+
+	return string([]rune(s)[*first : last+1])
+}
diff --git a/pkg/yqlib/decoder_yaml.go b/pkg/yqlib/decoder_yaml.go
new file mode 100644
index 00000000..58f1c520
--- /dev/null
+++ b/pkg/yqlib/decoder_yaml.go
@@ -0,0 +1,28 @@
+package yqlib
+
+import (
+	"io"
+
+	yaml "gopkg.in/yaml.v3"
+)
+
+type Decoder interface {
+	Init(reader io.Reader)
+	Decode(node *yaml.Node) error
+}
+
+type yamlDecoder struct {
+	decoder yaml.Decoder
+}
+
+func NewYamlDecoder() Decoder {
+	return &yamlDecoder{}
+}
+
+func (dec *yamlDecoder) Init(reader io.Reader) {
+	dec.decoder = *yaml.NewDecoder(reader)
+}
+
+func (dec *yamlDecoder) Decode(rootYamlNode *yaml.Node) error {
+	return dec.decoder.Decode(rootYamlNode)
+}
diff --git a/pkg/yqlib/doc/add.md b/pkg/yqlib/doc/operators/add.md
similarity index 100%
rename from pkg/yqlib/doc/add.md
rename to pkg/yqlib/doc/operators/add.md
diff --git a/pkg/yqlib/doc/alternative-default-value.md b/pkg/yqlib/doc/operators/alternative-default-value.md
similarity index 100%
rename from pkg/yqlib/doc/alternative-default-value.md
rename to pkg/yqlib/doc/operators/alternative-default-value.md
diff --git a/pkg/yqlib/doc/anchor-and-alias-operators.md b/pkg/yqlib/doc/operators/anchor-and-alias-operators.md
similarity index 100%
rename from pkg/yqlib/doc/anchor-and-alias-operators.md
rename to pkg/yqlib/doc/operators/anchor-and-alias-operators.md
diff --git a/pkg/yqlib/doc/assign-update.md b/pkg/yqlib/doc/operators/assign-update.md
similarity index 100%
rename from pkg/yqlib/doc/assign-update.md
rename to pkg/yqlib/doc/operators/assign-update.md
diff --git a/pkg/yqlib/doc/boolean-operators.md b/pkg/yqlib/doc/operators/boolean-operators.md
similarity index 100%
rename from pkg/yqlib/doc/boolean-operators.md
rename to pkg/yqlib/doc/operators/boolean-operators.md
diff --git a/pkg/yqlib/doc/collect-into-array.md b/pkg/yqlib/doc/operators/collect-into-array.md
similarity index 100%
rename from pkg/yqlib/doc/collect-into-array.md
rename to pkg/yqlib/doc/operators/collect-into-array.md
diff --git a/pkg/yqlib/doc/comment-operators.md b/pkg/yqlib/doc/operators/comment-operators.md
similarity index 100%
rename from pkg/yqlib/doc/comment-operators.md
rename to pkg/yqlib/doc/operators/comment-operators.md
diff --git a/pkg/yqlib/doc/contains.md b/pkg/yqlib/doc/operators/contains.md
similarity index 100%
rename from pkg/yqlib/doc/contains.md
rename to pkg/yqlib/doc/operators/contains.md
diff --git a/pkg/yqlib/doc/create-collect-into-object.md b/pkg/yqlib/doc/operators/create-collect-into-object.md
similarity index 100%
rename from pkg/yqlib/doc/create-collect-into-object.md
rename to pkg/yqlib/doc/operators/create-collect-into-object.md
diff --git a/pkg/yqlib/doc/delete.md b/pkg/yqlib/doc/operators/delete.md
similarity index 100%
rename from pkg/yqlib/doc/delete.md
rename to pkg/yqlib/doc/operators/delete.md
diff --git a/pkg/yqlib/doc/document-index.md b/pkg/yqlib/doc/operators/document-index.md
similarity index 100%
rename from pkg/yqlib/doc/document-index.md
rename to pkg/yqlib/doc/operators/document-index.md
diff --git a/pkg/yqlib/doc/encode-decode.md b/pkg/yqlib/doc/operators/encode-decode.md
similarity index 95%
rename from pkg/yqlib/doc/encode-decode.md
rename to pkg/yqlib/doc/operators/encode-decode.md
index d4416608..d77aa917 100644
--- a/pkg/yqlib/doc/encode-decode.md
+++ b/pkg/yqlib/doc/operators/encode-decode.md
@@ -271,3 +271,19 @@ cat	thing1,thing2	true	3.40
 dog	thing3	false	12
 ```
 
+## Decode a xml encoded string
+Given a sample.yml file of:
+```yaml
+a: <foo>bar</foo>
+```
+then
+```bash
+yq eval '.b = (.a | from_xml)' sample.yml
+```
+will output
+```yaml
+a: <foo>bar</foo>
+b:
+  foo: bar
+```
+
diff --git a/pkg/yqlib/doc/entries.md b/pkg/yqlib/doc/operators/entries.md
similarity index 100%
rename from pkg/yqlib/doc/entries.md
rename to pkg/yqlib/doc/operators/entries.md
diff --git a/pkg/yqlib/doc/env-variable-operators.md b/pkg/yqlib/doc/operators/env-variable-operators.md
similarity index 100%
rename from pkg/yqlib/doc/env-variable-operators.md
rename to pkg/yqlib/doc/operators/env-variable-operators.md
diff --git a/pkg/yqlib/doc/equals.md b/pkg/yqlib/doc/operators/equals.md
similarity index 100%
rename from pkg/yqlib/doc/equals.md
rename to pkg/yqlib/doc/operators/equals.md
diff --git a/pkg/yqlib/doc/file-operators.md b/pkg/yqlib/doc/operators/file-operators.md
similarity index 100%
rename from pkg/yqlib/doc/file-operators.md
rename to pkg/yqlib/doc/operators/file-operators.md
diff --git a/pkg/yqlib/doc/flatten.md b/pkg/yqlib/doc/operators/flatten.md
similarity index 100%
rename from pkg/yqlib/doc/flatten.md
rename to pkg/yqlib/doc/operators/flatten.md
diff --git a/pkg/yqlib/doc/group-by.md b/pkg/yqlib/doc/operators/group-by.md
similarity index 100%
rename from pkg/yqlib/doc/group-by.md
rename to pkg/yqlib/doc/operators/group-by.md
diff --git a/pkg/yqlib/doc/has.md b/pkg/yqlib/doc/operators/has.md
similarity index 100%
rename from pkg/yqlib/doc/has.md
rename to pkg/yqlib/doc/operators/has.md
diff --git a/pkg/yqlib/doc/headers/Main.md b/pkg/yqlib/doc/operators/headers/Main.md
similarity index 100%
rename from pkg/yqlib/doc/headers/Main.md
rename to pkg/yqlib/doc/operators/headers/Main.md
diff --git a/pkg/yqlib/doc/headers/add.md b/pkg/yqlib/doc/operators/headers/add.md
similarity index 100%
rename from pkg/yqlib/doc/headers/add.md
rename to pkg/yqlib/doc/operators/headers/add.md
diff --git a/pkg/yqlib/doc/headers/alternative-default-value.md b/pkg/yqlib/doc/operators/headers/alternative-default-value.md
similarity index 100%
rename from pkg/yqlib/doc/headers/alternative-default-value.md
rename to pkg/yqlib/doc/operators/headers/alternative-default-value.md
diff --git a/pkg/yqlib/doc/headers/anchor-and-alias-operators.md b/pkg/yqlib/doc/operators/headers/anchor-and-alias-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/anchor-and-alias-operators.md
rename to pkg/yqlib/doc/operators/headers/anchor-and-alias-operators.md
diff --git a/pkg/yqlib/doc/headers/assign-update.md b/pkg/yqlib/doc/operators/headers/assign-update.md
similarity index 100%
rename from pkg/yqlib/doc/headers/assign-update.md
rename to pkg/yqlib/doc/operators/headers/assign-update.md
diff --git a/pkg/yqlib/doc/headers/boolean-operators.md b/pkg/yqlib/doc/operators/headers/boolean-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/boolean-operators.md
rename to pkg/yqlib/doc/operators/headers/boolean-operators.md
diff --git a/pkg/yqlib/doc/headers/collect-into-array.md b/pkg/yqlib/doc/operators/headers/collect-into-array.md
similarity index 100%
rename from pkg/yqlib/doc/headers/collect-into-array.md
rename to pkg/yqlib/doc/operators/headers/collect-into-array.md
diff --git a/pkg/yqlib/doc/headers/comment-operators.md b/pkg/yqlib/doc/operators/headers/comment-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/comment-operators.md
rename to pkg/yqlib/doc/operators/headers/comment-operators.md
diff --git a/pkg/yqlib/doc/headers/contains.md b/pkg/yqlib/doc/operators/headers/contains.md
similarity index 100%
rename from pkg/yqlib/doc/headers/contains.md
rename to pkg/yqlib/doc/operators/headers/contains.md
diff --git a/pkg/yqlib/doc/headers/create-collect-into-object.md b/pkg/yqlib/doc/operators/headers/create-collect-into-object.md
similarity index 100%
rename from pkg/yqlib/doc/headers/create-collect-into-object.md
rename to pkg/yqlib/doc/operators/headers/create-collect-into-object.md
diff --git a/pkg/yqlib/doc/headers/delete.md b/pkg/yqlib/doc/operators/headers/delete.md
similarity index 100%
rename from pkg/yqlib/doc/headers/delete.md
rename to pkg/yqlib/doc/operators/headers/delete.md
diff --git a/pkg/yqlib/doc/headers/document-index.md b/pkg/yqlib/doc/operators/headers/document-index.md
similarity index 100%
rename from pkg/yqlib/doc/headers/document-index.md
rename to pkg/yqlib/doc/operators/headers/document-index.md
diff --git a/pkg/yqlib/doc/headers/encode-decode.md b/pkg/yqlib/doc/operators/headers/encode-decode.md
similarity index 97%
rename from pkg/yqlib/doc/headers/encode-decode.md
rename to pkg/yqlib/doc/operators/headers/encode-decode.md
index 92ff9f5a..847240b3 100644
--- a/pkg/yqlib/doc/headers/encode-decode.md
+++ b/pkg/yqlib/doc/operators/headers/encode-decode.md
@@ -14,6 +14,7 @@ These operators are useful to process yaml documents that have stringified embed
 | Properties |  | to_props/@props |
 | CSV |  | to_csv/@csv |
 | TSV |  | to_tsv/@tsv |
+| XML | from_xml |  |
 
 
 CSV and TSV format both accept either a single array or scalars (representing a single row), or an array of array of scalars (representing multiple rows). 
diff --git a/pkg/yqlib/doc/headers/entries.md b/pkg/yqlib/doc/operators/headers/entries.md
similarity index 100%
rename from pkg/yqlib/doc/headers/entries.md
rename to pkg/yqlib/doc/operators/headers/entries.md
diff --git a/pkg/yqlib/doc/headers/env-variable-operators.md b/pkg/yqlib/doc/operators/headers/env-variable-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/env-variable-operators.md
rename to pkg/yqlib/doc/operators/headers/env-variable-operators.md
diff --git a/pkg/yqlib/doc/headers/equals.md b/pkg/yqlib/doc/operators/headers/equals.md
similarity index 100%
rename from pkg/yqlib/doc/headers/equals.md
rename to pkg/yqlib/doc/operators/headers/equals.md
diff --git a/pkg/yqlib/doc/headers/file-operators.md b/pkg/yqlib/doc/operators/headers/file-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/file-operators.md
rename to pkg/yqlib/doc/operators/headers/file-operators.md
diff --git a/pkg/yqlib/doc/headers/flatten.md b/pkg/yqlib/doc/operators/headers/flatten.md
similarity index 100%
rename from pkg/yqlib/doc/headers/flatten.md
rename to pkg/yqlib/doc/operators/headers/flatten.md
diff --git a/pkg/yqlib/doc/headers/group-by.md b/pkg/yqlib/doc/operators/headers/group-by.md
similarity index 100%
rename from pkg/yqlib/doc/headers/group-by.md
rename to pkg/yqlib/doc/operators/headers/group-by.md
diff --git a/pkg/yqlib/doc/headers/has.md b/pkg/yqlib/doc/operators/headers/has.md
similarity index 100%
rename from pkg/yqlib/doc/headers/has.md
rename to pkg/yqlib/doc/operators/headers/has.md
diff --git a/pkg/yqlib/doc/headers/keys.md b/pkg/yqlib/doc/operators/headers/keys.md
similarity index 100%
rename from pkg/yqlib/doc/headers/keys.md
rename to pkg/yqlib/doc/operators/headers/keys.md
diff --git a/pkg/yqlib/doc/headers/length.md b/pkg/yqlib/doc/operators/headers/length.md
similarity index 100%
rename from pkg/yqlib/doc/headers/length.md
rename to pkg/yqlib/doc/operators/headers/length.md
diff --git a/pkg/yqlib/doc/headers/load.md b/pkg/yqlib/doc/operators/headers/load.md
similarity index 100%
rename from pkg/yqlib/doc/headers/load.md
rename to pkg/yqlib/doc/operators/headers/load.md
diff --git a/pkg/yqlib/doc/headers/map.md b/pkg/yqlib/doc/operators/headers/map.md
similarity index 100%
rename from pkg/yqlib/doc/headers/map.md
rename to pkg/yqlib/doc/operators/headers/map.md
diff --git a/pkg/yqlib/doc/headers/multiply-merge.md b/pkg/yqlib/doc/operators/headers/multiply-merge.md
similarity index 100%
rename from pkg/yqlib/doc/headers/multiply-merge.md
rename to pkg/yqlib/doc/operators/headers/multiply-merge.md
diff --git a/pkg/yqlib/doc/headers/parent.md b/pkg/yqlib/doc/operators/headers/parent.md
similarity index 100%
rename from pkg/yqlib/doc/headers/parent.md
rename to pkg/yqlib/doc/operators/headers/parent.md
diff --git a/pkg/yqlib/doc/headers/path.md b/pkg/yqlib/doc/operators/headers/path.md
similarity index 100%
rename from pkg/yqlib/doc/headers/path.md
rename to pkg/yqlib/doc/operators/headers/path.md
diff --git a/pkg/yqlib/doc/headers/pipe.md b/pkg/yqlib/doc/operators/headers/pipe.md
similarity index 100%
rename from pkg/yqlib/doc/headers/pipe.md
rename to pkg/yqlib/doc/operators/headers/pipe.md
diff --git a/pkg/yqlib/doc/headers/recursive-descent-glob.md b/pkg/yqlib/doc/operators/headers/recursive-descent-glob.md
similarity index 100%
rename from pkg/yqlib/doc/headers/recursive-descent-glob.md
rename to pkg/yqlib/doc/operators/headers/recursive-descent-glob.md
diff --git a/pkg/yqlib/doc/headers/reduce.md b/pkg/yqlib/doc/operators/headers/reduce.md
similarity index 100%
rename from pkg/yqlib/doc/headers/reduce.md
rename to pkg/yqlib/doc/operators/headers/reduce.md
diff --git a/pkg/yqlib/doc/headers/select.md b/pkg/yqlib/doc/operators/headers/select.md
similarity index 100%
rename from pkg/yqlib/doc/headers/select.md
rename to pkg/yqlib/doc/operators/headers/select.md
diff --git a/pkg/yqlib/doc/headers/sort-keys.md b/pkg/yqlib/doc/operators/headers/sort-keys.md
similarity index 100%
rename from pkg/yqlib/doc/headers/sort-keys.md
rename to pkg/yqlib/doc/operators/headers/sort-keys.md
diff --git a/pkg/yqlib/doc/headers/sort.md b/pkg/yqlib/doc/operators/headers/sort.md
similarity index 100%
rename from pkg/yqlib/doc/headers/sort.md
rename to pkg/yqlib/doc/operators/headers/sort.md
diff --git a/pkg/yqlib/doc/headers/split-into-documents.md b/pkg/yqlib/doc/operators/headers/split-into-documents.md
similarity index 100%
rename from pkg/yqlib/doc/headers/split-into-documents.md
rename to pkg/yqlib/doc/operators/headers/split-into-documents.md
diff --git a/pkg/yqlib/doc/headers/string-operators.md b/pkg/yqlib/doc/operators/headers/string-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/string-operators.md
rename to pkg/yqlib/doc/operators/headers/string-operators.md
diff --git a/pkg/yqlib/doc/headers/style.md b/pkg/yqlib/doc/operators/headers/style.md
similarity index 100%
rename from pkg/yqlib/doc/headers/style.md
rename to pkg/yqlib/doc/operators/headers/style.md
diff --git a/pkg/yqlib/doc/headers/subtract.md b/pkg/yqlib/doc/operators/headers/subtract.md
similarity index 100%
rename from pkg/yqlib/doc/headers/subtract.md
rename to pkg/yqlib/doc/operators/headers/subtract.md
diff --git a/pkg/yqlib/doc/headers/tag.md b/pkg/yqlib/doc/operators/headers/tag.md
similarity index 100%
rename from pkg/yqlib/doc/headers/tag.md
rename to pkg/yqlib/doc/operators/headers/tag.md
diff --git a/pkg/yqlib/doc/headers/traverse-read.md b/pkg/yqlib/doc/operators/headers/traverse-read.md
similarity index 100%
rename from pkg/yqlib/doc/headers/traverse-read.md
rename to pkg/yqlib/doc/operators/headers/traverse-read.md
diff --git a/pkg/yqlib/doc/headers/union.md b/pkg/yqlib/doc/operators/headers/union.md
similarity index 100%
rename from pkg/yqlib/doc/headers/union.md
rename to pkg/yqlib/doc/operators/headers/union.md
diff --git a/pkg/yqlib/doc/headers/unique.md b/pkg/yqlib/doc/operators/headers/unique.md
similarity index 100%
rename from pkg/yqlib/doc/headers/unique.md
rename to pkg/yqlib/doc/operators/headers/unique.md
diff --git a/pkg/yqlib/doc/headers/variable-operators.md b/pkg/yqlib/doc/operators/headers/variable-operators.md
similarity index 100%
rename from pkg/yqlib/doc/headers/variable-operators.md
rename to pkg/yqlib/doc/operators/headers/variable-operators.md
diff --git a/pkg/yqlib/doc/headers/with.md b/pkg/yqlib/doc/operators/headers/with.md
similarity index 100%
rename from pkg/yqlib/doc/headers/with.md
rename to pkg/yqlib/doc/operators/headers/with.md
diff --git a/pkg/yqlib/doc/keys.md b/pkg/yqlib/doc/operators/keys.md
similarity index 100%
rename from pkg/yqlib/doc/keys.md
rename to pkg/yqlib/doc/operators/keys.md
diff --git a/pkg/yqlib/doc/length.md b/pkg/yqlib/doc/operators/length.md
similarity index 100%
rename from pkg/yqlib/doc/length.md
rename to pkg/yqlib/doc/operators/length.md
diff --git a/pkg/yqlib/doc/load.md b/pkg/yqlib/doc/operators/load.md
similarity index 100%
rename from pkg/yqlib/doc/load.md
rename to pkg/yqlib/doc/operators/load.md
diff --git a/pkg/yqlib/doc/map.md b/pkg/yqlib/doc/operators/map.md
similarity index 100%
rename from pkg/yqlib/doc/map.md
rename to pkg/yqlib/doc/operators/map.md
diff --git a/pkg/yqlib/doc/multiply-merge.md b/pkg/yqlib/doc/operators/multiply-merge.md
similarity index 100%
rename from pkg/yqlib/doc/multiply-merge.md
rename to pkg/yqlib/doc/operators/multiply-merge.md
diff --git a/pkg/yqlib/doc/parent.md b/pkg/yqlib/doc/operators/parent.md
similarity index 100%
rename from pkg/yqlib/doc/parent.md
rename to pkg/yqlib/doc/operators/parent.md
diff --git a/pkg/yqlib/doc/path.md b/pkg/yqlib/doc/operators/path.md
similarity index 100%
rename from pkg/yqlib/doc/path.md
rename to pkg/yqlib/doc/operators/path.md
diff --git a/pkg/yqlib/doc/pipe.md b/pkg/yqlib/doc/operators/pipe.md
similarity index 100%
rename from pkg/yqlib/doc/pipe.md
rename to pkg/yqlib/doc/operators/pipe.md
diff --git a/pkg/yqlib/doc/recursive-descent-glob.md b/pkg/yqlib/doc/operators/recursive-descent-glob.md
similarity index 100%
rename from pkg/yqlib/doc/recursive-descent-glob.md
rename to pkg/yqlib/doc/operators/recursive-descent-glob.md
diff --git a/pkg/yqlib/doc/reduce.md b/pkg/yqlib/doc/operators/reduce.md
similarity index 100%
rename from pkg/yqlib/doc/reduce.md
rename to pkg/yqlib/doc/operators/reduce.md
diff --git a/pkg/yqlib/doc/select.md b/pkg/yqlib/doc/operators/select.md
similarity index 100%
rename from pkg/yqlib/doc/select.md
rename to pkg/yqlib/doc/operators/select.md
diff --git a/pkg/yqlib/doc/sort-keys.md b/pkg/yqlib/doc/operators/sort-keys.md
similarity index 100%
rename from pkg/yqlib/doc/sort-keys.md
rename to pkg/yqlib/doc/operators/sort-keys.md
diff --git a/pkg/yqlib/doc/sort.md b/pkg/yqlib/doc/operators/sort.md
similarity index 100%
rename from pkg/yqlib/doc/sort.md
rename to pkg/yqlib/doc/operators/sort.md
diff --git a/pkg/yqlib/doc/split-into-documents.md b/pkg/yqlib/doc/operators/split-into-documents.md
similarity index 100%
rename from pkg/yqlib/doc/split-into-documents.md
rename to pkg/yqlib/doc/operators/split-into-documents.md
diff --git a/pkg/yqlib/doc/string-operators.md b/pkg/yqlib/doc/operators/string-operators.md
similarity index 100%
rename from pkg/yqlib/doc/string-operators.md
rename to pkg/yqlib/doc/operators/string-operators.md
diff --git a/pkg/yqlib/doc/style.md b/pkg/yqlib/doc/operators/style.md
similarity index 100%
rename from pkg/yqlib/doc/style.md
rename to pkg/yqlib/doc/operators/style.md
diff --git a/pkg/yqlib/doc/subtract.md b/pkg/yqlib/doc/operators/subtract.md
similarity index 100%
rename from pkg/yqlib/doc/subtract.md
rename to pkg/yqlib/doc/operators/subtract.md
diff --git a/pkg/yqlib/doc/tag.md b/pkg/yqlib/doc/operators/tag.md
similarity index 100%
rename from pkg/yqlib/doc/tag.md
rename to pkg/yqlib/doc/operators/tag.md
diff --git a/pkg/yqlib/doc/traverse-read.md b/pkg/yqlib/doc/operators/traverse-read.md
similarity index 100%
rename from pkg/yqlib/doc/traverse-read.md
rename to pkg/yqlib/doc/operators/traverse-read.md
diff --git a/pkg/yqlib/doc/union.md b/pkg/yqlib/doc/operators/union.md
similarity index 100%
rename from pkg/yqlib/doc/union.md
rename to pkg/yqlib/doc/operators/union.md
diff --git a/pkg/yqlib/doc/unique.md b/pkg/yqlib/doc/operators/unique.md
similarity index 100%
rename from pkg/yqlib/doc/unique.md
rename to pkg/yqlib/doc/operators/unique.md
diff --git a/pkg/yqlib/doc/variable-operators.md b/pkg/yqlib/doc/operators/variable-operators.md
similarity index 100%
rename from pkg/yqlib/doc/variable-operators.md
rename to pkg/yqlib/doc/operators/variable-operators.md
diff --git a/pkg/yqlib/doc/with.md b/pkg/yqlib/doc/operators/with.md
similarity index 100%
rename from pkg/yqlib/doc/with.md
rename to pkg/yqlib/doc/operators/with.md
diff --git a/pkg/yqlib/doc/usage/headers/xml.md b/pkg/yqlib/doc/usage/headers/xml.md
new file mode 100644
index 00000000..9258afd0
--- /dev/null
+++ b/pkg/yqlib/doc/usage/headers/xml.md
@@ -0,0 +1,12 @@
+# XML
+
+At the moment, `yq` only supports decoding `xml` (into one of the other supported output formats).
+
+As yaml does not have the concept of attributes, these are converted to regular fields with a prefix to prevent clobbering. Consecutive xml nodes with the same name are assumed to be arrays.
+
+All values in XML are assumed to be strings - but you can use `from_yaml` to parse them into their correct types:
+
+
+```
+yq e -p=xml '.myNumberField |= from_yaml' my.xml
+```
diff --git a/pkg/yqlib/doc/usage/xml.md b/pkg/yqlib/doc/usage/xml.md
new file mode 100644
index 00000000..e084149e
--- /dev/null
+++ b/pkg/yqlib/doc/usage/xml.md
@@ -0,0 +1,88 @@
+# XML
+
+At the moment, `yq` only supports decoding `xml` (into one of the other supported output formats).
+
+As yaml does not have the concept of attributes, these are converted to regular fields with a prefix to prevent clobbering. Consecutive xml nodes with the same name are assumed to be arrays.
+
+All values in XML are assumed to be strings - but you can use `from_yaml` to parse them into their correct types:
+
+
+```
+yq e -p=xml '.myNumberField |= from_yaml' my.xml
+```
+
+## Parse xml: simple
+Given a sample.xml file of:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<cat>meow</cat>
+```
+then
+```bash
+yq e sample.xml
+```
+will output
+```yaml
+cat: meow
+```
+
+## Parse xml: array
+Consecutive nodes with identical xml names are assumed to be arrays.
+
+Given a sample.xml file of:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<animal>1</animal>
+<animal>2</animal>
+```
+then
+```bash
+yq e sample.xml
+```
+will output
+```yaml
+animal:
+  - "1"
+  - "2"
+```
+
+## Parse xml: attributes
+Attributes are converted to fields, with the attribute prefix.
+
+Given a sample.xml file of:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<cat legs="4">
+  <legs>7</legs>
+</cat>
+```
+then
+```bash
+yq e sample.xml
+```
+will output
+```yaml
+cat:
+  +legs: "4"
+  legs: "7"
+```
+
+## Parse xml: attributes with content
+Content is added as a field, using the content name
+
+Given a sample.xml file of:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<cat legs="4">meow</cat>
+```
+then
+```bash
+yq e sample.xml
+```
+will output
+```yaml
+cat:
+  +content: meow
+  +legs: "4"
+```
+
diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go
index 0fb05642..d812039c 100644
--- a/pkg/yqlib/expression_tokeniser.go
+++ b/pkg/yqlib/expression_tokeniser.go
@@ -326,36 +326,47 @@ func initLexer() (*lex.Lexer, error) {
 	lexer.Add([]byte(`to_json\([0-9]+\)`), encodeWithIndent(JsonOutputFormat))
 
 	lexer.Add([]byte(`toyaml`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: YamlOutputFormat, indent: 2}))
+	lexer.Add([]byte(`to_yaml`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: YamlOutputFormat, indent: 2}))
 	// 0 indent doesn't work with yaml.
 	lexer.Add([]byte(`@yaml`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: YamlOutputFormat, indent: 2}))
 
 	lexer.Add([]byte(`tojson`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: JsonOutputFormat, indent: 2}))
-	lexer.Add([]byte(`toprops`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
-	lexer.Add([]byte(`@props`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
-
-	lexer.Add([]byte(`to_yaml`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: YamlOutputFormat, indent: 2}))
 	lexer.Add([]byte(`to_json`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: JsonOutputFormat, indent: 2}))
 	lexer.Add([]byte(`@json`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: JsonOutputFormat, indent: 0}))
 
+	lexer.Add([]byte(`toprops`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
+	lexer.Add([]byte(`to_props`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
+	lexer.Add([]byte(`@props`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
+
+	lexer.Add([]byte(`tocsv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: CsvOutputFormat}))
 	lexer.Add([]byte(`to_csv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: CsvOutputFormat}))
 	lexer.Add([]byte(`@csv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: CsvOutputFormat}))
 
+	lexer.Add([]byte(`totsv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: TsvOutputFormat}))
 	lexer.Add([]byte(`to_tsv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: TsvOutputFormat}))
 	lexer.Add([]byte(`@tsv`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: TsvOutputFormat}))
 
-	lexer.Add([]byte(`to_props`), opTokenWithPrefs(encodeOpType, nil, encoderPreferences{format: PropsOutputFormat, indent: 2}))
-
-	lexer.Add([]byte(`fromyaml`), opToken(decodeOpType))
-	lexer.Add([]byte(`fromjson`), opToken(decodeOpType))
+	lexer.Add([]byte(`fromyaml`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: YamlInputFormat}))
+	lexer.Add([]byte(`fromjson`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: YamlInputFormat}))
+	lexer.Add([]byte(`fromxml`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: XmlInputFormat}))
 
-	lexer.Add([]byte(`from_yaml`), opToken(decodeOpType))
-	lexer.Add([]byte(`from_json`), opToken(decodeOpType))
+	lexer.Add([]byte(`from_yaml`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: YamlInputFormat}))
+	lexer.Add([]byte(`from_json`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: YamlInputFormat}))
+	lexer.Add([]byte(`from_xml`), opTokenWithPrefs(decodeOpType, nil, decoderPreferences{format: XmlInputFormat}))
 
 	lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
 	lexer.Add([]byte(`sort_keys`), opToken(sortKeysOpType))
 
-	lexer.Add([]byte(`load`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false}))
+	lexer.Add([]byte(`load`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false, decoder: NewYamlDecoder()}))
+
+	lexer.Add([]byte(`xmlload`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false, decoder: NewXmlDecoder("+", "+content")}))
+	lexer.Add([]byte(`load_xml`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false, decoder: NewXmlDecoder("+", "+content")}))
+	lexer.Add([]byte(`loadxml`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false, decoder: NewXmlDecoder("+", "+content")}))
+
 	lexer.Add([]byte(`strload`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: true}))
+	lexer.Add([]byte(`load_str`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: true}))
+	lexer.Add([]byte(`loadstr`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: true}))
+
 	lexer.Add([]byte(`select`), opToken(selectOpType))
 	lexer.Add([]byte(`has`), opToken(hasOpType))
 	lexer.Add([]byte(`unique`), opToken(uniqueOpType))
diff --git a/pkg/yqlib/operator_encoder_decoder.go b/pkg/yqlib/operator_encoder_decoder.go
index d4d61b3f..a702d0f1 100644
--- a/pkg/yqlib/operator_encoder_decoder.go
+++ b/pkg/yqlib/operator_encoder_decoder.go
@@ -68,9 +68,23 @@ func encodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
 	return context.ChildContext(results), nil
 }
 
+type decoderPreferences struct {
+	format InputFormat
+}
+
 /* takes a string and decodes it back into an object */
 func decodeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
 
+	preferences := expressionNode.Operation.Preferences.(decoderPreferences)
+
+	var decoder Decoder
+	switch preferences.format {
+	case YamlInputFormat:
+		decoder = NewYamlDecoder()
+	case XmlInputFormat:
+		decoder = NewXmlDecoder("+a", "+content")
+	}
+
 	var results = list.New()
 	for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
 		candidate := el.Value.(*CandidateNode)
@@ -79,7 +93,9 @@ func decodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
 
 		var dataBucket yaml.Node
 		log.Debugf("got: [%v]", candidate.Node.Value)
-		decoder := yaml.NewDecoder(strings.NewReader(unwrapDoc(candidate.Node).Value))
+
+		decoder.Init(strings.NewReader(unwrapDoc(candidate.Node).Value))
+
 		errorReading := decoder.Decode(&dataBucket)
 		if errorReading != nil {
 			return Context{}, errorReading
diff --git a/pkg/yqlib/operator_load.go b/pkg/yqlib/operator_load.go
index 4741b09a..426b8f25 100644
--- a/pkg/yqlib/operator_load.go
+++ b/pkg/yqlib/operator_load.go
@@ -12,6 +12,7 @@ import (
 
 type loadPrefs struct {
 	loadAsString bool
+	decoder      Decoder
 }
 
 func loadString(filename string) (*CandidateNode, error) {
@@ -26,7 +27,7 @@ func loadString(filename string) (*CandidateNode, error) {
 	return &CandidateNode{Node: &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: string(filebytes)}}, nil
 }
 
-func loadYaml(filename string) (*CandidateNode, error) {
+func loadYaml(filename string, decoder Decoder) (*CandidateNode, error) {
 
 	file, err := os.Open(filename) // #nosec
 	if err != nil {
@@ -34,7 +35,7 @@ func loadYaml(filename string) (*CandidateNode, error) {
 	}
 	reader := bufio.NewReader(file)
 
-	documents, err := readDocuments(reader, filename, 0)
+	documents, err := readDocuments(reader, filename, 0, decoder)
 	if err != nil {
 		return nil, err
 	}
@@ -83,7 +84,7 @@ func loadYamlOperator(d *dataTreeNavigator, context Context, expressionNode *Exp
 		if loadPrefs.loadAsString {
 			contentsCandidate, err = loadString(filename)
 		} else {
-			contentsCandidate, err = loadYaml(filename)
+			contentsCandidate, err = loadYaml(filename, loadPrefs.decoder)
 		}
 		if err != nil {
 			return Context{}, fmt.Errorf("Failed to load %v: %w", filename, err)
diff --git a/pkg/yqlib/stream_evaluator.go b/pkg/yqlib/stream_evaluator.go
index ddb777fb..96826f30 100644
--- a/pkg/yqlib/stream_evaluator.go
+++ b/pkg/yqlib/stream_evaluator.go
@@ -14,8 +14,8 @@ import (
 // Uses less memory than loading all documents and running the expression once, but this cannot process
 // cross document expressions.
 type StreamEvaluator interface {
-	Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string) (uint, error)
-	EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error
+	Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string, decoder Decoder) (uint, error)
+	EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error
 	EvaluateNew(expression string, printer Printer, leadingContent string) error
 }
 
@@ -51,7 +51,7 @@ func (s *streamEvaluator) EvaluateNew(expression string, printer Printer, leadin
 	return printer.PrintResults(result.MatchingNodes)
 }
 
-func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error {
+func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error {
 	var totalProcessDocs uint
 	node, err := s.treeCreator.ParseExpression(expression)
 	if err != nil {
@@ -70,7 +70,7 @@ func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, p
 		if err != nil {
 			return err
 		}
-		processedDocs, err := s.Evaluate(filename, reader, node, printer, leadingContent)
+		processedDocs, err := s.Evaluate(filename, reader, node, printer, leadingContent, decoder)
 		if err != nil {
 			return err
 		}
@@ -89,10 +89,10 @@ func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, p
 	return nil
 }
 
-func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string) (uint, error) {
+func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string, decoder Decoder) (uint, error) {
 
 	var currentIndex uint
-	decoder := yaml.NewDecoder(reader)
+	decoder.Init(reader)
 	for {
 		var dataBucket yaml.Node
 		errorReading := decoder.Decode(&dataBucket)
diff --git a/pkg/yqlib/utils.go b/pkg/yqlib/utils.go
index f8291a40..ecfa6a99 100644
--- a/pkg/yqlib/utils.go
+++ b/pkg/yqlib/utils.go
@@ -107,8 +107,8 @@ func processReadStream(reader *bufio.Reader) (io.Reader, string, error) {
 	}
 }
 
-func readDocuments(reader io.Reader, filename string, fileIndex int) (*list.List, error) {
-	decoder := yaml.NewDecoder(reader)
+func readDocuments(reader io.Reader, filename string, fileIndex int, decoder Decoder) (*list.List, error) {
+	decoder.Init(reader)
 	inputList := list.New()
 	var currentIndex uint
 
diff --git a/scripts/copy-docs.sh b/scripts/copy-docs.sh
index e3f39341..5c27efaa 100755
--- a/scripts/copy-docs.sh
+++ b/scripts/copy-docs.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
 
 cp how-it-works.md ../yq-gitbook/.
-cp pkg/yqlib/doc/*.md ../yq-gitbook/operators/.
\ No newline at end of file
+cp pkg/yqlib/doc/operators/*.md ../yq-gitbook/operators/.
+cp pkg/yqlib/doc/usage/*.md ../yq-gitbook/usage/.
\ No newline at end of file


Test patch:
diff --git a/pkg/yqlib/decode_xml_test.go b/pkg/yqlib/decode_xml_test.go
new file mode 100644
index 00000000..e0d72d21
--- /dev/null
+++ b/pkg/yqlib/decode_xml_test.go
@@ -0,0 +1,110 @@
+package yqlib
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/mikefarah/yq/v4/test"
+	yaml "gopkg.in/yaml.v3"
+)
+
+func decodeXml(t *testing.T, xml string) *CandidateNode {
+	decoder := NewXmlDecoder("+", "+content")
+
+	decoder.Init(strings.NewReader(xml))
+
+	node := &yaml.Node{}
+	err := decoder.Decode(node)
+	if err != nil {
+		t.Error(err, "fail to decode", xml)
+	}
+	return &CandidateNode{Node: node}
+}
+
+type xmlScenario struct {
+	inputXml       string
+	expected       string
+	description    string
+	subdescription string
+	skipDoc        bool
+}
+
+var xmlScenarios = []xmlScenario{
+	{
+		description: "Parse xml: simple",
+		inputXml:    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat>meow</cat>",
+		expected:    "D0, P[], (doc)::cat: meow\n",
+	},
+	{
+		description:    "Parse xml: array",
+		subdescription: "Consecutive nodes with identical xml names are assumed to be arrays.",
+		inputXml:       "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<animal>1</animal>\n<animal>2</animal>",
+		expected:       "D0, P[], (doc)::animal:\n    - \"1\"\n    - \"2\"\n",
+	},
+	{
+		description:    "Parse xml: attributes",
+		subdescription: "Attributes are converted to fields, with the attribute prefix.",
+		inputXml:       "<?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",
+	},
+	{
+		description:    "Parse xml: attributes with content",
+		subdescription: "Content is added as a field, using the content name",
+		inputXml:       "<?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",
+	},
+}
+
+func testXmlScenario(t *testing.T, s *xmlScenario) {
+	var actual = resultToString(t, decodeXml(t, s.inputXml))
+	test.AssertResult(t, s.expected, actual)
+}
+
+func documentXmlScenario(t *testing.T, w *bufio.Writer, i interface{}) {
+	s := i.(xmlScenario)
+
+	if s.skipDoc {
+		return
+	}
+	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
+
+	if s.subdescription != "" {
+		writeOrPanic(w, s.subdescription)
+		writeOrPanic(w, "\n\n")
+	}
+
+	writeOrPanic(w, "Given a sample.xml file of:\n")
+	writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.inputXml))
+
+	writeOrPanic(w, "then\n")
+	writeOrPanic(w, "```bash\nyq e sample.xml\n```\n")
+	writeOrPanic(w, "will output\n")
+
+	var output bytes.Buffer
+	printer := NewPrinterWithSingleWriter(bufio.NewWriter(&output), YamlOutputFormat, true, false, 2, true)
+
+	node := decodeXml(t, s.inputXml)
+
+	err := printer.PrintResults(node.AsList())
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", output.String()))
+
+}
+
+func TestXmlScenarios(t *testing.T) {
+	for _, tt := range xmlScenarios {
+		testXmlScenario(t, &tt)
+	}
+	genericScenarios := make([]interface{}, len(xmlScenarios))
+	for i, s := range xmlScenarios {
+		genericScenarios[i] = s
+	}
+	documentScenarios(t, "usage", "xml", genericScenarios, documentXmlScenario)
+}
diff --git a/pkg/yqlib/encoder_csv_test.go b/pkg/yqlib/encoder_csv_test.go
index 1db2859e..816cbfe4 100644
--- a/pkg/yqlib/encoder_csv_test.go
+++ b/pkg/yqlib/encoder_csv_test.go
@@ -14,7 +14,7 @@ func yamlToCsv(sampleYaml string, separator rune) string {
 	writer := bufio.NewWriter(&output)
 
 	var jsonEncoder = NewCsvEncoder(writer, separator)
-	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
diff --git a/pkg/yqlib/encoder_properties_test.go b/pkg/yqlib/encoder_properties_test.go
index df973734..f82af12a 100644
--- a/pkg/yqlib/encoder_properties_test.go
+++ b/pkg/yqlib/encoder_properties_test.go
@@ -14,7 +14,7 @@ func yamlToProps(sampleYaml string) string {
 	writer := bufio.NewWriter(&output)
 
 	var propsEncoder = NewPropertiesEncoder(writer)
-	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
diff --git a/pkg/yqlib/encoder_test.go b/pkg/yqlib/encoder_test.go
index 283e89ee..576cfb95 100644
--- a/pkg/yqlib/encoder_test.go
+++ b/pkg/yqlib/encoder_test.go
@@ -14,7 +14,7 @@ func yamlToJson(sampleYaml string, indent int) string {
 	writer := bufio.NewWriter(&output)
 
 	var jsonEncoder = NewJsonEncoder(writer, indent)
-	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
diff --git a/pkg/yqlib/operator_add_test.go b/pkg/yqlib/operator_add_test.go
index 445977e8..b303866a 100644
--- a/pkg/yqlib/operator_add_test.go
+++ b/pkg/yqlib/operator_add_test.go
@@ -150,5 +150,5 @@ func TestAddOperatorScenarios(t *testing.T) {
 	for _, tt := range addOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "add", addOperatorScenarios)
+	documentOperatorScenarios(t, "add", addOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_alternative_test.go b/pkg/yqlib/operator_alternative_test.go
index 0331213d..e23c8dbb 100644
--- a/pkg/yqlib/operator_alternative_test.go
+++ b/pkg/yqlib/operator_alternative_test.go
@@ -91,5 +91,5 @@ func TestAlternativeOperatorScenarios(t *testing.T) {
 	for _, tt := range alternativeOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "alternative-default-value", alternativeOperatorScenarios)
+	documentOperatorScenarios(t, "alternative-default-value", alternativeOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_anchors_aliases_test.go b/pkg/yqlib/operator_anchors_aliases_test.go
index 30642dcd..9222c177 100644
--- a/pkg/yqlib/operator_anchors_aliases_test.go
+++ b/pkg/yqlib/operator_anchors_aliases_test.go
@@ -233,5 +233,5 @@ func TestAnchorAliasOperatorScenarios(t *testing.T) {
 	for _, tt := range anchorOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios)
+	documentOperatorScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_assign_test.go b/pkg/yqlib/operator_assign_test.go
index 9ecca5d4..b5f6813c 100644
--- a/pkg/yqlib/operator_assign_test.go
+++ b/pkg/yqlib/operator_assign_test.go
@@ -178,5 +178,5 @@ func TestAssignOperatorScenarios(t *testing.T) {
 	for _, tt := range assignOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "assign-update", assignOperatorScenarios)
+	documentOperatorScenarios(t, "assign-update", assignOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_booleans_test.go b/pkg/yqlib/operator_booleans_test.go
index ac6de3eb..e9b72f99 100644
--- a/pkg/yqlib/operator_booleans_test.go
+++ b/pkg/yqlib/operator_booleans_test.go
@@ -229,5 +229,5 @@ func TestBooleanOperatorScenarios(t *testing.T) {
 	for _, tt := range booleanOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "boolean-operators", booleanOperatorScenarios)
+	documentOperatorScenarios(t, "boolean-operators", booleanOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_collect_object_test.go b/pkg/yqlib/operator_collect_object_test.go
index edbe63d2..4db923f7 100644
--- a/pkg/yqlib/operator_collect_object_test.go
+++ b/pkg/yqlib/operator_collect_object_test.go
@@ -160,5 +160,5 @@ func TestCollectObjectOperatorScenarios(t *testing.T) {
 	for _, tt := range collectObjectOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "create-collect-into-object", collectObjectOperatorScenarios)
+	documentOperatorScenarios(t, "create-collect-into-object", collectObjectOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_collect_test.go b/pkg/yqlib/operator_collect_test.go
index 0e23e340..fce899fb 100644
--- a/pkg/yqlib/operator_collect_test.go
+++ b/pkg/yqlib/operator_collect_test.go
@@ -108,5 +108,5 @@ func TestCollectOperatorScenarios(t *testing.T) {
 	for _, tt := range collectOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "collect-into-array", collectOperatorScenarios)
+	documentOperatorScenarios(t, "collect-into-array", collectOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_comments_test.go b/pkg/yqlib/operator_comments_test.go
index cc3ce4fe..223eeb6c 100644
--- a/pkg/yqlib/operator_comments_test.go
+++ b/pkg/yqlib/operator_comments_test.go
@@ -136,5 +136,5 @@ func TestCommentOperatorScenarios(t *testing.T) {
 	for _, tt := range commentOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "comment-operators", commentOperatorScenarios)
+	documentOperatorScenarios(t, "comment-operators", commentOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_contains_test.go b/pkg/yqlib/operator_contains_test.go
index e1d3dd01..5cef9d7c 100644
--- a/pkg/yqlib/operator_contains_test.go
+++ b/pkg/yqlib/operator_contains_test.go
@@ -78,5 +78,5 @@ func TestContainsOperatorScenarios(t *testing.T) {
 	for _, tt := range containsOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "contains", containsOperatorScenarios)
+	documentOperatorScenarios(t, "contains", containsOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_delete_test.go b/pkg/yqlib/operator_delete_test.go
index 02efcb75..76d95224 100644
--- a/pkg/yqlib/operator_delete_test.go
+++ b/pkg/yqlib/operator_delete_test.go
@@ -171,5 +171,5 @@ func TestDeleteOperatorScenarios(t *testing.T) {
 	for _, tt := range deleteOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "delete", deleteOperatorScenarios)
+	documentOperatorScenarios(t, "delete", deleteOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_document_index_test.go b/pkg/yqlib/operator_document_index_test.go
index 1e35cd19..23195505 100644
--- a/pkg/yqlib/operator_document_index_test.go
+++ b/pkg/yqlib/operator_document_index_test.go
@@ -54,5 +54,5 @@ func TestDocumentIndexScenarios(t *testing.T) {
 	for _, tt := range documentIndexScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "document-index", documentIndexScenarios)
+	documentOperatorScenarios(t, "document-index", documentIndexScenarios)
 }
diff --git a/pkg/yqlib/operator_encoder_decoder_test.go b/pkg/yqlib/operator_encoder_decoder_test.go
index c7597533..1c394356 100644
--- a/pkg/yqlib/operator_encoder_decoder_test.go
+++ b/pkg/yqlib/operator_encoder_decoder_test.go
@@ -168,11 +168,19 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
 			"D0, P[], (doc)::a: \"foo:\\n  a: frog\"\n",
 		},
 	},
+	{
+		description: "Decode a xml encoded string",
+		document:    `a: "<foo>bar</foo>"`,
+		expression:  `.b = (.a | from_xml)`,
+		expected: []string{
+			"D0, P[], (doc)::a: \"<foo>bar</foo>\"\nb:\n    foo: bar\n",
+		},
+	},
 }
 
 func TestEncoderDecoderOperatorScenarios(t *testing.T) {
 	for _, tt := range encoderDecoderOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "encode-decode", encoderDecoderOperatorScenarios)
+	documentOperatorScenarios(t, "encode-decode", encoderDecoderOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_entries_test.go b/pkg/yqlib/operator_entries_test.go
index 4bee227d..1dd120c3 100644
--- a/pkg/yqlib/operator_entries_test.go
+++ b/pkg/yqlib/operator_entries_test.go
@@ -76,5 +76,5 @@ func TestEntriesOperatorScenarios(t *testing.T) {
 	for _, tt := range entriesOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "entries", entriesOperatorScenarios)
+	documentOperatorScenarios(t, "entries", entriesOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_env_test.go b/pkg/yqlib/operator_env_test.go
index 1cc1ba67..eac19734 100644
--- a/pkg/yqlib/operator_env_test.go
+++ b/pkg/yqlib/operator_env_test.go
@@ -68,5 +68,5 @@ func TestEnvOperatorScenarios(t *testing.T) {
 	for _, tt := range envOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "env-variable-operators", envOperatorScenarios)
+	documentOperatorScenarios(t, "env-variable-operators", envOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_equals_test.go b/pkg/yqlib/operator_equals_test.go
index 69cd6e09..517dae36 100644
--- a/pkg/yqlib/operator_equals_test.go
+++ b/pkg/yqlib/operator_equals_test.go
@@ -186,5 +186,5 @@ func TestEqualOperatorScenarios(t *testing.T) {
 	for _, tt := range equalsOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "equals", equalsOperatorScenarios)
+	documentOperatorScenarios(t, "equals", equalsOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_file_test.go b/pkg/yqlib/operator_file_test.go
index 88d1422f..875d4187 100644
--- a/pkg/yqlib/operator_file_test.go
+++ b/pkg/yqlib/operator_file_test.go
@@ -53,5 +53,5 @@ func TestFileOperatorsScenarios(t *testing.T) {
 	for _, tt := range fileOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "file-operators", fileOperatorScenarios)
+	documentOperatorScenarios(t, "file-operators", fileOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_flatten_test.go b/pkg/yqlib/operator_flatten_test.go
index 4e826471..add71e1f 100644
--- a/pkg/yqlib/operator_flatten_test.go
+++ b/pkg/yqlib/operator_flatten_test.go
@@ -44,5 +44,5 @@ func TestFlattenOperatorScenarios(t *testing.T) {
 	for _, tt := range flattenOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "flatten", flattenOperatorScenarios)
+	documentOperatorScenarios(t, "flatten", flattenOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_group_by_test.go b/pkg/yqlib/operator_group_by_test.go
index 60462206..2c7803da 100644
--- a/pkg/yqlib/operator_group_by_test.go
+++ b/pkg/yqlib/operator_group_by_test.go
@@ -27,5 +27,5 @@ func TestGroupByOperatorScenarios(t *testing.T) {
 	for _, tt := range groupByOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "group-by", groupByOperatorScenarios)
+	documentOperatorScenarios(t, "group-by", groupByOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_has_test.go b/pkg/yqlib/operator_has_test.go
index fbd31d5b..3a631330 100644
--- a/pkg/yqlib/operator_has_test.go
+++ b/pkg/yqlib/operator_has_test.go
@@ -77,5 +77,5 @@ func TestHasOperatorScenarios(t *testing.T) {
 	for _, tt := range hasOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "has", hasOperatorScenarios)
+	documentOperatorScenarios(t, "has", hasOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_keys_test.go b/pkg/yqlib/operator_keys_test.go
index bd5087fb..b42306f7 100644
--- a/pkg/yqlib/operator_keys_test.go
+++ b/pkg/yqlib/operator_keys_test.go
@@ -81,5 +81,5 @@ func TestKeysOperatorScenarios(t *testing.T) {
 	for _, tt := range keysOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "keys", keysOperatorScenarios)
+	documentOperatorScenarios(t, "keys", keysOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_length_test.go b/pkg/yqlib/operator_length_test.go
index d08c1326..a67e627d 100644
--- a/pkg/yqlib/operator_length_test.go
+++ b/pkg/yqlib/operator_length_test.go
@@ -62,5 +62,5 @@ func TestLengthOperatorScenarios(t *testing.T) {
 	for _, tt := range lengthOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "length", lengthOperatorScenarios)
+	documentOperatorScenarios(t, "length", lengthOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_load_test.go b/pkg/yqlib/operator_load_test.go
index 4fe690f1..5cfae4ad 100644
--- a/pkg/yqlib/operator_load_test.go
+++ b/pkg/yqlib/operator_load_test.go
@@ -46,5 +46,5 @@ func TestLoadScenarios(t *testing.T) {
 	for _, tt := range loadScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "load", loadScenarios)
+	documentOperatorScenarios(t, "load", loadScenarios)
 }
diff --git a/pkg/yqlib/operator_map_test.go b/pkg/yqlib/operator_map_test.go
index 1dd5db57..715df336 100644
--- a/pkg/yqlib/operator_map_test.go
+++ b/pkg/yqlib/operator_map_test.go
@@ -47,5 +47,5 @@ func TestMapOperatorScenarios(t *testing.T) {
 	for _, tt := range mapOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "map", mapOperatorScenarios)
+	documentOperatorScenarios(t, "map", mapOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_multiply_test.go b/pkg/yqlib/operator_multiply_test.go
index 59a08849..ab136f13 100644
--- a/pkg/yqlib/operator_multiply_test.go
+++ b/pkg/yqlib/operator_multiply_test.go
@@ -441,5 +441,5 @@ func TestMultiplyOperatorScenarios(t *testing.T) {
 	for _, tt := range multiplyOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "multiply-merge", multiplyOperatorScenarios)
+	documentOperatorScenarios(t, "multiply-merge", multiplyOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_parent_test.go b/pkg/yqlib/operator_parent_test.go
index f4cbf236..35afd67e 100644
--- a/pkg/yqlib/operator_parent_test.go
+++ b/pkg/yqlib/operator_parent_test.go
@@ -33,5 +33,5 @@ func TestParentOperatorScenarios(t *testing.T) {
 	for _, tt := range parentOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "parent", parentOperatorScenarios)
+	documentOperatorScenarios(t, "parent", parentOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_path_test.go b/pkg/yqlib/operator_path_test.go
index a93830a2..faf06ce9 100644
--- a/pkg/yqlib/operator_path_test.go
+++ b/pkg/yqlib/operator_path_test.go
@@ -69,5 +69,5 @@ func TestPathOperatorsScenarios(t *testing.T) {
 	for _, tt := range pathOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "path", pathOperatorScenarios)
+	documentOperatorScenarios(t, "path", pathOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_pipe_test.go b/pkg/yqlib/operator_pipe_test.go
index fc36cde9..9e979603 100644
--- a/pkg/yqlib/operator_pipe_test.go
+++ b/pkg/yqlib/operator_pipe_test.go
@@ -27,5 +27,5 @@ func TestPipeOperatorScenarios(t *testing.T) {
 	for _, tt := range pipeOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "pipe", pipeOperatorScenarios)
+	documentOperatorScenarios(t, "pipe", pipeOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_recursive_descent_test.go b/pkg/yqlib/operator_recursive_descent_test.go
index 8d4ddc7e..45e63cfd 100644
--- a/pkg/yqlib/operator_recursive_descent_test.go
+++ b/pkg/yqlib/operator_recursive_descent_test.go
@@ -233,5 +233,5 @@ func TestRecursiveDescentOperatorScenarios(t *testing.T) {
 	for _, tt := range recursiveDescentOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "recursive-descent-glob", recursiveDescentOperatorScenarios)
+	documentOperatorScenarios(t, "recursive-descent-glob", recursiveDescentOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_reduce_test.go b/pkg/yqlib/operator_reduce_test.go
index dde2c33f..80b45a56 100644
--- a/pkg/yqlib/operator_reduce_test.go
+++ b/pkg/yqlib/operator_reduce_test.go
@@ -36,5 +36,5 @@ func TestReduceOperatorScenarios(t *testing.T) {
 	for _, tt := range reduceOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "reduce", reduceOperatorScenarios)
+	documentOperatorScenarios(t, "reduce", reduceOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_select_test.go b/pkg/yqlib/operator_select_test.go
index 4a1e8723..43c16a9b 100644
--- a/pkg/yqlib/operator_select_test.go
+++ b/pkg/yqlib/operator_select_test.go
@@ -99,5 +99,5 @@ func TestSelectOperatorScenarios(t *testing.T) {
 	for _, tt := range selectOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "select", selectOperatorScenarios)
+	documentOperatorScenarios(t, "select", selectOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_sort_keys_test.go b/pkg/yqlib/operator_sort_keys_test.go
index 462771e6..8d421556 100644
--- a/pkg/yqlib/operator_sort_keys_test.go
+++ b/pkg/yqlib/operator_sort_keys_test.go
@@ -36,5 +36,5 @@ func TestSortKeysOperatorScenarios(t *testing.T) {
 	for _, tt := range sortKeysOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "sort-keys", sortKeysOperatorScenarios)
+	documentOperatorScenarios(t, "sort-keys", sortKeysOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_sort_test.go b/pkg/yqlib/operator_sort_test.go
index 6828d73e..7c4128d6 100644
--- a/pkg/yqlib/operator_sort_test.go
+++ b/pkg/yqlib/operator_sort_test.go
@@ -76,5 +76,5 @@ func TestSortByOperatorScenarios(t *testing.T) {
 	for _, tt := range sortByOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "sort", sortByOperatorScenarios)
+	documentOperatorScenarios(t, "sort", sortByOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_split_document_test.go b/pkg/yqlib/operator_split_document_test.go
index 813f3173..ca490512 100644
--- a/pkg/yqlib/operator_split_document_test.go
+++ b/pkg/yqlib/operator_split_document_test.go
@@ -28,5 +28,5 @@ func TestSplitDocOperatorScenarios(t *testing.T) {
 	for _, tt := range splitDocOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "split-into-documents", splitDocOperatorScenarios)
+	documentOperatorScenarios(t, "split-into-documents", splitDocOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_strings_test.go b/pkg/yqlib/operator_strings_test.go
index c94a2fc8..c4e26214 100644
--- a/pkg/yqlib/operator_strings_test.go
+++ b/pkg/yqlib/operator_strings_test.go
@@ -170,5 +170,5 @@ func TestStringsOperatorScenarios(t *testing.T) {
 	for _, tt := range stringsOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "string-operators", stringsOperatorScenarios)
+	documentOperatorScenarios(t, "string-operators", stringsOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_style_test.go b/pkg/yqlib/operator_style_test.go
index 5f2490fd..ce263a7b 100644
--- a/pkg/yqlib/operator_style_test.go
+++ b/pkg/yqlib/operator_style_test.go
@@ -152,5 +152,5 @@ func TestStyleOperatorScenarios(t *testing.T) {
 	for _, tt := range styleOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "style", styleOperatorScenarios)
+	documentOperatorScenarios(t, "style", styleOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_subtract_test.go b/pkg/yqlib/operator_subtract_test.go
index 7e209d5e..e43e0bfe 100644
--- a/pkg/yqlib/operator_subtract_test.go
+++ b/pkg/yqlib/operator_subtract_test.go
@@ -99,5 +99,5 @@ func TestSubtractOperatorScenarios(t *testing.T) {
 	for _, tt := range subtractOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "subtract", subtractOperatorScenarios)
+	documentOperatorScenarios(t, "subtract", subtractOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_tag_test.go b/pkg/yqlib/operator_tag_test.go
index 08b22741..380f6028 100644
--- a/pkg/yqlib/operator_tag_test.go
+++ b/pkg/yqlib/operator_tag_test.go
@@ -64,5 +64,5 @@ func TestTagOperatorScenarios(t *testing.T) {
 	for _, tt := range tagOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "tag", tagOperatorScenarios)
+	documentOperatorScenarios(t, "tag", tagOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_traverse_path_test.go b/pkg/yqlib/operator_traverse_path_test.go
index b556ba28..94400ba2 100644
--- a/pkg/yqlib/operator_traverse_path_test.go
+++ b/pkg/yqlib/operator_traverse_path_test.go
@@ -519,5 +519,5 @@ func TestTraversePathOperatorScenarios(t *testing.T) {
 	for _, tt := range traversePathOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "traverse-read", traversePathOperatorScenarios)
+	documentOperatorScenarios(t, "traverse-read", traversePathOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_union_test.go b/pkg/yqlib/operator_union_test.go
index a5f12de7..1409fd7f 100644
--- a/pkg/yqlib/operator_union_test.go
+++ b/pkg/yqlib/operator_union_test.go
@@ -44,5 +44,5 @@ func TestUnionOperatorScenarios(t *testing.T) {
 	for _, tt := range unionOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "union", unionOperatorScenarios)
+	documentOperatorScenarios(t, "union", unionOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_unique_test.go b/pkg/yqlib/operator_unique_test.go
index 347f4d71..4a3ce79b 100644
--- a/pkg/yqlib/operator_unique_test.go
+++ b/pkg/yqlib/operator_unique_test.go
@@ -61,5 +61,5 @@ func TestUniqueOperatorScenarios(t *testing.T) {
 	for _, tt := range uniqueOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "unique", uniqueOperatorScenarios)
+	documentOperatorScenarios(t, "unique", uniqueOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_variables_test.go b/pkg/yqlib/operator_variables_test.go
index a6e8d854..e40f8dd8 100644
--- a/pkg/yqlib/operator_variables_test.go
+++ b/pkg/yqlib/operator_variables_test.go
@@ -66,5 +66,5 @@ func TestVariableOperatorScenarios(t *testing.T) {
 	for _, tt := range variableOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "variable-operators", variableOperatorScenarios)
+	documentOperatorScenarios(t, "variable-operators", variableOperatorScenarios)
 }
diff --git a/pkg/yqlib/operator_with_test.go b/pkg/yqlib/operator_with_test.go
index 11ea08c1..aad4d15c 100644
--- a/pkg/yqlib/operator_with_test.go
+++ b/pkg/yqlib/operator_with_test.go
@@ -34,5 +34,5 @@ func TestWithOperatorScenarios(t *testing.T) {
 	for _, tt := range withOperatorScenarios {
 		testScenario(t, &tt)
 	}
-	documentScenarios(t, "with", withOperatorScenarios)
+	documentOperatorScenarios(t, "with", withOperatorScenarios)
 }
diff --git a/pkg/yqlib/operators_test.go b/pkg/yqlib/operators_test.go
index b99d3865..c99c63a7 100644
--- a/pkg/yqlib/operators_test.go
+++ b/pkg/yqlib/operators_test.go
@@ -32,7 +32,7 @@ func readDocumentWithLeadingContent(content string, fakefilename string, fakeFil
 		return nil, err
 	}
 
-	inputs, err := readDocuments(reader, fakefilename, fakeFileIndex)
+	inputs, err := readDocuments(reader, fakefilename, fakeFileIndex, NewYamlDecoder())
 	if err != nil {
 		return nil, err
 	}
@@ -90,27 +90,32 @@ func testScenario(t *testing.T, s *expressionScenario) {
 	test.AssertResultComplexWithContext(t, s.expected, resultsToString(t, context.MatchingNodes), fmt.Sprintf("desc: %v\nexp: %v\ndoc: %v", s.description, s.expression, s.document))
 }
 
+func resultToString(t *testing.T, n *CandidateNode) string {
+	var valueBuffer bytes.Buffer
+	printer := NewPrinterWithSingleWriter(bufio.NewWriter(&valueBuffer), YamlOutputFormat, true, false, 4, true)
+
+	err := printer.PrintResults(n.AsList())
+	if err != nil {
+		t.Error(err)
+		return ""
+	}
+
+	tag := n.Node.Tag
+	if n.Node.Kind == yaml.DocumentNode {
+		tag = "doc"
+	} else if n.Node.Kind == yaml.AliasNode {
+		tag = "alias"
+	}
+	return fmt.Sprintf(`D%v, P%v, (%v)::%v`, n.Document, n.Path, tag, valueBuffer.String())
+}
+
 func resultsToString(t *testing.T, results *list.List) []string {
 	var pretty = make([]string, 0)
 
 	for el := results.Front(); el != nil; el = el.Next() {
 		n := el.Value.(*CandidateNode)
-		var valueBuffer bytes.Buffer
-		printer := NewPrinterWithSingleWriter(bufio.NewWriter(&valueBuffer), YamlOutputFormat, true, false, 4, true)
-
-		err := printer.PrintResults(n.AsList())
-		if err != nil {
-			t.Error(err)
-			return nil
-		}
 
-		tag := n.Node.Tag
-		if n.Node.Kind == yaml.DocumentNode {
-			tag = "doc"
-		} else if n.Node.Kind == yaml.AliasNode {
-			tag = "alias"
-		}
-		output := fmt.Sprintf(`D%v, P%v, (%v)::%v`, n.Document, n.Path, tag, valueBuffer.String())
+		output := resultToString(t, n)
 		pretty = append(pretty, output)
 	}
 	return pretty
@@ -123,8 +128,8 @@ func writeOrPanic(w *bufio.Writer, text string) {
 	}
 }
 
-func copyFromHeader(title string, out *os.File) error {
-	source := fmt.Sprintf("doc/headers/%v.md", title)
+func copyFromHeader(folder string, title string, out *os.File) error {
+	source := fmt.Sprintf("doc/%v/headers/%v.md", folder, title)
 	_, err := os.Stat(source)
 	if os.IsNotExist(err) {
 		return nil
@@ -147,15 +152,17 @@ func formatYaml(yaml string, filename string) string {
 		panic(err)
 	}
 	streamEvaluator := NewStreamEvaluator()
-	_, err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer, "")
+	_, err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer, "", NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
 	return output.String()
 }
 
-func documentScenarios(t *testing.T, title string, scenarios []expressionScenario) {
-	f, err := os.Create(fmt.Sprintf("doc/%v.md", title))
+type documentScenarioFunc func(t *testing.T, writer *bufio.Writer, scenario interface{})
+
+func documentScenarios(t *testing.T, folder string, title string, scenarios []interface{}, documentScenario documentScenarioFunc) {
+	f, err := os.Create(fmt.Sprintf("doc/%v/%v.md", folder, title))
 
 	if err != nil {
 		t.Error(err)
@@ -163,7 +170,7 @@ func documentScenarios(t *testing.T, title string, scenarios []expressionScenari
 	}
 	defer f.Close()
 
-	err = copyFromHeader(title, f)
+	err = copyFromHeader(folder, title, f)
 	if err != nil {
 		t.Error(err)
 		return
@@ -173,14 +180,26 @@ func documentScenarios(t *testing.T, title string, scenarios []expressionScenari
 	writeOrPanic(w, "\n")
 
 	for _, s := range scenarios {
-		if !s.skipDoc {
-			documentScenario(t, w, s)
-		}
+		documentScenario(t, w, s)
 	}
 	w.Flush()
 }
 
-func documentScenario(t *testing.T, w *bufio.Writer, s expressionScenario) {
+func documentOperatorScenarios(t *testing.T, title string, scenarios []expressionScenario) {
+	genericScenarios := make([]interface{}, len(scenarios))
+	for i, s := range scenarios {
+		genericScenarios[i] = s
+	}
+
+	documentScenarios(t, "operators", title, genericScenarios, documentOperatorScenario)
+}
+
+func documentOperatorScenario(t *testing.T, w *bufio.Writer, i interface{}) {
+	s := i.(expressionScenario)
+
+	if s.skipDoc {
+		return
+	}
 	writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
 
 	if s.subdescription != "" {
diff --git a/pkg/yqlib/printer_test.go b/pkg/yqlib/printer_test.go
index 8c74af87..d3172906 100644
--- a/pkg/yqlib/printer_test.go
+++ b/pkg/yqlib/printer_test.go
@@ -38,7 +38,7 @@ func TestPrinterMultipleDocsInSequence(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -76,7 +76,7 @@ func TestPrinterMultipleDocsInSequenceWithLeadingContent(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -118,7 +118,7 @@ func TestPrinterMultipleFilesInSequence(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -165,7 +165,7 @@ func TestPrinterMultipleFilesInSequenceWithLeadingContent(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -215,7 +215,7 @@ func TestPrinterMultipleDocsInSinglePrint(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -234,7 +234,7 @@ func TestPrinterMultipleDocsInSinglePrintWithLeadingDoc(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -263,7 +263,7 @@ func TestPrinterMultipleDocsInSinglePrintWithLeadingDocTrailing(t *testing.T) {
 	var writer = bufio.NewWriter(&output)
 	printer := NewPrinterWithSingleWriter(writer, YamlOutputFormat, true, false, 2, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -294,7 +294,7 @@ func TestPrinterScalarWithLeadingCont(t *testing.T) {
 		panic(err)
 	}
 	streamEvaluator := NewStreamEvaluator()
-	_, err = streamEvaluator.Evaluate("sample", strings.NewReader(multiDocSample), node, printer, "# blah\n")
+	_, err = streamEvaluator.Evaluate("sample", strings.NewReader(multiDocSample), node, printer, "# blah\n", NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}
@@ -316,7 +316,7 @@ func TestPrinterMultipleDocsJson(t *testing.T) {
 	// when outputing JSON.
 	printer := NewPrinterWithSingleWriter(writer, JsonOutputFormat, true, false, 0, true)
 
-	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
+	inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
 	if err != nil {
 		panic(err)
 	}