1
0
mirror of https://github.com/taigrr/yq synced 2025-01-18 04:53:17 -08:00

Compare commits

...

21 Commits

Author SHA1 Message Date
Mike Farah
fae2b2643c Added gofmt to format command 2021-06-01 10:52:14 +10:00
Mike Farah
dd86b5e7f2 Fixing doc 2021-05-28 17:00:25 +10:00
Mike Farah
f1f75683c1 Fixed nil RHS bug in alternative operator #838 2021-05-28 16:59:02 +10:00
Mike Farah
38b9856f50 Increment version 2021-05-22 08:24:08 +10:00
Mike Farah
48eeb2a9df Fixes update-assign with collect object issue #830 2021-05-22 08:22:45 +10:00
Mike Farah
af283315f2 Increment version 2021-05-21 14:27:24 +10:00
Mike Farah
d18a6963f6 Fixes nested array indexing #824 2021-05-21 14:18:24 +10:00
Mike Farah
77edbb9f5c Fixing readonly ops not to modify context when paths dont exist 2021-05-16 15:02:31 +10:00
Mike Farah
179c44aacc Fixing readonly ops not to modify context when paths dont exist 2021-05-16 14:36:13 +10:00
Mike Farah
bc70c1fb16 Added blank alias example 2021-05-16 14:18:18 +10:00
Mike Farah
0b71a40797 Fixing readonly ops not to modify context when paths dont exist 2021-05-16 14:17:13 +10:00
Mike Farah
3f51a44596 Fixing readonly ops not to modify context when paths dont exist 2021-05-16 14:00:30 +10:00
Mike Farah
afebf0e621 Increment version 2021-05-16 12:43:26 +10:00
Mike Farah
dc464a5b10 Added ability to escape double quotes in double quotes 2021-05-16 12:38:17 +10:00
Mike Farah
5340ed0ad3 Fixed handling of null expressions in equals op 2021-05-16 12:38:17 +10:00
Mike Farah
7b52c5fe0e Increment version 2021-05-14 15:04:56 +10:00
Mike Farah
f4392f8658 Added any_c and all_c operators 2021-05-14 15:03:28 +10:00
Mike Farah
8e14b3b393 Added any and all operators 2021-05-14 14:29:55 +10:00
Mike Farah
8627441705 Added unique operator 2021-05-14 09:43:52 +10:00
Mike Farah
aa95ecd012 Update operator docs 2021-05-11 14:35:59 +10:00
Mike Farah
a2bd463a91 Fixed null issue with entry operators 2021-05-10 10:42:43 +10:00
54 changed files with 1018 additions and 84 deletions

View File

@ -11,7 +11,7 @@ var (
GitDescribe string GitDescribe string
// Version is main version number that is being run at the moment. // Version is main version number that is being run at the moment.
Version = "4.8.0" Version = "4.9.3"
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string) // VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release // then it means that it is a final release. Otherwise, this is a pre-release

View File

@ -1,4 +1,4 @@
FROM mikefarah/yq:4.8.0 FROM mikefarah/yq:4.9.3
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh

View File

@ -12,6 +12,14 @@ type Context struct {
DontAutoCreate bool DontAutoCreate bool
} }
func (n *Context) SingleReadonlyChildContext(candidate *CandidateNode) Context {
list := list.New()
list.PushBack(candidate)
newContext := n.ChildContext(list)
newContext.DontAutoCreate = true
return newContext
}
func (n *Context) SingleChildContext(candidate *CandidateNode) Context { func (n *Context) SingleChildContext(candidate *CandidateNode) Context {
list := list.New() list := list.New()
list.PushBack(candidate) list.PushBack(candidate)
@ -52,3 +60,9 @@ func (n *Context) Clone() Context {
} }
return clone return clone
} }
func (n *Context) ReadOnlyClone() Context {
clone := n.Clone()
clone.DontAutoCreate = true
return clone
}

View File

@ -169,6 +169,22 @@ b: &meow purr
a: *meow a: *meow
``` ```
## Set alias to blank does nothing
Given a sample.yml file of:
```yaml
b: &meow purr
a: cat
```
then
```bash
yq eval '.a alias = ""' sample.yml
```
will output
```yaml
b: &meow purr
a: cat
```
## Set alias relatively using assign-update ## Set alias relatively using assign-update
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@ -1,5 +1,14 @@
The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes. The `or` and `and` operators take two parameters and return a boolean result.
## OR example
`not` flips a boolean from true to false, or vice versa.
`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true.
`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet
These are most commonly used with the `select` operator to filter particular nodes.
## `or` example
Running Running
```bash ```bash
yq eval --null-input 'true or false' yq eval --null-input 'true or false'
@ -9,7 +18,7 @@ will output
true true
``` ```
## AND example ## `and` example
Running Running
```bash ```bash
yq eval --null-input 'true and false' yq eval --null-input 'true and false'
@ -41,6 +50,104 @@ will output
b: fly b: fly
``` ```
## `any` returns true if any boolean in a given array is true
Given a sample.yml file of:
```yaml
- false
- true
```
then
```bash
yq eval 'any' sample.yml
```
will output
```yaml
true
```
## `any` returns false for an empty array
Given a sample.yml file of:
```yaml
[]
```
then
```bash
yq eval 'any' sample.yml
```
will output
```yaml
false
```
## `any_c` returns true if any element in the array is true for the given condition.
Given a sample.yml file of:
```yaml
a:
- rad
- awesome
b:
- meh
- whatever
```
then
```bash
yq eval '.[] |= any_c(. == "awesome")' sample.yml
```
will output
```yaml
a: true
b: false
```
## `all` returns true if all booleans in a given array are true
Given a sample.yml file of:
```yaml
- true
- true
```
then
```bash
yq eval 'all' sample.yml
```
will output
```yaml
true
```
## `all` returns true for an empty array
Given a sample.yml file of:
```yaml
[]
```
then
```bash
yq eval 'all' sample.yml
```
will output
```yaml
true
```
## `all_c` returns true if all elements in the array are true for the given condition.
Given a sample.yml file of:
```yaml
a:
- rad
- awesome
b:
- meh
- 12
```
then
```bash
yq eval '.[] |= all_c(tag == "!!str")' sample.yml
```
will output
```yaml
a: true
b: false
```
## Not true is false ## Not true is false
Running Running
```bash ```bash

View File

@ -35,6 +35,19 @@ will output
value: b value: b
``` ```
## to_entries null
Given a sample.yml file of:
```yaml
null
```
then
```bash
yq eval 'to_entries' sample.yml
```
will output
```yaml
```
## from_entries map ## from_entries map
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

