Skip to content

Commit da3f3b9

Browse files
committed
Added SETPATH operator
1 parent 7f4c8e1 commit da3f3b9

File tree

6 files changed

+174
-1
lines changed

6 files changed

+174
-1
lines changed

pkg/yqlib/doc/operators/path.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,59 @@ will output
9898
value: frog
9999
```
100100
101+
## Set path
102+
Given a sample.yml file of:
103+
```yaml
104+
a:
105+
b: cat
106+
```
107+
then
108+
```bash
109+
yq 'setpath(["a", "b"]; "things")' sample.yml
110+
```
111+
will output
112+
```yaml
113+
a:
114+
b: things
115+
```
116+
117+
## Set on empty document
118+
Running
119+
```bash
120+
yq --null-input 'setpath(["a", "b"]; "things")'
121+
```
122+
will output
123+
```yaml
124+
a:
125+
b: things
126+
```
127+
128+
## Set array path
129+
Given a sample.yml file of:
130+
```yaml
131+
a:
132+
- cat
133+
- frog
134+
```
135+
then
136+
```bash
137+
yq 'setpath(["a", 0]; "things")' sample.yml
138+
```
139+
will output
140+
```yaml
141+
a:
142+
- things
143+
- frog
144+
```
145+
146+
## Set array path empty
147+
Running
148+
```bash
149+
yq --null-input 'setpath(["a", 0]; "things")'
150+
```
151+
will output
152+
```yaml
153+
a:
154+
- things
155+
```
156+

pkg/yqlib/lexer_participle.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ var participleYqRules = []*participleYqRule{
128128
simpleOp("file_?name|fileName", getFilenameOpType),
129129
simpleOp("file_?index|fileIndex|fi", getFileIndexOpType),
130130
simpleOp("path", getPathOpType),
131+
simpleOp("set_?path", setPathOpType),
131132

132133
simpleOp("to_?entries|toEntries", toEntriesOpType),
133134
simpleOp("from_?entries|fromEntries", fromEntriesOpType),

pkg/yqlib/lib.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ var getAliasOpType = &operationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 5
130130
var getDocumentIndexOpType = &operationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: getDocumentIndexOperator}
131131
var getFilenameOpType = &operationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: getFilenameOperator}
132132
var getFileIndexOpType = &operationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: getFileIndexOperator}
133+
133134
var getPathOpType = &operationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: getPathOperator}
135+
var setPathOpType = &operationType{Type: "SET_PATH", NumArgs: 1, Precedence: 50, Handler: setPathOperator}
134136

135137
var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: explodeOperator}
136138
var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 50, Handler: sortByOperator}

pkg/yqlib/operator_path.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,90 @@ func createPathNodeFor(pathElement interface{}) *yaml.Node {
1616
}
1717
}
1818

19+
func getPathArrayFromExp(d *dataTreeNavigator, context Context, pathExp *ExpressionNode) ([]interface{}, error) {
20+
lhsPathContext, err := d.GetMatchingNodes(context.ReadOnlyClone(), pathExp)
21+
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
if lhsPathContext.MatchingNodes.Len() != 1 {
27+
return nil, fmt.Errorf("expected single path but found %v results instead", lhsPathContext.MatchingNodes.Len())
28+
}
29+
lhsValue := lhsPathContext.MatchingNodes.Front().Value.(*CandidateNode)
30+
if lhsValue.Node.Kind != yaml.SequenceNode {
31+
return nil, fmt.Errorf("expected path array, but got %v instead", lhsValue.Node.Tag)
32+
}
33+
34+
path := make([]interface{}, len(lhsValue.Node.Content))
35+
36+
for i, childNode := range lhsValue.Node.Content {
37+
if childNode.Tag == "!!str" {
38+
path[i] = childNode.Value
39+
} else if childNode.Tag == "!!int" {
40+
number, err := parseInt(childNode.Value)
41+
if err != nil {
42+
return nil, fmt.Errorf("could not parse %v as an int: %w", childNode.Value, err)
43+
}
44+
path[i] = number
45+
} else {
46+
return nil, fmt.Errorf("expected either a !!str or !!int in the path, found %v instead", childNode.Tag)
47+
}
48+
49+
}
50+
return path, nil
51+
}
52+
53+
// SETPATH(pathArray; value)
54+
func setPathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
55+
log.Debugf("SetPath")
56+
57+
if expressionNode.RHS.Operation.OperationType != blockOpType {
58+
return Context{}, fmt.Errorf("SETPATH must be given a block (;), got %v instead", expressionNode.RHS.Operation.OperationType.Type)
59+
}
60+
61+
lhsPath, err := getPathArrayFromExp(d, context, expressionNode.RHS.LHS)
62+
63+
if err != nil {
64+
return Context{}, err
65+
}
66+
67+
lhsTraversalTree := createTraversalTree(lhsPath, traversePreferences{}, false)
68+
69+
assignmentOp := &Operation{OperationType: assignOpType}
70+
71+
//TODO if context is empty, create a new one
72+
73+
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
74+
candidate := el.Value.(*CandidateNode)
75+
76+
targetContextValue, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.RHS.RHS)
77+
if err != nil {
78+
return Context{}, err
79+
}
80+
81+
if targetContextValue.MatchingNodes.Len() != 1 {
82+
return Context{}, fmt.Errorf("Expected single value on RHS but found %v", targetContextValue.MatchingNodes.Len())
83+
}
84+
85+
rhsOp := &Operation{OperationType: valueOpType, CandidateNode: targetContextValue.MatchingNodes.Front().Value.(*CandidateNode)}
86+
87+
assignmentOpNode := &ExpressionNode{
88+
Operation: assignmentOp,
89+
LHS: lhsTraversalTree,
90+
RHS: &ExpressionNode{Operation: rhsOp},
91+
}
92+
93+
_, err = d.GetMatchingNodes(context.SingleChildContext(candidate), assignmentOpNode)
94+
95+
if err != nil {
96+
return Context{}, err
97+
}
98+
99+
}
100+
return context, nil
101+
}
102+
19103
func getPathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
20104
log.Debugf("GetPath")
21105

pkg/yqlib/operator_path_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ var pathOperatorScenarios = []expressionScenario{
6363
"D0, P[a 2], (!!seq)::- path:\n - a\n - 2\n value: frog\n",
6464
},
6565
},
66+
{
67+
description: "Set path",
68+
document: `{a: {b: cat}}`,
69+
expression: `setpath(["a", "b"]; "things")`,
70+
expected: []string{
71+
"D0, P[], (doc)::{a: {b: things}}\n",
72+
},
73+
},
74+
{
75+
description: "Set on empty document",
76+
expression: `setpath(["a", "b"]; "things")`,
77+
expected: []string{
78+
"D0, P[], ()::a:\n b: things\n",
79+
},
80+
},
81+
{
82+
description: "Set array path",
83+
document: `a: [cat, frog]`,
84+
expression: `setpath(["a", 0]; "things")`,
85+
expected: []string{
86+
"D0, P[], (doc)::a: [things, frog]\n",
87+
},
88+
},
89+
{
90+
description: "Set array path empty",
91+
expression: `setpath(["a", 0]; "things")`,
92+
expected: []string{
93+
"D0, P[], ()::a:\n - things\n",
94+
},
95+
},
6696
}
6797

6898
func TestPathOperatorsScenarios(t *testing.T) {

pkg/yqlib/operator_with.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ func withOperator(d *dataTreeNavigator, context Context, expressionNode *Express
77
// with(path, exp)
88

99
if expressionNode.RHS.Operation.OperationType != blockOpType {
10-
return Context{}, fmt.Errorf("with must be given a block, got %v instead", expressionNode.RHS.Operation.OperationType.Type)
10+
return Context{}, fmt.Errorf("with must be given a block (;), got %v instead", expressionNode.RHS.Operation.OperationType.Type)
1111
}
1212

1313
pathExp := expressionNode.RHS.LHS

0 commit comments

Comments
 (0)