diff --git a/pkg/yqlib/doc/operators/array-to-map.md b/pkg/yqlib/doc/operators/array-to-map.md new file mode 100644 index 00000000..be9c0a6b --- /dev/null +++ b/pkg/yqlib/doc/operators/array-to-map.md @@ -0,0 +1,28 @@ +# Array to Map + +Use this operator to convert an array to..a map. Skips over null values. + +Behind the scenes, this is implemented using reduce: + +``` +(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i) +``` + +## Simple example +Given a sample.yml file of: +```yaml +cool: + - null + - null + - hello +``` +then +```bash +yq '.cool |= array_to_map' sample.yml +``` +will output +```yaml +cool: + 2: hello +``` + diff --git a/pkg/yqlib/doc/operators/headers/array-to-map.md b/pkg/yqlib/doc/operators/headers/array-to-map.md new file mode 100644 index 00000000..6da537b3 --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/array-to-map.md @@ -0,0 +1,9 @@ +# Array to Map + +Use this operator to convert an array to..a map. Skips over null values. + +Behind the scenes, this is implemented using reduce: + +``` +(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i) +``` diff --git a/pkg/yqlib/doc/usage/properties.md b/pkg/yqlib/doc/usage/properties.md index 4e21e08b..f3d01235 100644 --- a/pkg/yqlib/doc/usage/properties.md +++ b/pkg/yqlib/doc/usage/properties.md @@ -149,6 +149,23 @@ person: - pizza ``` +## Decode properties - array should be a map +If you have a numeric map key in your property files, use array_to_map to convert them to maps. + +Given a sample.properties file of: +```properties +things.10 = mike +``` +then +```bash +yq -p=props '.things |= array_to_map' sample.properties +``` +will output +```yaml +things: + 10: mike +``` + ## Roundtrip Given a sample.properties file of: ```properties diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index 89fe682e..2a243573 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -55,6 +55,8 @@ var participleYqRules = []*participleYqRule{ simpleOp("sortKeys", sortKeysOpType), simpleOp("sort_?keys", sortKeysOpType), + {"ArrayToMap", "array_?to_?map", expressionOpToken(`(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)`), 0}, + {"YamlEncodeWithIndent", `to_?yaml\([0-9]+\)`, encodeParseIndent(YamlOutputFormat), 0}, {"XMLEncodeWithIndent", `to_?xml\([0-9]+\)`, encodeParseIndent(XMLOutputFormat), 0}, {"JSONEncodeWithIndent", `to_?json\([0-9]+\)`, encodeParseIndent(JSONOutputFormat), 0}, @@ -291,6 +293,14 @@ func opTokenWithPrefs(opType *operationType, assignOpType *operationType, prefer } } +func expressionOpToken(expression string) yqAction { + return func(rawToken lexer.Token) (*token, error) { + prefs := expressionOpPreferences{expression: expression} + expressionOp := &Operation{OperationType: expressionOpType, Preferences: prefs} + return &token{TokenType: operationToken, Operation: expressionOp}, nil + } +} + func flattenWithDepth() yqAction { return func(rawToken lexer.Token) (*token, error) { value := rawToken.Value diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index d678c4ce..8f12a41d 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -80,6 +80,8 @@ var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Ha var lineOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: lineOperator} var columnOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: columnOperator} +var expressionOpType = &operationType{Type: "EXP", NumArgs: 0, Precedence: 50, Handler: expressionOperator} + var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator} var sliceArrayOpType = &operationType{Type: "SLICE", NumArgs: 0, Precedence: 50, Handler: sliceArrayOperator} var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator} diff --git a/pkg/yqlib/operator_array_to_map_test.go b/pkg/yqlib/operator_array_to_map_test.go new file mode 100644 index 00000000..406fec1c --- /dev/null +++ b/pkg/yqlib/operator_array_to_map_test.go @@ -0,0 +1,23 @@ +package yqlib + +import ( + "testing" +) + +var arrayToMapScenarios = []expressionScenario{ + { + description: "Simple example", + document: `cool: [null, null, hello]`, + expression: `.cool |= array_to_map`, + expected: []string{ + "D0, P[], (doc)::cool:\n 2: hello\n", + }, + }, +} + +func TestArrayToMapScenarios(t *testing.T) { + for _, tt := range arrayToMapScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "array-to-map", arrayToMapScenarios) +} diff --git a/pkg/yqlib/operator_expression.go b/pkg/yqlib/operator_expression.go new file mode 100644 index 00000000..e451f0ea --- /dev/null +++ b/pkg/yqlib/operator_expression.go @@ -0,0 +1,16 @@ +package yqlib + +type expressionOpPreferences struct { + expression string +} + +func expressionOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + + prefs := expressionNode.Operation.Preferences.(expressionOpPreferences) + expNode, err := ExpressionParser.ParseExpression(prefs.expression) + if err != nil { + return Context{}, err + } + + return d.GetMatchingNodes(context, expNode) +} diff --git a/pkg/yqlib/properties_test.go b/pkg/yqlib/properties_test.go index 9cdd7c8f..324a46d0 100644 --- a/pkg/yqlib/properties_test.go +++ b/pkg/yqlib/properties_test.go @@ -147,6 +147,14 @@ var propertyScenarios = []formatScenario{ expected: expectedDecodedYaml, scenarioType: "decode", }, + { + description: "Decode properties - array should be a map", + subdescription: "If you have a numeric map key in your property files, use array_to_map to convert them to maps.", + input: `things.10 = mike`, + expression: `.things |= array_to_map`, + expected: "things:\n 10: mike\n", + scenarioType: "decode", + }, { description: "does not expand automatically", skipDoc: true,