Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions pkg/yqlib/doc/operators/in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@

## Check key exists in map using variable binding
Given a sample.yml file of:
```yaml
a: 1
b: 2
c: 3
```
then
```bash
yq '. as $m | "a" | in($m)' sample.yml
```
will output
```yaml
true
```

## Check key does not exist in map
Given a sample.yml file of:
```yaml
a: 1
b: 2
c: 3
```
then
```bash
yq '. as $m | "d" | in($m)' sample.yml
```
will output
```yaml
false
```

## Check value exists in array
Given a sample.yml file of:
```yaml
- Tool
- Food
- Flower
```
then
```bash
yq '. as $m | "Food" | in($m)' sample.yml
```
will output
```yaml
true
```

## Check value does not exist in array
Given a sample.yml file of:
```yaml
- Tool
- Food
- Flower
```
then
```bash
yq '. as $m | "Animal" | in($m)' sample.yml
```
will output
```yaml
false
```

## Check in with select on array elements
Filter items whose type is in the given list

Given a sample.yml file of:
```yaml
- item: Pizza
type: Food
- item: Rose
type: Flower
- item: Hammer
type: Tool
```
then
```bash
yq '.[] | select(.type | in(["Tool", "Food"]))' sample.yml
```
will output
```yaml
item: Pizza
type: Food
item: Hammer
type: Tool
```

## In with variable binding - found
Given a sample.yml file of:
```yaml
a: 1
b: 2
c: 3
```
then
```bash
yq '. as $m | "b" | in($m)' sample.yml
```
will output
```yaml
true
```

## In with variable binding - not found
Given a sample.yml file of:
```yaml
a: 1
b: 2
c: 3
```
then
```bash
yq '. as $m | "z" | in($m)' sample.yml
```
will output
```yaml
false
```

1 change: 1 addition & 0 deletions pkg/yqlib/lexer_participle.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ var participleYqRules = []*participleYqRule{

simpleOp("select", selectOpType),
simpleOp("has", hasOpType),
simpleOp("in", inOpType),
simpleOp("unique_?by", uniqueByOpType),
simpleOp("unique", uniqueOpType),

Expand Down
1 change: 1 addition & 0 deletions pkg/yqlib/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs:

var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 52, Handler: selectOperator, CheckForPostTraverse: true}
var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator}
var inOpType = &operationType{Type: "IN", NumArgs: 1, Precedence: 50, Handler: inOperator}
var uniqueOpType = &operationType{Type: "UNIQUE", NumArgs: 0, Precedence: 52, Handler: unique, CheckForPostTraverse: true}
var uniqueByOpType = &operationType{Type: "UNIQUE_BY", NumArgs: 1, Precedence: 52, Handler: uniqueBy, CheckForPostTraverse: true}
var groupByOpType = &operationType{Type: "GROUP_BY", NumArgs: 1, Precedence: 52, Handler: groupBy, CheckForPostTraverse: true}
Expand Down
59 changes: 59 additions & 0 deletions pkg/yqlib/operator_in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package yqlib

import (
"container/list"
)

func inOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {

log.Debugf("inOperation")
var results = list.New()

rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.RHS)

if err != nil {
return Context{}, err
}

var collection *CandidateNode
if rhs.MatchingNodes.Len() != 0 {
collection = rhs.MatchingNodes.Front().Value.(*CandidateNode)
} else {
// no collection provided, return false for all
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
results.PushBack(createBooleanCandidate(candidate, false))
}
return context.ChildContext(results), nil
}

for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
isIn := false

switch collection.Kind {
case MappingNode:
// Check if candidate value exists as a key in the mapping
for index := 0; index < len(collection.Content); index = index + 2 {
key := collection.Content[index]
if key.Value == candidate.Value {
Comment thread
mikefarah marked this conversation as resolved.
isIn = true
break
}
}
case SequenceNode:
// Check if candidate value is present in the sequence (value membership)
for _, element := range collection.Content {
if element.Value == candidate.Value {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by Cursor, not the reviewer.

Blocking: Comparing only .Value breaks non-scalar membership. Mapping/sequence nodes have an empty .Value, so any object spuriously matches any array of objects:

yq -n '{"x": 99} | in([{"a": 1}, {"b": 2}])'  # true — should be false

Please use contains(element, candidate) here (same deep equality as the contains operator) instead of string .Value comparison.

isIn = true
break
}
}
default:
isIn = false
}

results.PushBack(createBooleanCandidate(candidate, isIn))
}
return context.ChildContext(results), nil
}
105 changes: 105 additions & 0 deletions pkg/yqlib/operator_in_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package yqlib

import (
"testing"
)

var inOperatorScenarios = []expressionScenario{
{
description: "Check key exists in map using variable binding",
document: "a: 1\nb: 2\nc: 3\n",
expression: `. as $m | "a" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "Check key does not exist in map",
document: "a: 1\nb: 2\nc: 3\n",
expression: `. as $m | "d" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Check value exists in array",
document: "- Tool\n- Food\n- Flower\n",
expression: `. as $m | "Food" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "Check value does not exist in array",
document: "- Tool\n- Food\n- Flower\n",
expression: `. as $m | "Animal" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Check in with select on array elements",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by Cursor, not the reviewer.

Please add test coverage for the non-scalar array bug, e.g.:

{
    skipDoc:    true,
    expression: `{"x": 99} | in([{"a": 1}, {"b": 2}])`,
    expected:   []string{"D0, P[], (!!bool)::false\n"},
},

Also consider tests for tag mismatch (1 | in(["1"])) and null in arrays.

subdescription: "Filter items whose type is in the given list",
document: "- {item: Pizza, type: Food}\n- {item: Rose, type: Flower}\n- {item: Hammer, type: Tool}\n",
expression: `.[] | select(.type | in(["Tool", "Food"]))`,
expected: []string{
"D0, P[0], (!!map)::{item: Pizza, type: Food}\n",
"D0, P[2], (!!map)::{item: Hammer, type: Tool}\n",
},
},
{
description: "In with variable binding - found",
document: "a: 1\nb: 2\nc: 3\n",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by Cursor, not the reviewer.

These "In with variable binding" scenarios duplicate "Check key exists/does not exist in map" above (lines 9–23). Consider removing the redundant pair to keep the test file lean.

expression: `. as $m | "b" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "In with variable binding - not found",
document: "a: 1\nb: 2\nc: 3\n",
expression: `. as $m | "z" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "- one\n- two\n- three\n",
expression: `. as $m | "one" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "- one\n- two\n- three\n",
expression: `. as $m | "five" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "key: value\nother: stuff\n",
expression: `. as $m | "key" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "key: value\nother: stuff\n",
expression: `. as $m | "missing" | in($m)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
}

func TestInOperatorScenarios(t *testing.T) {
for _, tt := range inOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "in", inOperatorScenarios)
}
Loading