diff --git a/pkg/yqlib/doc/operators/add.md b/pkg/yqlib/doc/operators/add.md index d7da2453..d145881e 100644 --- a/pkg/yqlib/doc/operators/add.md +++ b/pkg/yqlib/doc/operators/add.md @@ -4,10 +4,10 @@ Add behaves differently according to the type of the LHS: * arrays: concatenate * number scalars: arithmetic addition * string scalars: concatenate +* maps: shallow merge (use the multiply operator (`*`) to deeply merge) -Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. +Use `+=` as a relative append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. -Add is not (yet) supported for maps - however you can use merge `*` which will have a similar effect... ## Concatenate and assign arrays Given a sample.yml file of: @@ -267,6 +267,41 @@ will output cat ``` +## Add maps to shallow merge +Adding objects together shallow merges them. Use `*` to deeply merge. + +Given a sample.yml file of: +```yaml +a: + thing: + name: Astuff + value: x + a1: cool +b: + thing: + name: Bstuff + legs: 3 + b1: neat +``` +then +```bash +yq eval '.a += .b' sample.yml +``` +will output +```yaml +a: + thing: + name: Bstuff + legs: 3 + a1: cool + b1: neat +b: + thing: + name: Bstuff + legs: 3 + b1: neat +``` + ## Custom types: that are really strings When custom tags are encountered, yq will try to decode the underlying type. diff --git a/pkg/yqlib/doc/operators/headers/add.md b/pkg/yqlib/doc/operators/headers/add.md index 5a3b2040..4d8524a6 100644 --- a/pkg/yqlib/doc/operators/headers/add.md +++ b/pkg/yqlib/doc/operators/headers/add.md @@ -4,7 +4,7 @@ Add behaves differently according to the type of the LHS: * arrays: concatenate * number scalars: arithmetic addition * string scalars: concatenate +* maps: shallow merge (use the multiply operator (`*`) to deeply merge) -Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. +Use `+=` as a relative append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. -Add is not (yet) supported for maps - however you can use merge `*` which will have a similar effect... diff --git a/pkg/yqlib/operator_add.go b/pkg/yqlib/operator_add.go index 8470c61c..7d45168a 100644 --- a/pkg/yqlib/operator_add.go +++ b/pkg/yqlib/operator_add.go @@ -52,21 +52,19 @@ func add(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *Candida switch lhsNode.Kind { case yaml.MappingNode: - return nil, fmt.Errorf("maps not yet supported for addition") + addMaps(target, lhs, rhs) case yaml.SequenceNode: - target.Node.Kind = yaml.SequenceNode - target.Node.Style = lhsNode.Style - target.Node.Tag = lhsNode.Tag - target.Node.Content = append(lhsNode.Content, toNodes(rhs)...) + addSequences(target, lhs, rhs) case yaml.ScalarNode: if rhs.Node.Kind != yaml.ScalarNode { - return nil, fmt.Errorf("%v (%v) cannot be added to a 2%v", rhs.Node.Tag, rhs.Path, lhsNode.Tag) + return nil, fmt.Errorf("%v (%v) cannot be added to a %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag) } target.Node.Kind = yaml.ScalarNode target.Node.Style = lhsNode.Style - return addScalars(target, lhsNode, rhs.Node) + if err := addScalars(target, lhsNode, rhs.Node); err != nil { + return nil, err + } } - return target, nil } @@ -89,7 +87,7 @@ func guessTagFromCustomType(node *yaml.Node) string { return guessedTag } -func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*CandidateNode, error) { +func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) error { lhsTag := lhs.Tag rhsTag := rhs.Tag lhsIsCustom := false @@ -110,11 +108,11 @@ func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*Candida } else if lhsTag == "!!int" && rhsTag == "!!int" { format, lhsNum, err := parseInt(lhs.Value) if err != nil { - return nil, err + return err } _, rhsNum, err := parseInt(rhs.Value) if err != nil { - return nil, err + return err } sum := lhsNum + rhsNum target.Node.Tag = lhs.Tag @@ -122,11 +120,11 @@ func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*Candida } else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") { lhsNum, err := strconv.ParseFloat(lhs.Value, 64) if err != nil { - return nil, err + return err } rhsNum, err := strconv.ParseFloat(rhs.Value, 64) if err != nil { - return nil, err + return err } sum := lhsNum + rhsNum if lhsIsCustom { @@ -136,8 +134,42 @@ func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*Candida } target.Node.Value = fmt.Sprintf("%v", sum) } else { - return nil, fmt.Errorf("%v cannot be added to %v", lhsTag, rhsTag) + return fmt.Errorf("%v cannot be added to %v", lhsTag, rhsTag) } - - return target, nil + return nil +} + +func addSequences(target *CandidateNode, lhs *CandidateNode, rhs *CandidateNode) { + target.Node.Kind = yaml.SequenceNode + target.Node.Style = lhs.Node.Style + target.Node.Tag = lhs.Node.Tag + target.Node.Content = make([]*yaml.Node, len(lhs.Node.Content)) + copy(target.Node.Content, lhs.Node.Content) + target.Node.Content = append(target.Node.Content, toNodes(rhs)...) +} + +func addMaps(target *CandidateNode, lhsC *CandidateNode, rhsC *CandidateNode) { + lhs := lhsC.Node + rhs := rhsC.Node + + target.Node.Content = make([]*yaml.Node, len(lhs.Content)) + copy(target.Node.Content, lhs.Content) + + for index := 0; index < len(rhs.Content); index = index + 2 { + key := rhs.Content[index] + value := rhs.Content[index+1] + log.Debug("finding %v", key.Value) + indexInLhs := findInArray(target.Node, key) + log.Debug("indexInLhs %v", indexInLhs) + if indexInLhs < 0 { + // not in there, append it + target.Node.Content = append(target.Node.Content, key, value) + } else { + // it's there, replace it + target.Node.Content[indexInLhs+1] = value + } + } + target.Node.Kind = yaml.MappingNode + target.Node.Style = lhs.Style + target.Node.Tag = lhs.Tag } diff --git a/pkg/yqlib/operator_add_test.go b/pkg/yqlib/operator_add_test.go index df1a605f..65b56486 100644 --- a/pkg/yqlib/operator_add_test.go +++ b/pkg/yqlib/operator_add_test.go @@ -144,6 +144,15 @@ var addOperatorScenarios = []expressionScenario{ "D0, P[], (!!str)::cat\n", }, }, + { + description: "Add maps to shallow merge", + subdescription: "Adding objects together shallow merges them. Use `*` to deeply merge.", + document: "a: {thing: {name: Astuff, value: x}, a1: cool}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}", + expression: `.a += .b`, + expected: []string{ + "D0, P[], (doc)::a: {thing: {name: Bstuff, legs: 3}, a1: cool, b1: neat}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}\n", + }, + }, { description: "Custom types: that are really strings", subdescription: "When custom tags are encountered, yq will try to decode the underlying type.",