mirror of
https://github.com/taigrr/yq
synced 2025-01-18 04:53:17 -08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c3965dca3 | ||
|
|
bb3ffd40b5 | ||
|
|
cc08afc435 | ||
|
|
941a453163 | ||
|
|
77630ca179 | ||
|
|
ae4b606707 | ||
|
|
37f3e21970 | ||
|
|
25d0787011 | ||
|
|
b5b8da0a1d | ||
|
|
fa21510194 | ||
|
|
f541194250 | ||
|
|
38666f4db6 |
@@ -11,7 +11,7 @@ var (
|
||||
GitDescribe string
|
||||
|
||||
// Version is main version number that is being run at the moment.
|
||||
Version = "4.7.1"
|
||||
Version = "4.8.0"
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mikefarah/yq:4.7.1
|
||||
FROM mikefarah/yq:4.8.0
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
@@ -3,6 +3,97 @@ Use the `alias` and `anchor` operators to read and write yaml aliases and anchor
|
||||
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
|
||||
|
||||
|
||||
## Merge one map
|
||||
see https://yaml.org/type/merge.html
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
x: 0
|
||||
y: 2
|
||||
- &BIG
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<: *CENTER
|
||||
r: 10
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.[4] | explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
x: 1
|
||||
y: 2
|
||||
r: 10
|
||||
```
|
||||
|
||||
## Merge multiple maps
|
||||
see https://yaml.org/type/merge.html
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
x: 0
|
||||
y: 2
|
||||
- &BIG
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *CENTER
|
||||
- *BIG
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.[4] | explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
r: 10
|
||||
x: 1
|
||||
y: 2
|
||||
```
|
||||
|
||||
## Override
|
||||
see https://yaml.org/type/merge.html
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
x: 0
|
||||
y: 2
|
||||
- &BIG
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *BIG
|
||||
- *LEFT
|
||||
- *SMALL
|
||||
x: 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.[4] | explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
r: 10
|
||||
x: 1
|
||||
y: 2
|
||||
```
|
||||
|
||||
## Get anchor
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -183,9 +274,9 @@ bar:
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: bar_b
|
||||
a: foo_a
|
||||
thing: bar_thing
|
||||
thing: foo_thing
|
||||
c: foobarList_c
|
||||
a: foo_a
|
||||
foobar:
|
||||
c: foo_c
|
||||
a: foo_a
|
||||
|
||||
@@ -88,7 +88,7 @@ a: cat
|
||||
b: dog # leave this
|
||||
```
|
||||
|
||||
## Remove all comments
|
||||
## Remove (strip) all comments
|
||||
Note the use of `...` to ensure key nodes are included.
|
||||
|
||||
Given a sample.yml file of:
|
||||
|
||||
87
pkg/yqlib/doc/Entries.md
Normal file
87
pkg/yqlib/doc/Entries.md
Normal file
@@ -0,0 +1,87 @@
|
||||
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
|
||||
## to_entries Map
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1
|
||||
b: 2
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'to_entries' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- key: a
|
||||
value: 1
|
||||
- key: b
|
||||
value: 2
|
||||
```
|
||||
|
||||
## to_entries Array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a
|
||||
- b
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'to_entries' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- key: 0
|
||||
value: a
|
||||
- key: 1
|
||||
value: b
|
||||
```
|
||||
|
||||
## from_entries map
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1
|
||||
b: 2
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'to_entries | from_entries' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 1
|
||||
b: 2
|
||||
```
|
||||
|
||||
## from_entries with numeric key indexes
|
||||
from_entries always creates a map, even for numeric keys
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a
|
||||
- b
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'to_entries | from_entries' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
0: a
|
||||
1: b
|
||||
```
|
||||
|
||||
## Use with_entries to update keys
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1
|
||||
b: 2
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'with_entries(.key |= "KEY_" + .)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
KEY_a: 1
|
||||
KEY_b: 2
|
||||
```
|
||||
|
||||
@@ -93,3 +93,31 @@ will output
|
||||
true
|
||||
```
|
||||
|
||||
## Non exisitant key doesn't equal a value
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: frog
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'select(.b != "thing")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: frog
|
||||
```
|
||||
|
||||
## Two non existant keys are equal
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: frog
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval 'select(.b == .c)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: frog
|
||||
```
|
||||
|
||||
|
||||
@@ -231,6 +231,46 @@ will output
|
||||
age: 32
|
||||
```
|
||||
|
||||
## Merge arrays of objects together, matching on a key
|
||||
It's a complex command, the trickyness comes from needing to have the right context in the expressions.
|
||||
First we save the second array into a variable '$two' which lets us reference it later.
|
||||
We then need to update the first array. We will use the relative update (|=) because we need to update relative to the current element of the array in the LHS in the RHS expression.
|
||||
We set the current element of the first array as $cur. Now we multiply (merge) $cur with the matching entry in $two, by passing $two through a select filter.
|
||||
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: apple
|
||||
b: appleB
|
||||
- a: kiwi
|
||||
b: kiwiB
|
||||
- a: banana
|
||||
b: bananaB
|
||||
```
|
||||
And another sample another.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
c: bananaC
|
||||
- a: apple
|
||||
b: appleB2
|
||||
- a: dingo
|
||||
c: dingoC
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval-all '(select(fi==1) | .[]) as $two | select(fi==0) | .[] |= (. as $cur | $cur * ($two | select(.a == $cur.a)))' sample.yml another.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- a: apple
|
||||
b: appleB2
|
||||
- a: kiwi
|
||||
b: kiwiB
|
||||
- a: banana
|
||||
b: bananaB
|
||||
c: bananaC
|
||||
```
|
||||
|
||||
## Merge to prefix an element
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -1,4 +1,42 @@
|
||||
The style operator can be used to get or set the style of nodes (e.g. string style, yaml style)
|
||||
## Update and set style of a particular node (simple)
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b: thing
|
||||
c: something
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.a.b = "new" | .a.b style="double"' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b: "new"
|
||||
c: something
|
||||
```
|
||||
|
||||
## Update and set style of a particular node using path variables
|
||||
You can use a variable to re-use a path
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b: thing
|
||||
c: something
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.a.b as $x | $x = "new" | $x style="double"' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b: "new"
|
||||
c: something
|
||||
```
|
||||
|
||||
## Set tagged style
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -32,6 +32,21 @@ b: apple
|
||||
c: banana
|
||||
```
|
||||
|
||||
## Optional Splat
|
||||
Just like splat, but won't error if you run it against scalars
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.[]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## Special characters
|
||||
Use quotes with brackets around path elements with special characters
|
||||
|
||||
@@ -98,6 +113,23 @@ will output
|
||||
null
|
||||
```
|
||||
|
||||
## Optional identifier
|
||||
Like jq, does not output an error when the yaml is not an array or object as expected
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq eval '.a?' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## Wildcard matching
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -4,7 +4,7 @@ In `yq` expressions are made up of operators and pipes. A context of nodes is pa
|
||||
|
||||
Lets look at a couple of examples.
|
||||
|
||||
## Example 1 - simple example
|
||||
## Example with a simple operator
|
||||
|
||||
Given a document like:
|
||||
|
||||
@@ -37,10 +37,45 @@ This being the last operation in the expression, the results will be printed out
|
||||
3
|
||||
```
|
||||
|
||||
# Example 2 - operators with arguments.
|
||||
# Example with an operator that takes arguments.
|
||||
|
||||
Given a document like:
|
||||
|
||||
The `=` operator takes two arguments, a `lhs` expression and `rhs` expression. It runs the 'matching' nodes context against the `lhs` expression to find the nodes to update, lets call it `lhsNodes`, and then runs the matching nodes against the `rhs` to find the new values, lets call that `rhsNodes`. It updates the `lhsNodes` values with the `rhsNodes` values and _returns the original matching nodes_. This is important, where length changed the matching nodes to be new nodes with the length values, `=` returns the original matching nodes, albeit with some of the nodes values updated. So `.a = 3` will still return the parent matching node, but with the matching child updated.
|
||||
```yaml
|
||||
a: cat
|
||||
b: dog
|
||||
```
|
||||
|
||||
Please see the individual operator docs for more information and examples.
|
||||
with an expression:
|
||||
|
||||
```
|
||||
.a = .b
|
||||
```
|
||||
|
||||
The `=` operator takes two arguments, a `lhs` expression, which in this case is `.a` and `rhs` expression which is `.b`.
|
||||
|
||||
It pipes the current, lets call it 'root' context through the `lhs` expression of `.a` to return the node
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```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:
|
||||
|
||||
```yaml
|
||||
a: cat
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
1
pkg/yqlib/doc/headers/Entries.md
Normal file
1
pkg/yqlib/doc/headers/Entries.md
Normal file
@@ -0,0 +1 @@
|
||||
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
|
||||
@@ -55,7 +55,15 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
|
||||
log.Debugf("deleteing open bracket from opstack")
|
||||
|
||||
//and append a collect to the opStack
|
||||
result = append(result, &Operation{OperationType: collectOperator})
|
||||
// hack - see if there's the optional traverse flag
|
||||
// on the close op - move it to the collect op.
|
||||
// allows for .["cat"]?
|
||||
prefs := traversePreferences{}
|
||||
closeTokenMatch := string(currentToken.Match.Bytes)
|
||||
if closeTokenMatch[len(closeTokenMatch)-1:] == "?" {
|
||||
prefs.OptionalTraverse = true
|
||||
}
|
||||
result = append(result, &Operation{OperationType: collectOperator, Preferences: prefs})
|
||||
log.Debugf("put collect onto the result")
|
||||
result = append(result, &Operation{OperationType: shortPipeOpType})
|
||||
log.Debugf("put shortpipe onto the result")
|
||||
|
||||
@@ -62,6 +62,11 @@ var pathTests = []struct {
|
||||
append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"),
|
||||
append(make([]interface{}, 0), "b", "a", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.b[.a]?`,
|
||||
append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"),
|
||||
append(make([]interface{}, 0), "b", "a", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.[]`,
|
||||
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
||||
@@ -72,6 +77,11 @@ var pathTests = []struct {
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
||||
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.a[]?`,
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
||||
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.a.[]`,
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
||||
@@ -82,6 +92,11 @@ var pathTests = []struct {
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
||||
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.a[0]?`,
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
||||
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
||||
},
|
||||
{
|
||||
`.a.[0]`,
|
||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
||||
|
||||
@@ -29,8 +29,9 @@ const (
|
||||
type token struct {
|
||||
TokenType tokenType
|
||||
Operation *Operation
|
||||
AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it
|
||||
CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat
|
||||
AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it
|
||||
CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat
|
||||
Match *machines.Match // match that created this token
|
||||
|
||||
}
|
||||
|
||||
@@ -63,12 +64,19 @@ func (t *token) toString(detail bool) string {
|
||||
func pathToken(wrapped bool) lex.Action {
|
||||
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
|
||||
value := string(m.Bytes)
|
||||
prefs := traversePreferences{}
|
||||
|
||||
if value[len(value)-1:] == "?" {
|
||||
prefs.OptionalTraverse = true
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
|
||||
value = value[1:]
|
||||
if wrapped {
|
||||
value = unwrap(value)
|
||||
}
|
||||
log.Debug("PathToken %v", value)
|
||||
op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: traversePreferences{}}
|
||||
op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: prefs}
|
||||
return &token{TokenType: operationToken, Operation: op, CheckForPostTraverse: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -138,7 +146,7 @@ func assignAllCommentsOp(updateAssign bool) lex.Action {
|
||||
|
||||
func literalToken(pType tokenType, checkForPost bool) lex.Action {
|
||||
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
|
||||
return &token{TokenType: pType, CheckForPostTraverse: checkForPost}, nil
|
||||
return &token{TokenType: pType, CheckForPostTraverse: checkForPost, Match: m}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +286,9 @@ func initLexer() (*lex.Lexer, error) {
|
||||
lexer.Add([]byte(`fileIndex`), opToken(getFileIndexOpType))
|
||||
lexer.Add([]byte(`fi`), opToken(getFileIndexOpType))
|
||||
lexer.Add([]byte(`path`), opToken(getPathOpType))
|
||||
lexer.Add([]byte(`to_entries`), opToken(toEntriesOpType))
|
||||
lexer.Add([]byte(`from_entries`), opToken(fromEntriesOpType))
|
||||
lexer.Add([]byte(`with_entries`), opToken(withEntriesOpType))
|
||||
|
||||
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{LineComment: true}))
|
||||
|
||||
@@ -300,8 +311,8 @@ func initLexer() (*lex.Lexer, error) {
|
||||
|
||||
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
|
||||
|
||||
lexer.Add([]byte(`\."[^ "]+"`), pathToken(true))
|
||||
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+`), pathToken(false))
|
||||
lexer.Add([]byte(`\."[^ "]+"\??`), pathToken(true))
|
||||
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+\??`), pathToken(false))
|
||||
lexer.Add([]byte(`\.`), selfToken())
|
||||
|
||||
lexer.Add([]byte(`\|`), opToken(pipeOpType))
|
||||
@@ -321,7 +332,7 @@ func initLexer() (*lex.Lexer, error) {
|
||||
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
|
||||
|
||||
lexer.Add([]byte(`\[`), literalToken(openCollect, false))
|
||||
lexer.Add([]byte(`\]`), literalToken(closeCollect, true))
|
||||
lexer.Add([]byte(`\]\??`), literalToken(closeCollect, true))
|
||||
lexer.Add([]byte(`\{`), literalToken(openCollectObject, false))
|
||||
lexer.Add([]byte(`\}`), literalToken(closeCollectObject, true))
|
||||
lexer.Add([]byte(`\*[\+|\?d]*`), multiplyWithPrefs())
|
||||
|
||||
@@ -60,6 +60,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 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}
|
||||
|
||||
var splitDocumentOpType = &operationType{Type: "SPLIT_DOC", NumArgs: 0, Precedence: 50, Handler: splitDocumentOperator}
|
||||
var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator}
|
||||
var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
|
||||
|
||||
@@ -176,7 +176,7 @@ func explodeNode(node *yaml.Node, context Context) error {
|
||||
} else {
|
||||
if valueNode.Kind == yaml.SequenceNode {
|
||||
log.Debugf("an alias merge list!")
|
||||
for index := 0; index < len(valueNode.Content); index = index + 1 {
|
||||
for index := len(valueNode.Content) - 1; index >= 0; index = index - 1 {
|
||||
aliasNode := valueNode.Content[index]
|
||||
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,36 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var specDocument = `- &CENTER { x: 1, y: 2 }
|
||||
- &LEFT { x: 0, y: 2 }
|
||||
- &BIG { r: 10 }
|
||||
- &SMALL { r: 1 }
|
||||
`
|
||||
|
||||
var expectedSpecResult = "D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"
|
||||
|
||||
var anchorOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Merge one map",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : *CENTER\n r: 10\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{expectedSpecResult},
|
||||
},
|
||||
{
|
||||
description: "Merge multiple maps",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
{
|
||||
description: "Override",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
{
|
||||
description: "Get anchor",
|
||||
document: `a: &billyBob cat`,
|
||||
@@ -91,9 +120,9 @@ bar:
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: bar_b
|
||||
a: foo_a
|
||||
thing: bar_thing
|
||||
thing: foo_thing
|
||||
c: foobarList_c
|
||||
a: foo_a
|
||||
foobar:
|
||||
c: foo_c
|
||||
a: foo_a
|
||||
@@ -106,7 +135,7 @@ foobar:
|
||||
expression: `.foo* | explode(.) | (. style="flow")`,
|
||||
expected: []string{
|
||||
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
|
||||
"D0, P[foobarList], (!!map)::{b: bar_b, a: foo_a, thing: bar_thing, c: foobarList_c}\n",
|
||||
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
|
||||
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
|
||||
},
|
||||
},
|
||||
@@ -116,7 +145,7 @@ foobar:
|
||||
expression: `.foo* | explode(explode(.)) | (. style="flow")`,
|
||||
expected: []string{
|
||||
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
|
||||
"D0, P[foobarList], (!!map)::{b: bar_b, a: foo_a, thing: bar_thing, c: foobarList_c}\n",
|
||||
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
|
||||
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,20 +27,37 @@ type boolOp func(bool, bool) bool
|
||||
|
||||
func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||
lhs.Node = unwrapDoc(lhs.Node)
|
||||
rhs.Node = unwrapDoc(rhs.Node)
|
||||
owner := lhs
|
||||
|
||||
lhsTrue, errDecoding := isTruthy(lhs)
|
||||
if errDecoding != nil {
|
||||
return nil, errDecoding
|
||||
if lhs == nil && rhs == nil {
|
||||
owner = &CandidateNode{}
|
||||
} else if lhs == nil {
|
||||
owner = rhs
|
||||
}
|
||||
|
||||
rhsTrue, errDecoding := isTruthy(rhs)
|
||||
if errDecoding != nil {
|
||||
return nil, errDecoding
|
||||
}
|
||||
var errDecoding error
|
||||
lhsTrue := false
|
||||
if lhs != nil {
|
||||
lhs.Node = unwrapDoc(lhs.Node)
|
||||
lhsTrue, errDecoding = isTruthy(lhs)
|
||||
|
||||
return createBooleanCandidate(lhs, op(lhsTrue, rhsTrue)), nil
|
||||
if errDecoding != nil {
|
||||
return nil, errDecoding
|
||||
}
|
||||
}
|
||||
log.Debugf("-- lhsTrue", lhsTrue)
|
||||
|
||||
rhsTrue := false
|
||||
if rhs != nil {
|
||||
rhs.Node = unwrapDoc(rhs.Node)
|
||||
rhsTrue, errDecoding = isTruthy(rhs)
|
||||
if errDecoding != nil {
|
||||
return nil, errDecoding
|
||||
}
|
||||
}
|
||||
log.Debugf("-- rhsTrue", rhsTrue)
|
||||
|
||||
return createBooleanCandidate(owner, op(lhsTrue, rhsTrue)), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +65,9 @@ func orOperator(d *dataTreeNavigator, context Context, expressionNode *Expressio
|
||||
log.Debugf("-- orOp")
|
||||
return crossFunction(d, context, expressionNode, performBoolOp(
|
||||
func(b1 bool, b2 bool) bool {
|
||||
log.Debugf("-- peformingOrOp with %v and %v", b1, b2)
|
||||
return b1 || b2
|
||||
}), false)
|
||||
}), true)
|
||||
}
|
||||
|
||||
func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
@@ -57,7 +75,7 @@ func andOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
|
||||
return crossFunction(d, context, expressionNode, performBoolOp(
|
||||
func(b1 bool, b2 bool) bool {
|
||||
return b1 && b2
|
||||
}), false)
|
||||
}), true)
|
||||
}
|
||||
|
||||
func notOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
@@ -12,6 +12,22 @@ var booleanOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!bool)::true\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "b: hi",
|
||||
expression: `select(.a or .b)`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::b: hi\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "b: hi",
|
||||
expression: `select((.a and .b) | not)`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::b: hi\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "AND example",
|
||||
expression: `true and false`,
|
||||
|
||||
@@ -71,7 +71,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Remove all comments",
|
||||
description: "Remove (strip) all comments",
|
||||
subdescription: "Note the use of `...` to ensure key nodes are included.",
|
||||
document: "# hi\n\na: cat # comment\n\n# great\n\nb: # key comment",
|
||||
expression: `... comments=""`,
|
||||
|
||||
160
pkg/yqlib/operator_entries.go
Normal file
160
pkg/yqlib/operator_entries.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func entrySeqFor(key *yaml.Node, value *yaml.Node) *yaml.Node {
|
||||
var keyKey = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "key"}
|
||||
var valueKey = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "value"}
|
||||
|
||||
return &yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Tag: "!!map",
|
||||
Content: []*yaml.Node{keyKey, key, valueKey, value},
|
||||
}
|
||||
}
|
||||
|
||||
func toEntriesFromMap(candidateNode *CandidateNode) *CandidateNode {
|
||||
var sequence = &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
||||
var entriesNode = candidateNode.CreateChild(nil, sequence)
|
||||
|
||||
var contents = unwrapDoc(candidateNode.Node).Content
|
||||
for index := 0; index < len(contents); index = index + 2 {
|
||||
key := contents[index]
|
||||
value := contents[index+1]
|
||||
|
||||
sequence.Content = append(sequence.Content, entrySeqFor(key, value))
|
||||
}
|
||||
return entriesNode
|
||||
}
|
||||
|
||||
func toEntriesfromSeq(candidateNode *CandidateNode) *CandidateNode {
|
||||
var sequence = &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
||||
var entriesNode = candidateNode.CreateChild(nil, sequence)
|
||||
|
||||
var contents = unwrapDoc(candidateNode.Node).Content
|
||||
for index := 0; index < len(contents); index = index + 1 {
|
||||
key := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%v", index)}
|
||||
value := contents[index]
|
||||
|
||||
sequence.Content = append(sequence.Content, entrySeqFor(key, value))
|
||||
}
|
||||
return entriesNode
|
||||
}
|
||||
|
||||
func toEntriesOperator(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)
|
||||
|
||||
switch candidateNode.Kind {
|
||||
case yaml.MappingNode:
|
||||
results.PushBack(toEntriesFromMap(candidate))
|
||||
|
||||
case yaml.SequenceNode:
|
||||
results.PushBack(toEntriesfromSeq(candidate))
|
||||
default:
|
||||
return Context{}, fmt.Errorf("%v has no keys", candidate.Node.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
return context.ChildContext(results), nil
|
||||
}
|
||||
|
||||
func parseEntry(d *dataTreeNavigator, entry *yaml.Node, position int) (*yaml.Node, *yaml.Node, error) {
|
||||
prefs := traversePreferences{DontAutoCreate: true}
|
||||
candidateNode := &CandidateNode{Node: entry}
|
||||
|
||||
keyResults, err := traverseMap(Context{}, candidateNode, "key", prefs, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if keyResults.Len() != 1 {
|
||||
return nil, nil, fmt.Errorf("Expected to find one 'key' entry but found %v in position %v", keyResults.Len(), position)
|
||||
}
|
||||
|
||||
valueResults, err := traverseMap(Context{}, candidateNode, "value", prefs, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if valueResults.Len() != 1 {
|
||||
return nil, nil, fmt.Errorf("Expected to find one 'value' entry but found %v in position %v", valueResults.Len(), position)
|
||||
}
|
||||
|
||||
return keyResults.Front().Value.(*CandidateNode).Node, valueResults.Front().Value.(*CandidateNode).Node, nil
|
||||
|
||||
}
|
||||
|
||||
func fromEntries(d *dataTreeNavigator, candidateNode *CandidateNode) (*CandidateNode, error) {
|
||||
var node = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
|
||||
var mapCandidateNode = candidateNode.CreateChild(nil, node)
|
||||
|
||||
var contents = unwrapDoc(candidateNode.Node).Content
|
||||
|
||||
for index := 0; index < len(contents); index = index + 1 {
|
||||
key, value, err := parseEntry(d, contents[index], index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node.Content = append(node.Content, key, value)
|
||||
}
|
||||
return mapCandidateNode, nil
|
||||
}
|
||||
|
||||
func fromEntriesOperator(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)
|
||||
|
||||
switch candidateNode.Kind {
|
||||
case yaml.SequenceNode:
|
||||
mapResult, err := fromEntries(d, candidate)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
results.PushBack(mapResult)
|
||||
default:
|
||||
return Context{}, fmt.Errorf("from entries only runs against arrays")
|
||||
}
|
||||
}
|
||||
|
||||
return context.ChildContext(results), nil
|
||||
}
|
||||
|
||||
func withEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
//to_entries on the context
|
||||
toEntries, err := toEntriesOperator(d, context, expressionNode)
|
||||
if err != nil {
|
||||
return Context{}, nil
|
||||
}
|
||||
|
||||
//run expression against entries
|
||||
// splat toEntries and pipe it into Rhs
|
||||
splatted, err := splat(d, toEntries, traversePreferences{})
|
||||
if err != nil {
|
||||
return Context{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
collected, err := collectOperator(d, result, expressionNode)
|
||||
if err != nil {
|
||||
return Context{}, nil
|
||||
}
|
||||
|
||||
//from_entries on the result
|
||||
return fromEntriesOperator(d, collected, expressionNode)
|
||||
}
|
||||
56
pkg/yqlib/operator_entries_test.go
Normal file
56
pkg/yqlib/operator_entries_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var entriesOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "to_entries Map",
|
||||
document: `{a: 1, b: 2}`,
|
||||
expression: `to_entries`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- key: a\n value: 1\n- key: b\n value: 2\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "to_entries Array",
|
||||
document: `[a, b]`,
|
||||
expression: `to_entries`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- key: 0\n value: a\n- key: 1\n value: b\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "from_entries map",
|
||||
document: `{a: 1, b: 2}`,
|
||||
expression: `to_entries | from_entries`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: 1\nb: 2\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "from_entries with numeric key indexes",
|
||||
subdescription: "from_entries always creates a map, even for numeric keys",
|
||||
document: `[a,b]`,
|
||||
expression: `to_entries | from_entries`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::0: a\n1: b\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Use with_entries to update keys",
|
||||
document: `{a: 1, b: 2}`,
|
||||
expression: `with_entries(.key |= "KEY_" + .)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::KEY_a: 1\nKEY_b: 2\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEntriesOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range entriesOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentScenarios(t, "Entries", entriesOperatorScenarios)
|
||||
}
|
||||
@@ -4,13 +4,22 @@ 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), false)
|
||||
return crossFunction(d, context, 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
|
||||
|
||||
if lhs == nil && rhs == nil {
|
||||
owner := &CandidateNode{}
|
||||
return createBooleanCandidate(owner, !flip), nil
|
||||
} else if lhs == nil {
|
||||
return createBooleanCandidate(rhs, flip), nil
|
||||
} else if rhs == nil {
|
||||
return createBooleanCandidate(lhs, flip), nil
|
||||
}
|
||||
|
||||
lhsNode := unwrapDoc(lhs.Node)
|
||||
rhsNode := unwrapDoc(rhs.Node)
|
||||
|
||||
@@ -29,5 +38,5 @@ 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), false)
|
||||
return crossFunction(d, context, expressionNode, isEquals(true), true)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,22 @@ var equalsOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!bool)::true\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Non exisitant key doesn't equal a value",
|
||||
document: "a: frog",
|
||||
expression: `select(.b != "thing")`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: frog\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Two non existant keys are equal",
|
||||
document: "a: frog",
|
||||
expression: `select(.b == .c)`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: frog\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEqualOperatorScenarios(t *testing.T) {
|
||||
|
||||
@@ -24,6 +24,12 @@ list2:
|
||||
- "123"
|
||||
`
|
||||
|
||||
var mergeArraysObjectKeysText = `It's a complex command, the trickyness comes from needing to have the right context in the expressions.
|
||||
First we save the second array into a variable '$two' which lets us reference it later.
|
||||
We then need to update the first array. We will use the relative update (|=) because we need to update relative to the current element of the array in the LHS in the RHS expression.
|
||||
We set the current element of the first array as $cur. Now we multiply (merge) $cur with the matching entry in $two, by passing $two through a select filter.
|
||||
`
|
||||
|
||||
var multiplyOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Multiply integers",
|
||||
@@ -245,6 +251,16 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
"D0, P[a], (!!seq)::[{name: fred, age: 34}, {name: bob, age: 32}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Merge arrays of objects together, matching on a key",
|
||||
subdescription: mergeArraysObjectKeysText,
|
||||
document: `[{a: apple, b: appleB}, {a: kiwi, b: kiwiB}, {a: banana, b: bananaB}]`,
|
||||
document2: `[{a: banana, c: bananaC}, {a: apple, b: appleB2}, {a: dingo, c: dingoC}]`,
|
||||
expression: `(select(fi==1) | .[]) as $two | select(fi==0) | .[] |= (. as $cur | $cur * ($two | select(.a == $cur.a)))`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::[{a: apple, b: appleB2}, {a: kiwi, b: kiwiB}, {a: banana, b: bananaB, c: bananaC}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Merge to prefix an element",
|
||||
document: `{a: cat, b: dog}`,
|
||||
|
||||
@@ -14,6 +14,16 @@ var selectOperatorScenarios = []expressionScenario{
|
||||
"D0, P[1], (!!str)::goat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: hello",
|
||||
document2: "b: world",
|
||||
expression: `select(.a == "hello" or .b == "world")`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: hello\n",
|
||||
"D0, P[], (doc)::b: world\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[{animal: cat, legs: {cool: true}}, {animal: fish}]`,
|
||||
|
||||
@@ -5,6 +5,23 @@ import (
|
||||
)
|
||||
|
||||
var styleOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Update and set style of a particular node (simple)",
|
||||
document: `a: {b: thing, c: something}`,
|
||||
expression: `.a.b = "new" | .a.b style="double"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: {b: \"new\", c: something}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Update and set style of a particular node using path variables",
|
||||
subdescription: "You can use a variable to re-use a path",
|
||||
document: `a: {b: thing, c: something}`,
|
||||
expression: `.a.b as $x | $x = "new" | $x style="double"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: {b: \"new\", c: something}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Set tagged style",
|
||||
document: `{a: cat, b: 5, c: 3.2, e: true}`,
|
||||
|
||||
@@ -14,6 +14,7 @@ type traversePreferences struct {
|
||||
IncludeMapKeys bool
|
||||
DontAutoCreate bool // by default, we automatically create entries on the fly.
|
||||
DontIncludeMapValues bool
|
||||
OptionalTraverse bool // e.g. .adf?
|
||||
}
|
||||
|
||||
func splat(d *dataTreeNavigator, context Context, prefs traversePreferences) (Context, error) {
|
||||
@@ -60,7 +61,7 @@ func traverse(d *dataTreeNavigator, context Context, matchingNode *CandidateNode
|
||||
|
||||
case yaml.SequenceNode:
|
||||
log.Debug("its a sequence of %v things!", len(value.Content))
|
||||
return traverseArray(matchingNode, operation)
|
||||
return traverseArray(matchingNode, operation, operation.Preferences.(traversePreferences))
|
||||
|
||||
case yaml.AliasNode:
|
||||
log.Debug("its an alias!")
|
||||
@@ -89,14 +90,19 @@ 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)
|
||||
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
prefs := traversePreferences{}
|
||||
|
||||
if expressionNode.Rhs.Rhs != nil && expressionNode.Rhs.Rhs.Operation.Preferences != nil {
|
||||
prefs = expressionNode.Rhs.Rhs.Operation.Preferences.(traversePreferences)
|
||||
}
|
||||
var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Content
|
||||
|
||||
//now we traverse the result of the lhs against the indices we found
|
||||
result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, traversePreferences{})
|
||||
result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
@@ -130,7 +136,7 @@ func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesT
|
||||
matchingNode.Node = node.Alias
|
||||
return traverseArrayIndices(context, matchingNode, indicesToTraverse, prefs)
|
||||
} else if node.Kind == yaml.SequenceNode {
|
||||
return traverseArrayWithIndices(matchingNode, indicesToTraverse)
|
||||
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
||||
} else if node.Kind == yaml.MappingNode {
|
||||
return traverseMapWithIndices(context, matchingNode, indicesToTraverse, prefs)
|
||||
} else if node.Kind == yaml.DocumentNode {
|
||||
@@ -159,7 +165,7 @@ func traverseMapWithIndices(context Context, candidate *CandidateNode, indices [
|
||||
return matchingNodeMap, nil
|
||||
}
|
||||
|
||||
func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*list.List, error) {
|
||||
func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node, prefs traversePreferences) (*list.List, error) {
|
||||
log.Debug("traverseArrayWithIndices")
|
||||
var newMatches = list.New()
|
||||
node := unwrapDoc(candidate.Node)
|
||||
@@ -177,6 +183,9 @@ func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*
|
||||
for _, indexNode := range indices {
|
||||
log.Debug("traverseArrayWithIndices: '%v'", indexNode.Value)
|
||||
index, err := strconv.ParseInt(indexNode.Value, 10, 64)
|
||||
if err != nil && prefs.OptionalTraverse {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot index array with '%v' (%v)", indexNode.Value, err)
|
||||
}
|
||||
@@ -297,8 +306,8 @@ func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *C
|
||||
return nil
|
||||
}
|
||||
|
||||
func traverseArray(candidate *CandidateNode, operation *Operation) (*list.List, error) {
|
||||
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}}
|
||||
return traverseArrayWithIndices(candidate, indices)
|
||||
return traverseArrayWithIndices(candidate, indices, prefs)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[1], (!!map)::{c: banana}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Optional Splat",
|
||||
subdescription: "Just like splat, but won't error if you run it against scalars",
|
||||
document: `"cat"`,
|
||||
expression: `.[]`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "Special characters",
|
||||
subdescription: "Use quotes with brackets around path elements with special characters",
|
||||
@@ -97,6 +104,19 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[a b], (!!null)::null\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Optional identifier",
|
||||
subdescription: "Like jq, does not output an error when the yaml is not an array or object as expected",
|
||||
document: `[1,2,3]`,
|
||||
expression: `.a?`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[[1,2,3], {a: frog}]`,
|
||||
expression: `.[] | .["a"]?`,
|
||||
expected: []string{"D0, P[1 a], (!!str)::frog\n"},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: ``,
|
||||
|
||||
@@ -71,7 +71,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
|
||||
t.Error(fmt.Errorf("%v: %v", err, s.expression))
|
||||
return
|
||||
}
|
||||
test.AssertResultComplexWithContext(t, s.expected, resultsToString(context.MatchingNodes), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document))
|
||||
test.AssertResultComplexWithContext(t, s.expected, resultsToString(context.MatchingNodes), fmt.Sprintf("desc: %v\nexp: %v\ndoc: %v", s.description, s.expression, s.document))
|
||||
}
|
||||
|
||||
func resultsToString(results *list.List) []string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: yq
|
||||
version: '4.7.1'
|
||||
version: '4.8.0'
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user