Repository: TomWright/dasel
Pull request #69 (2020-12-19 22:25:35)

Add put document support and fix some issues:
- `dasel put document` is now supported.
- Fix an issue in that the read + write flags were ignored in `dasel put object`.
- Fix `dasel put object` command name/description.
- Update command descriptions.
- Fix an issue that stopped you being able to modify the root node.
- Fix some invalid selectors in the README.


Patch:
diff --git a/README.md b/README.md
index ed3aa78..93862ae 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ Dasel uses a standard selector syntax no matter the data format. This means that
   * [Select](#select)
   * [Put](#put)
   * [Put Object](#put-object)
+  * [Put Document](#put-document)
 * [Supported file types](#supported-file-types)
   * [JSON](#json)
   * [TOML](#toml)
@@ -549,6 +550,102 @@ my:
     number: 3
 ```
 
+### Put Document
+
+You are able to put entire documents using `put document`.
+
+```bash
+dasel put document -f <file> -o <out> -p <parser> -m <selector> -d <document-parser> <document>
+```
+
+#### Arguments
+
+##### `-f`, `--file`
+
+Specify the file to query. This is required unless you are piping in data.
+
+If piping in data you can optionally pass `-f stdin`/`-f -`.
+
+##### `-o`, `--out`
+
+Specify the output file. If present, results will be written to the given file. If not present, results will be written to the input file (or stdout if none given).
+
+To force output to be written to stdout, pass `-o stdout`/`-o -`.
+
+##### `-d`, `--document-parser`, `<document-parser>`
+
+Specify the parser to use when reading the document value.
+
+If no value is provided, the read parser is used.
+
+See [supported parsers](#supported-file-types).
+
+##### `-r`, `--read`
+
+Specify the parser to use when reading the input data.
+
+This is required if you are piping in data, otherwise dasel will use the given file extension to guess which parser to use.
+
+See [supported parsers](#supported-file-types).
+
+##### `-w`, `--write`
+
+Specify the parser to use when writing the output data.
+
+If not provided dasel will attempt to use the `--out` and `--read` flags to determine which parser to use.
+
+See [supported parsers](#supported-file-types).
+
+##### `-p`, `--parser`
+
+Shorthand for `-r <value> -w <value>`
+
+##### `-m`, `--multiple`
+
+Tells dasel to put multiple items.
+
+This causes the [dynamic](#dynamic) selector to return all matching results rather than the first, and enables the [any index](#any-index) selector.
+
+##### `-s`, `--selector`, `<selector>`
+
+Specify the selector to use. See [Selectors](#selectors) for more information.
+
+If no selector flag is given, dasel assumes the first argument given is the selector.
+
+This is required.
+
+##### `document`, `<document>`
+
+The document you want to put, as a string.
+
+This is required.
+
+#### Example
+
+```bash
+echo '{"people":[]}' | dasel put document -p json -d yaml '.people.[]' 'name: Tom
+colours:
+- red
+- green
+- blue'
+```
+
+Results in the following:
+```json
+{
+  "people": [
+    {
+      "colours": [
+        "red",
+        "green",
+        "blue"
+      ],
+      "name": "Tom"
+    }
+  ]
+}
+```
+
 ## Supported file types
 Dasel attempts to find the correct parser for the given file type, but if that fails you can choose which parser to use with the `-p` or `--parser` flag. 
 
@@ -980,7 +1077,7 @@ echo '{"users": [{"name": "Tom"}]}' | jq '.users += [{"name": "Frank"}]'
   ]
 }
 
-echo '{"users": [{"name": "Tom"}]}' | dasel put object -p json -s '.users[]' -t string name=Frank
+echo '{"users": [{"name": "Tom"}]}' | dasel put object -p json -s '.users.[]' -t string name=Frank
 {
   "users": [
     {
@@ -1122,7 +1219,7 @@ users:
 
 
 echo 'users:
-- name: Tom' | dasel put object -p yaml -s '.users[]' -t string name=Frank
+- name: Tom' | dasel put object -p yaml -s '.users.[]' -t string name=Frank
 users:
 - name: Tom
 - name: Frank
diff --git a/internal/command/put.go b/internal/command/put.go
index e62a15d..9b12c47 100644
--- a/internal/command/put.go
+++ b/internal/command/put.go
@@ -272,6 +272,7 @@ func putCommand() *cobra.Command {
 		putBoolCommand(),
 		putIntCommand(),
 		putObjectCommand(),
+		putDocumentCommand(),
 	)
 
 	cmd.PersistentFlags().StringVarP(&fileFlag, "file", "f", "", "The file to query.")
diff --git a/internal/command/put_bool.go b/internal/command/put_bool.go
index 1f2781f..660e0fd 100644
--- a/internal/command/put_bool.go
+++ b/internal/command/put_bool.go
@@ -7,7 +7,7 @@ import (
 func putBoolCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "bool -f <file> -s <selector> <value>",
-		Short: "Update a bool property in the given file.",
+		Short: "Update a bool property in the given document.",
 		Args:  cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runGenericPutCommand(genericPutOptions{
diff --git a/internal/command/put_document_internal.go b/internal/command/put_document_internal.go
new file mode 100644
index 0000000..a096293
--- /dev/null
+++ b/internal/command/put_document_internal.go
@@ -0,0 +1,119 @@
+package command
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"github.com/tomwright/dasel/internal/storage"
+	"io"
+)
+
+type putDocumentOpts struct {
+	File           string
+	Out            string
+	ReadParser     string
+	WriteParser    string
+	Parser         string
+	Selector       string
+	DocumentString string
+	DocumentParser string
+	Reader         io.Reader
+	Writer         io.Writer
+	Multi          bool
+}
+
+func runPutDocumentCommand(opts putDocumentOpts, cmd *cobra.Command) error {
+	readParser, err := getReadParser(opts.File, opts.ReadParser, opts.Parser)
+	if err != nil {
+		return err
+	}
+	rootNode, err := getRootNode(getRootNodeOpts{
+		File:   opts.File,
+		Parser: readParser,
+		Reader: opts.Reader,
+	}, cmd)
+	if err != nil {
+		return err
+	}
+
+	documentParser, err := getPutDocumentParser(readParser, opts.DocumentParser)
+	if err != nil {
+		return err
+	}
+
+	documentValue, err := documentParser.FromBytes([]byte(opts.DocumentString))
+	if err != nil {
+		return fmt.Errorf("could not parse document: %w", err)
+	}
+
+	if opts.Multi {
+		if err := rootNode.PutMultiple(opts.Selector, documentValue); err != nil {
+			return fmt.Errorf("could not put document multi value: %w", err)
+		}
+	} else {
+		if err := rootNode.Put(opts.Selector, documentValue); err != nil {
+			return fmt.Errorf("could not put document value: %w", err)
+		}
+	}
+
+	writeParser, err := getWriteParser(readParser, opts.WriteParser, opts.Parser, opts.Out, opts.File)
+	if err != nil {
+		return err
+	}
+
+	if err := writeNodeToOutput(writeNodeToOutputOpts{
+		Node:   rootNode,
+		Parser: writeParser,
+		File:   opts.File,
+		Out:    opts.Out,
+		Writer: opts.Writer,
+	}, cmd); err != nil {
+		return fmt.Errorf("could not write output: %w", err)
+	}
+
+	return nil
+}
+
+func putDocumentCommand() *cobra.Command {
+	var documentParserFlag string
+
+	cmd := &cobra.Command{
+		Use:   "document -f <file> -d <document-parser> -s <selector> <document>",
+		Short: "Put an entire document into the given document.",
+		Args:  cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts := putDocumentOpts{
+				File:           cmd.Flag("file").Value.String(),
+				Out:            cmd.Flag("out").Value.String(),
+				ReadParser:     cmd.Flag("read").Value.String(),
+				WriteParser:    cmd.Flag("write").Value.String(),
+				Parser:         cmd.Flag("parser").Value.String(),
+				Selector:       cmd.Flag("selector").Value.String(),
+				DocumentParser: documentParserFlag,
+				DocumentString: args[0],
+			}
+			opts.Multi, _ = cmd.Flags().GetBool("multiple")
+
+			if opts.Selector == "" && len(args) > 1 {
+				opts.Selector = args[0]
+				opts.DocumentString = args[1]
+			}
+			return runPutDocumentCommand(opts, cmd)
+		},
+	}
+
+	cmd.Flags().StringVarP(&documentParserFlag, "document-parser", "d", "", "The parser to use when reading the document")
+
+	return cmd
+}
+
+func getPutDocumentParser(readParser storage.ReadParser, documentParserFlag string) (storage.ReadParser, error) {
+	if documentParserFlag == "" {
+		return readParser, nil
+	}
+
+	parser, err := storage.NewReadParserFromString(documentParserFlag)
+	if err != nil {
+		return nil, fmt.Errorf("could not get document parser: %w", err)
+	}
+	return parser, nil
+}
diff --git a/internal/command/put_int.go b/internal/command/put_int.go
index 436bc21..1aba991 100644
--- a/internal/command/put_int.go
+++ b/internal/command/put_int.go
@@ -7,7 +7,7 @@ import (
 func putIntCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "int -f <file> -s <selector> <value>",
-		Short: "Update an int property in the given file.",
+		Short: "Update an int property in the given document.",
 		Args:  cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runGenericPutCommand(genericPutOptions{
diff --git a/internal/command/put_object_internal.go b/internal/command/put_object_internal.go
index a0af55e..dfc6c62 100644
--- a/internal/command/put_object_internal.go
+++ b/internal/command/put_object_internal.go
@@ -95,12 +95,14 @@ func putObjectCommand() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "object -f <file> -s <selector> <value>",
-		Short: "Update a string property in the given file.",
+		Short: "Put an object in the given document.",
 		Args:  cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			opts := putObjectOpts{
 				File:        cmd.Flag("file").Value.String(),
 				Out:         cmd.Flag("out").Value.String(),
+				ReadParser:  cmd.Flag("read").Value.String(),
+				WriteParser: cmd.Flag("write").Value.String(),
 				Parser:      cmd.Flag("parser").Value.String(),
 				Selector:    cmd.Flag("selector").Value.String(),
 				InputTypes:  typeList.Strings,
diff --git a/internal/command/put_string.go b/internal/command/put_string.go
index ec1d22e..935c989 100644
--- a/internal/command/put_string.go
+++ b/internal/command/put_string.go
@@ -7,7 +7,7 @@ import (
 func putStringCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "string -f <file> -s <selector> <value>",
-		Short: "Update a string property in the given file.",
+		Short: "Update a string property in the given document.",
 		Args:  cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runGenericPutCommand(genericPutOptions{
diff --git a/node.go b/node.go
index e6c6fab..2cb5a7d 100644
--- a/node.go
+++ b/node.go
@@ -105,19 +105,10 @@ func unwrapValue(value reflect.Value) reflect.Value {
 
 // New returns a new root node with the given value.
 func New(value interface{}) *Node {
-	var baseValue reflect.Value
-	switch typed := value.(type) {
-	case storage.RealValue:
-		baseValue = reflect.ValueOf(typed.RealValue())
-	default:
-		baseValue = reflect.ValueOf(value)
-	}
 	rootNode := &Node{
-		Previous:      nil,
-		Next:          nil,
-		NextMultiple:  nil,
-		OriginalValue: value,
-		Value:         baseValue,
+		Previous:     nil,
+		Next:         nil,
+		NextMultiple: nil,
 		Selector: Selector{
 			Raw:       ".",
 			Current:   ".",
@@ -126,5 +117,46 @@ func New(value interface{}) *Node {
 			Property:  "",
 		},
 	}
+	rootNode.setRealValue(value)
 	return rootNode
 }
+
+func (n *Node) setValue(newValue interface{}) {
+	n.Value = reflect.ValueOf(newValue)
+	if n.Selector.Type == "ROOT" {
+		n.OriginalValue = newValue
+	}
+}
+
+func (n *Node) setRealValue(newValue interface{}) {
+	switch typed := newValue.(type) {
+	case storage.RealValue:
+		n.Value = reflect.ValueOf(typed.RealValue())
+	default:
+		n.Value = reflect.ValueOf(typed)
+	}
+	if n.Selector.Type == "ROOT" {
+		n.OriginalValue = newValue
+	}
+}
+
+func (n *Node) setReflectValue(newValue reflect.Value) {
+	n.Value = newValue
+	if n.Selector.Type == "ROOT" {
+		n.OriginalValue = unwrapValue(newValue).Interface()
+	}
+}
+
+func (n *Node) setRealReflectValue(newValue reflect.Value) {
+	val := unwrapValue(newValue).Interface()
+	switch typed := val.(type) {
+	case storage.RealValue:
+		n.OriginalValue = typed
+		n.Value = reflect.ValueOf(typed.RealValue())
+	default:
+		n.Value = newValue
+	}
+	if n.Selector.Type == "ROOT" {
+		n.OriginalValue = val
+	}
+}
diff --git a/node_propagate.go b/node_propagate.go
index af17537..eaeed6d 100644
--- a/node_propagate.go
+++ b/node_propagate.go
@@ -64,7 +64,7 @@ func propagateValueIndex(n *Node) error {
 			value.Index(n.Selector.Index).Set(n.Value)
 			return nil
 		}
-		n.Previous.Value = reflect.Append(value, n.Value)
+		n.Previous.setReflectValue(reflect.Append(value, n.Value))
 		return nil
 	}
 
@@ -80,7 +80,7 @@ func propagateValueNextAvailableIndex(n *Node) error {
 	value := unwrapValue(n.Previous.Value)
 
 	if value.Kind() == reflect.Slice {
-		n.Previous.Value = reflect.Append(value, n.Value)
+		n.Previous.setReflectValue(reflect.Append(value, n.Value))
 		return nil
 	}
 
diff --git a/node_put.go b/node_put.go
index 368f0a0..5a893a2 100644
--- a/node_put.go
+++ b/node_put.go
@@ -2,13 +2,16 @@ package dasel
 
 import (
 	"fmt"
+	"github.com/tomwright/dasel/internal/storage"
 	"reflect"
 )
 
 // Put finds the node using the given selector and updates it's value.
 // It then attempts to propagate the value back up the chain to the root element.
 func (n *Node) Put(selector string, newValue interface{}) error {
-	n.Selector.Remaining = selector
+	if selector != "." {
+		n.Selector.Remaining = selector
+	}
 
 	if err := buildPutChain(n); err != nil {
 		return err
@@ -16,10 +19,17 @@ func (n *Node) Put(selector string, newValue interface{}) error {
 
 	final := lastNode(n)
 
-	final.Value = reflect.ValueOf(newValue)
+	_, isRealValue := newValue.(storage.RealValue)
+	if isRealValue {
+		final.setRealValue(newValue)
+	} else {
+		final.setValue(newValue)
+	}
 
-	if err := propagate(final); err != nil {
-		return err
+	if final.Selector.Type != "ROOT" {
+		if err := propagate(final); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -28,7 +38,9 @@ func (n *Node) Put(selector string, newValue interface{}) error {
 // PutMultiple all applicable nodes for the given selector and updates all of their values to the given value.
 // It then attempts to propagate the value back up the chain to the root element.
 func (n *Node) PutMultiple(selector string, newValue interface{}) error {
-	n.Selector.Remaining = selector
+	if selector != "." {
+		n.Selector.Remaining = selector
+	}
 
 	if err := buildPutMultipleChain(n); err != nil {
 		return err
@@ -37,9 +49,14 @@ func (n *Node) PutMultiple(selector string, newValue interface{}) error {
 	final := lastNodes(n)
 
 	val := reflect.ValueOf(newValue)
+	_, isRealValue := newValue.(storage.RealValue)
 
 	for _, n := range final {
-		n.Value = val
+		if isRealValue {
+			n.setRealReflectValue(val)
+		} else {
+			n.setReflectValue(val)
+		}
 		if err := propagate(n); err != nil {
 			return err
 		}


Test patch:
diff --git a/internal/command/put_document_internal_test.go b/internal/command/put_document_internal_test.go
new file mode 100644
index 0000000..26152a5
--- /dev/null
+++ b/internal/command/put_document_internal_test.go
@@ -0,0 +1,56 @@
+package command
+
+import (
+	"errors"
+	"github.com/tomwright/dasel/internal/storage"
+	"strings"
+	"testing"
+)
+
+func TestPut_Document(t *testing.T) {
+	t.Run("SingleFailingWriter", func(t *testing.T) {
+		err := runPutDocumentCommand(putDocumentOpts{
+			Parser:         "json",
+			Selector:       ".[0]",
+			Reader:         strings.NewReader(`[{"name": "Tom"}]`),
+			DocumentString: `{"name": "Frank"}`,
+			Writer:         &failingWriter{},
+		}, nil)
+
+		if err == nil || !errors.Is(err, errFailingWriterErr) {
+			t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
+			return
+		}
+	})
+	t.Run("MultiFailingWriter", func(t *testing.T) {
+		err := runPutDocumentCommand(putDocumentOpts{
+			Parser:         "json",
+			Selector:       ".[*]",
+			Reader:         strings.NewReader(`[{"name": "Tom"}]`),
+			DocumentString: `{"name": "Frank"}`,
+			Writer:         &failingWriter{},
+			Multi:          true,
+		}, nil)
+
+		if err == nil || !errors.Is(err, errFailingWriterErr) {
+			t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
+			return
+		}
+	})
+	t.Run("InvalidDocumentParser", func(t *testing.T) {
+		err := runPutDocumentCommand(putDocumentOpts{
+			Parser:         "json",
+			Selector:       ".[*]",
+			Reader:         strings.NewReader(`[{"name": "Tom"}]`),
+			DocumentString: `{"name": "Frank"}`,
+			DocumentParser: "bad",
+		}, nil)
+
+		exp := &storage.UnknownParserErr{Parser: "bad"}
+
+		if err == nil || !strings.HasSuffix(err.Error(), exp.Error()) {
+			t.Errorf("expected error %v, got %v", exp, err)
+			return
+		}
+	})
+}
diff --git a/internal/command/root_put_test.go b/internal/command/root_put_test.go
index bf37eb9..779a9bf 100644
--- a/internal/command/root_put_test.go
+++ b/internal/command/root_put_test.go
@@ -2,6 +2,7 @@ package command_test
 
 import (
 	"bytes"
+	"fmt"
 	"github.com/tomwright/dasel/internal/command"
 	"io/ioutil"
 	"os"
@@ -197,6 +198,14 @@ func TestRootCMD_Put_JSON(t *testing.T) {
   ]
 }`, nil))
 
+	t.Run("OverwriteObjectAtRoot", putObjectTest(`{
+  "rank": 1,
+  "number": "one"
+}`, "json", ".", []string{"number=five", "rank=5"}, []string{"string", "int"}, `{
+  "number": "five",
+  "rank": 5
+}`, nil))
+
 	t.Run("MultipleObject", putObjectTest(`{
   "numbers": [
     {
@@ -531,6 +540,87 @@ func TestRootCMD_Put_JSON(t *testing.T) {
   "f"
 ]
 `, nil))
+
+	t.Run("InsertDocumentAtProperty", putDocumentTest(`{}`, "json", ".person", `{"name":"Tom"}`, `{
+  "person": {
+    "name": "Tom"
+  }
+}
+`, nil))
+
+	t.Run("InvalidDocumentParser", putDocumentTest(`{}`, "json", ".person", `name: Tom`, ``,
+		fmt.Errorf("could not get document parser: unknown parser: bad"), "-d", "bad"))
+
+	t.Run("InsertDocumentAtPropertyWithDifferentParser", putDocumentTest(`{}`, "json", ".person", `name: Tom`, `{
+  "person": {
+    "name": "Tom"
+  }
+}
+`, nil, "-d", "yaml"))
+
+	t.Run("AppendDocumentToProperty", putDocumentTest(`{"people": []}`, "json", ".people.[]", `{"name":"Tom"}`, `{
+  "people": [
+    {
+      "name": "Tom"
+    }
+  ]
+}
+`, nil))
+
+	t.Run("AppendDocumentToPropertyMulti", putDocumentTest(`{"people": []}{"people": []}`, "json", ".[*].people.[]", `{"name":"Tom"}`, `{
+  "people": [
+    {
+      "name": "Tom"
+    }
+  ]
+}
+{
+  "people": [
+    {
+      "name": "Tom"
+    }
+  ]
+}
+`, nil, "-m"))
+
+	t.Run("InsertDocumentAtRoot", putDocumentTest(`{}`, "json", ".", `{"name":"Tom"}`, `{
+  "name": "Tom"
+}
+`, nil))
+
+	t.Run("AppendDocumentAtRoot", putDocumentTest(`[]`, "json", ".[]", `{"name":"Tom"}`, `[
+  {
+    "name": "Tom"
+  }
+]
+`, nil))
+
+	t.Run("AppendDocumentAtRootMulti", putDocumentTest(`[][]`, "json", ".[*].[]", `{"name":"Tom"}`, `[
+  {
+    "name": "Tom"
+  }
+]
+[
+  {
+    "name": "Tom"
+  }
+]
+`, nil, "-m"))
+
+	// https://github.com/TomWright/dasel/issues/66
+	t.Run("PutJSONDocumentAtYAMLProperty", putDocumentTest(`foo: true
+bar: 5
+baz:
+  qux: false
+  quux: "yes"
+  quuz: 7`, "yaml", ".baz", `{"qux": false,"quux": "no","quuz": 8}`, `bar: 5
+baz:
+  quux: "no"
+  quuz: 8
+  qux: false
+foo: true
+`, nil, "-d", "json"))
+
 }
 
 func TestRootCMD_Put_YAML(t *testing.T) {
@@ -742,6 +832,54 @@ func putObjectTest(in string, parser string, selector string, values []string, t
 	}
 }
 
+func putDocumentTest(in string, parser string, selector string, document string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
+	return func(t *testing.T) {
+		cmd := command.NewRootCMD()
+		outputBuffer := bytes.NewBuffer([]byte{})
+
+		args := []string{
+			"put", "document", "-p", parser, "-o", "stdout",
+		}
+		args = append(args, additionalArgs...)
+		args = append(args, selector)
+		args = append(args, document)
+
+		cmd.SetOut(outputBuffer)
+		cmd.SetIn(strings.NewReader(in))
+		cmd.SetArgs(args)
+
+		err := cmd.Execute()
+
+		if expErr == nil && err != nil {
+			t.Errorf("expected err %v, got %v", expErr, err)
+			return
+		}
+		if expErr != nil && err == nil {
+			t.Errorf("expected err %v, got %v", expErr, err)
+			return
+		}
+		if expErr != nil && err != nil && err.Error() != expErr.Error() {
+			t.Errorf("expected err %v, got %v", expErr, err)
+			return
+		}
+		if expErr != nil {
+			return
+		}
+
+		output, err := ioutil.ReadAll(outputBuffer)
+		if err != nil {
+			t.Errorf("unexpected error reading output buffer: %s", err)
+			return
+		}
+
+		out = strings.TrimSpace(out)
+		outputStr := strings.TrimSpace(string(output))
+		if out != outputStr {
+			t.Errorf("expected result %v, got %v", out, outputStr)
+		}
+	}
+}
+
 func putStringTest(in string, parser string, selector string, value string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
 	return putTest(in, "string", parser, selector, value, out, expErr, additionalArgs...)
 }