From d21c94cf4f2b5b07f83ed5cffa7a7dbe3e3c0ffb Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Thu, 14 Jan 2021 14:46:50 +1100 Subject: [PATCH] Added join strings operator --- pkg/yqlib/doc/String Operators.md | 19 ++++++++++++ pkg/yqlib/expression_tokeniser.go | 2 ++ pkg/yqlib/lib.go | 1 + pkg/yqlib/operator_strings.go | 50 ++++++++++++++++++++++++++++++ pkg/yqlib/operator_strings_test.go | 23 ++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 pkg/yqlib/doc/String Operators.md create mode 100644 pkg/yqlib/operator_strings.go create mode 100644 pkg/yqlib/operator_strings_test.go diff --git a/pkg/yqlib/doc/String Operators.md b/pkg/yqlib/doc/String Operators.md new file mode 100644 index 00000000..a3e73fb2 --- /dev/null +++ b/pkg/yqlib/doc/String Operators.md @@ -0,0 +1,19 @@ + +## Join strings +Given a sample.yml file of: +```yaml +- cat +- meow +- 1 +- null +- true +``` +then +```bash +yq eval 'join("; ")' sample.yml +``` +will output +```yaml +cat; meow; 1; ; true +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 5339bd4f..4b6e58eb 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -243,6 +243,8 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`di`), opToken(getDocumentIndexOpType)) lexer.Add([]byte(`splitDoc`), opToken(splitDocumentOpType)) + lexer.Add([]byte(`join`), opToken(joinStringOpType)) + lexer.Add([]byte(`style`), opAssignableToken(getStyleOpType, assignStyleOpType)) lexer.Add([]byte(`tag`), opAssignableToken(getTagOpType, assignTagOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index f5f9428f..abcdeecf 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -64,6 +64,7 @@ var getPathOpType = &operationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: explodeOperator} var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator} +var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator} var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator} var traversePathOpType = &operationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: traversePathOperator} diff --git a/pkg/yqlib/operator_strings.go b/pkg/yqlib/operator_strings.go new file mode 100644 index 00000000..c5bf278d --- /dev/null +++ b/pkg/yqlib/operator_strings.go @@ -0,0 +1,50 @@ +package yqlib + +import ( + "container/list" + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +func joinStringOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) { + log.Debugf("-- joinStringOperator") + joinStr := "" + + rhs, err := d.GetMatchingNodes(matchMap, expressionNode.Rhs) + if err != nil { + return nil, err + } + if rhs.Front() != nil { + joinStr = rhs.Front().Value.(*CandidateNode).Node.Value + } + + var results = list.New() + + for el := matchMap.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + node := unwrapDoc(candidate.Node) + if node.Kind != yaml.SequenceNode { + return nil, fmt.Errorf("Cannot join with %v, can only join arrays of scalars", node.Tag) + } + targetNode := join(node.Content, joinStr) + result := candidate.CreateChild(nil, targetNode) + results.PushBack(result) + } + + return results, nil +} + +func join(content []*yaml.Node, joinStr string) *yaml.Node { + var stringsToJoin []string + for _, node := range content { + str := node.Value + if node.Tag == "!!null" { + str = "" + } + stringsToJoin = append(stringsToJoin, str) + } + + return &yaml.Node{Kind: yaml.ScalarNode, Value: strings.Join(stringsToJoin, joinStr), Tag: "!!str"} +} diff --git a/pkg/yqlib/operator_strings_test.go b/pkg/yqlib/operator_strings_test.go new file mode 100644 index 00000000..c33cb5cd --- /dev/null +++ b/pkg/yqlib/operator_strings_test.go @@ -0,0 +1,23 @@ +package yqlib + +import ( + "testing" +) + +var stringsOperatorScenarios = []expressionScenario{ + { + description: "Join strings", + document: `[cat, meow, 1, null, true]`, + expression: `join("; ")`, + expected: []string{ + "D0, P[], (!!str)::cat; meow; 1; ; true\n", + }, + }, +} + +func TestStringsOperatorScenarios(t *testing.T) { + for _, tt := range stringsOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "String Operators", stringsOperatorScenarios) +}