Repository: mikefarah/yq
Pull request #324 (2020-01-13 09:20:44)

#323 Refactor the cobra command with standard structure

RyanSiu1995: @mikefarah Can you please reactivate the CI so that we can ensure the code is valid? #321 
mikefarah: Hasn't been disabled? 
https://travis-ci.org/mikefarah/yq

mikefarah: This looks really good - just need the tests to pass.
mikefarah: not sure why travis CI is not hooking into the PR
mikefarah: I think travis-ci is fixed now - try pushing another change...
RyanSiu1995: Ya, I will try to fix the test first.
Thank you @mikefarah !

Patch:
diff --git a/cmd/constant.go b/cmd/constant.go
new file mode 100644
index 00000000..8302a624
--- /dev/null
+++ b/cmd/constant.go
@@ -0,0 +1,22 @@
+package cmd
+
+import (
+	"github.com/mikefarah/yq/v3/pkg/yqlib"
+	logging "gopkg.in/op/go-logging.v1"
+)
+
+var customTag = ""
+var printMode = "v"
+var writeInplace = false
+var writeScript = ""
+var outputToJSON = false
+var overwriteFlag = false
+var autoCreateFlag = true
+var allowEmptyFlag = false
+var appendFlag = false
+var verbose = false
+var version = false
+var docIndex = "0"
+var log = logging.MustGetLogger("yq")
+var lib = yqlib.NewYqLib()
+var valueParser = yqlib.NewValueParser()
diff --git a/cmd/delete.go b/cmd/delete.go
new file mode 100644
index 00000000..35ac8808
--- /dev/null
+++ b/cmd/delete.go
@@ -0,0 +1,41 @@
+package cmd
+
+import (
+	"github.com/mikefarah/yq/v3/pkg/yqlib"
+	errors "github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+func createDeleteCmd() *cobra.Command {
+	var cmdDelete = &cobra.Command{
+		Use:     "delete [yaml_file] [path_expression]",
+		Aliases: []string{"d"},
+		Short:   "yq d [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
+		Example: `
+yq delete things.yaml 'a.b.c'
+yq delete things.yaml 'a.*.c'
+yq delete things.yaml 'a.(child.subchild==co*).c'
+yq delete things.yaml 'a.**'
+yq delete --inplace things.yaml 'a.b.c'
+yq delete --inplace -- things.yaml '--key-starting-with-dash' # need to use '--' to stop processing arguments as flags
+yq d -i things.yaml 'a.b.c'
+	`,
+		Long: `Deletes the nodes matching the given path expression from the YAML file.
+Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
+`,
+		RunE: deleteProperty,
+	}
+	cmdDelete.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
+	cmdDelete.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
+	return cmdDelete
+}
+
+func deleteProperty(cmd *cobra.Command, args []string) error {
+	if len(args) < 2 {
+		return errors.New("Must provide <filename> <path_to_delete>")
+	}
+	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 1)
+	updateCommands[0] = yqlib.UpdateCommand{Command: "delete", Path: args[1]}
+
+	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
+}
diff --git a/cmd/merge.go b/cmd/merge.go
new file mode 100644
index 00000000..9ea736ae
--- /dev/null
+++ b/cmd/merge.go
@@ -0,0 +1,61 @@
+package cmd
+
+import (
+	"github.com/mikefarah/yq/v3/pkg/yqlib"
+	errors "github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	"strings"
+)
+
+func createMergeCmd() *cobra.Command {
+	var cmdMerge = &cobra.Command{
+		Use:     "merge [initial_yaml_file] [additional_yaml_file]...",
+		Aliases: []string{"m"},
+		Short:   "yq m [--inplace/-i] [--doc/-d index] [--overwrite/-x] [--append/-a] sample.yaml sample2.yaml",
+		Example: `
+yq merge things.yaml other.yaml
+yq merge --inplace things.yaml other.yaml
+yq m -i things.yaml other.yaml
+yq m --overwrite things.yaml other.yaml
+yq m -i -x things.yaml other.yaml
+yq m -i -a things.yaml other.yaml
+yq m -i --autocreate=false things.yaml other.yaml
+      `,
+		Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s).
+Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
+
+If overwrite flag is set then existing values will be overwritten using the values from each additional yaml file.
+If append flag is set then existing arrays will be merged with the arrays from each additional yaml file.
+`,
+		RunE: mergeProperties,
+	}
+	cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
+	cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values")
+	cmdMerge.PersistentFlags().BoolVarP(&autoCreateFlag, "autocreate", "c", true, "automatically create any missing entries")
+	cmdMerge.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "update the yaml file by appending array values")
+	cmdMerge.PersistentFlags().BoolVarP(&allowEmptyFlag, "allow-empty", "e", false, "allow empty yaml files")
+	cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
+	return cmdMerge
+}
+
+func mergeProperties(cmd *cobra.Command, args []string) error {
+	if len(args) < 2 {
+		return errors.New("Must provide at least 2 yaml files")
+	}
+	// first generate update commands from the file
+	var filesToMerge = args[1:]
+	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
+
+	for _, fileToMerge := range filesToMerge {
+		matchingNodes, errorProcessingFile := readYamlFile(fileToMerge, "**", false, 0)
+		if errorProcessingFile != nil && (!allowEmptyFlag || !strings.HasPrefix(errorProcessingFile.Error(), "Could not process document index")) {
+			return errorProcessingFile
+		}
+		for _, matchingNode := range matchingNodes {
+			mergePath := lib.MergePathStackToString(matchingNode.PathStack, appendFlag)
+			updateCommands = append(updateCommands, yqlib.UpdateCommand{Command: "update", Path: mergePath, Value: matchingNode.Node, Overwrite: overwriteFlag})
+		}
+	}
+
+	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
+}
diff --git a/cmd/new.go b/cmd/new.go
new file mode 100644
index 00000000..3ca842d0
--- /dev/null
+++ b/cmd/new.go
@@ -0,0 +1,54 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+	yaml "gopkg.in/yaml.v3"
+)
+
+func createNewCmd() *cobra.Command {
+	var cmdNew = &cobra.Command{
+		Use:     "new [path] [value]",
+		Aliases: []string{"n"},
+		Short:   "yq n [--script/-s script_file] a.b.c newValue",
+		Example: `
+yq new 'a.b.c' cat
+yq n 'a.b.c' --tag '!!str' true # force 'true' to be interpreted as a string instead of bool
+yq n 'a.b[+]' cat
+yq n -- '--key-starting-with-dash' cat # need to use '--' to stop processing arguments as flags
+yq n --script create_script.yaml
+      `,
+		Long: `Creates a new yaml w.r.t the given path and value.
+Outputs to STDOUT
+
+Create Scripts:
+Note that you can give a create script to perform more sophisticated yaml. This follows the same format as the update script.
+`,
+		RunE: newProperty,
+	}
+	cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for creating yaml")
+	cmdNew.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
+	return cmdNew
+}
+
+func newProperty(cmd *cobra.Command, args []string) error {
+	var updateCommands, updateCommandsError = readUpdateCommands(args, 2, "Must provide <path_to_update> <value>")
+	if updateCommandsError != nil {
+		return updateCommandsError
+	}
+	newNode := lib.New(updateCommands[0].Path)
+
+	for _, updateCommand := range updateCommands {
+
+		errorUpdating := lib.Update(&newNode, updateCommand, true)
+
+		if errorUpdating != nil {
+			return errorUpdating
+		}
+	}
+
+	var encoder = yaml.NewEncoder(cmd.OutOrStdout())
+	encoder.SetIndent(2)
+	errorEncoding := encoder.Encode(&newNode)
+	encoder.Close()
+	return errorEncoding
+}
diff --git a/cmd/prefix.go b/cmd/prefix.go
new file mode 100644
index 00000000..ae5c28ae
--- /dev/null
+++ b/cmd/prefix.go
@@ -0,0 +1,50 @@
+package cmd
+
+import (
+	"github.com/mikefarah/yq/v3/pkg/yqlib"
+	errors "github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	yaml "gopkg.in/yaml.v3"
+)
+
+func createPrefixCmd() *cobra.Command {
+	var cmdPrefix = &cobra.Command{
+		Use:     "prefix [yaml_file] [path]",
+		Aliases: []string{"p"},
+		Short:   "yq p [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
+		Example: `
+yq prefix things.yaml 'a.b.c'
+yq prefix --inplace things.yaml 'a.b.c'
+yq prefix --inplace -- things.yaml '--key-starting-with-dash' # need to use '--' to stop processing arguments as flags
+yq p -i things.yaml 'a.b.c'
+yq p --doc 2 things.yaml 'a.b.d'
+yq p -d2 things.yaml 'a.b.d'
+      `,
+		Long: `Prefixes w.r.t to the yaml file at the given path.
+Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
+`,
+		RunE: prefixProperty,
+	}
+	cmdPrefix.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
+	cmdPrefix.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
+	return cmdPrefix
+}
+
+func prefixProperty(cmd *cobra.Command, args []string) error {
+
+	if len(args) < 2 {
+		return errors.New("Must provide <filename> <prefixed_path>")
+	}
+	updateCommand := yqlib.UpdateCommand{Command: "update", Path: args[1]}
+	log.Debugf("args %v", args)
+
+	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
+	if errorParsingDocIndex != nil {
+		return errorParsingDocIndex
+	}
+
+	var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
+		return prefixDocument(updateAll, docIndexInt, currentIndex, dataBucket, updateCommand)
+	}
+	return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
+}
diff --git a/cmd/read.go b/cmd/read.go
new file mode 100644
index 00000000..a669a080
--- /dev/null
+++ b/cmd/read.go
@@ -0,0 +1,52 @@
+package cmd
+
+import (
+	errors "github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+func createReadCmd() *cobra.Command {
+	var cmdRead = &cobra.Command{
+		Use:     "read [yaml_file] [path_expression]",
+		Aliases: []string{"r"},
+		Short:   "yq r [--doc/-d index] sample.yaml 'a.b.c'",
+		Example: `
+yq read things.yaml 'a.b.c'
+yq r - 'a.b.c' # reads from stdin
+yq r things.yaml 'a.*.c'
+yq r things.yaml 'a.**.c' # deep splat
+yq r things.yaml 'a.(child.subchild==co*).c'
+yq r -d1 things.yaml 'a.array[0].blah'
+yq r things.yaml 'a.array[*].blah'
+yq r -- things.yaml '--key-starting-with-dashes.blah'
+      `,
+		Long: "Outputs the value of the given path in the yaml file to STDOUT",
+		RunE: readProperty,
+	}
+	cmdRead.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
+	cmdRead.PersistentFlags().StringVarP(&printMode, "printMode", "p", "v", "print mode (v (values, default), p (paths), pv (path and value pairs)")
+	return cmdRead
+}
+
+func readProperty(cmd *cobra.Command, args []string) error {
+	var path = ""
+
+	if len(args) < 1 {
+		return errors.New("Must provide filename")
+	} else if len(args) > 1 {
+		path = args[1]
+	}
+
+	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
+	if errorParsingDocIndex != nil {
+		return errorParsingDocIndex
+	}
+
+	matchingNodes, errorReadingStream := readYamlFile(args[0], path, updateAll, docIndexInt)
+
+	if errorReadingStream != nil {
+		return errorReadingStream
+	}
+
+	return printResults(matchingNodes, cmd)
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 00000000..ca1c5264
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,55 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+	logging "gopkg.in/op/go-logging.v1"
+	"os"
+)
+
+func New() *cobra.Command {
+	var rootCmd = &cobra.Command{
+		Use:   "yq",
+		Short: "yq is a lightweight and portable command-line YAML processor.",
+		Long:  `yq is a lightweight and portable command-line YAML processor. It aims to be the jq or sed of yaml files.`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if version {
+				cmd.Print(GetVersionDisplay())
+				return nil
+			}
+			cmd.Println(cmd.UsageString())
+
+			return nil
+		},
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			var format = logging.MustStringFormatter(
+				`%{color}%{time:15:04:05} %{shortfunc} [%{level:.4s}]%{color:reset} %{message}`,
+			)
+			var backend = logging.AddModuleLevel(
+				logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format))
+
+			if verbose {
+				backend.SetLevel(logging.DEBUG, "")
+			} else {
+				backend.SetLevel(logging.ERROR, "")
+			}
+
+			logging.SetBackend(backend)
+		},
+	}
+
+	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
+	rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
+	rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
+
+	rootCmd.AddCommand(
+		createReadCmd(),
+		createWriteCmd(),
+		createPrefixCmd(),
+		createDeleteCmd(),
+		createNewCmd(),
+		createMergeCmd(),
+	)
+	rootCmd.SetOutput(os.Stdout)
+
+	return rootCmd
+}
diff --git a/cmd/utils.go b/cmd/utils.go
new file mode 100644
index 00000000..eb7b7531
--- /dev/null
+++ b/cmd/utils.go
@@ -0,0 +1,372 @@
+package cmd
+
+import (
+	"bufio"
+	"fmt"
+	"github.com/mikefarah/yq/v3/pkg/yqlib"
+	errors "github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	yaml "gopkg.in/yaml.v3"
+	"io"
+	"io/ioutil"
+	"os"
+	"strconv"
+)
+
+func readYamlFile(filename string, path string, updateAll bool, docIndexInt int) ([]*yqlib.NodeContext, error) {
+	var matchingNodes []*yqlib.NodeContext
+
+	var currentIndex = 0
+	var errorReadingStream = readStream(filename, func(decoder *yaml.Decoder) error {
+		for {
+			var dataBucket yaml.Node
+			errorReading := decoder.Decode(&dataBucket)
+
+			if errorReading == io.EOF {
+				return handleEOF(updateAll, docIndexInt, currentIndex)
+			}
+			var errorParsing error
+			matchingNodes, errorParsing = appendDocument(matchingNodes, dataBucket, path, updateAll, docIndexInt, currentIndex)
+			if errorParsing != nil {
+				return errorParsing
+			}
+			currentIndex = currentIndex + 1
+		}
+	})
+	return matchingNodes, errorReadingStream
+}
+
+func handleEOF(updateAll bool, docIndexInt int, currentIndex int) error {
+	log.Debugf("done %v / %v", currentIndex, docIndexInt)
+	if !updateAll && currentIndex <= docIndexInt {
+		return fmt.Errorf("Could not process document index %v as there are only %v document(s)", docIndex, currentIndex)
+	}
+	return nil
+}
+
+func appendDocument(originalMatchingNodes []*yqlib.NodeContext, dataBucket yaml.Node, path string, updateAll bool, docIndexInt int, currentIndex int) ([]*yqlib.NodeContext, error) {
+	log.Debugf("processing document %v - requested index %v", currentIndex, docIndexInt)
+	yqlib.DebugNode(&dataBucket)
+	if !updateAll && currentIndex != docIndexInt {
+		return originalMatchingNodes, nil
+	}
+	log.Debugf("reading %v in document %v", path, currentIndex)
+	matchingNodes, errorParsing := lib.Get(&dataBucket, path)
+	if errorParsing != nil {
+		return nil, errors.Wrapf(errorParsing, "Error reading path in document index %v", currentIndex)
+	}
+	return append(originalMatchingNodes, matchingNodes...), nil
+}
+
+func printValue(node *yaml.Node, cmd *cobra.Command) error {
+	if node.Kind == yaml.ScalarNode {
+		cmd.Print(node.Value)
+		return nil
+	}
+
+	bufferedWriter := bufio.NewWriter(cmd.OutOrStdout())
+	defer safelyFlush(bufferedWriter)
+
+	var encoder yqlib.Encoder
+	if outputToJSON {
+		encoder = yqlib.NewJsonEncoder(bufferedWriter)
+	} else {
+		encoder = yqlib.NewYamlEncoder(bufferedWriter)
+	}
+	if err := encoder.Encode(node); err != nil {
+		return err
+	}
+	return nil
+}
+
+func printResults(matchingNodes []*yqlib.NodeContext, cmd *cobra.Command) error {
+	if len(matchingNodes) == 0 {
+		log.Debug("no matching results, nothing to print")
+		return nil
+	}
+
+	for index, mappedDoc := range matchingNodes {
+		switch printMode {
+		case "p":
+			cmd.Print(lib.PathStackToString(mappedDoc.PathStack))
+			if index < len(matchingNodes)-1 {
+				cmd.Print("\n")
+			}
+		case "pv", "vp":
+			// put it into a node and print that.
+			var parentNode = yaml.Node{Kind: yaml.MappingNode}
+			parentNode.Content = make([]*yaml.Node, 2)
+			parentNode.Content[0] = &yaml.Node{Kind: yaml.ScalarNode, Value: lib.PathStackToString(mappedDoc.PathStack)}
+			parentNode.Content[1] = mappedDoc.Node
+			if err := printValue(&parentNode, cmd); err != nil {
+				return err
+			}
+		default:
+			if err := printValue(mappedDoc.Node, cmd); err != nil {
+				return err
+			}
+			// Printing our Scalars does not print a new line at the end
+			// we only want to do that if there are more values (so users can easily script extraction of values in the yaml)
+			if index < len(matchingNodes)-1 && mappedDoc.Node.Kind == yaml.ScalarNode {
+				cmd.Print("\n")
+			}
+		}
+	}
+
+	return nil
+}
+
+func parseDocumentIndex() (bool, int, error) {
+	if docIndex == "*" {
+		return true, -1, nil
+	}
+	docIndexInt64, err := strconv.ParseInt(docIndex, 10, 32)
+	if err != nil {
+		return false, -1, errors.Wrapf(err, "Document index %v is not a integer or *", docIndex)
+	}
+	return false, int(docIndexInt64), nil
+}
+
+type updateDataFn func(dataBucket *yaml.Node, currentIndex int) error
+
+func mapYamlDecoder(updateData updateDataFn, encoder yqlib.Encoder) yamlDecoderFn {
+	return func(decoder *yaml.Decoder) error {
+		var dataBucket yaml.Node
+		var errorReading error
+		var errorWriting error
+		var errorUpdating error
+		var currentIndex = 0
+
+		var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
+		if errorParsingDocIndex != nil {
+			return errorParsingDocIndex
+		}
+
+		for {
+			log.Debugf("Read doc %v", currentIndex)
+			errorReading = decoder.Decode(&dataBucket)
+
+			if errorReading == io.EOF {
+				if !updateAll && currentIndex <= docIndexInt {
+					return fmt.Errorf("asked to process document index %v but there are only %v document(s)", docIndex, currentIndex)
+				}
+				return nil
+			} else if errorReading != nil {
+				return errors.Wrapf(errorReading, "Error reading document at index %v, %v", currentIndex, errorReading)
+			}
+			errorUpdating = updateData(&dataBucket, currentIndex)
+			if errorUpdating != nil {
+				return errors.Wrapf(errorUpdating, "Error updating document at index %v", currentIndex)
+			}
+
+			errorWriting = encoder.Encode(&dataBucket)
+
+			if errorWriting != nil {
+				return errors.Wrapf(errorWriting, "Error writing document at index %v, %v", currentIndex, errorWriting)
+			}
+			currentIndex = currentIndex + 1
+		}
+	}
+}
+
+func prefixDocument(updateAll bool, docIndexInt int, currentIndex int, dataBucket *yaml.Node, updateCommand yqlib.UpdateCommand) error {
+	if updateAll || currentIndex == docIndexInt {
+		log.Debugf("Prefixing document %v", currentIndex)
+		yqlib.DebugNode(dataBucket)
+		updateCommand.Value = dataBucket.Content[0]
+		dataBucket.Content = make([]*yaml.Node, 1)
+
+		newNode := lib.New(updateCommand.Path)
+		dataBucket.Content[0] = &newNode
+
+		errorUpdating := lib.Update(dataBucket, updateCommand, true)
+		if errorUpdating != nil {
+			return errorUpdating
+		}
+	}
+	return nil
+}
+
+func updateDoc(inputFile string, updateCommands []yqlib.UpdateCommand, writer io.Writer) error {
+	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
+	if errorParsingDocIndex != nil {
+		return errorParsingDocIndex
+	}
+
+	var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
+		if updateAll || currentIndex == docIndexInt {
+			log.Debugf("Updating doc %v", currentIndex)
+			for _, updateCommand := range updateCommands {
+				log.Debugf("Processing update to Path %v", updateCommand.Path)
+				errorUpdating := lib.Update(dataBucket, updateCommand, autoCreateFlag)
+				if errorUpdating != nil {
+					return errorUpdating
+				}
+			}
+		}
+		return nil
+	}
+	return readAndUpdate(writer, inputFile, updateData)
+}
+
+func readAndUpdate(stdOut io.Writer, inputFile string, updateData updateDataFn) error {
+	var destination io.Writer
+	var destinationName string
+	if writeInplace {
+		info, err := os.Stat(inputFile)
+		if err != nil {
+			return err
+		}
+		tempFile, err := ioutil.TempFile("", "temp")
+		if err != nil {
+			return err
+		}
+		destinationName = tempFile.Name()
+		err = os.Chmod(destinationName, info.Mode())
+		if err != nil {
+			return err
+		}
+		destination = tempFile
+		defer func() {
+			safelyCloseFile(tempFile)
+			safelyRenameFile(tempFile.Name(), inputFile)
+		}()
+	} else {
+		destination = stdOut
+		destinationName = "Stdout"
+	}
+
+	log.Debugf("Writing to %v from %v", destinationName, inputFile)
+
+	bufferedWriter := bufio.NewWriter(destination)
+	defer safelyFlush(bufferedWriter)
+
+	var encoder yqlib.Encoder
+	if outputToJSON {
+		encoder = yqlib.NewJsonEncoder(bufferedWriter)
+	} else {
+		encoder = yqlib.NewYamlEncoder(bufferedWriter)
+	}
+	return readStream(inputFile, mapYamlDecoder(updateData, encoder))
+}
+
+type updateCommandParsed struct {
+	Command string
+	Path    string
+	Value   yaml.Node
+}
+
+func readUpdateCommands(args []string, expectedArgs int, badArgsMessage string) ([]yqlib.UpdateCommand, error) {
+	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
+	if writeScript != "" {
+		var parsedCommands = make([]updateCommandParsed, 0)
+
+		err := readData(writeScript, 0, &parsedCommands)
+
+		if err != nil && err != io.EOF {
+			return nil, err
+		}
+
+		log.Debugf("Read write commands file '%v'", parsedCommands)
+		for index := range parsedCommands {
+			parsedCommand := parsedCommands[index]
+			updateCommand := yqlib.UpdateCommand{Command: parsedCommand.Command, Path: parsedCommand.Path, Value: &parsedCommand.Value, Overwrite: true}
+			updateCommands = append(updateCommands, updateCommand)
+		}
+
+		log.Debugf("Read write commands file '%v'", updateCommands)
+	} else if len(args) < expectedArgs {
+		return nil, errors.New(badArgsMessage)
+	} else {
+		updateCommands = make([]yqlib.UpdateCommand, 1)
+		log.Debug("args %v", args)
+		log.Debug("path %v", args[expectedArgs-2])
+		log.Debug("Value %v", args[expectedArgs-1])
+		updateCommands[0] = yqlib.UpdateCommand{Command: "update", Path: args[expectedArgs-2], Value: valueParser.Parse(args[expectedArgs-1], customTag), Overwrite: true}
+	}
+	return updateCommands, nil
+}
+
+func safelyRenameFile(from string, to string) {
+	if renameError := os.Rename(from, to); renameError != nil {
+		log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to)
+		log.Debug(renameError.Error())
+		// can't do this rename when running in docker to a file targeted in a mounted volume,
+		// so gracefully degrade to copying the entire contents.
+		if copyError := copyFileContents(from, to); copyError != nil {
+			log.Errorf("Failed copying from %v to %v", from, to)
+			log.Error(copyError.Error())
+		} else {
+			removeErr := os.Remove(from)
+			if removeErr != nil {
+				log.Errorf("failed removing original file: %s", from)
+			}
+		}
+	}
+}
+
+// thanks https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
+func copyFileContents(src, dst string) (err error) {
+	in, err := os.Open(src) // nolint gosec
+	if err != nil {
+		return err
+	}
+	defer safelyCloseFile(in)
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer safelyCloseFile(out)
+	if _, err = io.Copy(out, in); err != nil {
+		return err
+	}
+	return out.Sync()
+}
+
+func safelyFlush(writer *bufio.Writer) {
+	if err := writer.Flush(); err != nil {
+		log.Error("Error flushing writer!")
+		log.Error(err.Error())
+	}
+
+}
+func safelyCloseFile(file *os.File) {
+	err := file.Close()
+	if err != nil {
+		log.Error("Error closing file!")
+		log.Error(err.Error())
+	}
+}
+
+type yamlDecoderFn func(*yaml.Decoder) error
+
+func readStream(filename string, yamlDecoder yamlDecoderFn) error {
+	if filename == "" {
+		return errors.New("Must provide filename")
+	}
+
+	var stream io.Reader
+	if filename == "-" {
+		stream = bufio.NewReader(os.Stdin)
+	} else {
+		file, err := os.Open(filename) // nolint gosec
+		if err != nil {
+			return err
+		}
+		defer safelyCloseFile(file)
+		stream = file
+	}
+	return yamlDecoder(yaml.NewDecoder(stream))
+}
+
+func readData(filename string, indexToRead int, parsedData interface{}) error {
+	return readStream(filename, func(decoder *yaml.Decoder) error {
+		for currentIndex := 0; currentIndex < indexToRead; currentIndex++ {
+			errorSkipping := decoder.Decode(parsedData)
+			if errorSkipping != nil {
+				return errors.Wrapf(errorSkipping, "Error processing document at index %v, %v", currentIndex, errorSkipping)
+			}
+		}
+		return decoder.Decode(parsedData)
+	})
+}
diff --git a/version.go b/cmd/version.go
similarity index 98%
rename from version.go
rename to cmd/version.go
index 588f6f73..11a00675 100644
--- a/version.go
+++ b/cmd/version.go
@@ -1,4 +1,4 @@
-package main
+package cmd
 
 import (
 	"fmt"
diff --git a/cmd/write.go b/cmd/write.go
new file mode 100644
index 00000000..04422f4b
--- /dev/null
+++ b/cmd/write.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+func createWriteCmd() *cobra.Command {
+	var cmdWrite = &cobra.Command{
+		Use:     "write [yaml_file] [path_expression] [value]",
+		Aliases: []string{"w"},
+		Short:   "yq w [--inplace/-i] [--script/-s script_file] [--doc/-d index] sample.yaml a.b.c newValue",
+		Example: `
+yq write things.yaml 'a.b.c' true
+yq write things.yaml 'a.*.c' true
+yq write things.yaml 'a.**' true
+yq write things.yaml 'a.(child.subchild==co*).c' true
+yq write things.yaml 'a.b.c' --tag '!!str' true # force 'true' to be interpreted as a string instead of bool
+yq write things.yaml 'a.b.c' --tag '!!float' 3
+yq write --inplace -- things.yaml 'a.b.c' '--cat' # need to use '--' to stop processing arguments as flags
+yq w -i things.yaml 'a.b.c' cat
+yq w -i -s update_script.yaml things.yaml
+yq w things.yaml 'a.b.d[+]' foo # appends a new node to the 'd' array
+yq w --doc 2 things.yaml 'a.b.d[+]' foo # updates the 3rd document of the yaml file
+      `,
+		Long: `Updates the yaml file w.r.t the given path and value.
+Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
+
+Append value to array adds the value to the end of array.
+
+Update Scripts:
+Note that you can give an update script to perform more sophisticated update. Update script
+format is list of update commands (update or delete) like so:
+---
+- command: update
+  path: b.c
+  value:
+    #great
+    things: frog # wow!
+- command: delete
+  path: b.d
+`,
+		RunE: writeProperty,
+	}
+	cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
+	cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
+	cmdWrite.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
+	cmdWrite.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
+	return cmdWrite
+}
+
+func writeProperty(cmd *cobra.Command, args []string) error {
+	var updateCommands, updateCommandsError = readUpdateCommands(args, 3, "Must provide <filename> <path_to_update> <value>")
+	if updateCommandsError != nil {
+		return updateCommandsError
+	}
+	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
+}
diff --git a/yq.go b/yq.go
index ed3a65a9..ef17961a 100644
--- a/yq.go
+++ b/yq.go
@@ -1,725 +1,16 @@
 package main
 
 import (
-	"bufio"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"strconv"
-	"strings"
-
-	"github.com/mikefarah/yq/v3/pkg/yqlib"
-
-	errors "github.com/pkg/errors"
-
-	"github.com/spf13/cobra"
+	command "github.com/mikefarah/yq/v3/cmd"
 	logging "gopkg.in/op/go-logging.v1"
-	yaml "gopkg.in/yaml.v3"
+	"os"
 )
 
-var customTag = ""
-var printMode = "v"
-var writeInplace = false
-var writeScript = ""
-var outputToJSON = false
-var overwriteFlag = false
-var autoCreateFlag = true
-var allowEmptyFlag = false
-var appendFlag = false
-var verbose = false
-var version = false
-var docIndex = "0"
-var log = logging.MustGetLogger("yq")
-var lib = yqlib.NewYqLib()
-var valueParser = yqlib.NewValueParser()
-
 func main() {
-	cmd := newCommandCLI()
+	cmd := command.New()
+	log := logging.MustGetLogger("yq")
 	if err := cmd.Execute(); err != nil {
 		log.Error(err.Error())
 		os.Exit(1)
 	}
 }
-
-func newCommandCLI() *cobra.Command {
-	var rootCmd = &cobra.Command{
-		Use:   "yq",
-		Short: "yq is a lightweight and portable command-line YAML processor.",
-		Long:  `yq is a lightweight and portable command-line YAML processor. It aims to be the jq or sed of yaml files.`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			if version {
-				cmd.Print(GetVersionDisplay())
-				return nil
-			}
-			cmd.Println(cmd.UsageString())
-
-			return nil
-		},
-		PersistentPreRun: func(cmd *cobra.Command, args []string) {
-			var format = logging.MustStringFormatter(
-				`%{color}%{time:15:04:05} %{shortfunc} [%{level:.4s}]%{color:reset} %{message}`,
-			)
-			var backend = logging.AddModuleLevel(
-				logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format))
-
-			if verbose {
-				backend.SetLevel(logging.DEBUG, "")
-			} else {
-				backend.SetLevel(logging.ERROR, "")
-			}
-
-			logging.SetBackend(backend)
-		},
-	}
-
-	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
-	rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
-	rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
-
-	rootCmd.AddCommand(
-		createReadCmd(),
-		createWriteCmd(),
-		createPrefixCmd(),
-		createDeleteCmd(),
-		createNewCmd(),
-		createMergeCmd(),
-	)
-	rootCmd.SetOutput(os.Stdout)
-
-	return rootCmd
-}
-
-func createReadCmd() *cobra.Command {
-	var cmdRead = &cobra.Command{
-		Use:     "read [yaml_file] [path_expression]",
-		Aliases: []string{"r"},
-		Short:   "yq r [--doc/-d index] sample.yaml 'a.b.c'",
-		Example: `
-yq read things.yaml 'a.b.c'
-yq r - 'a.b.c' # reads from stdin
-yq r things.yaml 'a.*.c'
-yq r things.yaml 'a.**.c' # deep splat
-yq r things.yaml 'a.(child.subchild==co*).c'
-yq r -d1 things.yaml 'a.array[0].blah'
-yq r things.yaml 'a.array[*].blah'
-yq r -- things.yaml '--key-starting-with-dashes.blah'
-      `,
-		Long: "Outputs the value of the given path in the yaml file to STDOUT",
-		RunE: readProperty,
-	}
-	cmdRead.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
-	cmdRead.PersistentFlags().StringVarP(&printMode, "printMode", "p", "v", "print mode (v (values, default), p (paths), pv (path and value pairs)")
-	return cmdRead
-}
-
-func createWriteCmd() *cobra.Command {
-	var cmdWrite = &cobra.Command{
-		Use:     "write [yaml_file] [path_expression] [value]",
-		Aliases: []string{"w"},
-		Short:   "yq w [--inplace/-i] [--script/-s script_file] [--doc/-d index] sample.yaml 'a.b.c' newValue",
-		Example: `
-yq write things.yaml 'a.b.c' true
-yq write things.yaml 'a.*.c' true
-yq write things.yaml 'a.**' true
-yq write things.yaml 'a.(child.subchild==co*).c' true
-yq write things.yaml 'a.b.c' --tag '!!str' true # force 'true' to be interpreted as a string instead of bool
-yq write things.yaml 'a.b.c' --tag '!!float' 3
-yq write --inplace -- things.yaml 'a.b.c' '--cat' # need to use '--' to stop processing arguments as flags
-yq w -i things.yaml 'a.b.c' cat
-yq w --script update_script.yaml things.yaml
-yq w -i -s update_script.yaml things.yaml
-yq w things.yaml 'a.b.d[+]' foo # appends a new node to the 'd' array
-yq w --doc 2 things.yaml 'a.b.d[+]' foo # updates the 3rd document of the yaml file
-      `,
-		Long: `Updates the matching nodes of path expression to the specified value
-Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
-
-Append value to array adds the value to the end of array.
-
-Update Scripts:
-Note that you can give an update script to perform more sophisticated update. Update script
-format is list of update commands (update or delete) like so:
----
-- command: update 
-  path: b.c
-  value:
-    #great 
-    things: frog # wow!
-- command: delete
-  path: b.d
-`,
-		RunE: writeProperty,
-	}
-	cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
-	cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
-	cmdWrite.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
-	cmdWrite.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
-	return cmdWrite
-}
-
-func createPrefixCmd() *cobra.Command {
-	var cmdPrefix = &cobra.Command{
-		Use:     "prefix [yaml_file] [path]",
-		Aliases: []string{"p"},
-		Short:   "yq p [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
-		Example: `
-yq prefix things.yaml 'a.b.c'
-yq prefix --inplace things.yaml 'a.b.c'
-yq prefix --inplace -- things.yaml '--key-starting-with-dash' # need to use '--' to stop processing arguments as flags
-yq p -i things.yaml 'a.b.c'
-yq p --doc 2 things.yaml 'a.b.d'
-yq p -d2 things.yaml 'a.b.d'
-      `,
-		Long: `Prefixes w.r.t to the yaml file at the given path.
-Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
-`,
-		RunE: prefixProperty,
-	}
-	cmdPrefix.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
-	cmdPrefix.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
-	return cmdPrefix
-}
-
-func createDeleteCmd() *cobra.Command {
-	var cmdDelete = &cobra.Command{
-		Use:     "delete [yaml_file] [path_expression]",
-		Aliases: []string{"d"},
-		Short:   "yq d [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
-		Example: `
-yq delete things.yaml 'a.b.c'
-yq delete things.yaml 'a.*.c'
-yq delete things.yaml 'a.(child.subchild==co*).c'
-yq delete things.yaml 'a.**'
-yq delete --inplace things.yaml 'a.b.c'
-yq delete --inplace -- things.yaml '--key-starting-with-dash' # need to use '--' to stop processing arguments as flags
-yq d -i things.yaml 'a.b.c'
-	`,
-		Long: `Deletes the nodes matching the given path expression from the YAML file.
-Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
-`,
-		RunE: deleteProperty,
-	}
-	cmdDelete.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
-	cmdDelete.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
-	return cmdDelete
-}
-
-func createNewCmd() *cobra.Command {
-	var cmdNew = &cobra.Command{
-		Use:     "new [path] [value]",
-		Aliases: []string{"n"},
-		Short:   "yq n [--script/-s script_file] a.b.c newValue",
-		Example: `
-yq new 'a.b.c' cat
-yq n 'a.b.c' --tag '!!str' true # force 'true' to be interpreted as a string instead of bool
-yq n 'a.b[+]' cat
-yq n -- '--key-starting-with-dash' cat # need to use '--' to stop processing arguments as flags
-yq n --script create_script.yaml
-      `,
-		Long: `Creates a new yaml w.r.t the given path and value.
-Outputs to STDOUT
-
-Create Scripts:
-Note that you can give a create script to perform more sophisticated yaml. This follows the same format as the update script.
-`,
-		RunE: newProperty,
-	}
-	cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for creating yaml")
-	cmdNew.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
-	return cmdNew
-}
-
-func createMergeCmd() *cobra.Command {
-	var cmdMerge = &cobra.Command{
-		Use:     "merge [initial_yaml_file] [additional_yaml_file]...",
-		Aliases: []string{"m"},
-		Short:   "yq m [--inplace/-i] [--doc/-d index] [--overwrite/-x] [--append/-a] sample.yaml sample2.yaml",
-		Example: `
-yq merge things.yaml other.yaml
-yq merge --inplace things.yaml other.yaml
-yq m -i things.yaml other.yaml
-yq m --overwrite things.yaml other.yaml
-yq m -i -x things.yaml other.yaml
-yq m -i -a things.yaml other.yaml
-yq m -i --autocreate=false things.yaml other.yaml
-      `,
-		Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s).
-Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
-
-If overwrite flag is set then existing values will be overwritten using the values from each additional yaml file.
-If append flag is set then existing arrays will be merged with the arrays from each additional yaml file.
-`,
-		RunE: mergeProperties,
-	}
-	cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
-	cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values")
-	cmdMerge.PersistentFlags().BoolVarP(&autoCreateFlag, "autocreate", "c", true, "automatically create any missing entries")
-	cmdMerge.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "update the yaml file by appending array values")
-	cmdMerge.PersistentFlags().BoolVarP(&allowEmptyFlag, "allow-empty", "e", false, "allow empty yaml files")
-	cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
-	return cmdMerge
-}
-
-func readProperty(cmd *cobra.Command, args []string) error {
-	var path = ""
-
-	if len(args) < 1 {
-		return errors.New("Must provide filename")
-	} else if len(args) > 1 {
-		path = args[1]
-	}
-
-	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
-	if errorParsingDocIndex != nil {
-		return errorParsingDocIndex
-	}
-
-	matchingNodes, errorReadingStream := readYamlFile(args[0], path, updateAll, docIndexInt)
-
-	if errorReadingStream != nil {
-		return errorReadingStream
-	}
-
-	return printResults(matchingNodes, cmd)
-}
-
-func readYamlFile(filename string, path string, updateAll bool, docIndexInt int) ([]*yqlib.NodeContext, error) {
-	var matchingNodes []*yqlib.NodeContext
-
-	var currentIndex = 0
-	var errorReadingStream = readStream(filename, func(decoder *yaml.Decoder) error {
-		for {
-			var dataBucket yaml.Node
-			errorReading := decoder.Decode(&dataBucket)
-
-			if errorReading == io.EOF {
-				return handleEOF(updateAll, docIndexInt, currentIndex)
-			}
-			var errorParsing error
-			matchingNodes, errorParsing = appendDocument(matchingNodes, dataBucket, path, updateAll, docIndexInt, currentIndex)
-			if errorParsing != nil {
-				return errorParsing
-			}
-			currentIndex = currentIndex + 1
-		}
-	})
-	return matchingNodes, errorReadingStream
-}
-
-func handleEOF(updateAll bool, docIndexInt int, currentIndex int) error {
-	log.Debugf("done %v / %v", currentIndex, docIndexInt)
-	if !updateAll && currentIndex <= docIndexInt {
-		return fmt.Errorf("Could not process document index %v as there are only %v document(s)", docIndex, currentIndex)
-	}
-	return nil
-}
-
-func appendDocument(originalMatchingNodes []*yqlib.NodeContext, dataBucket yaml.Node, path string, updateAll bool, docIndexInt int, currentIndex int) ([]*yqlib.NodeContext, error) {
-	log.Debugf("processing document %v - requested index %v", currentIndex, docIndexInt)
-	yqlib.DebugNode(&dataBucket)
-	if !updateAll && currentIndex != docIndexInt {
-		return originalMatchingNodes, nil
-	}
-	log.Debugf("reading %v in document %v", path, currentIndex)
-	matchingNodes, errorParsing := lib.Get(&dataBucket, path)
-	if errorParsing != nil {
-		return nil, errors.Wrapf(errorParsing, "Error reading path in document index %v", currentIndex)
-	}
-	return append(originalMatchingNodes, matchingNodes...), nil
-}
-
-func printValue(node *yaml.Node, cmd *cobra.Command) error {
-	if node.Kind == yaml.ScalarNode {
-		cmd.Print(node.Value)
-		return nil
-	}
-
-	bufferedWriter := bufio.NewWriter(cmd.OutOrStdout())
-	defer safelyFlush(bufferedWriter)
-
-	var encoder yqlib.Encoder
-	if outputToJSON {
-		encoder = yqlib.NewJsonEncoder(bufferedWriter)
-	} else {
-		encoder = yqlib.NewYamlEncoder(bufferedWriter)
-	}
-	if err := encoder.Encode(node); err != nil {
-		return err
-	}
-	return nil
-}
-
-func printResults(matchingNodes []*yqlib.NodeContext, cmd *cobra.Command) error {
-	if len(matchingNodes) == 0 {
-		log.Debug("no matching results, nothing to print")
-		return nil
-	}
-
-	for index, mappedDoc := range matchingNodes {
-		switch printMode {
-		case "p":
-			cmd.Print(lib.PathStackToString(mappedDoc.PathStack))
-			if index < len(matchingNodes)-1 {
-				cmd.Print("\n")
-			}
-		case "pv", "vp":
-			// put it into a node and print that.
-			var parentNode = yaml.Node{Kind: yaml.MappingNode}
-			parentNode.Content = make([]*yaml.Node, 2)
-			parentNode.Content[0] = &yaml.Node{Kind: yaml.ScalarNode, Value: lib.PathStackToString(mappedDoc.PathStack)}
-			parentNode.Content[1] = mappedDoc.Node
-			if err := printValue(&parentNode, cmd); err != nil {
-				return err
-			}
-		default:
-			if err := printValue(mappedDoc.Node, cmd); err != nil {
-				return err
-			}
-			// Printing our Scalars does not print a new line at the end
-			// we only want to do that if there are more values (so users can easily script extraction of values in the yaml)
-			if index < len(matchingNodes)-1 && mappedDoc.Node.Kind == yaml.ScalarNode {
-				cmd.Print("\n")
-			}
-		}
-	}
-
-	return nil
-}
-
-func parseDocumentIndex() (bool, int, error) {
-	if docIndex == "*" {
-		return true, -1, nil
-	}
-	docIndexInt64, err := strconv.ParseInt(docIndex, 10, 32)
-	if err != nil {
-		return false, -1, errors.Wrapf(err, "Document index %v is not a integer or *", docIndex)
-	}
-	return false, int(docIndexInt64), nil
-}
-
-type updateDataFn func(dataBucket *yaml.Node, currentIndex int) error
-
-func mapYamlDecoder(updateData updateDataFn, encoder yqlib.Encoder) yamlDecoderFn {
-	return func(decoder *yaml.Decoder) error {
-		var dataBucket yaml.Node
-		var errorReading error
-		var errorWriting error
-		var errorUpdating error
-		var currentIndex = 0
-
-		var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
-		if errorParsingDocIndex != nil {
-			return errorParsingDocIndex
-		}
-
-		for {
-			log.Debugf("Read doc %v", currentIndex)
-			errorReading = decoder.Decode(&dataBucket)
-
-			if errorReading == io.EOF {
-				if !updateAll && currentIndex <= docIndexInt {
-					return fmt.Errorf("asked to process document index %v but there are only %v document(s)", docIndex, currentIndex)
-				}
-				return nil
-			} else if errorReading != nil {
-				return errors.Wrapf(errorReading, "Error reading document at index %v, %v", currentIndex, errorReading)
-			}
-			errorUpdating = updateData(&dataBucket, currentIndex)
-			if errorUpdating != nil {
-				return errors.Wrapf(errorUpdating, "Error updating document at index %v", currentIndex)
-			}
-
-			errorWriting = encoder.Encode(&dataBucket)
-
-			if errorWriting != nil {
-				return errors.Wrapf(errorWriting, "Error writing document at index %v, %v", currentIndex, errorWriting)
-			}
-			currentIndex = currentIndex + 1
-		}
-	}
-}
-
-func writeProperty(cmd *cobra.Command, args []string) error {
-	var updateCommands, updateCommandsError = readUpdateCommands(args, 3, "Must provide <filename> <path_to_update> <value>")
-	if updateCommandsError != nil {
-		return updateCommandsError
-	}
-	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
-}
-
-func mergeProperties(cmd *cobra.Command, args []string) error {
-	if len(args) < 2 {
-		return errors.New("Must provide at least 2 yaml files")
-	}
-	// first generate update commands from the file
-	var filesToMerge = args[1:]
-	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
-
-	for _, fileToMerge := range filesToMerge {
-		matchingNodes, errorProcessingFile := readYamlFile(fileToMerge, "**", false, 0)
-		if errorProcessingFile != nil && (!allowEmptyFlag || !strings.HasPrefix(errorProcessingFile.Error(), "Could not process document index")) {
-			return errorProcessingFile
-		}
-		for _, matchingNode := range matchingNodes {
-			mergePath := lib.MergePathStackToString(matchingNode.PathStack, appendFlag)
-			updateCommands = append(updateCommands, yqlib.UpdateCommand{Command: "update", Path: mergePath, Value: matchingNode.Node, Overwrite: overwriteFlag})
-		}
-	}
-
-	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
-}
-
-func newProperty(cmd *cobra.Command, args []string) error {
-	var updateCommands, updateCommandsError = readUpdateCommands(args, 2, "Must provide <path_to_update> <value>")
-	if updateCommandsError != nil {
-		return updateCommandsError
-	}
-	newNode := lib.New(updateCommands[0].Path)
-
-	for _, updateCommand := range updateCommands {
-
-		errorUpdating := lib.Update(&newNode, updateCommand, true)
-
-		if errorUpdating != nil {
-			return errorUpdating
-		}
-	}
-
-	var encoder = yaml.NewEncoder(cmd.OutOrStdout())
-	encoder.SetIndent(2)
-	errorEncoding := encoder.Encode(&newNode)
-	encoder.Close()
-	return errorEncoding
-}
-
-func prefixProperty(cmd *cobra.Command, args []string) error {
-
-	if len(args) < 2 {
-		return errors.New("Must provide <filename> <prefixed_path>")
-	}
-	updateCommand := yqlib.UpdateCommand{Command: "update", Path: args[1]}
-	log.Debugf("args %v", args)
-
-	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
-	if errorParsingDocIndex != nil {
-		return errorParsingDocIndex
-	}
-
-	var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
-		return prefixDocument(updateAll, docIndexInt, currentIndex, dataBucket, updateCommand)
-	}
-	return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
-}
-
-func prefixDocument(updateAll bool, docIndexInt int, currentIndex int, dataBucket *yaml.Node, updateCommand yqlib.UpdateCommand) error {
-	if updateAll || currentIndex == docIndexInt {
-		log.Debugf("Prefixing document %v", currentIndex)
-		yqlib.DebugNode(dataBucket)
-		updateCommand.Value = dataBucket.Content[0]
-		dataBucket.Content = make([]*yaml.Node, 1)
-
-		newNode := lib.New(updateCommand.Path)
-		dataBucket.Content[0] = &newNode
-
-		errorUpdating := lib.Update(dataBucket, updateCommand, true)
-		if errorUpdating != nil {
-			return errorUpdating
-		}
-	}
-	return nil
-}
-
-func deleteProperty(cmd *cobra.Command, args []string) error {
-	if len(args) < 2 {
-		return errors.New("Must provide <filename> <path_to_delete>")
-	}
-	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 1)
-	updateCommands[0] = yqlib.UpdateCommand{Command: "delete", Path: args[1]}
-
-	return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
-}
-
-func updateDoc(inputFile string, updateCommands []yqlib.UpdateCommand, writer io.Writer) error {
-	var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
-	if errorParsingDocIndex != nil {
-		return errorParsingDocIndex
-	}
-
-	var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
-		if updateAll || currentIndex == docIndexInt {
-			log.Debugf("Updating doc %v", currentIndex)
-			for _, updateCommand := range updateCommands {
-				log.Debugf("Processing update to Path %v", updateCommand.Path)
-				errorUpdating := lib.Update(dataBucket, updateCommand, autoCreateFlag)
-				if errorUpdating != nil {
-					return errorUpdating
-				}
-			}
-		}
-		return nil
-	}
-	return readAndUpdate(writer, inputFile, updateData)
-}
-
-func readAndUpdate(stdOut io.Writer, inputFile string, updateData updateDataFn) error {
-	var destination io.Writer
-	var destinationName string
-	if writeInplace {
-		info, err := os.Stat(inputFile)
-		if err != nil {
-			return err
-		}
-		tempFile, err := ioutil.TempFile("", "temp")
-		if err != nil {
-			return err
-		}
-		destinationName = tempFile.Name()
-		err = os.Chmod(destinationName, info.Mode())
-		if err != nil {
-			return err
-		}
-		destination = tempFile
-		defer func() {
-			safelyCloseFile(tempFile)
-			safelyRenameFile(tempFile.Name(), inputFile)
-		}()
-	} else {
-		destination = stdOut
-		destinationName = "Stdout"
-	}
-
-	log.Debugf("Writing to %v from %v", destinationName, inputFile)
-
-	bufferedWriter := bufio.NewWriter(destination)
-	defer safelyFlush(bufferedWriter)
-
-	var encoder yqlib.Encoder
-	if outputToJSON {
-		encoder = yqlib.NewJsonEncoder(bufferedWriter)
-	} else {
-		encoder = yqlib.NewYamlEncoder(bufferedWriter)
-	}
-	return readStream(inputFile, mapYamlDecoder(updateData, encoder))
-}
-
-type updateCommandParsed struct {
-	Command string
-	Path    string
-	Value   yaml.Node
-}
-
-func readUpdateCommands(args []string, expectedArgs int, badArgsMessage string) ([]yqlib.UpdateCommand, error) {
-	var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
-	if writeScript != "" {
-		var parsedCommands = make([]updateCommandParsed, 0)
-
-		err := readData(writeScript, 0, &parsedCommands)
-
-		if err != nil && err != io.EOF {
-			return nil, err
-		}
-
-		log.Debugf("Read write commands file '%v'", parsedCommands)
-		for index := range parsedCommands {
-			parsedCommand := parsedCommands[index]
-			updateCommand := yqlib.UpdateCommand{Command: parsedCommand.Command, Path: parsedCommand.Path, Value: &parsedCommand.Value, Overwrite: true}
-			updateCommands = append(updateCommands, updateCommand)
-		}
-
-		log.Debugf("Read write commands file '%v'", updateCommands)
-	} else if len(args) < expectedArgs {
-		return nil, errors.New(badArgsMessage)
-	} else {
-		updateCommands = make([]yqlib.UpdateCommand, 1)
-		log.Debug("args %v", args)
-		log.Debug("path %v", args[expectedArgs-2])
-		log.Debug("Value %v", args[expectedArgs-1])
-		updateCommands[0] = yqlib.UpdateCommand{Command: "update", Path: args[expectedArgs-2], Value: valueParser.Parse(args[expectedArgs-1], customTag), Overwrite: true}
-	}
-	return updateCommands, nil
-}
-
-func safelyRenameFile(from string, to string) {
-	if renameError := os.Rename(from, to); renameError != nil {
-		log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to)
-		log.Debug(renameError.Error())
-		// can't do this rename when running in docker to a file targeted in a mounted volume,
-		// so gracefully degrade to copying the entire contents.
-		if copyError := copyFileContents(from, to); copyError != nil {
-			log.Errorf("Failed copying from %v to %v", from, to)
-			log.Error(copyError.Error())
-		} else {
-			removeErr := os.Remove(from)
-			if removeErr != nil {
-				log.Errorf("failed removing original file: %s", from)
-			}
-		}
-	}
-}
-
-// thanks https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
-func copyFileContents(src, dst string) (err error) {
-	in, err := os.Open(src) // nolint gosec
-	if err != nil {
-		return err
-	}
-	defer safelyCloseFile(in)
-	out, err := os.Create(dst)
-	if err != nil {
-		return err
-	}
-	defer safelyCloseFile(out)
-	if _, err = io.Copy(out, in); err != nil {
-		return err
-	}
-	return out.Sync()
-}
-
-func safelyFlush(writer *bufio.Writer) {
-	if err := writer.Flush(); err != nil {
-		log.Error("Error flushing writer!")
-		log.Error(err.Error())
-	}
-
-}
-func safelyCloseFile(file *os.File) {
-	err := file.Close()
-	if err != nil {
-		log.Error("Error closing file!")
-		log.Error(err.Error())
-	}
-}
-
-type yamlDecoderFn func(*yaml.Decoder) error
-
-func readStream(filename string, yamlDecoder yamlDecoderFn) error {
-	if filename == "" {
-		return errors.New("Must provide filename")
-	}
-
-	var stream io.Reader
-	if filename == "-" {
-		stream = bufio.NewReader(os.Stdin)
-	} else {
-		file, err := os.Open(filename) // nolint gosec
-		if err != nil {
-			return err
-		}
-		defer safelyCloseFile(file)
-		stream = file
-	}
-	return yamlDecoder(yaml.NewDecoder(stream))
-}
-
-func readData(filename string, indexToRead int, parsedData interface{}) error {
-	return readStream(filename, func(decoder *yaml.Decoder) error {
-		for currentIndex := 0; currentIndex < indexToRead; currentIndex++ {
-			errorSkipping := decoder.Decode(parsedData)
-			if errorSkipping != nil {
-				return errors.Wrapf(errorSkipping, "Error processing document at index %v, %v", currentIndex, errorSkipping)
-			}
-		}
-		return decoder.Decode(parsedData)
-	})
-}