82
pkg/yqlib/doc/Unique.md Normal file
View File

@ -0,0 +1,82 @@
This is used to filter out duplicated items in an array.
## Unique array of scalars (string/numbers)
Given a sample.yml file of:
```yaml
- 1
- 2
- 3
- 2
```
then
```bash
yq eval 'unique' sample.yml
```
will output
```yaml
- 1
- 2
- 3
```
## Unique nulls
Unique works on the node value, so it considers different representations of nulls to be different
Given a sample.yml file of:
```yaml
- ~
- null
- ~
- null
```
then
```bash
yq eval 'unique' sample.yml
```
will output
```yaml
- ~
- null
```
## Unique all nulls
Run against the node tag to unique all the nulls
Given a sample.yml file of:
```yaml
- ~
- null
- ~
- null
```
then
```bash
yq eval 'unique_by(tag)' sample.yml
```
will output
```yaml
- ~
```
## Unique array object fields
Given a sample.yml file of:
```yaml
- name: harry
pet: cat
- name: billy
pet: dog
- name: harry
pet: dog
```
then
```bash
yq eval 'unique_by(.name)' sample.yml
```
will output
```yaml
- name: harry
pet: cat
- name: billy
pet: dog
```

View File

@ -62,20 +62,55 @@ cat
Note that this node holds not only its value 'cat', but comments and metadata too, including path and parent information. Note that this node holds not only its value 'cat', but comments and metadata too, including path and parent information.
The `=` operator then pipes the 'root' context through the `rhs` expression of `.b` to retun the node The `=` operator then pipes the 'root' context through the `rhs` expression of `.b` to return the node
```yaml ```yaml
dog dog
``` ```
Both sides have now been evaluated, so the operator performs its actual operation of assignment, and copies across the value from the RHS to the value on the LHS, and it returns the now updated 'root' context: Both sides have now been evaluated, so now the operator copies across the value from the RHS to the value on the LHS, and it returns the now updated context:
```yaml ```yaml
a: cat a: dog
b: dog b: dog
``` ```
# Relative update (e.g. `|=`) # Relative update (e.g. `|=`)
There is another form of the `=` operator which we call the relative form. It's very similar to `=` but with one key differnce when evaluating the RHS expression. There is another form of the `=` operator which we call the relative form. It's very similar to `=` but with one key difference when evaluating the RHS expression.
In the plain form, we pass in the 'root' level context to the RHS expresssion. In relative form, we pass in each result of the LHS to the RHS expression. Let's go through an example. In the plain form, we pass in the 'root' level context to the RHS expression. In relative form, we pass in _each result of the LHS_ to the RHS expression. Let's go through an example.
Given a document like:
```yaml
a: 1
b: thing
```
with an expression:
```
.a |= . + 1
```
Similar to the `=` operator, `|=` takes two operands, the LHS and RHS.
It pipes the current context (the whole document) through the LHS expression of `.a` to get the node value:
```
1
```
Now it pipes _that LHS context_ into the RHS expression `. + 1` (whereas in the `=` plain form it piped the original document context into the RHS) to yield:
```
2
```
The assignment operator then copies across the value from the RHS to the value on the LHS, and it returns the now updated 'root' context:
```yaml
a: 2
b: thing
```

View File

@ -1 +1,9 @@
The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes. The `or` and `and` operators take two parameters and return a boolean result.
`not` flips a boolean from true to false, or vice versa.
`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true.
`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet
These are most commonly used with the `select` operator to filter particular nodes.

View File

@ -0,0 +1 @@
This is used to filter out duplicated items in an array.

View File

