Repository: mikefarah/yq
Pull request #318 (2020-01-05 22:51:21)

Allow merge commands to specify overwrite and append simultaneously
Mergo allows specifying `WithOverride` and `WithAppendSlice`, resulting in slices being merged and scalars being replaced. `yqlib.Merge` currently only allows one or the other.

mikefarah: Thank @cjnosal - I'm actually working on v3 of this which is a significant rewrite, I've taken your changes and ported them across ;)

Patch:
diff --git a/README.md b/README.md
index d8098ad5..63664bc4 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,21 @@ a lightweight and portable command-line YAML processor
 
 The aim of the project is to be the [jq](https://github.com/stedolan/jq) or sed of yaml files.
 
+
+## Major upgrade - V3 beta is out! 
+
+This addresses a number of features requests and issues that have been raised :)
+
+Currently only available only available as a [binary release here](https://github.com/mikefarah/yq/releases/tag/3.0.0-beta) or via docker mikefarah/yq:3.0.0-beta!
+
+It does have a few breaking changes listed on the [release page](https://github.com/mikefarah/yq/releases/tag/3.0.0-beta)
+
+Looking forward to feedback - once this is out of beta it will be added to the remaining package managers, and be the default version downloaded (and merged into master).
+
+V2 will no longer have any new features added, and will be moved to a branch (v2). It will have limited maintenance for bugs for a few months.
+
 ## Install
+
 ### On MacOS:
 ```
 brew install yq
@@ -21,16 +35,16 @@ snap install yq
 `yq` installs with with [_strict confinement_](https://docs.snapcraft.io/snap-confinement/6233) in snap, this means it doesn't have direct access to root files. To read root files you can:
 
 ```
-sudo cat /etc/myfile | yq -r - somecommand
+sudo cat /etc/myfile | yq r - a.path
 ```
 
 And to write to a root file you can either use [sponge](https://linux.die.net/man/1/sponge):
 ```
-sudo cat /etc/myfile | yq -r - somecommand | sudo sponge /etc/myfile
+sudo cat /etc/myfile | yq w - a.path value | sudo sponge /etc/myfile
 ```
 or write to a temporary file:
 ```
-sudo cat /etc/myfile | yq -r - somecommand | sudo tee /etc/myfile.tmp
+sudo cat /etc/myfile | yq w - a.path value | sudo tee /etc/myfile.tmp
 sudo mv /etc/myfile.tmp /etc/myfile
 rm /etc/myfile.tmp
 ```
diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go
index 8d330063..f61c37a2 100644
--- a/pkg/yqlib/lib.go
+++ b/pkg/yqlib/lib.go
@@ -58,11 +58,13 @@ func (l *lib) DeletePath(dataBucket interface{}, path string) (interface{}, erro
 	return l.navigator.DeleteChildValue(dataBucket, paths)
 }
 
-func (l *lib) Merge(dst interface{}, src interface{}, overwrite bool, append bool) error {
-	if overwrite {
-		return mergo.Merge(dst, src, mergo.WithOverride)
-	} else if append {
-		return mergo.Merge(dst, src, mergo.WithAppendSlice)
+func (l *lib) Merge(dst interface{}, src interface{}, overwriteFlag bool, appendFlag bool) error {
+	opts := []func(*mergo.Config){}
+	if overwriteFlag {
+		opts = append(opts, mergo.WithOverride)
 	}
-	return mergo.Merge(dst, src)
+	if appendFlag {
+		opts = append(opts, mergo.WithAppendSlice)
+	}
+	return mergo.Merge(dst, src, opts...)
 }
diff --git a/pkg/yqlib/value_parser.go b/pkg/yqlib/value_parser.go
index d08da020..0fb515f9 100644
--- a/pkg/yqlib/value_parser.go
+++ b/pkg/yqlib/value_parser.go
@@ -18,9 +18,16 @@ func (v *valueParser) ParseValue(argument string) interface{} {
 	var value, err interface{}
 	var inQuotes = len(argument) > 0 && argument[0] == '"'
 	if !inQuotes {
-		value, err = strconv.ParseFloat(argument, 64)
-		if err == nil {
-			return value
+		intValue, intErr := strconv.ParseInt(argument, 10, 64)
+		floatValue, floatErr := strconv.ParseFloat(argument, 64)
+		if intErr == nil && floatErr == nil {
+			if int64(floatValue) == intValue {
+				return intValue
+			}
+			return floatValue
+		} else if floatErr == nil {
+			// In case cannot parse the int due to large precision
+			return floatValue
 		}
 		value, err = strconv.ParseBool(argument)
 		if err == nil {
diff --git a/yq.go b/yq.go
index d24d44c0..31154110 100644
--- a/yq.go
+++ b/yq.go
@@ -25,6 +25,7 @@ var writeInplace = false
 var writeScript = ""
 var outputToJSON = false
 var overwriteFlag = false
+var keyOnlyFlag = false
 var allowEmptyFlag = false
 var appendFlag = false
 var verbose = false
@@ -111,6 +112,7 @@ yq r -- things.yaml --key-starting-with-dashes
 	}
 	cmdRead.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
 	cmdRead.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
+	cmdRead.PersistentFlags().BoolVarP(&keyOnlyFlag, "keyonly", "k", false, "output with top level keys only")
 	return cmdRead
 }
 
@@ -304,6 +306,13 @@ func readProperty(cmd *cobra.Command, args []string) error {
 		dataBucket = mappedDocs
 	}
 
+	if keyOnlyFlag {
+		for _, value := range dataBucket.(yaml.MapSlice) {
+			cmd.Println(value.Key)
+		}
+		return nil
+	}
+
 	dataStr, err := toString(dataBucket)
 	if err != nil {
 		return err


Test patch:
diff --git a/commands_test.go b/commands_test.go
index 90409cb8..7b547650 100644
--- a/commands_test.go
+++ b/commands_test.go
@@ -1091,6 +1091,25 @@ c:
 `
 	test.AssertResult(t, expectedOutput, result.Output)
 }
+
+func TestMergeOverwriteAndAppendCmd(t *testing.T) {
+	cmd := getRootCommand()
+	result := test.RunCmd(cmd, "merge --append --overwrite examples/data1.yaml examples/data2.yaml")
+	if result.Error != nil {
+		t.Error(result.Error)
+	}
+	expectedOutput := `a: other
+b:
+- 1
+- 2
+- 3
+- 4
+c:
+  test: 1
+`
+	test.AssertResult(t, expectedOutput, result.Output)
+}
+
 func TestMergeArraysCmd(t *testing.T) {
 	cmd := getRootCommand()
 	result := test.RunCmd(cmd, "merge --append examples/sample_array.yaml examples/sample_array_2.yaml")
diff --git a/pkg/yqlib/lib_test.go b/pkg/yqlib/lib_test.go
index bcfac5ff..d28acb9e 100644
--- a/pkg/yqlib/lib_test.go
+++ b/pkg/yqlib/lib_test.go
@@ -156,6 +156,23 @@ b: 2
 		test.AssertResult(t, `[{a b} {c d} {a 1} {b 2}]`, fmt.Sprintf("%v", mergedData["root"]))
 	})
 
