diff --git a/examples/merge-anchor.yml b/examples/merge-anchor.yml new file mode 100644 index 00000000..cbfcc0e6 --- /dev/null +++ b/examples/merge-anchor.yml @@ -0,0 +1,17 @@ +foo: &foo + a: original + thing: coolasdf + +bar: &bar + b: 2 + + +overrideA: + <<: [*foo,*bar] + a: vanilla + c: 3 + +foobar: + <<: *foo + thing: ice + c: 3 diff --git a/pkg/yqlib/data_navigator.go b/pkg/yqlib/data_navigator.go index e1493eb3..d8524749 100644 --- a/pkg/yqlib/data_navigator.go +++ b/pkg/yqlib/data_navigator.go @@ -96,7 +96,7 @@ func (n *navigator) Delete(rootNode *yaml.Node, path []string) error { // need to delete in reverse - otherwise the matching indexes // become incorrect. matchingIndices := make([]int, 0) - _, errorVisiting := n.visitMatchingEntries(nodeToUpdate.Content, lastBit, func(indexInMap int) error { + _, errorVisiting := n.visitMatchingEntries(nodeToUpdate.Content, lastBit, func(matchingNode []*yaml.Node, indexInMap int) error { matchingIndices = append(matchingIndices, indexInMap) n.log.Debug("matchingIndices %v", indexInMap) return nil @@ -153,6 +153,9 @@ func (n *navigator) GuessKind(tail []string, guess yaml.Kind) yaml.Kind { n.log.Debug("guess was an alias, okey doke.") return guess } + n.log.Debug("forcing a mapping node") + n.log.Debug("yaml.SequenceNode ?", guess == yaml.SequenceNode) + n.log.Debug("yaml.ScalarNode ?", guess == yaml.ScalarNode) return yaml.MappingNode } @@ -221,9 +224,9 @@ func (n *navigator) splatMap(value *yaml.Node, tail []string, visitor VisitorFn) } func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, visitor VisitorFn) error { - visited, errorVisiting := n.visitMatchingEntries(value.Content, head, func(indexInMap int) error { - value.Content[indexInMap+1] = n.getOrReplace(value.Content[indexInMap+1], n.GuessKind(tail, value.Content[indexInMap+1].Kind)) - return n.Visit(value.Content[indexInMap+1], tail, visitor) + visited, errorVisiting := n.visitMatchingEntries(value.Content, head, func(contents []*yaml.Node, indexInMap int) error { + contents[indexInMap+1] = n.getOrReplace(contents[indexInMap+1], n.GuessKind(tail, contents[indexInMap+1].Kind)) + return n.Visit(contents[indexInMap+1], tail, visitor) }) if errorVisiting != nil { @@ -242,18 +245,36 @@ func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, vis return n.Visit(&mapEntryValue, tail, visitor) } -type mapVisitorFn func(int) error +// need to pass the node in, as it may be aliased +type mapVisitorFn func([]*yaml.Node, int) error func (n *navigator) visitMatchingEntries(contents []*yaml.Node, key string, visit mapVisitorFn) (bool, error) { visited := false - + n.log.Debug("visitMatchingEntries %v in %v", key, contents) // value.Content is a concatenated array of key, value, // so keys are in the even indexes, values in odd. - for index := 0; index < len(contents); index = index + 2 { + // merge aliases are defined first, but we only want to traverse them + // if we dont find a match on this node first. + for index := len(contents) - 2; index >= 0; index = index - 2 { content := contents[index] - n.log.Debug("index %v, checking %v", index, content.Value)) + n.log.Debug("index %v, checking %v, %v", index, content.Value, content.Tag) + + // only visit aliases if we didn't find a match in this object. + if n.followAliases && !visited && contents[index+1].Kind == yaml.AliasNode { + valueNode := contents[index+1] + + n.log.Debug("need to visit the alias too") + n.DebugNode(valueNode) + visitedAlias, errorInAlias := n.visitMatchingEntries(valueNode.Alias.Content, key, visit) + if errorInAlias != nil { + return false, errorInAlias + } + visited = visited || visitedAlias + } + if n.matchesKey(key, content.Value) { - errorVisiting := visit(index) + n.log.Debug("found a match! %v", content.Value) + errorVisiting := visit(contents, index) if errorVisiting != nil { return visited, errorVisiting } diff --git a/test.yml b/test.yml index 71f1098c..c83b2dcf 100644 --- a/test.yml +++ b/test.yml @@ -1,2 +1,4 @@ -a: apple -c: cat +foo: &foo + a: 1 + +foobar: *foo \ No newline at end of file diff --git a/yq.go b/yq.go index d4998e69..14a862bc 100644 --- a/yq.go +++ b/yq.go @@ -79,7 +79,7 @@ func newCommandCLI() *cobra.Command { createPrefixCmd(), createDeleteCmd(), createNewCmd(), - // createMergeCmd(), + createMergeCmd(), ) rootCmd.SetOutput(os.Stdout) @@ -211,36 +211,36 @@ Note that you can give a create script to perform more sophisticated yaml. This 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 -// `, -// 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. +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 + `, + 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. +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. -// Note that if you set both flags only overwrite will take effect. -// `, -// 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(&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 -// } +Note that if you set both flags only overwrite will take effect. +`, + 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(&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 = "" @@ -397,6 +397,11 @@ func writeProperty(cmd *cobra.Command, args []string) error { return updateDoc(args[0], updateCommands, cmd.OutOrStdout()) } +func mergeProperties(cmd *cobra.Command, args []string) error { + // first generate update commands from the file + return nil +} + func newProperty(cmd *cobra.Command, args []string) error { var updateCommands, updateCommandsError = readUpdateCommands(args, 2, "Must provide ") if updateCommandsError != nil {