Feature: Adds merge command

Adds merge command for merging multiple yaml files together.

Resolves: #31
This commit is contained in:
kenjones 2017-09-22 23:29:37 -04:00 committed by Mike Farah
parent 2933ea1684
commit 6980be3800
11 changed files with 229 additions and 6 deletions

View File

@ -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.
``` ```

View File

@ -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)
}

View File

@ -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
View File

@ -0,0 +1,2 @@
a: simple
b: [1, 2]

3
examples/data2.yaml Normal file
View File

@ -0,0 +1,3 @@
a: other
c:
test: 1

5
examples/data3.yaml Normal file
View File

@ -0,0 +1,5 @@
b: [2, 3, 4]
c:
test: 2
other: true
d: false

12
merge.go Normal file
View 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
View 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)
}

View File

@ -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
View File

@ -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
View File

@ -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 != "" {