mirror of
https://github.com/mikefarah/yq.git
synced 2025-01-23 22:25:42 +00:00
Added contains operator
This commit is contained in:
parent
5f154eb1b6
commit
2db8140d7f
85
pkg/yqlib/doc/Contains.md
Normal file
85
pkg/yqlib/doc/Contains.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
## Array contains array
|
||||||
|
Array is equal or subset of
|
||||||
|
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
- foobar
|
||||||
|
- foobaz
|
||||||
|
- blarp
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'contains(["baz", "bar"])' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object included in array
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
"foo": 12
|
||||||
|
"bar":
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- "barp": 12
|
||||||
|
"blip": 13
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'contains({"bar": [{"barp": 12}]})' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object not included in array
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
"foo": 12
|
||||||
|
"bar":
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- "barp": 12
|
||||||
|
"blip": 13
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'contains({"foo": 12, "bar": [{"barp": 15}]})' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
false
|
||||||
|
```
|
||||||
|
|
||||||
|
## String contains substring
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
foobar
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'contains("bar")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## String equals string
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
meow
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'contains("meow")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
Use the `with` operator to conveniently make multiple updates to a deeply nested path.
|
Use the `with` operator to conveniently make multiple updates to a deeply nested path, or to update array elements relatively to each other.
|
||||||
|
|
||||||
## Update and style
|
## Update and style
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
@ -18,3 +18,43 @@ a:
|
|||||||
nested: 'newValue'
|
nested: 'newValue'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Update multiple deeply nested properties
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
a:
|
||||||
|
deeply:
|
||||||
|
nested: value
|
||||||
|
other: thing
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'with(.a.deeply ; .nested = "newValue" | .other= "newThing")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
a:
|
||||||
|
deeply:
|
||||||
|
nested: newValue
|
||||||
|
other: newThing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update array elements relatively
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
myArray:
|
||||||
|
- a: apple
|
||||||
|
- a: banana
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'with(.myArray[] ; .b = .a + " yum")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
myArray:
|
||||||
|
- a: apple
|
||||||
|
b: apple yum
|
||||||
|
- a: banana
|
||||||
|
b: banana yum
|
||||||
|
```
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
Use the `with` operator to conveniently make multiple updates to a deeply nested path.
|
Use the `with` operator to conveniently make multiple updates to a deeply nested path, or to update array elements relatively to each other.
|
||||||
|
@ -298,6 +298,7 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
|
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
|
||||||
lexer.Add([]byte(`all`), opToken(allOpType))
|
lexer.Add([]byte(`all`), opToken(allOpType))
|
||||||
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
|
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
|
||||||
|
lexer.Add([]byte(`contains`), opToken(containsOpType))
|
||||||
|
|
||||||
lexer.Add([]byte(`split`), opToken(splitStringOpType))
|
lexer.Add([]byte(`split`), opToken(splitStringOpType))
|
||||||
lexer.Add([]byte(`keys`), opToken(keysOpType))
|
lexer.Add([]byte(`keys`), opToken(keysOpType))
|
||||||
|
@ -62,6 +62,7 @@ var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50,
|
|||||||
|
|
||||||
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
|
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
|
||||||
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
|
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
|
||||||
|
var containsOpType = &operationType{Type: "CONTAINS", NumArgs: 1, Precedence: 50, Handler: containsOperator}
|
||||||
var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator}
|
var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator}
|
||||||
var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, Precedence: 50, Handler: allOperator}
|
var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, Precedence: 50, Handler: allOperator}
|
||||||
|
|
||||||
|
111
pkg/yqlib/operator_contains.go
Normal file
111
pkg/yqlib/operator_contains.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func containsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
return crossFunction(d, context.ReadOnlyClone(), expressionNode, containsWithNodes, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsArrayElement(array *yaml.Node, item *yaml.Node) (bool, error) {
|
||||||
|
for index := 0; index < len(array.Content); index = index + 1 {
|
||||||
|
containedInArray, err := contains(array.Content[index], item)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if containedInArray {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsArray(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
|
||||||
|
if rhs.Kind != yaml.SequenceNode {
|
||||||
|
return containsArrayElement(lhs, rhs)
|
||||||
|
}
|
||||||
|
for index := 0; index < len(rhs.Content); index = index + 1 {
|
||||||
|
itemInArray, err := containsArrayElement(lhs, rhs.Content[index])
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !itemInArray {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsObject(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
|
||||||
|
if rhs.Kind != yaml.MappingNode {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for index := 0; index < len(rhs.Content); index = index + 2 {
|
||||||
|
rhsKey := rhs.Content[index]
|
||||||
|
rhsValue := rhs.Content[index+1]
|
||||||
|
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
|
||||||
|
lhsKeyIndex := findInArray(lhs, rhsKey)
|
||||||
|
log.Debugf("index is %v", lhsKeyIndex)
|
||||||
|
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
lhsValue := lhs.Content[lhsKeyIndex+1]
|
||||||
|
log.Debugf("lhsValue is %v", lhsValue.Value)
|
||||||
|
|
||||||
|
itemInArray, err := contains(lhsValue, rhsValue)
|
||||||
|
log.Debugf("rhsValue is %v", rhsValue.Value)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !itemInArray {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsScalars(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
|
||||||
|
if lhs.Tag == "!!str" {
|
||||||
|
return strings.Contains(lhs.Value, rhs.Value), nil
|
||||||
|
}
|
||||||
|
return lhs.Value == rhs.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
|
||||||
|
switch lhs.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
return containsObject(lhs, rhs)
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
return containsArray(lhs, rhs)
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
if rhs.Kind != yaml.ScalarNode || lhs.Tag != rhs.Tag {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if lhs.Tag == "!!null" {
|
||||||
|
return rhs.Tag == "!!null", nil
|
||||||
|
}
|
||||||
|
return containsScalars(lhs, rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("%v not yet supported for contains", lhs.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsWithNodes(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||||
|
lhs.Node = unwrapDoc(lhs.Node)
|
||||||
|
rhs.Node = unwrapDoc(rhs.Node)
|
||||||
|
|
||||||
|
if lhs.Node.Kind != rhs.Node.Kind {
|
||||||
|
return nil, fmt.Errorf("%v cannot check contained in %v", rhs.Node.Tag, lhs.Node.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := contains(lhs.Node, rhs.Node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createBooleanCandidate(lhs, result), nil
|
||||||
|
}
|
82
pkg/yqlib/operator_contains_test.go
Normal file
82
pkg/yqlib/operator_contains_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var containsOperatorScenarios = []expressionScenario{
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `null | contains(~)`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `3 | contains(3)`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `3 | contains(32)`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Array contains array",
|
||||||
|
subdescription: "Array is equal or subset of",
|
||||||
|
document: `["foobar", "foobaz", "blarp"]`,
|
||||||
|
expression: `contains(["baz", "bar"])`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `["dog", "cat", "giraffe"] | contains(["camel"])`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Object included in array",
|
||||||
|
document: `{"foo": 12, "bar":[1,2,{"barp":12, "blip":13}]}`,
|
||||||
|
expression: `contains({"bar": [{"barp": 12}]})`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Object not included in array",
|
||||||
|
document: `{"foo": 12, "bar":[1,2,{"barp":12, "blip":13}]}`,
|
||||||
|
expression: `contains({"foo": 12, "bar": [{"barp": 15}]})`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "String contains substring",
|
||||||
|
document: `"foobar"`,
|
||||||
|
expression: `contains("bar")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "String equals string",
|
||||||
|
document: `"meow"`,
|
||||||
|
expression: `contains("meow")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsOperatorScenarios(t *testing.T) {
|
||||||
|
for _, tt := range containsOperatorScenarios {
|
||||||
|
testScenario(t, &tt)
|
||||||
|
}
|
||||||
|
documentScenarios(t, "Contains", containsOperatorScenarios)
|
||||||
|
}
|
@ -11,6 +11,22 @@ var withOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (doc)::a: {deeply: {nested: 'newValue'}}\n",
|
"D0, P[], (doc)::a: {deeply: {nested: 'newValue'}}\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Update multiple deeply nested properties",
|
||||||
|
document: `a: {deeply: {nested: value, other: thing}}`,
|
||||||
|
expression: `with(.a.deeply ; .nested = "newValue" | .other= "newThing")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (doc)::a: {deeply: {nested: newValue, other: newThing}}\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Update array elements relatively",
|
||||||
|
document: `myArray: [{a: apple},{a: banana}]`,
|
||||||
|
expression: `with(.myArray[] ; .b = .a + " yum")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (doc)::myArray: [{a: apple, b: apple yum}, {a: banana, b: banana yum}]\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithOperatorScenarios(t *testing.T) {
|
func TestWithOperatorScenarios(t *testing.T) {
|
||||||
|
@ -10,6 +10,7 @@ Sorry for any inconvenience caused!.
|
|||||||
|
|
||||||
|
|
||||||
- New `with` operator for making multiple changes to a given path
|
- New `with` operator for making multiple changes to a given path
|
||||||
|
- New `contains` operator, works like the `jq` equivalent
|
||||||
- Subtract operator now supports subtracting elements from arrays!
|
- Subtract operator now supports subtracting elements from arrays!
|
||||||
- Fixed Swapping values using variables #934
|
- Fixed Swapping values using variables #934
|
||||||
- Github Action now properly supports multiline output #936, thanks @pjxiao
|
- Github Action now properly supports multiline output #936, thanks @pjxiao
|
||||||
|
Loading…
Reference in New Issue
Block a user