diff --git a/pkg/yqlib/doc/operators/path.md b/pkg/yqlib/doc/operators/path.md index 27ccf016..2d296d6e 100644 --- a/pkg/yqlib/doc/operators/path.md +++ b/pkg/yqlib/doc/operators/path.md @@ -98,3 +98,59 @@ will output value: frog ``` +## Set path +Given a sample.yml file of: +```yaml +a: + b: cat +``` +then +```bash +yq 'setpath(["a", "b"]; "things")' sample.yml +``` +will output +```yaml +a: + b: things +``` + +## Set on empty document +Running +```bash +yq --null-input 'setpath(["a", "b"]; "things")' +``` +will output +```yaml +a: + b: things +``` + +## Set array path +Given a sample.yml file of: +```yaml +a: + - cat + - frog +``` +then +```bash +yq 'setpath(["a", 0]; "things")' sample.yml +``` +will output +```yaml +a: + - things + - frog +``` + +## Set array path empty +Running +```bash +yq --null-input 'setpath(["a", 0]; "things")' +``` +will output +```yaml +a: + - things +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index 61454801..1e421cac 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -128,6 +128,7 @@ var participleYqRules = []*participleYqRule{ simpleOp("file_?name|fileName", getFilenameOpType), simpleOp("file_?index|fileIndex|fi", getFileIndexOpType), simpleOp("path", getPathOpType), + simpleOp("set_?path", setPathOpType), simpleOp("to_?entries|toEntries", toEntriesOpType), simpleOp("from_?entries|fromEntries", fromEntriesOpType), diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 0ff6e806..d0ee6c04 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -130,7 +130,9 @@ var getAliasOpType = &operationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 5 var getDocumentIndexOpType = &operationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: getDocumentIndexOperator} var getFilenameOpType = &operationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: getFilenameOperator} var getFileIndexOpType = &operationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: getFileIndexOperator} + var getPathOpType = &operationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: getPathOperator} +var setPathOpType = &operationType{Type: "SET_PATH", NumArgs: 1, Precedence: 50, Handler: setPathOperator} var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: explodeOperator} var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 50, Handler: sortByOperator} diff --git a/pkg/yqlib/operator_path.go b/pkg/yqlib/operator_path.go index 1c249b74..84a91ede 100644 --- a/pkg/yqlib/operator_path.go +++ b/pkg/yqlib/operator_path.go @@ -16,6 +16,90 @@ func createPathNodeFor(pathElement interface{}) *yaml.Node { } } +func getPathArrayFromExp(d *dataTreeNavigator, context Context, pathExp *ExpressionNode) ([]interface{}, error) { + lhsPathContext, err := d.GetMatchingNodes(context.ReadOnlyClone(), pathExp) + + if err != nil { + return nil, err + } + + if lhsPathContext.MatchingNodes.Len() != 1 { + return nil, fmt.Errorf("expected single path but found %v results instead", lhsPathContext.MatchingNodes.Len()) + } + lhsValue := lhsPathContext.MatchingNodes.Front().Value.(*CandidateNode) + if lhsValue.Node.Kind != yaml.SequenceNode { + return nil, fmt.Errorf("expected path array, but got %v instead", lhsValue.Node.Tag) + } + + path := make([]interface{}, len(lhsValue.Node.Content)) + + for i, childNode := range lhsValue.Node.Content { + if childNode.Tag == "!!str" { + path[i] = childNode.Value + } else if childNode.Tag == "!!int" { + number, err := parseInt(childNode.Value) + if err != nil { + return nil, fmt.Errorf("could not parse %v as an int: %w", childNode.Value, err) + } + path[i] = number + } else { + return nil, fmt.Errorf("expected either a !!str or !!int in the path, found %v instead", childNode.Tag) + } + + } + return path, nil +} + +// SETPATH(pathArray; value) +func setPathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("SetPath") + + if expressionNode.RHS.Operation.OperationType != blockOpType { + return Context{}, fmt.Errorf("SETPATH must be given a block (;), got %v instead", expressionNode.RHS.Operation.OperationType.Type) + } + + lhsPath, err := getPathArrayFromExp(d, context, expressionNode.RHS.LHS) + + if err != nil { + return Context{}, err + } + + lhsTraversalTree := createTraversalTree(lhsPath, traversePreferences{}, false) + + assignmentOp := &Operation{OperationType: assignOpType} + + //TODO if context is empty, create a new one + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + + targetContextValue, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.RHS.RHS) + if err != nil { + return Context{}, err + } + + if targetContextValue.MatchingNodes.Len() != 1 { + return Context{}, fmt.Errorf("Expected single value on RHS but found %v", targetContextValue.MatchingNodes.Len()) + } + + rhsOp := &Operation{OperationType: valueOpType, CandidateNode: targetContextValue.MatchingNodes.Front().Value.(*CandidateNode)} + + assignmentOpNode := &ExpressionNode{ + Operation: assignmentOp, + LHS: lhsTraversalTree, + RHS: &ExpressionNode{Operation: rhsOp}, + } + + _, err = d.GetMatchingNodes(context.SingleChildContext(candidate), assignmentOpNode) + + if err != nil { + return Context{}, err + } + + } + return context, nil +} + func getPathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { log.Debugf("GetPath") diff --git a/pkg/yqlib/operator_path_test.go b/pkg/yqlib/operator_path_test.go index faf06ce9..549a07f3 100644 --- a/pkg/yqlib/operator_path_test.go +++ b/pkg/yqlib/operator_path_test.go @@ -63,6 +63,36 @@ var pathOperatorScenarios = []expressionScenario{ "D0, P[a 2], (!!seq)::- path:\n - a\n - 2\n value: frog\n", }, }, + { + description: "Set path", + document: `{a: {b: cat}}`, + expression: `setpath(["a", "b"]; "things")`, + expected: []string{ + "D0, P[], (doc)::{a: {b: things}}\n", + }, + }, + { + description: "Set on empty document", + expression: `setpath(["a", "b"]; "things")`, + expected: []string{ + "D0, P[], ()::a:\n b: things\n", + }, + }, + { + description: "Set array path", + document: `a: [cat, frog]`, + expression: `setpath(["a", 0]; "things")`, + expected: []string{ + "D0, P[], (doc)::a: [things, frog]\n", + }, + }, + { + description: "Set array path empty", + expression: `setpath(["a", 0]; "things")`, + expected: []string{ + "D0, P[], ()::a:\n - things\n", + }, + }, } func TestPathOperatorsScenarios(t *testing.T) { diff --git a/pkg/yqlib/operator_with.go b/pkg/yqlib/operator_with.go index 92da7348..fd76ec65 100644 --- a/pkg/yqlib/operator_with.go +++ b/pkg/yqlib/operator_with.go @@ -7,7 +7,7 @@ func withOperator(d *dataTreeNavigator, context Context, expressionNode *Express // with(path, exp) if expressionNode.RHS.Operation.OperationType != blockOpType { - return Context{}, fmt.Errorf("with must be given a block, got %v instead", expressionNode.RHS.Operation.OperationType.Type) + return Context{}, fmt.Errorf("with must be given a block (;), got %v instead", expressionNode.RHS.Operation.OperationType.Type) } pathExp := expressionNode.RHS.LHS