diff --git a/pkg/yqlib/doc/operators/string-operators.md b/pkg/yqlib/doc/operators/string-operators.md index c9447a998a..47c3e3204e 100644 --- a/pkg/yqlib/doc/operators/string-operators.md +++ b/pkg/yqlib/doc/operators/string-operators.md @@ -49,6 +49,38 @@ Note that versions prior to 4.18 require the 'eval/e' command to be specified.&# `yq e ` {% endhint %} +## To up (upper) case +Works with unicode characters + +Given a sample.yml file of: +```yaml +água +``` +then +```bash +yq 'upcase' sample.yml +``` +will output +```yaml +ÁGUA +``` + +## To down (lower) case +Works with unicode characters + +Given a sample.yml file of: +```yaml +ÁgUA +``` +then +```bash +yq 'downcase' sample.yml +``` +will output +```yaml +água +``` + ## Join strings Given a sample.yml file of: ```yaml diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index ddd582d679..9f02453641 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -414,6 +414,12 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`capture`), opToken(captureOpType)) lexer.Add([]byte(`test`), opToken(testOpType)) + lexer.Add([]byte(`upcase`), opTokenWithPrefs(changeCaseOpType, nil, changeCasePrefs{ToUpperCase: true})) + lexer.Add([]byte(`ascii_upcase`), opTokenWithPrefs(changeCaseOpType, nil, changeCasePrefs{ToUpperCase: true})) + + lexer.Add([]byte(`downcase`), opTokenWithPrefs(changeCaseOpType, nil, changeCasePrefs{ToUpperCase: false})) + lexer.Add([]byte(`ascii_downcase`), opTokenWithPrefs(changeCaseOpType, nil, changeCasePrefs{ToUpperCase: false})) + lexer.Add([]byte(`sort`), opToken(sortOpType)) lexer.Add([]byte(`sort_by`), opToken(sortByOpType)) lexer.Add([]byte(`reverse`), opToken(reverseOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index dd07d5c12a..0adc308633 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -125,13 +125,16 @@ var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 50, Handler: sortByOperator} var reverseOpType = &operationType{Type: "REVERSE", NumArgs: 0, Precedence: 50, Handler: reverseOperator} var sortOpType = &operationType{Type: "SORT", NumArgs: 0, Precedence: 50, Handler: sortOperator} + var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator} + var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator} var subStringOpType = &operationType{Type: "SUBSTR", NumArgs: 1, Precedence: 50, Handler: substituteStringOperator} var matchOpType = &operationType{Type: "MATCH", NumArgs: 1, Precedence: 50, Handler: matchOperator} var captureOpType = &operationType{Type: "CAPTURE", NumArgs: 1, Precedence: 50, Handler: captureOperator} var testOpType = &operationType{Type: "TEST", NumArgs: 1, Precedence: 50, Handler: testOperator} var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator} +var changeCaseOpType = &operationType{Type: "CHANGE_CASE", NumArgs: 0, Precedence: 50, Handler: changeCaseOperator} var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 50, Handler: loadYamlOperator} diff --git a/pkg/yqlib/operator_strings.go b/pkg/yqlib/operator_strings.go index 2ab60fd30d..394ca3e5c4 100644 --- a/pkg/yqlib/operator_strings.go +++ b/pkg/yqlib/operator_strings.go @@ -9,6 +9,37 @@ import ( "gopkg.in/yaml.v3" ) +type changeCasePrefs struct { + ToUpperCase bool +} + +func changeCaseOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + results := list.New() + prefs := expressionNode.Operation.Preferences.(changeCasePrefs) + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + + node := unwrapDoc(candidate.Node) + + if guessTagFromCustomType(node) != "!!str" { + return Context{}, fmt.Errorf("cannot change case with %v, can only operate on strings. ", node.Tag) + } + + newStringNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: node.Tag, Style: node.Style} + if prefs.ToUpperCase { + newStringNode.Value = strings.ToUpper(node.Value) + } else { + newStringNode.Value = strings.ToLower(node.Value) + } + results.PushBack(candidate.CreateReplacement(newStringNode)) + + } + + return context.ChildContext(results), nil + +} + func getSubstituteParameters(d *dataTreeNavigator, block *ExpressionNode, context Context) (string, string, error) { regEx := "" replacementText := "" diff --git a/pkg/yqlib/operator_strings_test.go b/pkg/yqlib/operator_strings_test.go index cf65f8a6e8..04c49f7fe4 100644 --- a/pkg/yqlib/operator_strings_test.go +++ b/pkg/yqlib/operator_strings_test.go @@ -5,6 +5,40 @@ import ( ) var stringsOperatorScenarios = []expressionScenario{ + { + description: "To up (upper) case", + subdescription: "Works with unicode characters", + document: `água`, + expression: "upcase", + expected: []string{ + "D0, P[], (!!str)::ÁGUA\n", + }, + }, + { + skipDoc: true, + document: `!camel água`, + expression: "upcase", + expected: []string{ + "D0, P[], (!camel)::ÁGUA\n", + }, + }, + { + description: "To down (lower) case", + subdescription: "Works with unicode characters", + document: `ÁgUA`, + expression: "downcase", + expected: []string{ + "D0, P[], (!!str)::água\n", + }, + }, + { + skipDoc: true, + document: `!camel ÁgUA`, + expression: "downcase", + expected: []string{ + "D0, P[], (!camel)::água\n", + }, + }, { description: "Join strings", document: `[cat, meow, 1, null, true]`,