diff --git a/pkg/yqlib/doc/Add.md b/pkg/yqlib/doc/Add.md new file mode 100644 index 00000000..6eee9cc0 --- /dev/null +++ b/pkg/yqlib/doc/Add.md @@ -0,0 +1,107 @@ +Add behaves differently according to the type of the LHS: +- arrays: concatenate +- number scalars: arithmetic addition (soon) +- string scalars: concatenate (soon) +## Concatenate arrays +Given a sample.yml file of: +```yaml +a: + - 1 + - 2 +b: + - 3 + - 4 +``` +then +```bash +yq eval '.a + .b' sample.yml +``` +will output +```yaml +- 1 +- 2 +- 3 +- 4 +``` + +## Concatenate null to array +Given a sample.yml file of: +```yaml +a: + - 1 + - 2 +``` +then +```bash +yq eval '.a + null' sample.yml +``` +will output +```yaml +- 1 +- 2 +``` + +## Add object to array +Given a sample.yml file of: +```yaml +a: + - 1 + - 2 +c: + cat: meow +``` +then +```bash +yq eval '.a + .c' sample.yml +``` +will output +```yaml +- 1 +- 2 +- cat: meow +``` + +## Add string to array +Given a sample.yml file of: +```yaml +a: + - 1 + - 2 +``` +then +```bash +yq eval '.a + "hello"' sample.yml +``` +will output +```yaml +- 1 +- 2 +- hello +``` + +## Update array (append) +Given a sample.yml file of: +```yaml +a: + - 1 + - 2 +b: + - 3 + - 4 +``` +then +```bash +yq eval '.a = .a + .b' sample.yml +``` +will output +```yaml +a: + - 1 + - 2 + - 3 + - 4 +b: + - 3 + - 4 +``` + diff --git a/pkg/yqlib/doc/headers/Add.md b/pkg/yqlib/doc/headers/Add.md new file mode 100644 index 00000000..dd9f2a38 --- /dev/null +++ b/pkg/yqlib/doc/headers/Add.md @@ -0,0 +1,4 @@ +Add behaves differently according to the type of the LHS: +- arrays: concatenate +- number scalars: arithmetic addition (soon) +- string scalars: concatenate (soon) \ No newline at end of file diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 59fdace9..e9a393b8 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -36,7 +36,8 @@ var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 4 var AssignTag = &OperationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: AssignTagOperator} var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator} -var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 40, Handler: MultiplyOperator} +var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: MultiplyOperator} +var Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator} var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator} var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator} diff --git a/pkg/yqlib/operator_add.go b/pkg/yqlib/operator_add.go new file mode 100644 index 00000000..fad85ecd --- /dev/null +++ b/pkg/yqlib/operator_add.go @@ -0,0 +1,69 @@ +package yqlib + +import ( + "fmt" + + "container/list" + + yaml "gopkg.in/yaml.v3" +) + +func toNodes(candidates *list.List) []*yaml.Node { + + if candidates.Len() == 0 { + return []*yaml.Node{} + } + candidate := candidates.Front().Value.(*CandidateNode) + + if candidate.Node.Tag == "!!null" { + return []*yaml.Node{} + } + + switch candidate.Node.Kind { + case yaml.SequenceNode: + return candidate.Node.Content + default: + return []*yaml.Node{candidate.Node} + } + +} + +func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { + log.Debugf("Add operator") + var results = list.New() + lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) + if err != nil { + return nil, err + } + rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) + + if err != nil { + return nil, err + } + + for el := lhs.Front(); el != nil; el = el.Next() { + lhsCandidate := el.Value.(*CandidateNode) + lhsNode := UnwrapDoc(lhsCandidate.Node) + + var newBlank = &CandidateNode{ + Path: lhsCandidate.Path, + Document: lhsCandidate.Document, + Filename: lhsCandidate.Filename, + Node: &yaml.Node{}, + } + + switch lhsNode.Kind { + case yaml.MappingNode: + return nil, fmt.Errorf("Maps not yet supported for addition") + case yaml.SequenceNode: + newBlank.Node.Kind = yaml.SequenceNode + newBlank.Node.Style = lhsNode.Style + newBlank.Node.Tag = "!!seq" + newBlank.Node.Content = append(lhsNode.Content, toNodes(rhs)...) + results.PushBack(newBlank) + case yaml.ScalarNode: + return nil, fmt.Errorf("Scalars not yet supported for addition") + } + } + return results, nil +} diff --git a/pkg/yqlib/operator_add_test.go b/pkg/yqlib/operator_add_test.go new file mode 100644 index 00000000..8f4459ed --- /dev/null +++ b/pkg/yqlib/operator_add_test.go @@ -0,0 +1,55 @@ +package yqlib + +import ( + "testing" +) + +var addOperatorScenarios = []expressionScenario{ + { + description: "Concatenate arrays", + document: `{a: [1,2], b: [3,4]}`, + expression: `.a + .b`, + expected: []string{ + "D0, P[a], (!!seq)::[1, 2, 3, 4]\n", + }, + }, + { + description: "Concatenate null to array", + document: `{a: [1,2]}`, + expression: `.a + null`, + expected: []string{ + "D0, P[a], (!!seq)::[1, 2]\n", + }, + }, + { + description: "Add object to array", + document: `{a: [1,2], c: {cat: meow}}`, + expression: `.a + .c`, + expected: []string{ + "D0, P[a], (!!seq)::[1, 2, {cat: meow}]\n", + }, + }, + { + description: "Add string to array", + document: `{a: [1,2]}`, + expression: `.a + "hello"`, + expected: []string{ + "D0, P[a], (!!seq)::[1, 2, hello]\n", + }, + }, + { + description: "Update array (append)", + document: `{a: [1,2], b: [3,4]}`, + expression: `.a = .a + .b`, + expected: []string{ + "D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n", + }, + }, +} + +func TestAddOperatorScenarios(t *testing.T) { + for _, tt := range addOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "Add", addOperatorScenarios) +} diff --git a/pkg/yqlib/path_tokeniser.go b/pkg/yqlib/path_tokeniser.go index 9d5d3b99..9d0faba6 100644 --- a/pkg/yqlib/path_tokeniser.go +++ b/pkg/yqlib/path_tokeniser.go @@ -252,6 +252,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false)) lexer.Add([]byte(`\}`), literalToken(CloseCollectObject, true)) lexer.Add([]byte(`\*`), opToken(Multiply)) + lexer.Add([]byte(`\+`), opToken(Add)) err := lexer.Compile() if err != nil {