Added append equals, merge append. Fixed creating numeric arrays

This commit is contained in:
Mike Farah 2020-11-28 11:24:16 +11:00
parent 3f48201a19
commit 3a030651a3
17 changed files with 92 additions and 46 deletions

View File

@ -3,6 +3,31 @@ Add behaves differently according to the type of the LHS:
- number scalars: arithmetic addition (soon) - number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon) - string scalars: concatenate (soon)
Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`.
## Concatenate and assign arrays
Given a sample.yml file of:
```yaml
a:
val: thing
b:
- cat
- dog
```
then
```bash
yq eval '.a.b += ["cow"]' sample.yml
```
will output
```yaml
a:
val: thing
b:
- cat
- dog
- cow
```
## Concatenate arrays ## Concatenate arrays
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@ -148,7 +148,7 @@ Given a sample.yml file of:
``` ```
then then
```bash ```bash
yq eval '.a.b[0] |= "bogs"' sample.yml yq eval '.a.b.[0] |= "bogs"' sample.yml
``` ```
will output will output
```yaml ```yaml

View File

@ -2,6 +2,8 @@ Like the multiple operator in `jq`, depending on the operands, this multiply ope
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings). Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings).
To concatenate when merging objects, use the `*+` form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below. Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
## Merging files ## Merging files
@ -66,8 +68,8 @@ b:
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
a: {things: great} a: {things: great}
b: b:
also: "me" also: "me"
``` ```
then then
@ -76,6 +78,9 @@ yq eval '. * {"a":.b}' sample.yml
``` ```
will output will output
```yaml ```yaml
a: {things: great, also: "me"}
b:
also: "me"
``` ```
## Merge arrays ## Merge arrays

View File

@ -138,7 +138,7 @@ Given a sample.yml file of:
``` ```
then then
```bash ```bash
yq eval '[0]' sample.yml yq eval '.[0]' sample.yml
``` ```
will output will output
```yaml ```yaml
@ -152,7 +152,7 @@ Given a sample.yml file of:
``` ```
then then
```bash ```bash
yq eval '[2]' sample.yml yq eval '.[2]' sample.yml
``` ```
will output will output
```yaml ```yaml
@ -166,7 +166,7 @@ a: b
``` ```
then then
```bash ```bash
yq eval '[0]' sample.yml yq eval '.[0]' sample.yml
``` ```
will output will output
```yaml ```yaml

View File

@ -2,3 +2,5 @@ Add behaves differently according to the type of the LHS:
- arrays: concatenate - arrays: concatenate
- number scalars: arithmetic addition (soon) - number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon) - string scalars: concatenate (soon)
Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`.

View File

@ -2,6 +2,8 @@ Like the multiple operator in `jq`, depending on the operands, this multiply ope
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings). Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings).
To concatenate when merging objects, use the `*+` form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below. Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
## Merging files ## Merging files

View File

@ -31,6 +31,7 @@ var And = &OperationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: AndOp
var Union = &OperationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: UnionOperator} var Union = &OperationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: UnionOperator}
var Assign = &OperationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: AssignUpdateOperator} var Assign = &OperationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: AssignUpdateOperator}
var AddAssign = &OperationType{Type: "ADD_ASSIGN", NumArgs: 2, Precedence: 40, Handler: AddAssignOperator}
var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: AssignAttributesOperator} var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: AssignAttributesOperator}
var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator} var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator}

View File

