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
// 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)
// 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

View File

@ -12,6 +12,14 @@ type Context struct {
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 {
list := list.New()
list.PushBack(candidate)
@ -52,3 +60,9 @@ func (n *Context) Clone() Context {
}
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
```
## 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
Given a sample.yml file of:
```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.
## OR example
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.
## `or` example
Running
```bash
yq eval --null-input 'true or false'
@ -9,7 +18,7 @@ will output
true
```
## AND example
## `and` example
Running
```bash
yq eval --null-input 'true and false'
@ -41,6 +50,104 @@ will output
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
Running
```bash

View File

@ -35,6 +35,19 @@ will output
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
Given a sample.yml file of:
```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.
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
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
a: cat
a: dog
b: dog
```
# 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]
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
// on the close op - move it to the collect op.
// allows for .["cat"]?
@ -68,6 +68,12 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
result = append(result, &Operation{OperationType: shortPipeOpType})
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:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket {
opStack, result = popOpToResult(opStack, result)

View File

@ -12,6 +12,21 @@ var pathTests = []struct {
expectedTokens []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(".")`,
append(make([]interface{}, 0), "[", "EMPTY", "]", "PIPE", "JOIN", "(", ". (string)", ")"),

View File

@ -189,6 +189,7 @@ func stringValue(wrapped bool) lex.Action {
if wrapped {
value = unwrap(value)
}
value = strings.ReplaceAll(value, "\\\"", "\"")
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(`select`), opToken(selectOpType))
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(`or`), opToken(orOpType))
lexer.Add([]byte(`and`), opToken(andOpType))
@ -274,6 +277,11 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`join`), opToken(joinStringOpType))
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(`keys`), opToken(keysOpType))
@ -327,7 +335,7 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`[Nn][Uu][Ll][Ll]`), nullValue())
lexer.Add([]byte(`~`), nullValue())
lexer.Add([]byte(`"[^"]*"`), stringValue(true))
lexer.Add([]byte(`"([^"\\]*(\\.[^"\\]*)*)"`), stringValue(true))
lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true))
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
@ -403,16 +411,19 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
skipNextToken = false
currentToken := tokens[index]
log.Debug("processing %v", currentToken.toString(true))
if currentToken.TokenType == traverseArrayCollect {
//need to put a traverse array then a collect currentToken
// do this by adding traverse then converting currentToken to collect
if index == 0 || tokens[index-1].TokenType != operationToken ||
tokens[index-1].Operation.OperationType != traversePathOpType {
log.Debug(" adding self")
op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
log.Debug(" adding traverse array")
op := &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"}
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 &&
tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == assignOpType {
log.Debug(" its an update assign")
currentToken.Operation = currentToken.AssignOperation
currentToken.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
skipNextToken = true
}
log.Debug(" adding token to the fixed list")
postProcessedTokens = append(postProcessedTokens, currentToken)
if index != len(tokens)-1 &&
((currentToken.TokenType == openCollect && tokens[index+1].TokenType == closeCollect) ||
(currentToken.TokenType == openCollectObject && tokens[index+1].TokenType == closeCollectObject)) {
log.Debug(" adding empty")
op := &Operation{OperationType: emptyOpType, StringValue: "EMPTY"}
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 &&
tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == traversePathOpType {
log.Debug(" adding pipe because the next thing is traverse")
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
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}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}

View File

@ -20,9 +20,6 @@ type operationType struct {
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 andOpType = &operationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: andOperator}
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 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 fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator}
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 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}
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) {
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) {

View File

@ -14,6 +14,22 @@ var addOperatorScenarios = []expressionScenario{
"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",
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) {
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) {

View File

@ -5,6 +5,20 @@ import (
)
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",
expression: `.a // "hello"`,

View File

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

View File

@ -58,6 +58,22 @@ var anchorOperatorScenarios = []expressionScenario{
"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",
document: `{b: &billyBob meow, a: *billyBob}`,
@ -74,6 +90,30 @@ var anchorOperatorScenarios = []expressionScenario{
"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",
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 {
testScenario(t, &tt)
}

View File

@ -7,7 +7,7 @@ func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode
}
var rhs Context
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() {
@ -44,7 +44,7 @@ func assignAttributesOperator(d *dataTreeNavigator, context Context, expressionN
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
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 {
return Context{}, err

View File

@ -12,6 +12,22 @@ var assignOperatorScenarios = []expressionScenario{
"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",
document: `{a: {b: {g: foof}}}`,

View File

@ -2,14 +2,13 @@ package yqlib
import (
"container/list"
"fmt"
yaml "gopkg.in/yaml.v3"
)
func isTruthy(c *CandidateNode) (bool, error) {
node := unwrapDoc(c.Node)
func isTruthyNode(node *yaml.Node) (bool, error) {
value := true
if node.Tag == "!!null" {
return false, nil
}
@ -23,6 +22,11 @@ func isTruthy(c *CandidateNode) (bool, error) {
return value, nil
}
func isTruthy(c *CandidateNode) (bool, error) {
node := unwrapDoc(c.Node)
return isTruthyNode(node)
}
type boolOp func(bool, bool) bool
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) {
log.Debugf("-- orOp")
return crossFunction(d, context, expressionNode, performBoolOp(
return crossFunction(d, context.ReadOnlyClone(), expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool {
log.Debugf("-- peformingOrOp with %v and %v", 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) {
log.Debugf("-- AndOp")
return crossFunction(d, context, expressionNode, performBoolOp(
return crossFunction(d, context.ReadOnlyClone(), expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool {
return b1 && b2
}), true)

View File

@ -6,12 +6,20 @@ import (
var booleanOperatorScenarios = []expressionScenario{
{
description: "OR example",
description: "`or` example",
expression: `true or false`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "b: hi",
expression: `.a or .c`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "b: hi",
@ -29,7 +37,7 @@ var booleanOperatorScenarios = []expressionScenario{
},
},
{
description: "AND example",
description: "`and` example",
expression: `true and false`,
expected: []string{
"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",
},
},
{
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,
expression: `false or false`,
@ -61,6 +149,22 @@ var booleanOperatorScenarios = []expressionScenario{
"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",
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")
context := originalContext.Clone()
context.DontAutoCreate = false
if context.MatchingNodes.Len() == 0 {
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"}
candidate := &CandidateNode{Node: node}

View File

@ -5,6 +5,30 @@ import (
)
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`,
document: ``,

View File

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

View File

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

View File

@ -62,6 +62,22 @@ var commentOperatorScenarios = []expressionScenario{
"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",
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) {
contextToUse := context.Clone()
contextToUse.DontAutoCreate = true
nodesToDelete, err := d.GetMatchingNodes(contextToUse, expressionNode.Rhs)
nodesToDelete, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
return Context{}, err

View File

@ -59,7 +59,9 @@ func toEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *Ex
case yaml.SequenceNode:
results.PushBack(toEntriesfromSeq(candidate))
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
toEntries, err := toEntriesOperator(d, context, expressionNode)
if err != nil {
return Context{}, nil
return Context{}, err
}
//run expression against entries
// splat toEntries and pipe it into Rhs
splatted, err := splat(d, toEntries, traversePreferences{})
if err != nil {
return Context{}, nil
return Context{}, err
}
result, err := d.GetMatchingNodes(splatted, expressionNode.Rhs)
log.Debug("expressionNode.Rhs %v", expressionNode.Rhs.Operation.OperationType)
log.Debug("result %v", result)
if err != nil {
return Context{}, nil
return Context{}, err
}
collected, err := collectOperator(d, result, expressionNode)
if err != nil {
return Context{}, nil
return Context{}, err
}
//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",
},
},
{
description: "to_entries null",
document: `null`,
expression: `to_entries`,
expected: []string{},
},
{
description: "from_entries map",
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) {
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) {
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
value := false
log.Debugf("-- isEquals cross function")
if lhs == nil && rhs == nil {
owner := &CandidateNode{}
return createBooleanCandidate(owner, !flip), 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 {
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)
@ -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) {
log.Debugf("-- equalsOperation")
return crossFunction(d, context, expressionNode, isEquals(true), true)
log.Debugf("-- notEqualsOperator")
return crossFunction(d, context.ReadOnlyClone(), expressionNode, isEquals(true), true)
}

View File

@ -5,6 +5,13 @@ import (
)
var equalsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: ".a == .b",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "cat",
@ -14,6 +21,60 @@ var equalsOperatorScenarios = []expressionScenario{
"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,
document: "{a: { b: {things: \"\"}, f: [1], g: [] }}",

View File

@ -12,14 +12,19 @@ func hasOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
log.Debugf("-- hasOperation")
var results = list.New()
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
wanted := rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
wantedKey := wanted.Value
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
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() {
candidate := el.Value.(*CandidateNode)

View File

@ -13,6 +13,22 @@ var hasOperatorScenarios = []expressionScenario{
"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",
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() {
candidate := el.Value.(*CandidateNode)
childContext := context.SingleChildContext(candidate)
childContext.DontAutoCreate = true
rhs, err := d.GetMatchingNodes(childContext, expressionNode.Rhs)
rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil {
return Context{}, err
@ -24,7 +22,9 @@ func selectOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
if first != nil {
result := first.Value.(*CandidateNode)
log.Debugf("result %v", NodeToString(result))
includeResult, errDecoding := isTruthy(result)
log.Debugf("isTruthy %v", includeResult)
if errDecoding != nil {
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() {
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 {
return Context{}, err
}

View File

@ -13,6 +13,14 @@ var sortKeysOperatorScenarios = []expressionScenario{
"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",
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 := ""
replacementText := ""
regExNodes, err := d.GetMatchingNodes(context, block.Lhs)
regExNodes, err := d.GetMatchingNodes(context.ReadOnlyClone(), block.Lhs)
if err != nil {
return "", "", err
}
@ -78,7 +78,7 @@ func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *E
log.Debugf("-- joinStringOperator")
joinStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
return Context{}, err
}
@ -119,7 +119,7 @@ func splitStringOperator(d *dataTreeNavigator, context Context, expressionNode *
log.Debugf("-- splitStringOperator")
splitStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
return Context{}, err
}

View File

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

View File

@ -5,6 +5,14 @@ import (
)
var subtractOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `{}`,
expression: "(.a - .b) as $x",
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
description: "Number subtraction - float",
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 := ""
if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
return Context{}, err
}
@ -32,7 +32,7 @@ func assignTagOperator(d *dataTreeNavigator, context Context, expressionNode *Ex
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting tag of : %v", candidate.GetKey())
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 {
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, err := d.GetMatchingNodes(context, expressionNode.Rhs)
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
if err != nil {
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) {
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)
}

View File

@ -27,6 +27,22 @@ foobar:
`
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",
document: `{a: {b: apple}}`,
@ -78,6 +94,14 @@ var traversePathOperatorScenarios = []expressionScenario{
"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",
subdescription: `Expressions within [] can be used to dynamically lookup / calculate keys`,

View File

@ -5,6 +5,20 @@ import (
)
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",
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) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
lhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Lhs)
if err != nil {
return Context{}, nil
}

View File

@ -5,6 +5,14 @@ import (
)
var variableOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `{}`,
expression: `.a.b as $foo`,
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
description: "Single value variable",
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)
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() {
log.Debugf("Applying calc")
rhsCandidate := rightEl.Value.(*CandidateNode)
@ -32,7 +44,9 @@ func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *Candidat
if err != nil {
return err
}
results.PushBack(resultCandidate)
if resultCandidate != nil {
results.PushBack(resultCandidate)
}
}
return nil
}
@ -52,14 +66,7 @@ func doCrossFunc(d *dataTreeNavigator, context Context, expressionNode *Expressi
}
if calcWhenEmpty && lhs.MatchingNodes.Len() == 0 {
if rhs.MatchingNodes.Len() == 0 {
resultCandidate, err := calculation(d, context, nil, nil)
if err != nil {
return Context{}, err
}
results.PushBack(resultCandidate)
}
err := resultsForRhs(d, context, nil, rhs, calculation, results)
err := resultsForRhs(d, context, nil, rhs, calculation, results, calcWhenEmpty)
if err != nil {
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() {
lhsCandidate := el.Value.(*CandidateNode)
err := resultsForRhs(d, context, lhsCandidate, rhs, calculation, results)
err := resultsForRhs(d, context, lhsCandidate, rhs, calculation, results, calcWhenEmpty)
if err != nil {
return Context{}, err
}

View File

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

View File

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