+	t.Run("TestMerge_WithAppendAndOverwrite", func(t *testing.T) {
+		var dst = map[interface{}]interface{}{
+			"a": "initial",
+			"b": []string{"old"},
+		}
+		var src = map[interface{}]interface{}{
+			"a": "replaced",
+			"b": []string{"new"},
+		}
+
+		err := subject.Merge(&dst, src, true, true)
+		if err != nil {
+			t.Fatal("Unexpected error")
+		}
+		test.AssertResult(t, `map[a:replaced b:[old new]]`, fmt.Sprintf("%v", dst))
+	})
+
 	t.Run("TestMerge_WithError", func(t *testing.T) {
 		err := subject.Merge(nil, nil, false, false)
 		if err == nil {
diff --git a/pkg/yqlib/value_parser_test.go b/pkg/yqlib/value_parser_test.go
index 2246b398..1533b2be 100644
--- a/pkg/yqlib/value_parser_test.go
+++ b/pkg/yqlib/value_parser_test.go
@@ -16,6 +16,7 @@ var parseValueTests = []struct {
 	{"3.4", 3.4, "number"},
 	{"\"3.4\"", "3.4", "number as string"},
 	{"", "", "empty string"},
+	{"1212121", int64(1212121), "big number"},
 }
 
 func TestParseValue(t *testing.T) {
diff --git a/test/fixture/keyonly.yaml b/test/fixture/keyonly.yaml
new file mode 100644
index 00000000..f2dd3309
--- /dev/null
+++ b/test/fixture/keyonly.yaml
@@ -0,0 +1,6 @@
+a:
+  b:
+    c: 1
+  d:
+    e: 2
+  f:
diff --git a/yq_test.go b/yq_test.go
index 677f7bd0..2379adb9 100644
--- a/yq_test.go
+++ b/yq_test.go
@@ -1,12 +1,14 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
 	"runtime"
 	"testing"
 
 	"github.com/mikefarah/yq/v2/pkg/marshal"
 	"github.com/mikefarah/yq/v2/test"
+	"github.com/spf13/cobra"
 )
 
 func TestMultilineString(t *testing.T) {
@@ -33,6 +35,14 @@ func TestNewYamlArray(t *testing.T) {
 		formattedResult)
 }
 
+func TestNewYamlBigInt(t *testing.T) {
+	result, _ := newYaml([]string{"b", "1212121"})
+	formattedResult := fmt.Sprintf("%v", result)
+	test.AssertResult(t,
+		"[{b 1212121}]",
+		formattedResult)
+}
+
 func TestNewYaml_WithScript(t *testing.T) {
 	writeScript = "examples/instruction_sample.yaml"
 	expectedResult := `b:
@@ -58,3 +68,27 @@ func TestNewYaml_WithUnknownScript(t *testing.T) {
 	}
 	test.AssertResult(t, expectedOutput, err.Error())
 }
+
+func TestReadWithKeyOnly(t *testing.T) {
+	readCmd := createReadCmd()
+	expectedResult := `b
+d
+f
+`
+	actualResult, err := executeTestCommand(readCmd, "test/fixture/keyonly.yaml", "a", "-k")
+	if err != nil {
+		t.Error(err.Error())
+	}
+	test.AssertResult(t, expectedResult, actualResult)
+}
+
+func executeTestCommand(command *cobra.Command, args ...string) (output string, err error) {
+	buf := new(bytes.Buffer)
+
+	command.SetOutput(buf)
+	command.SetArgs(args)
+
+	_, err = command.ExecuteC()
+
+	return buf.String(), err
+}