diff --git a/pkg/yqlib/doc/String Operators.md b/pkg/yqlib/doc/String Operators.md new file mode 100644 index 0000000..a3e73fb --- /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 5339bd4..4b6e58e 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 f5f9428..abcdeec 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 0000000..c5bf278 --- /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 0000000..c33cb5c --- /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) +}