mirror of
https://github.com/mikefarah/yq.git
synced 2024-12-19 20:19:04 +00:00
Feature: Adds merge command
Adds merge command for merging multiple yaml files together. Resolves: #31
This commit is contained in:
parent
2933ea1684
commit
6980be3800
14
README.md
14
README.md
@ -20,6 +20,8 @@ go get github.com/mikefarah/yaml
|
|||||||
- Convert from json to yaml
|
- Convert from json to yaml
|
||||||
- Convert from yaml to json
|
- Convert from yaml to json
|
||||||
- Pipe data in by using '-'
|
- Pipe data in by using '-'
|
||||||
|
- Merge multiple yaml files where each additional file sets values for missing or null value keys.
|
||||||
|
- Merge multiple yaml files with overwrite to support overriding previous values.
|
||||||
|
|
||||||
## [Usage](http://mikefarah.github.io/yaml/)
|
## [Usage](http://mikefarah.github.io/yaml/)
|
||||||
|
|
||||||
@ -30,15 +32,17 @@ Usage:
|
|||||||
yaml [command]
|
yaml [command]
|
||||||
|
|
||||||
Available Commands:
|
Available Commands:
|
||||||
|
help Help about any command
|
||||||
|
merge yaml m [--inplace/-i] [--overwrite/-x] sample.yaml sample2.yaml
|
||||||
|
new yaml n [--script/-s script_file] a.b.c newValueForC
|
||||||
read yaml r sample.yaml a.b.c
|
read yaml r sample.yaml a.b.c
|
||||||
write yaml w [--inplace/-i] [--script/-s script_file] sample.yaml a.b.c newValueForC
|
write yaml w [--inplace/-i] [--script/-s script_file] sample.yaml a.b.c newValueForC
|
||||||
new yaml n [--script/-s script_file] a.b.c newValueForC
|
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-h, --help[=false]: help for yaml
|
-h, --help help for yaml
|
||||||
-j, --tojson[=false]: output as json
|
-j, --tojson output as json
|
||||||
-t, --trim[=true]: trim yaml output
|
-t, --trim trim yaml output (default true)
|
||||||
-v, --verbose[=false]: verbose mode
|
-v, --verbose verbose mode
|
||||||
|
|
||||||
Use "yaml [command] --help" for more information about a command.
|
Use "yaml [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
@ -303,3 +303,74 @@ func TestWriteCmd_Inplace(t *testing.T) {
|
|||||||
c: 7`
|
c: 7`
|
||||||
assertResult(t, expectedOutput, gotOutput)
|
assertResult(t, expectedOutput, gotOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeCmd(t *testing.T) {
|
||||||
|
cmd := getRootCommand()
|
||||||
|
result := runCmd(cmd, "merge examples/data1.yaml examples/data2.yaml")
|
||||||
|
if result.Error != nil {
|
||||||
|
t.Error(result.Error)
|
||||||
|
}
|
||||||
|
expectedOutput := `a: simple
|
||||||
|
b:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
c:
|
||||||
|
test: 1
|
||||||
|
`
|
||||||
|
assertResult(t, expectedOutput, result.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCmd_Error(t *testing.T) {
|
||||||
|
cmd := getRootCommand()
|
||||||
|
result := runCmd(cmd, "merge examples/data1.yaml")
|
||||||
|
if result.Error == nil {
|
||||||
|
t.Error("Expected command to fail due to missing arg")
|
||||||
|
}
|
||||||
|
expectedOutput := `Must provide at least 2 yaml files`
|
||||||
|
assertResult(t, expectedOutput, result.Error.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCmd_ErrorUnreadableFile(t *testing.T) {
|
||||||
|
cmd := getRootCommand()
|
||||||
|
result := runCmd(cmd, "merge examples/data1.yaml fake-unknown")
|
||||||
|
if result.Error == nil {
|
||||||
|
t.Error("Expected command to fail due to unknown file")
|
||||||
|
}
|
||||||
|
expectedOutput := `open fake-unknown: no such file or directory`
|
||||||
|
assertResult(t, expectedOutput, result.Error.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCmd_Verbose(t *testing.T) {
|
||||||
|
cmd := getRootCommand()
|
||||||
|
result := runCmd(cmd, "-v merge examples/data1.yaml examples/data2.yaml")
|
||||||
|
if result.Error != nil {
|
||||||
|
t.Error(result.Error)
|
||||||
|
}
|
||||||
|
expectedOutput := `a: simple
|
||||||
|
b:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
c:
|
||||||
|
test: 1
|
||||||
|
`
|
||||||
|
assertResult(t, expectedOutput, result.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCmd_Inplace(t *testing.T) {
|
||||||
|
filename := writeTempYamlFile(readTempYamlFile("examples/data1.yaml"))
|
||||||
|
defer removeTempYamlFile(filename)
|
||||||
|
|
||||||
|
cmd := getRootCommand()
|
||||||
|
result := runCmd(cmd, fmt.Sprintf("merge -i %s examples/data2.yaml", filename))
|
||||||
|
if result.Error != nil {
|
||||||
|
t.Error(result.Error)
|
||||||
|
}
|
||||||
|
gotOutput := readTempYamlFile(filename)
|
||||||
|
expectedOutput := `a: simple
|
||||||
|
b:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
c:
|
||||||
|
test: 1`
|
||||||
|
assertResult(t, expectedOutput, gotOutput)
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
@ -175,3 +176,38 @@ func calculateValue(value interface{}, tail []string) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapToMapSlice(data map[interface{}]interface{}) yaml.MapSlice {
|
||||||
|
var mapSlice yaml.MapSlice
|
||||||
|
|
||||||
|
for k, v := range data {
|
||||||
|
if mv, ok := v.(map[interface{}]interface{}); ok {
|
||||||
|
v = mapToMapSlice(mv)
|
||||||
|
}
|
||||||
|
item := yaml.MapItem{Key: k, Value: v}
|
||||||
|
mapSlice = append(mapSlice, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// because the parsing of the yaml was done via a map the order will be inconsistent
|
||||||
|
// apply order to allow a consistent output
|
||||||
|
// Only available in Go 1.8+
|
||||||
|
// sort.SliceStable(mapSlice, func(i, j int) bool { return mapSlice[i].Key < mapSlice[j].Key })
|
||||||
|
sort.Sort(sortMap{mapSlice})
|
||||||
|
return mapSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortMap struct {
|
||||||
|
ms yaml.MapSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m sortMap) Len() int {
|
||||||
|
return len(m.ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m sortMap) Less(i, j int) bool {
|
||||||
|
return m.ms[i].Key.(string) < m.ms[j].Key.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m sortMap) Swap(i, j int) {
|
||||||
|
m.ms[i], m.ms[j] = m.ms[j], m.ms[i]
|
||||||
|
}
|
||||||
|
2
examples/data1.yaml
Normal file
2
examples/data1.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
a: simple
|
||||||
|
b: [1, 2]
|
3
examples/data2.yaml
Normal file
3
examples/data2.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
a: other
|
||||||
|
c:
|
||||||
|
test: 1
|
5
examples/data3.yaml
Normal file
5
examples/data3.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
b: [2, 3, 4]
|
||||||
|
c:
|
||||||
|
test: 2
|
||||||
|
other: true
|
||||||
|
d: false
|
12
merge.go
Normal file
12
merge.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func merge(dst, src interface{}, overwrite bool) error {
|
||||||
|
if overwrite {
|
||||||
|
return mergo.MergeWithOverwrite(dst, src)
|
||||||
|
}
|
||||||
|
return mergo.Merge(dst, src)
|
||||||
|
}
|
30
merge_test.go
Normal file
30
merge_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
result, _ := mergeYaml([]string{"examples/data1.yaml", "examples/data2.yaml", "examples/data3.yaml"})
|
||||||
|
expected := yaml.MapSlice{
|
||||||
|
yaml.MapItem{Key: "a", Value: "simple"},
|
||||||
|
yaml.MapItem{Key: "b", Value: []interface{}{1, 2}},
|
||||||
|
yaml.MapItem{Key: "c", Value: yaml.MapSlice{yaml.MapItem{Key: "other", Value: true}, yaml.MapItem{Key: "test", Value: 1}}},
|
||||||
|
yaml.MapItem{Key: "d", Value: false},
|
||||||
|
}
|
||||||
|
assertResultComplex(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeWithOverwrite(t *testing.T) {
|
||||||
|
overwriteFlag = true
|
||||||
|
result, _ := mergeYaml([]string{"examples/data1.yaml", "examples/data2.yaml", "examples/data3.yaml"})
|
||||||
|
expected := yaml.MapSlice{
|
||||||
|
yaml.MapItem{Key: "a", Value: "other"},
|
||||||
|
yaml.MapItem{Key: "b", Value: []interface{}{2, 3, 4}},
|
||||||
|
yaml.MapItem{Key: "c", Value: yaml.MapSlice{yaml.MapItem{Key: "other", Value: true}, yaml.MapItem{Key: "test", Value: 2}}},
|
||||||
|
yaml.MapItem{Key: "d", Value: false},
|
||||||
|
}
|
||||||
|
assertResultComplex(t, expected, result)
|
||||||
|
}
|
@ -5,4 +5,5 @@ set -e
|
|||||||
govendor fetch github.com/op/go-logging
|
govendor fetch github.com/op/go-logging
|
||||||
govendor fetch github.com/spf13/cobra
|
govendor fetch github.com/spf13/cobra
|
||||||
govendor fetch gopkg.in/yaml.v2
|
govendor fetch gopkg.in/yaml.v2
|
||||||
|
govendor fetch github.com/imdario/mergo
|
||||||
govendor sync
|
govendor sync
|
||||||
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -2,6 +2,12 @@
|
|||||||
"comment": "",
|
"comment": "",
|
||||||
"ignore": "test",
|
"ignore": "test",
|
||||||
"package": [
|
"package": [
|
||||||
|
{
|
||||||
|
"checksumSHA1": "66lykxpWgSmQodnhkADqn6tnroQ=",
|
||||||
|
"path": "github.com/imdario/mergo",
|
||||||
|
"revision": "e3000cb3d28c72b837601cac94debd91032d19fe",
|
||||||
|
"revisionTime": "2017-06-20T10:47:01Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "40vJyUB4ezQSn/NSadsKEOrudMc=",
|
"checksumSHA1": "40vJyUB4ezQSn/NSadsKEOrudMc=",
|
||||||
"path": "github.com/inconshreveable/mousetrap",
|
"path": "github.com/inconshreveable/mousetrap",
|
||||||
|
55
yaml.go
55
yaml.go
@ -17,6 +17,7 @@ var trimOutput = true
|
|||||||
var writeInplace = false
|
var writeInplace = false
|
||||||
var writeScript = ""
|
var writeScript = ""
|
||||||
var outputToJSON = false
|
var outputToJSON = false
|
||||||
|
var overwriteFlag = false
|
||||||
var verbose = false
|
var verbose = false
|
||||||
var log = logging.MustGetLogger("yaml")
|
var log = logging.MustGetLogger("yaml")
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ func newCommandCLI() *cobra.Command {
|
|||||||
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
|
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
|
||||||
|
|
||||||
rootCmd.AddCommand(createReadCmd(), createWriteCmd(), createNewCmd())
|
rootCmd.AddCommand(createReadCmd(), createWriteCmd(), createNewCmd(), createMergeCmd())
|
||||||
rootCmd.SetOutput(os.Stdout)
|
rootCmd.SetOutput(os.Stdout)
|
||||||
|
|
||||||
return rootCmd
|
return rootCmd
|
||||||
@ -127,6 +128,30 @@ Note that you can give a create script to perform more sophisticated yaml. This
|
|||||||
return cmdNew
|
return cmdNew
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createMergeCmd() *cobra.Command {
|
||||||
|
var cmdMerge = &cobra.Command{
|
||||||
|
Use: "merge [initial_yaml_file] [additional_yaml_file]...",
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
Short: "yaml m [--inplace/-i] [--overwrite/-x] sample.yaml sample2.yaml",
|
||||||
|
Example: `
|
||||||
|
yaml merge things.yaml other.yaml
|
||||||
|
yaml merge --inplace things.yaml other.yaml
|
||||||
|
yaml m -i things.yaml other.yaml
|
||||||
|
yaml m --overwrite things.yaml other.yaml
|
||||||
|
yaml m -i -x 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.
|
||||||
|
`,
|
||||||
|
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")
|
||||||
|
return cmdMerge
|
||||||
|
}
|
||||||
|
|
||||||
func readProperty(cmd *cobra.Command, args []string) error {
|
func readProperty(cmd *cobra.Command, args []string) error {
|
||||||
data, err := read(args)
|
data, err := read(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -233,6 +258,34 @@ func write(cmd *cobra.Command, filename string, updatedData interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mergeProperties(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return errors.New("Must provide at least 2 yaml files")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedData, err := mergeYaml(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return write(cmd, args[0], updatedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeYaml(args []string) (interface{}, error) {
|
||||||
|
var updatedData map[interface{}]interface{}
|
||||||
|
|
||||||
|
for _, f := range args {
|
||||||
|
var parsedData map[interface{}]interface{}
|
||||||
|
if err := readData(f, &parsedData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := merge(&updatedData, parsedData, overwriteFlag); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToMapSlice(updatedData), nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, prependCommand string) (interface{}, error) {
|
func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, prependCommand string) (interface{}, error) {
|
||||||
var prefix = ""
|
var prefix = ""
|
||||||
if prependCommand != "" {
|
if prependCommand != "" {
|
||||||
|
Loading…
Reference in New Issue
Block a user