mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
Adding first operator
This commit is contained in:
parent
49b6477c49
commit
3050ca5303
345
pkg/yqlib/doc/operators/first.md
Normal file
345
pkg/yqlib/doc/operators/first.md
Normal file
@ -0,0 +1,345 @@
|
||||
|
||||
## First matching element from array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from array with multiple matches
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
- a: cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from array with numeric condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: 10
|
||||
- a: 100
|
||||
- a: 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 100
|
||||
```
|
||||
|
||||
## First matching element from array with boolean condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: false
|
||||
- a: true
|
||||
- a: false
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == true)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: true
|
||||
```
|
||||
|
||||
## First matching element from array with null values
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: null
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a != null)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from array with complex condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: dog
|
||||
b: 5
|
||||
- a: cat
|
||||
b: 3
|
||||
- a: apple
|
||||
b: 7
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.b > 4)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: dog
|
||||
b: 5
|
||||
```
|
||||
|
||||
## First matching element from map
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
x:
|
||||
a: banana
|
||||
y:
|
||||
a: cat
|
||||
z:
|
||||
a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from map with numeric condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
x:
|
||||
a: 10
|
||||
y:
|
||||
a: 100
|
||||
z:
|
||||
a: 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 100
|
||||
```
|
||||
|
||||
## First matching element from nested structure
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
items:
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.items | first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element with no matches
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "dog")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from empty array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
[]
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from scalar node
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
hello
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "hello")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from null node
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
null
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "hello")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element with string condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a | test("^c"))' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element with length condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: hi
|
||||
- a: hello
|
||||
- a: world
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a | length > 4)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: hello
|
||||
```
|
||||
|
||||
## First matching element from array of strings
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- banana
|
||||
- cat
|
||||
- apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
cat
|
||||
```
|
||||
|
||||
## First matching element from array of numbers
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- 10
|
||||
- 100
|
||||
- 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
100
|
||||
```
|
||||
|
||||
## First element with no RHS from array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- 10
|
||||
- 100
|
||||
- 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
10
|
||||
```
|
||||
|
||||
## First element with no RHS from array of maps
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: 10
|
||||
- a: 100
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 10
|
||||
```
|
||||
|
||||
## No RHS on empty array returns nothing
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
[]
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## No RHS on scalar returns nothing
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
hello
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## No RHS on null returns nothing
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
null
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
@ -54,6 +54,12 @@ func (p *expressionParserImpl) createExpressionTree(postFixPath []*Operation) (*
|
||||
switch numArgs {
|
||||
case 1:
|
||||
if len(stack) < 1 {
|
||||
// Allow certain unary ops to accept zero args by interpreting missing RHS as nil
|
||||
// TODO - make this more general on OperationType
|
||||
if Operation.OperationType == firstOpType {
|
||||
// no RHS provided; proceed without popping
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("'%v' expects 1 arg but received none", strings.TrimSpace(Operation.StringValue))
|
||||
}
|
||||
remaining, rhs := stack[:len(stack)-1], stack[len(stack)-1]
|
||||
|
||||
@ -118,6 +118,7 @@ var participleYqRules = []*participleYqRule{
|
||||
|
||||
simpleOp("sort_?by", sortByOpType),
|
||||
simpleOp("sort", sortOpType),
|
||||
simpleOp("first", firstOpType),
|
||||
|
||||
simpleOp("reverse", reverseOpType),
|
||||
|
||||
|
||||
@ -143,6 +143,7 @@ var delPathsOpType = &operationType{Type: "DEL_PATHS", NumArgs: 1, Precedence: 5
|
||||
|
||||
var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 52, Handler: explodeOperator, CheckForPostTraverse: true}
|
||||
var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 52, Handler: sortByOperator, CheckForPostTraverse: true}
|
||||
var firstOpType = &operationType{Type: "FIRST", NumArgs: 1, Precedence: 52, Handler: firstOperator, CheckForPostTraverse: true}
|
||||
var reverseOpType = &operationType{Type: "REVERSE", NumArgs: 0, Precedence: 52, Handler: reverseOperator, CheckForPostTraverse: true}
|
||||
var sortOpType = &operationType{Type: "SORT", NumArgs: 0, Precedence: 52, Handler: sortOperator, CheckForPostTraverse: true}
|
||||
var shuffleOpType = &operationType{Type: "SHUFFLE", NumArgs: 0, Precedence: 52, Handler: shuffleOperator, CheckForPostTraverse: true}
|
||||
|
||||
51
pkg/yqlib/operator_first.go
Normal file
51
pkg/yqlib/operator_first.go
Normal file
@ -0,0 +1,51 @@
|
||||
package yqlib
|
||||
|
||||
import "container/list"
|
||||
|
||||
func firstOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
results := list.New()
|
||||
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
|
||||
// If no RHS expression is provided, simply return the first entry in candidate.Content
|
||||
if expressionNode == nil || expressionNode.RHS == nil {
|
||||
if len(candidate.Content) > 0 {
|
||||
results.PushBack(candidate.Content[0])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
splatted, err := splat(context.SingleChildContext(candidate), traversePreferences{})
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
for splatEl := splatted.MatchingNodes.Front(); splatEl != nil; splatEl = splatEl.Next() {
|
||||
splatCandidate := splatEl.Value.(*CandidateNode)
|
||||
// Create a new context for this splatted candidate
|
||||
splatContext := context.SingleChildContext(splatCandidate)
|
||||
// Evaluate the RHS expression against this splatted candidate
|
||||
rhs, err := d.GetMatchingNodes(splatContext, expressionNode.RHS)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
includeResult := false
|
||||
|
||||
for resultEl := rhs.MatchingNodes.Front(); resultEl != nil; resultEl = resultEl.Next() {
|
||||
result := resultEl.Value.(*CandidateNode)
|
||||
includeResult = isTruthyNode(result)
|
||||
if includeResult {
|
||||
break
|
||||
}
|
||||
}
|
||||
if includeResult {
|
||||
results.PushBack(splatCandidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return context.ChildContext(results), nil
|
||||
}
|
||||
184
pkg/yqlib/operator_first_test.go
Normal file
184
pkg/yqlib/operator_first_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
package yqlib
|
||||
|
||||
import "testing"
|
||||
|
||||
var firstOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "First matching element from array",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with multiple matches",
|
||||
document: "[{a: banana},{a: cat},{a: apple},{a: cat}]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with numeric condition",
|
||||
document: "[{a: 10},{a: 100},{a: 1}]",
|
||||
expression: `first(.a > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: 100}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with boolean condition",
|
||||
document: "[{a: false},{a: true},{a: false}]",
|
||||
expression: `first(.a == true)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: true}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with null values",
|
||||
document: "[{a: null},{a: cat},{a: apple}]",
|
||||
expression: `first(.a != null)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with complex condition",
|
||||
document: "[{a: dog, b: 5},{a: cat, b: 3},{a: apple, b: 7}]",
|
||||
expression: `first(.b > 4)`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!map)::{a: dog, b: 5}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from map",
|
||||
document: "x: {a: banana}\ny: {a: cat}\nz: {a: apple}",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[y], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from map with numeric condition",
|
||||
document: "x: {a: 10}\ny: {a: 100}\nz: {a: 1}",
|
||||
expression: `first(.a > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[y], (!!map)::{a: 100}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from nested structure",
|
||||
document: "items: [{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `.items | first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[items 1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with no matches",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a == "dog")`,
|
||||
expected: []string{
|
||||
// No output expected when no matches
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from empty array",
|
||||
document: "[]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
// No output expected when array is empty
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from scalar node",
|
||||
document: "hello",
|
||||
expression: `first(. == "hello")`,
|
||||
expected: []string{
|
||||
// No output expected when node is scalar (no content to splat)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from null node",
|
||||
document: "null",
|
||||
expression: `first(. == "hello")`,
|
||||
expected: []string{
|
||||
// No output expected when node is null (no content to splat)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with string condition",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a | test("^c"))`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with length condition",
|
||||
document: "[{a: hi},{a: hello},{a: world}]",
|
||||
expression: `first(.a | length > 4)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: hello}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array of strings",
|
||||
document: "[banana, cat, apple]",
|
||||
expression: `first(. == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!str)::cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array of numbers",
|
||||
document: "[10, 100, 1]",
|
||||
expression: `first(. > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!int)::100\n",
|
||||
},
|
||||
},
|
||||
// New tests for no RHS (return first child)
|
||||
{
|
||||
description: "First element with no RHS from array",
|
||||
document: "[10, 100, 1]",
|
||||
expression: `first`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!int)::10\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First element with no RHS from array of maps",
|
||||
document: "[{a: 10},{a: 100}]",
|
||||
expression: `first`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!map)::{a: 10}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "No RHS on empty array returns nothing",
|
||||
document: "[]",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "No RHS on scalar returns nothing",
|
||||
document: "hello",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "No RHS on null returns nothing",
|
||||
document: "null",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
func TestFirstOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range firstOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "first", firstOperatorScenarios)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user