Test patch:
diff --git a/commands_test.go b/cmd/commands_test.go
similarity index 90%
rename from commands_test.go
rename to cmd/commands_test.go
index 9a514d66..772885a4 100644
--- a/commands_test.go
+++ b/cmd/commands_test.go
@@ -1,4 +1,4 @@
-package main
+package cmd
 
 import (
 	"fmt"
@@ -12,7 +12,7 @@ import (
 )
 
 func getRootCommand() *cobra.Command {
-	return newCommandCLI()
+	return New()
 }
 
 func TestRootCmd(t *testing.T) {
@@ -87,7 +87,7 @@ func TestRootCmd_VersionLong(t *testing.T) {
 
 func TestReadCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read ../examples/sample.yaml b.c")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -96,7 +96,7 @@ func TestReadCmd(t *testing.T) {
 
 func TestReadWithAdvancedFilterCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -v examples/sample.yaml b.e(name==sam).value")
+	result := test.RunCmd(cmd, "read -v ../examples/sample.yaml b.e(name==sam).value")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -105,7 +105,7 @@ func TestReadWithAdvancedFilterCmd(t *testing.T) {
 
 func TestReadWithAdvancedFilterMapCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -v examples/sample.yaml b.e[name==fr*]")
+	result := test.RunCmd(cmd, "read -v ../examples/sample.yaml b.e[name==fr*]")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -117,7 +117,7 @@ value: 3
 
 func TestReadWithKeyAndValueCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read -p pv ../examples/sample.yaml b.c")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -126,7 +126,7 @@ func TestReadWithKeyAndValueCmd(t *testing.T) {
 
 func TestReadArrayCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/sample.yaml b.e.1.name")
+	result := test.RunCmd(cmd, "read -p pv ../examples/sample.yaml b.e.1.name")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -135,7 +135,7 @@ func TestReadArrayCmd(t *testing.T) {
 
 func TestReadDeepSplatCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/sample.yaml b.**")
+	result := test.RunCmd(cmd, "read -p pv ../examples/sample.yaml b.**")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -153,7 +153,7 @@ b.e.[1].value: 4
 
 func TestReadDeepSplatWithSuffixCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/sample.yaml b.**.name")
+	result := test.RunCmd(cmd, "read -p pv ../examples/sample.yaml b.**.name")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -165,7 +165,7 @@ b.e.[1].name: sam
 
 func TestReadWithKeyCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p p examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read -p p ../examples/sample.yaml b.c")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -174,7 +174,7 @@ func TestReadWithKeyCmd(t *testing.T) {
 
 func TestReadAnchorsCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/simple-anchor.yaml foobar.a")
+	result := test.RunCmd(cmd, "read ../examples/simple-anchor.yaml foobar.a")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -183,7 +183,7 @@ func TestReadAnchorsCmd(t *testing.T) {
 
 func TestReadAnchorsWithKeyAndValueCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/simple-anchor.yaml foobar.a")
+	result := test.RunCmd(cmd, "read -p pv ../examples/simple-anchor.yaml foobar.a")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -192,7 +192,7 @@ func TestReadAnchorsWithKeyAndValueCmd(t *testing.T) {
 
 func TestReadMergeAnchorsOriginalCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/merge-anchor.yaml foobar.a")
+	result := test.RunCmd(cmd, "read ../examples/merge-anchor.yaml foobar.a")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -201,7 +201,7 @@ func TestReadMergeAnchorsOriginalCmd(t *testing.T) {
 
 func TestReadMergeAnchorsOverrideCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/merge-anchor.yaml foobar.thing")
+	result := test.RunCmd(cmd, "read ../examples/merge-anchor.yaml foobar.thing")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -210,7 +210,7 @@ func TestReadMergeAnchorsOverrideCmd(t *testing.T) {
 
 func TestReadMergeAnchorsPrefixMatchCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "r -p pv examples/merge-anchor.yaml foobar.th*")
+	result := test.RunCmd(cmd, "r -p pv ../examples/merge-anchor.yaml foobar.th*")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -223,7 +223,7 @@ foobar.thirsty: yep
 
 func TestReadMergeAnchorsListOriginalCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/merge-anchor.yaml foobarList.a")
+	result := test.RunCmd(cmd, "read ../examples/merge-anchor.yaml foobarList.a")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -232,7 +232,7 @@ func TestReadMergeAnchorsListOriginalCmd(t *testing.T) {
 
 func TestReadMergeAnchorsListOverrideInListCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/merge-anchor.yaml foobarList.thing")
+	result := test.RunCmd(cmd, "read ../examples/merge-anchor.yaml foobarList.thing")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -241,7 +241,7 @@ func TestReadMergeAnchorsListOverrideInListCmd(t *testing.T) {
 
 func TestReadMergeAnchorsListOverrideCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/merge-anchor.yaml foobarList.c")
+	result := test.RunCmd(cmd, "read ../examples/merge-anchor.yaml foobarList.c")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -250,7 +250,7 @@ func TestReadMergeAnchorsListOverrideCmd(t *testing.T) {
 
 func TestReadInvalidDocumentIndexCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -df examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read -df ../examples/sample.yaml b.c")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to invalid path")
 	}
@@ -260,7 +260,7 @@ func TestReadInvalidDocumentIndexCmd(t *testing.T) {
 
 func TestReadBadDocumentIndexCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -d1 examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read -d1 ../examples/sample.yaml b.c")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to invalid path")
 	}
@@ -270,7 +270,7 @@ func TestReadBadDocumentIndexCmd(t *testing.T) {
 
 func TestReadOrderCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/order.yaml")
+	result := test.RunCmd(cmd, "read ../examples/order.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -283,7 +283,7 @@ application: MyApp
 
 func TestReadMultiCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -d 1 examples/multiple_docs.yaml another.document")
+	result := test.RunCmd(cmd, "read -d 1 ../examples/multiple_docs.yaml another.document")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -292,7 +292,7 @@ func TestReadMultiCmd(t *testing.T) {
 
 func TestReadMultiWithKeyAndValueCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p vp -d 1 examples/multiple_docs.yaml another.document")
+	result := test.RunCmd(cmd, "read -p vp -d 1 ../examples/multiple_docs.yaml another.document")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -301,7 +301,7 @@ func TestReadMultiWithKeyAndValueCmd(t *testing.T) {
 
 func TestReadMultiAllCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -d* examples/multiple_docs.yaml commonKey")
+	result := test.RunCmd(cmd, "read -d* ../examples/multiple_docs.yaml commonKey")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -313,7 +313,7 @@ third document`, result.Output)
 
 func TestReadMultiAllWithKeyAndValueCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv -d* examples/multiple_docs.yaml commonKey")
+	result := test.RunCmd(cmd, "read -p pv -d* ../examples/multiple_docs.yaml commonKey")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -326,7 +326,7 @@ commonKey: third document
 
 func TestReadCmd_ArrayYaml(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [0].gather_facts")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [0].gather_facts")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -335,7 +335,7 @@ func TestReadCmd_ArrayYaml(t *testing.T) {
 
 func TestReadCmd_ArrayYaml_NoPath(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -355,7 +355,7 @@ func TestReadCmd_ArrayYaml_NoPath(t *testing.T) {
 
 func TestReadCmd_ArrayYaml_OneElement(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [0]")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [0]")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -373,7 +373,7 @@ serial: 1
 
 func TestReadCmd_ArrayYaml_SplatCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [*]")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [*]")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -393,7 +393,7 @@ gather_facts: true
 
 func TestReadCmd_ArrayYaml_SplatWithKeyAndValueCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p pv examples/array.yaml [*]")
+	result := test.RunCmd(cmd, "read -p pv ../examples/array.yaml [*]")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -415,7 +415,7 @@ func TestReadCmd_ArrayYaml_SplatWithKeyAndValueCmd(t *testing.T) {
 
 func TestReadCmd_ArrayYaml_SplatWithKeyCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -p p examples/array.yaml [*]")
+	result := test.RunCmd(cmd, "read -p p ../examples/array.yaml [*]")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -426,7 +426,7 @@ func TestReadCmd_ArrayYaml_SplatWithKeyCmd(t *testing.T) {
 
 func TestReadCmd_ArrayYaml_SplatKey(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [*].gather_facts")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [*].gather_facts")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -437,7 +437,7 @@ true`
 
 func TestReadCmd_ArrayYaml_ErrorBadPath(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [x].gather_facts")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [x].gather_facts")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to missing arg")
 	}
@@ -447,7 +447,7 @@ func TestReadCmd_ArrayYaml_ErrorBadPath(t *testing.T) {
 
 func TestReadCmd_ArrayYaml_Splat_ErrorBadPath(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read examples/array.yaml [*].roles[x]")
+	result := test.RunCmd(cmd, "read ../examples/array.yaml [*].roles[x]")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to missing arg")
 	}
@@ -511,7 +511,7 @@ func TestReadCmd_ErrorBadPath(t *testing.T) {
 
 func TestReadCmd_Verbose(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "read -v examples/sample.yaml b.c")
+	result := test.RunCmd(cmd, "read -v ../examples/sample.yaml b.c")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -520,7 +520,7 @@ func TestReadCmd_Verbose(t *testing.T) {
 
 // func TestReadCmd_ToJson(t *testing.T) {
 // 	cmd := getRootCommand()
-// 	result := test.RunCmd(cmd, "read -j examples/sample.yaml b.c")
+// 	result := test.RunCmd(cmd, "read -j ../examples/sample.yaml b.c")
 // 	if result.Error != nil {
 // 		t.Error(result.Error)
 // 	}
@@ -529,7 +529,7 @@ func TestReadCmd_Verbose(t *testing.T) {
 
 // func TestReadCmd_ToJsonLong(t *testing.T) {
 // 	cmd := getRootCommand()
-// 	result := test.RunCmd(cmd, "read --tojson examples/sample.yaml b.c")
+// 	result := test.RunCmd(cmd, "read --tojson ../examples/sample.yaml b.c")
 // 	if result.Error != nil {
 // 		t.Error(result.Error)
 // 	}
@@ -875,7 +875,7 @@ func TestWriteCmdScript(t *testing.T) {
 	filename := test.WriteTempYamlFile(content)
 	defer test.RemoveTempYamlFile(filename)
 
-	updateScript := `- command: update 
+	updateScript := `- command: update
   path: b.c
   value: 7`
 	scriptFilename := test.WriteTempYamlFile(updateScript)
@@ -1324,7 +1324,7 @@ something: else`
 
 func TestMergeCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge examples/data1.yaml examples/data2.yaml")
+	result := test.RunCmd(cmd, "merge ../examples/data1.yaml ../examples/data2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1341,7 +1341,7 @@ c:
 
 func TestMergeNoAutoCreateCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge -c=false examples/data1.yaml examples/data2.yaml")
+	result := test.RunCmd(cmd, "merge -c=false ../examples/data1.yaml ../examples/data2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1355,7 +1355,7 @@ c:
 
 func TestMergeOverwriteCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge -c=false --overwrite examples/data1.yaml examples/data2.yaml")
+	result := test.RunCmd(cmd, "merge -c=false --overwrite ../examples/data1.yaml ../examples/data2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1369,7 +1369,7 @@ c:
 
 func TestMergeAppendCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge --autocreate=false --append examples/data1.yaml examples/data2.yaml")
+	result := test.RunCmd(cmd, "merge --autocreate=false --append ../examples/data1.yaml ../examples/data2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1383,7 +1383,7 @@ c:
 
 func TestMergeOverwriteAndAppendCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge --autocreate=false --append --overwrite examples/data1.yaml examples/data2.yaml")
+	result := test.RunCmd(cmd, "merge --autocreate=false --append --overwrite ../examples/data1.yaml ../examples/data2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1397,7 +1397,7 @@ c:
 
 func TestMergeArraysCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge --append examples/sample_array.yaml examples/sample_array_2.yaml")
+	result := test.RunCmd(cmd, "merge --append ../examples/sample_array.yaml ../examples/sample_array_2.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1408,7 +1408,7 @@ func TestMergeArraysCmd(t *testing.T) {
 
 func TestMergeCmd_Multi(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge -d1 examples/multiple_docs_small.yaml examples/data1.yaml")
+	result := test.RunCmd(cmd, "merge -d1 ../examples/multiple_docs_small.yaml ../examples/data1.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1491,7 +1491,7 @@ apples: red
 
 func TestMergeCmd_Error(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge examples/data1.yaml")
+	result := test.RunCmd(cmd, "merge ../examples/data1.yaml")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to missing arg")
 	}
@@ -1501,7 +1501,7 @@ func TestMergeCmd_Error(t *testing.T) {
 
 func TestMergeCmd_ErrorUnreadableFile(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge examples/data1.yaml fake-unknown")
+	result := test.RunCmd(cmd, "merge ../examples/data1.yaml fake-unknown")
 	if result.Error == nil {
 		t.Error("Expected command to fail due to unknown file")
 	}
@@ -1515,7 +1515,7 @@ func TestMergeCmd_ErrorUnreadableFile(t *testing.T) {
 }
 
 func TestMergeCmd_Inplace(t *testing.T) {
-	filename := test.WriteTempYamlFile(test.ReadTempYamlFile("examples/data1.yaml"))
+	filename := test.WriteTempYamlFile(test.ReadTempYamlFile("../examples/data1.yaml"))
 	err := os.Chmod(filename, os.FileMode(int(0666)))
 	if err != nil {
 		t.Error(err)
@@ -1523,7 +1523,7 @@ func TestMergeCmd_Inplace(t *testing.T) {
 	defer test.RemoveTempYamlFile(filename)
 
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, fmt.Sprintf("merge -i %s examples/data2.yaml", filename))
+	result := test.RunCmd(cmd, fmt.Sprintf("merge -i %s ../examples/data2.yaml", filename))
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1543,7 +1543,7 @@ c:
 
 func TestMergeAllowEmptyCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge --allow-empty examples/data1.yaml examples/empty.yaml")
+	result := test.RunCmd(cmd, "merge --allow-empty ../examples/data1.yaml ../examples/empty.yaml")
 	if result.Error != nil {
 		t.Error(result.Error)
 	}
@@ -1557,7 +1557,7 @@ c:
 
 func TestMergeDontAllowEmptyCmd(t *testing.T) {
 	cmd := getRootCommand()
-	result := test.RunCmd(cmd, "merge examples/data1.yaml examples/empty.yaml")
+	result := test.RunCmd(cmd, "merge ../examples/data1.yaml ../examples/empty.yaml")
 	expectedOutput := `Could not process document index 0 as there are only 0 document(s)`
 	test.AssertResult(t, expectedOutput, result.Error.Error())
 }
diff --git a/version_test.go b/cmd/version_test.go
similarity index 98%
rename from version_test.go
rename to cmd/version_test.go
index 9f21427f..b012e652 100644
--- a/version_test.go
+++ b/cmd/version_test.go
@@ -1,4 +1,4 @@
-package main
+package cmd
 
 import "testing"