@ -54,7 +54,7 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
opStack = opStack[0 : len(opStack)-1] opStack = opStack[0 : len(opStack)-1]
log.Debugf("deleteing open bracket from opstack") log.Debugf("deleteing open bracket from opstack")
//and append a collect to the opStack //and append a collect to the result
// hack - see if there's the optional traverse flag // hack - see if there's the optional traverse flag
// on the close op - move it to the collect op. // on the close op - move it to the collect op.
// allows for .["cat"]? // allows for .["cat"]?
@ -68,6 +68,12 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
result = append(result, &Operation{OperationType: shortPipeOpType}) result = append(result, &Operation{OperationType: shortPipeOpType})
log.Debugf("put shortpipe onto the result") log.Debugf("put shortpipe onto the result")
//traverseArrayCollect is a sneaky op that needs to be included too
//when closing a []
if len(opStack) > 0 && opStack[len(opStack)-1].Operation != nil && opStack[len(opStack)-1].Operation.OperationType == traverseArrayOpType {
opStack, result = popOpToResult(opStack, result)
}
case closeBracket: case closeBracket:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket { for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket {
opStack, result = popOpToResult(opStack, result) opStack, result = popOpToResult(opStack, result)

View File

@ -12,6 +12,21 @@ var pathTests = []struct {
expectedTokens []interface{} expectedTokens []interface{}
expectedPostFix []interface{} expectedPostFix []interface{}
}{ }{
{
`.[0]`,
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
append(make([]interface{}, 0), "SELF", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
},
{
`.[0][1]`,
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "0 (int64)", "]", "TRAVERSE_ARRAY", "[", "1 (int64)", "]"),
append(make([]interface{}, 0), "SELF", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "1 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
},
{
`"\""`,
append(make([]interface{}, 0), "\" (string)"),
append(make([]interface{}, 0), "\" (string)"),
},
{ {
`[]|join(".")`, `[]|join(".")`,
append(make([]interface{}, 0), "[", "EMPTY", "]", "PIPE", "JOIN", "(", ". (string)", ")"), append(make([]interface{}, 0), "[", "EMPTY", "]", "PIPE", "JOIN", "(", ". (string)", ")"),

View File

@ -189,6 +189,7 @@ func stringValue(wrapped bool) lex.Action {
if wrapped { if wrapped {
value = unwrap(value) value = unwrap(value)
} }
value = strings.ReplaceAll(value, "\\\"", "\"")
return &token{TokenType: operationToken, Operation: createValueOperation(value, value)}, nil return &token{TokenType: operationToken, Operation: createValueOperation(value, value)}, nil
} }
} }
@ -259,6 +260,8 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType)) lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
lexer.Add([]byte(`select`), opToken(selectOpType)) lexer.Add([]byte(`select`), opToken(selectOpType))
lexer.Add([]byte(`has`), opToken(hasOpType)) lexer.Add([]byte(`has`), opToken(hasOpType))
lexer.Add([]byte(`unique`), opToken(uniqueOpType))
lexer.Add([]byte(`unique_by`), opToken(uniqueByOpType))
lexer.Add([]byte(`explode`), opToken(explodeOpType)) lexer.Add([]byte(`explode`), opToken(explodeOpType))
lexer.Add([]byte(`or`), opToken(orOpType)) lexer.Add([]byte(`or`), opToken(orOpType))
lexer.Add([]byte(`and`), opToken(andOpType)) lexer.Add([]byte(`and`), opToken(andOpType))
@ -274,6 +277,11 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`join`), opToken(joinStringOpType)) lexer.Add([]byte(`join`), opToken(joinStringOpType))
lexer.Add([]byte(`sub`), opToken(subStringOpType)) lexer.Add([]byte(`sub`), opToken(subStringOpType))
lexer.Add([]byte(`any`), opToken(anyOpType))
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
lexer.Add([]byte(`all`), opToken(allOpType))
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
lexer.Add([]byte(`split`), opToken(splitStringOpType)) lexer.Add([]byte(`split`), opToken(splitStringOpType))
lexer.Add([]byte(`keys`), opToken(keysOpType)) lexer.Add([]byte(`keys`), opToken(keysOpType))
@ -327,7 +335,7 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`[Nn][Uu][Ll][Ll]`), nullValue()) lexer.Add([]byte(`[Nn][Uu][Ll][Ll]`), nullValue())
lexer.Add([]byte(`~`), nullValue()) lexer.Add([]byte(`~`), nullValue())
lexer.Add([]byte(`"[^"]*"`), stringValue(true)) lexer.Add([]byte(`"([^"\\]*(\\.[^"\\]*)*)"`), stringValue(true))
lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true)) lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true))
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false)) lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
@ -403,16 +411,19 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
skipNextToken = false skipNextToken = false
currentToken := tokens[index] currentToken := tokens[index]
log.Debug("processing %v", currentToken.toString(true))
if currentToken.TokenType == traverseArrayCollect { if currentToken.TokenType == traverseArrayCollect {
//need to put a traverse array then a collect currentToken //need to put a traverse array then a collect currentToken
// do this by adding traverse then converting currentToken to collect // do this by adding traverse then converting currentToken to collect
if index == 0 || tokens[index-1].TokenType != operationToken || if index == 0 || tokens[index-1].TokenType != operationToken ||
tokens[index-1].Operation.OperationType != traversePathOpType { tokens[index-1].Operation.OperationType != traversePathOpType {
log.Debug(" adding self")
op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"} op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
} }
log.Debug(" adding traverse array")
op := &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"} op := &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
@ -423,17 +434,19 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
if index != len(tokens)-1 && currentToken.AssignOperation != nil && if index != len(tokens)-1 && currentToken.AssignOperation != nil &&
tokens[index+1].TokenType == operationToken && tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == assignOpType { tokens[index+1].Operation.OperationType == assignOpType {
log.Debug(" its an update assign")
currentToken.Operation = currentToken.AssignOperation currentToken.Operation = currentToken.AssignOperation
currentToken.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign currentToken.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
skipNextToken = true skipNextToken = true
} }
log.Debug(" adding token to the fixed list")
postProcessedTokens = append(postProcessedTokens, currentToken) postProcessedTokens = append(postProcessedTokens, currentToken)
if index != len(tokens)-1 && if index != len(tokens)-1 &&
((currentToken.TokenType == openCollect && tokens[index+1].TokenType == closeCollect) || ((currentToken.TokenType == openCollect && tokens[index+1].TokenType == closeCollect) ||
(currentToken.TokenType == openCollectObject && tokens[index+1].TokenType == closeCollectObject)) { (currentToken.TokenType == openCollectObject && tokens[index+1].TokenType == closeCollectObject)) {
log.Debug(" adding empty")
op := &Operation{OperationType: emptyOpType, StringValue: "EMPTY"} op := &Operation{OperationType: emptyOpType, StringValue: "EMPTY"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
} }
@ -441,12 +454,19 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
if index != len(tokens)-1 && currentToken.CheckForPostTraverse && if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == operationToken && tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == traversePathOpType { tokens[index+1].Operation.OperationType == traversePathOpType {
log.Debug(" adding pipe because the next thing is traverse")
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"} op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
} }
if index != len(tokens)-1 && currentToken.CheckForPostTraverse && if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == openCollect { tokens[index+1].TokenType == openCollect {
// if tokens[index].TokenType == closeCollect {
// log.Debug(" adding pipe because next is opencollect")
// op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
// postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
// }
log.Debug(" adding traverArray because next is opencollect")
op := &Operation{OperationType: traverseArrayOpType} op := &Operation{OperationType: traverseArrayOpType}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
} }

View File

@ -20,9 +20,6 @@ type operationType struct {
Handler operatorHandler Handler operatorHandler
} }
// operators TODO:
// - mergeEmpty (sets only if the document is empty, do I do that now?)
var orOpType = &operationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: orOperator} var orOpType = &operationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: orOperator}
var andOpType = &operationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: andOperator} var andOpType = &operationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: andOperator}
var reduceOpType = &operationType{Type: "REDUCE", NumArgs: 2, Precedence: 35, Handler: reduceOperator} var reduceOpType = &operationType{Type: "REDUCE", NumArgs: 2, Precedence: 35, Handler: reduceOperator}
@ -61,6 +58,11 @@ var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence:
var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator} var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator}
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator}
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
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 toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator} var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator}
var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator} var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator}
var withEntriesOpType = &operationType{Type: "WITH_ENTRIES", NumArgs: 1, Precedence: 50, Handler: withEntriesOperator} var withEntriesOpType = &operationType{Type: "WITH_ENTRIES", NumArgs: 1, Precedence: 50, Handler: withEntriesOperator}
@ -99,6 +101,8 @@ var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs:
var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: selectOperator} var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: selectOperator}
var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator} var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator}
var uniqueOpType = &operationType{Type: "UNIQUE", NumArgs: 0, Precedence: 50, Handler: unique}
var uniqueByOpType = &operationType{Type: "UNIQUE_BY", NumArgs: 1, Precedence: 50, Handler: uniqueBy}
var deleteChildOpType = &operationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: deleteChildOperator} var deleteChildOpType = &operationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: deleteChildOperator}
type Operation struct { type Operation struct {

View File

@ -39,7 +39,7 @@ func toNodes(candidate *CandidateNode) []*yaml.Node {
func addOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func addOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("Add operator") log.Debugf("Add operator")
return crossFunction(d, context, expressionNode, add, false) return crossFunction(d, context.ReadOnlyClone(), expressionNode, add, false)
} }
func add(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func add(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {

View File

@ -14,6 +14,22 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[1 a], (!!int)::3\n", "D0, P[1 a], (!!int)::3\n",
}, },
}, },
{
skipDoc: true,
document: `{}`,
expression: "(.a + .b) as $x",
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
skipDoc: true,
document: `a: 0`,
expression: ".a += .b.c",
expected: []string{
"D0, P[], (doc)::a: 0\n",
},
},
{ {
description: "Concatenate and assign arrays", description: "Concatenate and assign arrays",
document: `{a: {val: thing, b: [cat,dog]}}`, document: `{a: {val: thing, b: [cat,dog]}}`,

View File

@ -2,7 +2,7 @@ package yqlib
func alternativeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func alternativeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- alternative") log.Debugf("-- alternative")
return crossFunction(d, context, expressionNode, alternativeFunc, true) return crossFunction(d, context.ReadOnlyClone(), expressionNode, alternativeFunc, true)
} }
func alternativeFunc(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func alternativeFunc(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {

View File

@ -5,6 +5,20 @@ import (
) )
var alternativeOperatorScenarios = []expressionScenario{ var alternativeOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: `.b // .c`,
document: `a: bridge`,
expected: []string{},
},
{
skipDoc: true,
expression: `(.b // "hello") as $x`,
document: `a: bridge`,
expected: []string{
"D0, P[], (doc)::a: bridge\n",
},
},
{ {
description: "LHS is defined", description: "LHS is defined",
expression: `.a // "hello"`, expression: `.a // "hello"`,

View File

@ -12,7 +12,7 @@ func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *
aliasName := "" aliasName := ""
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -32,7 +32,7 @@ func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *
log.Debugf("Setting aliasName : %v", candidate.GetKey()) log.Debugf("Setting aliasName : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -41,8 +41,10 @@ func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *
} }
} }
candidate.Node.Kind = yaml.AliasNode if aliasName != "" {
candidate.Node.Value = aliasName candidate.Node.Kind = yaml.AliasNode
candidate.Node.Value = aliasName
}
} }
return context, nil return context, nil
} }
@ -66,7 +68,7 @@ func assignAnchorOperator(d *dataTreeNavigator, context Context, expressionNode
anchorName := "" anchorName := ""
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -87,7 +89,7 @@ func assignAnchorOperator(d *dataTreeNavigator, context Context, expressionNode
log.Debugf("Setting anchorName of : %v", candidate.GetKey()) log.Debugf("Setting anchorName of : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -58,6 +58,22 @@ var anchorOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::a: &cat {b: cat}\n", "D0, P[], (doc)::a: &cat {b: cat}\n",
}, },
}, },
{
skipDoc: true,
document: `a: {c: cat}`,
expression: `.a anchor |= .b`,
expected: []string{
"D0, P[], (doc)::a: {c: cat}\n",
},
},
{
skipDoc: true,
document: `a: {c: cat}`,
expression: `.a anchor = .b`,
expected: []string{
"D0, P[], (doc)::a: {c: cat}\n",
},
},
{ {
description: "Get alias", description: "Get alias",
document: `{b: &billyBob meow, a: *billyBob}`, document: `{b: &billyBob meow, a: *billyBob}`,
@ -74,6 +90,30 @@ var anchorOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{b: &meow purr, a: *meow}\n", "D0, P[], (doc)::{b: &meow purr, a: *meow}\n",
}, },
}, },
{
description: "Set alias to blank does nothing",
document: `{b: &meow purr, a: cat}`,
expression: `.a alias = ""`,
expected: []string{
"D0, P[], (doc)::{b: &meow purr, a: cat}\n",
},
},
{
skipDoc: true,
document: `{b: &meow purr, a: cat}`,
expression: `.a alias = .c`,
expected: []string{
"D0, P[], (doc)::{b: &meow purr, a: cat}\n",
},
},
{
skipDoc: true,
document: `{b: &meow purr, a: cat}`,
expression: `.a alias |= .c`,
expected: []string{
"D0, P[], (doc)::{b: &meow purr, a: cat}\n",
},
},
{ {
description: "Set alias relatively using assign-update", description: "Set alias relatively using assign-update",
document: `{b: &meow purr, a: {f: meow}}`, document: `{b: &meow purr, a: {f: meow}}`,
@ -159,7 +199,7 @@ foobar:
}, },
} }
func TestAnchorAliaseOperatorScenarios(t *testing.T) { func TestAnchorAliasOperatorScenarios(t *testing.T) {
for _, tt := range anchorOperatorScenarios { for _, tt := range anchorOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }

View File

@ -7,7 +7,7 @@ func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode
} }
var rhs Context var rhs Context
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err = d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
} }
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
@ -44,7 +44,7 @@ func assignAttributesOperator(d *dataTreeNavigator, context Context, expressionN
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err

View File

@ -12,6 +12,22 @@ var assignOperatorScenarios = []expressionScenario{
"D0, P[], ()::a:\n b: cat\nx: frog\n", "D0, P[], ()::a:\n b: cat\nx: frog\n",
}, },
}, },
{
skipDoc: true,
document: "{}",
expression: `.a |= .b`,
expected: []string{
"D0, P[], (doc)::{a: null}\n",
},
},
{
skipDoc: true,
document: "{}",
expression: `.a = .b`,
expected: []string{
"D0, P[], (doc)::{a: null}\n",
},
},
{ {
description: "Update node to be the child value", description: "Update node to be the child value",
document: `{a: {b: {g: foof}}}`, document: `{a: {b: {g: foof}}}`,

View File

@ -2,14 +2,13 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func isTruthy(c *CandidateNode) (bool, error) { func isTruthyNode(node *yaml.Node) (bool, error) {
node := unwrapDoc(c.Node)
value := true value := true
if node.Tag == "!!null" { if node.Tag == "!!null" {
return false, nil return false, nil
} }
@ -23,6 +22,11 @@ func isTruthy(c *CandidateNode) (bool, error) {
return value, nil return value, nil
} }
func isTruthy(c *CandidateNode) (bool, error) {
node := unwrapDoc(c.Node)
return isTruthyNode(node)
}
type boolOp func(bool, bool) bool type boolOp func(bool, bool) bool
func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
@ -61,9 +65,76 @@ func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *C
} }
} }
func findBoolean(wantBool bool, d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, sequenceNode *yaml.Node) (bool, error) {
for _, node := range sequenceNode.Content {
if expressionNode != nil {
//need to evaluate the expression against the node
candidate := &CandidateNode{Node: node}
rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode)
if err != nil {
return false, err
}
if rhs.MatchingNodes.Len() > 0 {
node = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
} else {
// no results found, ignore this entry
continue
}
}
truthy, err := isTruthyNode(node)
if err != nil {
return false, err
}
if truthy == wantBool {
return true, nil
}
}
return false, nil
}
func allOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
candidateNode := unwrapDoc(candidate.Node)
if candidateNode.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
}
booleanResult, err := findBoolean(false, d, context, expressionNode.Rhs, candidateNode)
if err != nil {
return Context{}, err
}
result := createBooleanCandidate(candidate, !booleanResult)
results.PushBack(result)
}
return context.ChildContext(results), nil
}
func anyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
candidateNode := unwrapDoc(candidate.Node)
if candidateNode.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
}
booleanResult, err := findBoolean(true, d, context, expressionNode.Rhs, candidateNode)
if err != nil {
return Context{}, err
}
result := createBooleanCandidate(candidate, booleanResult)
results.PushBack(result)
}
return context.ChildContext(results), nil
}
func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- orOp") log.Debugf("-- orOp")
return crossFunction(d, context, expressionNode, performBoolOp( return crossFunction(d, context.ReadOnlyClone(), expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool { func(b1 bool, b2 bool) bool {
log.Debugf("-- peformingOrOp with %v and %v", b1, b2) log.Debugf("-- peformingOrOp with %v and %v", b1, b2)
return b1 || b2 return b1 || b2
@ -72,7 +143,7 @@ func orOperator(d *dataTreeNavigator, context Context, expressionNode *Expressio
func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- AndOp") log.Debugf("-- AndOp")
return crossFunction(d, context, expressionNode, performBoolOp( return crossFunction(d, context.ReadOnlyClone(), expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool { func(b1 bool, b2 bool) bool {
return b1 && b2 return b1 && b2
}), true) }), true)

View File

@ -6,12 +6,20 @@ import (
var booleanOperatorScenarios = []expressionScenario{ var booleanOperatorScenarios = []expressionScenario{
{ {
description: "OR example", description: "`or` example",
expression: `true or false`, expression: `true or false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
}, },
}, },
{
skipDoc: true,
document: "b: hi",
expression: `.a or .c`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: "b: hi", document: "b: hi",
@ -29,7 +37,7 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "AND example", description: "`and` example",
expression: `true and false`, expression: `true and false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
@ -43,6 +51,86 @@ var booleanOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n", "D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n",
}, },
}, },
{
description: "`any` returns true if any boolean in a given array is true",
document: `[false, true]`,
expression: "any",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "`any` returns false for an empty array",
document: `[]`,
expression: "any",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "`any_c` returns true if any element in the array is true for the given condition.",
document: "a: [rad, awesome]\nb: [meh, whatever]",
expression: `.[] |= any_c(. == "awesome")`,
expected: []string{
"D0, P[], (doc)::a: true\nb: false\n",
},
},
{
skipDoc: true,
document: `[{pet: cat}]`,
expression: `any_c(.name == "harry") as $c`,
expected: []string{
"D0, P[], (doc)::[{pet: cat}]\n",
},
},
{
skipDoc: true,
document: `[{pet: cat}]`,
expression: `all_c(.name == "harry") as $c`,
expected: []string{
"D0, P[], (doc)::[{pet: cat}]\n",
},
},
{
skipDoc: true,
document: `[false, false]`,
expression: "any",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "`all` returns true if all booleans in a given array are true",
document: `[true, true]`,
expression: "all",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `[false, true]`,
expression: "all",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "`all` returns true for an empty array",
document: `[]`,
expression: "all",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "`all_c` returns true if all elements in the array are true for the given condition.",
document: "a: [rad, awesome]\nb: [meh, 12]",
expression: `.[] |= all_c(tag == "!!str")`,
expected: []string{
"D0, P[], (doc)::a: true\nb: false\n",
},
},
{ {
skipDoc: true, skipDoc: true,
expression: `false or false`, expression: `false or false`,
@ -61,6 +149,22 @@ var booleanOperatorScenarios = []expressionScenario{
"D0, P[b], (!!bool)::true\n", "D0, P[b], (!!bool)::true\n",
}, },
}, },
{
skipDoc: true,
document: `{}`,
expression: `(.a.b or .c) as $x`,
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
skipDoc: true,
document: `{}`,
expression: `(.a.b and .c) as $x`,
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{ {
description: "Not true is false", description: "Not true is false",
expression: `true | not`, expression: `true | not`,

View File

@ -17,9 +17,12 @@ import (
... ...
*/ */
func collectObjectOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func collectObjectOperator(d *dataTreeNavigator, originalContext Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- collectObjectOperation") log.Debugf("-- collectObjectOperation")
context := originalContext.Clone()
context.DontAutoCreate = false
if context.MatchingNodes.Len() == 0 { if context.MatchingNodes.Len() == 0 {
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"} node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"}
candidate := &CandidateNode{Node: node} candidate := &CandidateNode{Node: node}

View File

@ -5,6 +5,30 @@ import (
) )
var collectObjectOperatorScenarios = []expressionScenario{ var collectObjectOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: "a: []",
expression: `.a += [{"key": "att2", "value": "val2"}]`,
expected: []string{
"D0, P[], (doc)::a: [{key: att2, value: val2}]\n",
},
},
{
skipDoc: true,
document: "",
expression: `.a += {"key": "att2", "value": "val2"}`,
expected: []string{
"D0, P[], ()::a:\n key: att2\n value: val2\n",
},
},
{
skipDoc: true,
document: "",
expression: `.a += [0]`,
expected: []string{
"D0, P[], ()::a:\n - 0\n",
},
},
{ {
description: `Collect empty object`, description: `Collect empty object`,
document: ``, document: ``,

View File

@ -5,6 +5,14 @@ import (
) )
var collectOperatorScenarios = []expressionScenario{ var collectOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: ``,
expression: `.a += [0]`,
expected: []string{
"D0, P[], ()::a:\n - 0\n",
},
},
{ {
description: "Collect empty", description: "Collect empty",
document: ``, document: ``,

View File

@ -27,7 +27,7 @@ func assignCommentsOperator(d *dataTreeNavigator, context Context, expressionNod
comment := "" comment := ""
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -41,7 +41,7 @@ func assignCommentsOperator(d *dataTreeNavigator, context Context, expressionNod
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
if expressionNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -62,6 +62,22 @@ var commentOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::a: cat\n\n# cat\n", "D0, P[], (doc)::a: cat\n\n# cat\n",
}, },
}, },
{
skipDoc: true,
document: `a: cat`,
expression: `. footComment=.b.d`,
expected: []string{
"D0, P[], (doc)::a: cat\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: `. footComment|=.b.d`,
expected: []string{
"D0, P[], (doc)::a: cat\n",
},
},
{ {
description: "Remove comment", description: "Remove comment",
document: "a: cat # comment\nb: dog # leave this", document: "a: cat # comment\nb: dog # leave this",

View File

@ -7,9 +7,7 @@ import (
) )
func deleteChildOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func deleteChildOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
contextToUse := context.Clone() nodesToDelete, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
contextToUse.DontAutoCreate = true
nodesToDelete, err := d.GetMatchingNodes(contextToUse, expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err

View File

@ -59,7 +59,9 @@ func toEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *Ex
case yaml.SequenceNode: case yaml.SequenceNode:
results.PushBack(toEntriesfromSeq(candidate)) results.PushBack(toEntriesfromSeq(candidate))
default: default:
return Context{}, fmt.Errorf("%v has no keys", candidate.Node.Tag) if candidateNode.Tag != "!!null" {
return Context{}, fmt.Errorf("%v has no keys", candidate.Node.Tag)
}
} }
} }
@ -133,26 +135,26 @@ func withEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *
//to_entries on the context //to_entries on the context
toEntries, err := toEntriesOperator(d, context, expressionNode) toEntries, err := toEntriesOperator(d, context, expressionNode)
if err != nil { if err != nil {
return Context{}, nil return Context{}, err
} }
//run expression against entries //run expression against entries
// splat toEntries and pipe it into Rhs // splat toEntries and pipe it into Rhs
splatted, err := splat(d, toEntries, traversePreferences{}) splatted, err := splat(d, toEntries, traversePreferences{})
if err != nil { if err != nil {
return Context{}, nil return Context{}, err
} }
result, err := d.GetMatchingNodes(splatted, expressionNode.Rhs) result, err := d.GetMatchingNodes(splatted, expressionNode.Rhs)
log.Debug("expressionNode.Rhs %v", expressionNode.Rhs.Operation.OperationType) log.Debug("expressionNode.Rhs %v", expressionNode.Rhs.Operation.OperationType)
log.Debug("result %v", result) log.Debug("result %v", result)
if err != nil { if err != nil {
return Context{}, nil return Context{}, err
} }
collected, err := collectOperator(d, result, expressionNode) collected, err := collectOperator(d, result, expressionNode)
if err != nil { if err != nil {
return Context{}, nil return Context{}, err
} }
//from_entries on the result //from_entries on the result

View File

@ -21,6 +21,12 @@ var entriesOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::- key: 0\n value: a\n- key: 1\n value: b\n", "D0, P[], (!!seq)::- key: 0\n value: a\n- key: 1\n value: b\n",
}, },
}, },
{
description: "to_entries null",
document: `null`,
expression: `to_entries`,
expected: []string{},
},
{ {
description: "from_entries map", description: "from_entries map",
document: `{a: 1, b: 2}`, document: `{a: 1, b: 2}`,

View File

@ -4,20 +4,32 @@ import "gopkg.in/yaml.v3"
func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- equalsOperation") log.Debugf("-- equalsOperation")
return crossFunction(d, context, expressionNode, isEquals(false), true) return crossFunction(d, context.ReadOnlyClone(), expressionNode, isEquals(false), true)
} }
func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
value := false value := false
log.Debugf("-- isEquals cross function")
if lhs == nil && rhs == nil { if lhs == nil && rhs == nil {
owner := &CandidateNode{} owner := &CandidateNode{}
return createBooleanCandidate(owner, !flip), nil return createBooleanCandidate(owner, !flip), nil
} else if lhs == nil { } else if lhs == nil {
return createBooleanCandidate(rhs, flip), nil log.Debugf("lhs nil, but rhs is not")
rhsNode := unwrapDoc(rhs.Node)
value := rhsNode.Tag == "!!null"
if flip {
value = !value
}
return createBooleanCandidate(rhs, value), nil
} else if rhs == nil { } else if rhs == nil {
return createBooleanCandidate(lhs, flip), nil log.Debugf("lhs not nil, but rhs is")
lhsNode := unwrapDoc(lhs.Node)
value := lhsNode.Tag == "!!null"
if flip {
value = !value
}
return createBooleanCandidate(lhs, value), nil
} }
lhsNode := unwrapDoc(lhs.Node) lhsNode := unwrapDoc(lhs.Node)
@ -37,6 +49,6 @@ func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *Candid
} }
func notEqualsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func notEqualsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- equalsOperation") log.Debugf("-- notEqualsOperator")
return crossFunction(d, context, expressionNode, isEquals(true), true) return crossFunction(d, context.ReadOnlyClone(), expressionNode, isEquals(true), true)
} }

View File

@ -5,6 +5,13 @@ import (
) )
var equalsOperatorScenarios = []expressionScenario{ var equalsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: ".a == .b",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: "cat", document: "cat",
@ -14,6 +21,60 @@ var equalsOperatorScenarios = []expressionScenario{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
}, },
}, },
{
skipDoc: true,
document: "{}",
expression: "(.a == .b) as $x",
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
skipDoc: true,
document: "{}",
expression: ".a == .b",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "{}",
expression: "(.a != .b) as $x",
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
skipDoc: true,
document: "{}",
expression: ".a != .b",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(.c != null)",
expected: []string{},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(.d == .c)",
expected: []string{
"D0, P[], (doc)::{a: {b: 10}}\n",
},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(null == .c)",
expected: []string{
"D0, P[], (doc)::{a: {b: 10}}\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: "{a: { b: {things: \"\"}, f: [1], g: [] }}", document: "{a: { b: {things: \"\"}, f: [1], g: [] }}",

View File

@ -12,14 +12,19 @@ func hasOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
log.Debugf("-- hasOperation") log.Debugf("-- hasOperation")
var results = list.New() var results = list.New()
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
wanted := rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
wantedKey := wanted.Value
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
wantedKey := "null"
wanted := &yaml.Node{Tag: "!!null"}
if rhs.MatchingNodes.Len() != 0 {
wanted = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
wantedKey = wanted.Value
}
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)

View File

@ -13,6 +13,22 @@ var hasOperatorScenarios = []expressionScenario{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
}, },
}, },
{
skipDoc: true,
document: `a: hello`,
expression: `has(.b) as $c`,
expected: []string{
"D0, P[], (doc)::a: hello\n",
},
},
{
skipDoc: true,
document: `a: hello`,
expression: `has(.b)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{ {
description: "Has map key", description: "Has map key",
document: `- a: "yes" document: `- a: "yes"

View File

@ -11,9 +11,7 @@ func selectOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
childContext := context.SingleChildContext(candidate) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
childContext.DontAutoCreate = true
rhs, err := d.GetMatchingNodes(childContext, expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
@ -24,7 +22,9 @@ func selectOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
if first != nil { if first != nil {
result := first.Value.(*CandidateNode) result := first.Value.(*CandidateNode)
log.Debugf("result %v", NodeToString(result))
includeResult, errDecoding := isTruthy(result) includeResult, errDecoding := isTruthy(result)
log.Debugf("isTruthy %v", includeResult)
if errDecoding != nil { if errDecoding != nil {
return Context{}, errDecoding return Context{}, errDecoding
} }

View File

@ -10,7 +10,7 @@ func sortKeysOperator(d *dataTreeNavigator, context Context, expressionNode *Exp
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -13,6 +13,14 @@ var sortKeysOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: blah, b: bing, c: frog}\n", "D0, P[], (doc)::{a: blah, b: bing, c: frog}\n",
}, },
}, },
{
skipDoc: true,
document: `{c: frog}`,
expression: `sortKeys(.d)`,
expected: []string{
"D0, P[], (doc)::{c: frog}\n",
},
},
{ {
description: "Sort keys recursively", description: "Sort keys recursively",
subdescription: "Note the array elements are left unsorted, but maps inside arrays are sorted", subdescription: "Note the array elements are left unsorted, but maps inside arrays are sorted",

View File

@ -13,7 +13,7 @@ func getSubstituteParameters(d *dataTreeNavigator, block *ExpressionNode, contex
regEx := "" regEx := ""
replacementText := "" replacementText := ""
regExNodes, err := d.GetMatchingNodes(context, block.Lhs) regExNodes, err := d.GetMatchingNodes(context.ReadOnlyClone(), block.Lhs)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -78,7 +78,7 @@ func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *E
log.Debugf("-- joinStringOperator") log.Debugf("-- joinStringOperator")
joinStr := "" joinStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -119,7 +119,7 @@ func splitStringOperator(d *dataTreeNavigator, context Context, expressionNode *
log.Debugf("-- splitStringOperator") log.Debugf("-- splitStringOperator")
splitStr := "" splitStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -31,7 +31,7 @@ func assignStyleOperator(d *dataTreeNavigator, context Context, expressionNode *
log.Debugf("AssignStyleOperator: %v") log.Debugf("AssignStyleOperator: %v")
var style yaml.Style var style yaml.Style
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -54,7 +54,7 @@ func assignStyleOperator(d *dataTreeNavigator, context Context, expressionNode *
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting style of : %v", candidate.GetKey()) log.Debugf("Setting style of : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -25,7 +25,7 @@ func subtractAssignOperator(d *dataTreeNavigator, context Context, expressionNod
func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("Subtract operator") log.Debugf("Subtract operator")
return crossFunction(d, context, expressionNode, subtract, false) return crossFunction(d, context.ReadOnlyClone(), expressionNode, subtract, false)
} }
func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {

View File

@ -5,6 +5,14 @@ import (
) )
var subtractOperatorScenarios = []expressionScenario{ var subtractOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `{}`,
expression: "(.a - .b) as $x",
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{ {
description: "Number subtraction - float", description: "Number subtraction - float",
subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.", subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",

View File

@ -12,7 +12,7 @@ func assignTagOperator(d *dataTreeNavigator, context Context, expressionNode *Ex
tag := "" tag := ""
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -32,7 +32,7 @@ func assignTagOperator(d *dataTreeNavigator, context Context, expressionNode *Ex
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting tag of : %v", candidate.GetKey()) log.Debugf("Setting tag of : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -89,7 +89,7 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode
// rhs is a collect expression that will yield indexes to retreive of the arrays // rhs is a collect expression that will yield indexes to retreive of the arrays
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
@ -308,6 +308,6 @@ func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *C
func traverseArray(candidate *CandidateNode, operation *Operation, prefs traversePreferences) (*list.List, error) { func traverseArray(candidate *CandidateNode, operation *Operation, prefs traversePreferences) (*list.List, error) {
log.Debug("operation Value %v", operation.Value) log.Debug("operation Value %v", operation.Value)
indices := []*yaml.Node{&yaml.Node{Value: operation.StringValue}} indices := []*yaml.Node{{Value: operation.StringValue}}
return traverseArrayWithIndices(candidate, indices, prefs) return traverseArrayWithIndices(candidate, indices, prefs)
} }

View File

@ -27,6 +27,22 @@ foobar:
` `
var traversePathOperatorScenarios = []expressionScenario{ var traversePathOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `[[1]]`,
expression: `.[0][0]`,
expected: []string{
"D0, P[0 0], (!!int)::1\n",
},
},
{
skipDoc: true,
document: `[[[1]]]`,
expression: `.[0][0][0]`,
expected: []string{
"D0, P[0 0 0], (!!int)::1\n",
},
},
{ {
description: "Simple map navigation", description: "Simple map navigation",
document: `{a: {b: apple}}`, document: `{a: {b: apple}}`,
@ -78,6 +94,14 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[flying fox], (!!str)::frog\n", "D0, P[flying fox], (!!str)::frog\n",
}, },
}, },
{
skipDoc: true,
document: `c: dog`,
expression: `.[.a.b] as $x`,
expected: []string{
"D0, P[], (doc)::c: dog\n",
},
},
{ {
description: "Dynamic keys", description: "Dynamic keys",
subdescription: `Expressions within [] can be used to dynamically lookup / calculate keys`, subdescription: `Expressions within [] can be used to dynamically lookup / calculate keys`,

View File

@ -5,6 +5,20 @@ import (
) )
var unionOperatorScenarios = []expressionScenario{ var unionOperatorScenarios = []expressionScenario{
// {
// skipDoc: true,
// document: "{}",
// expression: `(.a, .b.c) as $x`,
// expected: []string{
// "D0, P[], (doc)::{}\n",
// },
// },
// {
// skipDoc: true,
// document: "{}",
// expression: `(.a, .b.c)`,
// expected: []string{},
// },
{ {
description: "Combine scalars", description: "Combine scalars",
expression: `1, true, "cat"`, expression: `1, true, "cat"`,

View File

@ -0,0 +1,64 @@
package yqlib
import (
"container/list"
"fmt"
"github.com/elliotchance/orderedmap"
yaml "gopkg.in/yaml.v3"
)
func unique(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}}
uniqueByExpression := &ExpressionNode{Operation: &Operation{OperationType: uniqueByOpType}, Rhs: selfExpression}
return uniqueBy(d, context, uniqueByExpression)
}
func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- uniqueBy Operator")
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
candidateNode := unwrapDoc(candidate.Node)
if candidateNode.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("Only arrays are supported for unique")
}
var newMatches = orderedmap.NewOrderedMap()
for _, node := range candidateNode.Content {
child := &CandidateNode{Node: node}
rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(child), expressionNode.Rhs)
if err != nil {
return Context{}, err
}
keyValue := "null"
if rhs.MatchingNodes.Len() > 0 {
first := rhs.MatchingNodes.Front()
keyCandidate := first.Value.(*CandidateNode)
keyValue = keyCandidate.Node.Value
}
_, exists := newMatches.Get(keyValue)
if !exists {
newMatches.Set(keyValue, child.Node)
}
}
resultNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
for el := newMatches.Front(); el != nil; el = el.Next() {
resultNode.Content = append(resultNode.Content, el.Value.(*yaml.Node))
}
results.PushBack(candidate.CreateChild(nil, resultNode))
}
return context.ChildContext(results), nil
}

View File

@ -0,0 +1,65 @@
package yqlib
import (
"testing"
)
var uniqueOperatorScenarios = []expressionScenario{
{
description: "Unique array of scalars (string/numbers)",
document: `[1,2,3,2]`,
expression: `unique`,
expected: []string{
"D0, P[], (!!seq)::- 1\n- 2\n- 3\n",
},
},
{
description: "Unique nulls",
subdescription: "Unique works on the node value, so it considers different representations of nulls to be different",
document: `[~,null, ~, null]`,
expression: `unique`,
expected: []string{
"D0, P[], (!!seq)::- ~\n- null\n",
},
},
{
description: "Unique all nulls",
subdescription: "Run against the node tag to unique all the nulls",
document: `[~,null, ~, null]`,
expression: `unique_by(tag)`,
expected: []string{
"D0, P[], (!!seq)::- ~\n",
},
},
{
description: "Unique array object fields",
document: `[{name: harry, pet: cat}, {name: billy, pet: dog}, {name: harry, pet: dog}]`,
expression: `unique_by(.name)`,
expected: []string{
"D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {name: billy, pet: dog}\n",
},
},
{
skipDoc: true,
document: `[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]`,
expression: `unique_by(.name)`,
expected: []string{
"D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {pet: fish}\n",
},
},
{
skipDoc: true,
document: `[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]`,
expression: `unique_by(.cat.dog)`,
expected: []string{
"D0, P[], (!!seq)::- {name: harry, pet: cat}\n",
},
},
}
func TestUniqueOperatorScenarios(t *testing.T) {
for _, tt := range uniqueOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Unique", uniqueOperatorScenarios)
}

View File

@ -16,7 +16,7 @@ func getVariableOperator(d *dataTreeNavigator, context Context, expressionNode *
} }
func assignVariableOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func assignVariableOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs) lhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Lhs)
if err != nil { if err != nil {
return Context{}, nil return Context{}, nil
} }

View File

@ -5,6 +5,14 @@ import (
) )
var variableOperatorScenarios = []expressionScenario{ var variableOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `{}`,
expression: `.a.b as $foo`,
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{ {
description: "Single value variable", description: "Single value variable",
document: `a: cat`, document: `a: cat`,

View File

@ -24,7 +24,19 @@ func emptyOperator(d *dataTreeNavigator, context Context, expressionNode *Expres
type crossFunctionCalculation func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) type crossFunctionCalculation func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error)
func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *CandidateNode, rhs Context, calculation crossFunctionCalculation, results *list.List) error { func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *CandidateNode, rhs Context, calculation crossFunctionCalculation, results *list.List, calcWhenEmpty bool) error {
if calcWhenEmpty && rhs.MatchingNodes.Len() == 0 {
resultCandidate, err := calculation(d, context, lhsCandidate, nil)
if err != nil {
return err
}
if resultCandidate != nil {
results.PushBack(resultCandidate)
}
return nil
}
for rightEl := rhs.MatchingNodes.Front(); rightEl != nil; rightEl = rightEl.Next() { for rightEl := rhs.MatchingNodes.Front(); rightEl != nil; rightEl = rightEl.Next() {
log.Debugf("Applying calc") log.Debugf("Applying calc")
rhsCandidate := rightEl.Value.(*CandidateNode) rhsCandidate := rightEl.Value.(*CandidateNode)
@ -32,7 +44,9 @@ func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *Candidat
if err != nil { if err != nil {
return err return err
} }
results.PushBack(resultCandidate) if resultCandidate != nil {
results.PushBack(resultCandidate)
}
} }
return nil return nil
} }
@ -52,14 +66,7 @@ func doCrossFunc(d *dataTreeNavigator, context Context, expressionNode *Expressi
} }
if calcWhenEmpty && lhs.MatchingNodes.Len() == 0 { if calcWhenEmpty && lhs.MatchingNodes.Len() == 0 {
if rhs.MatchingNodes.Len() == 0 { err := resultsForRhs(d, context, nil, rhs, calculation, results, calcWhenEmpty)
resultCandidate, err := calculation(d, context, nil, nil)
if err != nil {
return Context{}, err
}
results.PushBack(resultCandidate)
}
err := resultsForRhs(d, context, nil, rhs, calculation, results)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -68,7 +75,7 @@ func doCrossFunc(d *dataTreeNavigator, context Context, expressionNode *Expressi
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode) lhsCandidate := el.Value.(*CandidateNode)
err := resultsForRhs(d, context, lhsCandidate, rhs, calculation, results) err := resultsForRhs(d, context, lhsCandidate, rhs, calculation, results, calcWhenEmpty)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
find . \( -path ./vendor \) -prune -o -name "*.go" -exec goimports -w {} \; find . \( -path ./vendor \) -prune -o -name "*.go" -exec goimports -w {} \;
gofmt -w -s .
go mod tidy go mod tidy
go mod vendor go mod vendor

View File

@ -1,5 +1,5 @@
name: yq name: yq
version: '4.8.0' version: '4.9.3'
summary: A lightweight and portable command-line YAML processor summary: A lightweight and portable command-line YAML processor
description: | description: |
The aim of the project is to be the jq or sed of yaml files. The aim of the project is to be the jq or sed of yaml files.