@ -8,8 +8,18 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type AddPreferences struct { func createSelfAddOp(rhs *PathTreeNode) *PathTreeNode {
InPlace bool return &PathTreeNode{Operation: &Operation{OperationType: Add},
Lhs: &PathTreeNode{Operation: &Operation{OperationType: SelfReference}},
Rhs: rhs}
}
func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
assignmentOp := &Operation{OperationType: Assign}
assignmentOp.Preferences = &AssignOpPreferences{true}
assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createSelfAddOp(pathNode.Rhs)}
return d.GetMatchingNodes(matchingNodes, assignmentOpNode)
} }
func toNodes(candidates *list.List) []*yaml.Node { func toNodes(candidates *list.List) []*yaml.Node {
@ -45,24 +55,15 @@ func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
return nil, err return nil, err
} }
preferences := pathNode.Operation.Preferences
inPlace := preferences != nil && preferences.(*AddPreferences).InPlace
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode) lhsCandidate := el.Value.(*CandidateNode)
lhsNode := UnwrapDoc(lhsCandidate.Node) lhsNode := UnwrapDoc(lhsCandidate.Node)
var target *CandidateNode target := &CandidateNode{
Path: lhsCandidate.Path,
if inPlace { Document: lhsCandidate.Document,
target = lhsCandidate Filename: lhsCandidate.Filename,
} else { Node: &yaml.Node{},
target = &CandidateNode{
Path: lhsCandidate.Path,
Document: lhsCandidate.Document,
Filename: lhsCandidate.Filename,
Node: &yaml.Node{},
}
} }
switch lhsNode.Kind { switch lhsNode.Kind {

View File

@ -6,9 +6,12 @@ import (
var addOperatorScenarios = []expressionScenario{ var addOperatorScenarios = []expressionScenario{
{ {
description: "+= test and doc", description: "Concatenate and assign arrays",
expression: ".a.b+= .e.f" document: `{a: {val: thing, b: [cat,dog]}}`,
expected: []string{"add .e.g to be, return top level node"} expression: ".a.b += [\"cow\"]",
expected: []string{
"D0, P[], (doc)::{a: {val: thing, b: [cat, dog, cow]}}\n",
},
}, },
{ {
description: "Concatenate arrays", description: "Concatenate arrays",

View File

@ -99,7 +99,7 @@ var assignOperatorScenarios = []expressionScenario{
description: "Update empty object and array", description: "Update empty object and array",
dontFormatInputForDoc: true, dontFormatInputForDoc: true,
document: `{}`, document: `{}`,
expression: `.a.b[0] |= "bogs"`, expression: `.a.b.[0] |= "bogs"`,
expected: []string{ expected: []string{
"D0, P[], (doc)::{a: {b: [bogs]}}\n", "D0, P[], (doc)::{a: {b: [bogs]}}\n",
}, },
@ -107,7 +107,7 @@ var assignOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{}`, document: `{}`,
expression: `.a.b[1].c |= "bogs"`, expression: `.a.b.[1].c |= "bogs"`,
expected: []string{ expected: []string{
"D0, P[], (doc)::{a: {b: [null, {c: bogs}]}}\n", "D0, P[], (doc)::{a: {b: [null, {c: bogs}]}}\n",
}, },

View File

@ -13,6 +13,14 @@ var collectOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[]\n", "D0, P[], (!!seq)::[]\n",
}, },
}, },
{
skipDoc: true,
document: ``,
expression: `[3]`,
expected: []string{
"D0, P[], (!!seq)::- 3\n",
},
},
{ {
description: "Collect single", description: "Collect single",
document: ``, document: ``,

View File

@ -128,9 +128,7 @@ func applyAssignment(d *dataTreeNavigator, pathIndexToStartFrom int, lhs *Candid
assignmentOp.OperationType = Assign assignmentOp.OperationType = Assign
assignmentOp.Preferences = &AssignOpPreferences{false} assignmentOp.Preferences = &AssignOpPreferences{false}
} else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode { } else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode {
log.Debugf("append! lhs %v, rhs: %v", NodeToString(lhs), NodeToString(rhs)) assignmentOp.OperationType = AddAssign
assignmentOp.OperationType = Add
assignmentOp.Preferences = &AddPreferences{InPlace: true}
} }
rhsOp := &Operation{OperationType: ValueOp, CandidateNode: rhs} rhsOp := &Operation{OperationType: ValueOp, CandidateNode: rhs}

View File

@ -5,9 +5,6 @@ import (
) )
var multiplyOperatorScenarios = []expressionScenario{ var multiplyOperatorScenarios = []expressionScenario{
{
description: "*+ doc",
},
{ {
skipDoc: true, skipDoc: true,
document: `{a: {also: [1]}, b: {also: me}}`, document: `{a: {also: [1]}, b: {also: me}}`,
@ -83,15 +80,15 @@ var multiplyOperatorScenarios = []expressionScenario{
description: "Merge keeps style of LHS", description: "Merge keeps style of LHS",
dontFormatInputForDoc: true, dontFormatInputForDoc: true,
document: `a: {things: great} document: `a: {things: great}
b: b:
also: "me" also: "me"
`, `,
expression: `. * {"a":.b}`, expression: `. * {"a":.b}`,
expected: []string{ expected: []string{
`D0, P[], (!!map)::a: {things: great, also: "me"} `D0, P[], (!!map)::a: {things: great, also: "me"}
b: b:
also: "me" also: "me"
`, `,
}, },
}, },
{ {

View File

@ -159,7 +159,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Traversing arrays by index", description: "Traversing arrays by index",
document: `[1,2,3]`, document: `[1,2,3]`,
expression: `[0]`, expression: `.[0]`,
expected: []string{ expected: []string{
"D0, P[0], (!!int)::1\n", "D0, P[0], (!!int)::1\n",
}, },
@ -167,7 +167,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Maps with numeric keys", description: "Maps with numeric keys",
document: `{2: cat}`, document: `{2: cat}`,
expression: `[2]`, expression: `.[2]`,
expected: []string{ expected: []string{
"D0, P[2], (!!str)::cat\n", "D0, P[2], (!!str)::cat\n",
}, },
@ -175,7 +175,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Maps with non existing numeric keys", description: "Maps with non existing numeric keys",
document: `{a: b}`, document: `{a: b}`,
expression: `[0]`, expression: `.[0]`,
expected: []string{ expected: []string{
"D0, P[0], (!!null)::null\n", "D0, P[0], (!!null)::null\n",
}, },

View File

@ -37,7 +37,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
if s.document != "" { if s.document != "" {
inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml", 0) inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml", 0)
if err != nil { if err != nil {
t.Error(err) t.Error(err, s.document)
return return
} }
} }

View File

@ -49,6 +49,11 @@ var pathTests = []struct {
append(make([]interface{}, 0), "[", "]"), append(make([]interface{}, 0), "[", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "PIPE"), append(make([]interface{}, 0), "EMPTY", "COLLECT", "PIPE"),
}, },
{
`[3]`,
append(make([]interface{}, 0), "[", "3 (int64)", "]"),
append(make([]interface{}, 0), "3 (int64)", "COLLECT", "PIPE"),
},
{ {
`d0.a`, `d0.a`,
append(make([]interface{}, 0), "d0", "PIPE", "a"), append(make([]interface{}, 0), "d0", "PIPE", "a"),

View File

@ -223,7 +223,6 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\s*\|=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{true})) lexer.Add([]byte(`\s*\|=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{true}))
lexer.Add([]byte(`\[-?[0-9]+\]`), arrayIndextoken(false))
lexer.Add([]byte(`\.\[-?[0-9]+\]`), arrayIndextoken(true)) lexer.Add([]byte(`\.\[-?[0-9]+\]`), arrayIndextoken(true))
lexer.Add([]byte("( |\t|\n|\r)+"), skip) lexer.Add([]byte("( |\t|\n|\r)+"), skip)
@ -254,7 +253,7 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\*`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: false})) lexer.Add([]byte(`\*`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: false}))
lexer.Add([]byte(`\*\+`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: true})) lexer.Add([]byte(`\*\+`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: true}))
lexer.Add([]byte(`\+`), opToken(Add)) lexer.Add([]byte(`\+`), opToken(Add))
lexer.Add([]byte(`\+=`), opTokenWithPrefs(Add, nil, &AddPreferences{InPlace: true})) lexer.Add([]byte(`\+=`), opToken(AddAssign))
err := lexer.Compile() err := lexer.Compile()
if err != nil { if err != nil {