mirror of
https://github.com/taigrr/yq
synced 2025-01-18 04:53:17 -08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b52c5fe0e | ||
|
|
f4392f8658 | ||
|
|
8e14b3b393 | ||
|
|
8627441705 | ||
|
|
aa95ecd012 | ||
|
|
a2bd463a91 | ||
|
|
6c3965dca3 | ||
|
|
bb3ffd40b5 | ||
|
|
cc08afc435 | ||
|
|
941a453163 | ||
|
|
77630ca179 | ||
|
|
ae4b606707 | ||
|
|
37f3e21970 | ||
|
|
25d0787011 | ||
|
|
b5b8da0a1d | ||
|
|
fa21510194 | ||
|
|
f541194250 | ||
|
|
38666f4db6 |
@@ -11,7 +11,7 @@ var (
|
|||||||
GitDescribe string
|
GitDescribe string
|
||||||
|
|
||||||
// Version is main version number that is being run at the moment.
|
// Version is main version number that is being run at the moment.
|
||||||
Version = "4.7.1"
|
Version = "4.9.0"
|
||||||
|
|
||||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
||||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mikefarah/yq:4.7.1
|
FROM mikefarah/yq:4.9.0
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
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.
|
`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
|
## Get anchor
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
```yaml
|
```yaml
|
||||||
@@ -183,9 +274,9 @@ bar:
|
|||||||
c: bar_c
|
c: bar_c
|
||||||
foobarList:
|
foobarList:
|
||||||
b: bar_b
|
b: bar_b
|
||||||
a: foo_a
|
thing: foo_thing
|
||||||
thing: bar_thing
|
|
||||||
c: foobarList_c
|
c: foobarList_c
|
||||||
|
a: foo_a
|
||||||
foobar:
|
foobar:
|
||||||
c: foo_c
|
c: foo_c
|
||||||
a: foo_a
|
a: foo_a
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes.
|
The `or` and `and` operators take two parameters and return a boolean result.
|
||||||
## OR example
|
|
||||||
|
`not` flips a boolean from true to false, or vice versa.
|
||||||
|
|
||||||
|
`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true.
|
||||||
|
|
||||||
|
`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet
|
||||||
|
|
||||||
|
These are most commonly used with the `select` operator to filter particular nodes.
|
||||||
|
|
||||||
|
## `or` example
|
||||||
Running
|
Running
|
||||||
```bash
|
```bash
|
||||||
yq eval --null-input 'true or false'
|
yq eval --null-input 'true or false'
|
||||||
@@ -9,7 +18,7 @@ will output
|
|||||||
true
|
true
|
||||||
```
|
```
|
||||||
|
|
||||||
## AND example
|
## `and` example
|
||||||
Running
|
Running
|
||||||
```bash
|
```bash
|
||||||
yq eval --null-input 'true and false'
|
yq eval --null-input 'true and false'
|
||||||
@@ -41,6 +50,104 @@ will output
|
|||||||
b: fly
|
b: fly
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `any` returns true if any boolean in a given array is true
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
- false
|
||||||
|
- true
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'any' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## `any` returns false for an empty array
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'any' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
false
|
||||||
|
```
|
||||||
|
|
||||||
|
## `any_c` returns true if any element in the array is true for the given condition.
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
a:
|
||||||
|
- rad
|
||||||
|
- awesome
|
||||||
|
b:
|
||||||
|
- meh
|
||||||
|
- whatever
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval '.[] |= any_c(. == "awesome")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
a: true
|
||||||
|
b: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## `all` returns true if all booleans in a given array are true
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
- true
|
||||||
|
- true
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'all' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## `all` returns true for an empty array
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval 'all' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
## `all_c` returns true if all elements in the array are true for the given condition.
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
a:
|
||||||
|
- rad
|
||||||
|
- awesome
|
||||||
|
b:
|
||||||
|
- meh
|
||||||
|
- 12
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq eval '.[] |= all_c(tag == "!!str")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
a: true
|
||||||
|
b: false
|
||||||
|
```
|
||||||
|
|
||||||
## Not true is false
|
## Not true is false
|
||||||
Running
|
Running
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ a: cat
|
|||||||
b: dog # leave this
|
b: dog # leave this
|
||||||
```
|
```
|
||||||
|
|
||||||
## Remove all comments
|
## Remove (strip) all comments
|
||||||
Note the use of `...` to ensure key nodes are included.
|
Note the use of `...` to ensure key nodes are included.
|
||||||
|
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
|
|||||||
100
pkg/yqlib/doc/Entries.md
Normal file
100
pkg/yqlib/doc/Entries.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
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
|
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
|
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
|
## Merge to prefix an element
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
```yaml
|
```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)
|
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
|
## Set tagged style
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ b: apple
|
|||||||
c: banana
|
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
|
## Special characters
|
||||||
Use quotes with brackets around path elements with special characters
|
Use quotes with brackets around path elements with special characters
|
||||||
|
|
||||||
@@ -98,6 +113,23 @@ will output
|
|||||||
null
|
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
|
## Wildcard matching
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
82
pkg/yqlib/doc/Unique.md
Normal file
82
pkg/yqlib/doc/Unique.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
@@ -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.
|
Lets look at a couple of examples.
|
||||||
|
|
||||||
## Example 1 - simple example
|
## Example with a simple operator
|
||||||
|
|
||||||
Given a document like:
|
Given a document like:
|
||||||
|
|
||||||
@@ -37,10 +37,80 @@ This being the last operation in the expression, the results will be printed out
|
|||||||
3
|
3
|
||||||
```
|
```
|
||||||
|
|
||||||
# Example 2 - operators with arguments.
|
# Example with an operator that takes arguments.
|
||||||
|
|
||||||
|
Given a document like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
a: cat
|
||||||
|
b: dog
|
||||||
|
```
|
||||||
|
|
||||||
|
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 return the node
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dog
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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 difference when evaluating the RHS expression.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
|
||||||
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.
|
```
|
||||||
|
2
|
||||||
|
```
|
||||||
|
|
||||||
Please see the individual operator docs for more information and examples.
|
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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
|
|||||||
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.
|
||||||
1
pkg/yqlib/doc/headers/Unique.md
Normal file
1
pkg/yqlib/doc/headers/Unique.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is used to filter out duplicated items in an array.
|
||||||
@@ -55,7 +55,15 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
|
|||||||
log.Debugf("deleteing open bracket from opstack")
|
log.Debugf("deleteing open bracket from opstack")
|
||||||
|
|
||||||
//and append a collect to the opStack
|
//and append a collect to the 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")
|
log.Debugf("put collect onto the result")
|
||||||
result = append(result, &Operation{OperationType: shortPipeOpType})
|
result = append(result, &Operation{OperationType: shortPipeOpType})
|
||||||
log.Debugf("put shortpipe onto the result")
|
log.Debugf("put shortpipe onto the result")
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ var pathTests = []struct {
|
|||||||
append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"),
|
append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"),
|
||||||
append(make([]interface{}, 0), "b", "a", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
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", "]"),
|
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", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
||||||
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
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.[]`,
|
`.a.[]`,
|
||||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
|
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", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
||||||
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
|
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]`,
|
`.a.[0]`,
|
||||||
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type token struct {
|
|||||||
Operation *Operation
|
Operation *Operation
|
||||||
AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it
|
AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it
|
||||||
CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat
|
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 {
|
func pathToken(wrapped bool) lex.Action {
|
||||||
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
|
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
|
||||||
value := string(m.Bytes)
|
value := string(m.Bytes)
|
||||||
|
prefs := traversePreferences{}
|
||||||
|
|
||||||
|
if value[len(value)-1:] == "?" {
|
||||||
|
prefs.OptionalTraverse = true
|
||||||
|
value = value[:len(value)-1]
|
||||||
|
}
|
||||||
|
|
||||||
value = value[1:]
|
value = value[1:]
|
||||||
if wrapped {
|
if wrapped {
|
||||||
value = unwrap(value)
|
value = unwrap(value)
|
||||||
}
|
}
|
||||||
log.Debug("PathToken %v", 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
|
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 {
|
func literalToken(pType tokenType, checkForPost bool) lex.Action {
|
||||||
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +259,8 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
|
lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
|
||||||
lexer.Add([]byte(`select`), opToken(selectOpType))
|
lexer.Add([]byte(`select`), opToken(selectOpType))
|
||||||
lexer.Add([]byte(`has`), opToken(hasOpType))
|
lexer.Add([]byte(`has`), opToken(hasOpType))
|
||||||
|
lexer.Add([]byte(`unique`), opToken(uniqueOpType))
|
||||||
|
lexer.Add([]byte(`unique_by`), opToken(uniqueByOpType))
|
||||||
lexer.Add([]byte(`explode`), opToken(explodeOpType))
|
lexer.Add([]byte(`explode`), opToken(explodeOpType))
|
||||||
lexer.Add([]byte(`or`), opToken(orOpType))
|
lexer.Add([]byte(`or`), opToken(orOpType))
|
||||||
lexer.Add([]byte(`and`), opToken(andOpType))
|
lexer.Add([]byte(`and`), opToken(andOpType))
|
||||||
@@ -266,6 +276,11 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
lexer.Add([]byte(`join`), opToken(joinStringOpType))
|
lexer.Add([]byte(`join`), opToken(joinStringOpType))
|
||||||
lexer.Add([]byte(`sub`), opToken(subStringOpType))
|
lexer.Add([]byte(`sub`), opToken(subStringOpType))
|
||||||
|
|
||||||
|
lexer.Add([]byte(`any`), opToken(anyOpType))
|
||||||
|
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
|
||||||
|
lexer.Add([]byte(`all`), opToken(allOpType))
|
||||||
|
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
|
||||||
|
|
||||||
lexer.Add([]byte(`split`), opToken(splitStringOpType))
|
lexer.Add([]byte(`split`), opToken(splitStringOpType))
|
||||||
lexer.Add([]byte(`keys`), opToken(keysOpType))
|
lexer.Add([]byte(`keys`), opToken(keysOpType))
|
||||||
|
|
||||||
@@ -278,6 +293,9 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
lexer.Add([]byte(`fileIndex`), opToken(getFileIndexOpType))
|
lexer.Add([]byte(`fileIndex`), opToken(getFileIndexOpType))
|
||||||
lexer.Add([]byte(`fi`), opToken(getFileIndexOpType))
|
lexer.Add([]byte(`fi`), opToken(getFileIndexOpType))
|
||||||
lexer.Add([]byte(`path`), opToken(getPathOpType))
|
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}))
|
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{LineComment: true}))
|
||||||
|
|
||||||
@@ -300,8 +318,8 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
|
|
||||||
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
|
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
|
||||||
|
|
||||||
lexer.Add([]byte(`\."[^ "]+"`), pathToken(true))
|
lexer.Add([]byte(`\."[^ "]+"\??`), pathToken(true))
|
||||||
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+`), pathToken(false))
|
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+\??`), pathToken(false))
|
||||||
lexer.Add([]byte(`\.`), selfToken())
|
lexer.Add([]byte(`\.`), selfToken())
|
||||||
|
|
||||||
lexer.Add([]byte(`\|`), opToken(pipeOpType))
|
lexer.Add([]byte(`\|`), opToken(pipeOpType))
|
||||||
@@ -321,7 +339,7 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
|
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
|
||||||
|
|
||||||
lexer.Add([]byte(`\[`), literalToken(openCollect, 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(openCollectObject, false))
|
||||||
lexer.Add([]byte(`\}`), literalToken(closeCollectObject, true))
|
lexer.Add([]byte(`\}`), literalToken(closeCollectObject, true))
|
||||||
lexer.Add([]byte(`\*[\+|\?d]*`), multiplyWithPrefs())
|
lexer.Add([]byte(`\*[\+|\?d]*`), multiplyWithPrefs())
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence:
|
|||||||
|
|
||||||
var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator}
|
var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator}
|
||||||
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator}
|
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator}
|
||||||
|
|
||||||
|
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
|
||||||
|
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
|
||||||
|
var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator}
|
||||||
|
var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, Precedence: 50, Handler: allOperator}
|
||||||
|
|
||||||
|
var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator}
|
||||||
|
var 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 splitDocumentOpType = &operationType{Type: "SPLIT_DOC", NumArgs: 0, Precedence: 50, Handler: splitDocumentOperator}
|
||||||
var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator}
|
var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator}
|
||||||
var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
|
var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
|
||||||
@@ -94,6 +104,8 @@ var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs:
|
|||||||
|
|
||||||
var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: selectOperator}
|
var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: selectOperator}
|
||||||
var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator}
|
var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator}
|
||||||
|
var uniqueOpType = &operationType{Type: "UNIQUE", NumArgs: 0, Precedence: 50, Handler: unique}
|
||||||
|
var uniqueByOpType = &operationType{Type: "UNIQUE_BY", NumArgs: 1, Precedence: 50, Handler: uniqueBy}
|
||||||
var deleteChildOpType = &operationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: deleteChildOperator}
|
var deleteChildOpType = &operationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: deleteChildOperator}
|
||||||
|
|
||||||
type Operation struct {
|
type Operation struct {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func explodeNode(node *yaml.Node, context Context) error {
|
|||||||
} else {
|
} else {
|
||||||
if valueNode.Kind == yaml.SequenceNode {
|
if valueNode.Kind == yaml.SequenceNode {
|
||||||
log.Debugf("an alias merge list!")
|
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]
|
aliasNode := valueNode.Content[index]
|
||||||
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
|
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,7 +4,36 @@ import (
|
|||||||
"testing"
|
"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{
|
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",
|
description: "Get anchor",
|
||||||
document: `a: &billyBob cat`,
|
document: `a: &billyBob cat`,
|
||||||
@@ -91,9 +120,9 @@ bar:
|
|||||||
c: bar_c
|
c: bar_c
|
||||||
foobarList:
|
foobarList:
|
||||||
b: bar_b
|
b: bar_b
|
||||||
a: foo_a
|
thing: foo_thing
|
||||||
thing: bar_thing
|
|
||||||
c: foobarList_c
|
c: foobarList_c
|
||||||
|
a: foo_a
|
||||||
foobar:
|
foobar:
|
||||||
c: foo_c
|
c: foo_c
|
||||||
a: foo_a
|
a: foo_a
|
||||||
@@ -106,7 +135,7 @@ foobar:
|
|||||||
expression: `.foo* | explode(.) | (. style="flow")`,
|
expression: `.foo* | explode(.) | (. style="flow")`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
|
"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",
|
"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")`,
|
expression: `.foo* | explode(explode(.)) | (. style="flow")`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
|
"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",
|
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package yqlib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isTruthy(c *CandidateNode) (bool, error) {
|
func isTruthyNode(node *yaml.Node) (bool, error) {
|
||||||
node := unwrapDoc(c.Node)
|
|
||||||
value := true
|
value := true
|
||||||
|
|
||||||
if node.Tag == "!!null" {
|
if node.Tag == "!!null" {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -23,33 +22,123 @@ func isTruthy(c *CandidateNode) (bool, error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTruthy(c *CandidateNode) (bool, error) {
|
||||||
|
node := unwrapDoc(c.Node)
|
||||||
|
return isTruthyNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
type boolOp func(bool, bool) bool
|
type boolOp func(bool, bool) bool
|
||||||
|
|
||||||
func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||||
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||||
|
owner := lhs
|
||||||
|
|
||||||
|
if lhs == nil && rhs == nil {
|
||||||
|
owner = &CandidateNode{}
|
||||||
|
} else if lhs == nil {
|
||||||
|
owner = rhs
|
||||||
|
}
|
||||||
|
|
||||||
|
var errDecoding error
|
||||||
|
lhsTrue := false
|
||||||
|
if lhs != nil {
|
||||||
lhs.Node = unwrapDoc(lhs.Node)
|
lhs.Node = unwrapDoc(lhs.Node)
|
||||||
|
lhsTrue, errDecoding = isTruthy(lhs)
|
||||||
|
|
||||||
|
if errDecoding != nil {
|
||||||
|
return nil, errDecoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("-- lhsTrue", lhsTrue)
|
||||||
|
|
||||||
|
rhsTrue := false
|
||||||
|
if rhs != nil {
|
||||||
rhs.Node = unwrapDoc(rhs.Node)
|
rhs.Node = unwrapDoc(rhs.Node)
|
||||||
|
rhsTrue, errDecoding = isTruthy(rhs)
|
||||||
lhsTrue, errDecoding := isTruthy(lhs)
|
|
||||||
if errDecoding != nil {
|
if errDecoding != nil {
|
||||||
return nil, errDecoding
|
return nil, errDecoding
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("-- rhsTrue", rhsTrue)
|
||||||
|
|
||||||
rhsTrue, errDecoding := isTruthy(rhs)
|
return createBooleanCandidate(owner, op(lhsTrue, rhsTrue)), nil
|
||||||
if errDecoding != nil {
|
}
|
||||||
return nil, errDecoding
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBooleanCandidate(lhs, op(lhsTrue, rhsTrue)), nil
|
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.SingleChildContext(candidate), expressionNode)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
if rhs.MatchingNodes.Len() > 0 {
|
||||||
|
node = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
|
||||||
|
} else {
|
||||||
|
// no results found, ignore this entry
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
truthy, err := isTruthyNode(node)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if truthy == wantBool {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func allOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
var results = list.New()
|
||||||
|
|
||||||
|
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||||
|
candidate := el.Value.(*CandidateNode)
|
||||||
|
candidateNode := unwrapDoc(candidate.Node)
|
||||||
|
if candidateNode.Kind != yaml.SequenceNode {
|
||||||
|
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
|
||||||
|
}
|
||||||
|
booleanResult, err := findBoolean(false, d, context, expressionNode.Rhs, candidateNode)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
result := createBooleanCandidate(candidate, !booleanResult)
|
||||||
|
results.PushBack(result)
|
||||||
|
}
|
||||||
|
return context.ChildContext(results), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
var results = list.New()
|
||||||
|
|
||||||
|
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||||
|
candidate := el.Value.(*CandidateNode)
|
||||||
|
candidateNode := unwrapDoc(candidate.Node)
|
||||||
|
if candidateNode.Kind != yaml.SequenceNode {
|
||||||
|
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
|
||||||
|
}
|
||||||
|
booleanResult, err := findBoolean(true, d, context, expressionNode.Rhs, candidateNode)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
result := createBooleanCandidate(candidate, booleanResult)
|
||||||
|
results.PushBack(result)
|
||||||
|
}
|
||||||
|
return context.ChildContext(results), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
log.Debugf("-- orOp")
|
log.Debugf("-- orOp")
|
||||||
return crossFunction(d, context, expressionNode, performBoolOp(
|
return crossFunction(d, context, expressionNode, performBoolOp(
|
||||||
func(b1 bool, b2 bool) bool {
|
func(b1 bool, b2 bool) bool {
|
||||||
|
log.Debugf("-- peformingOrOp with %v and %v", b1, b2)
|
||||||
return b1 || b2
|
return b1 || b2
|
||||||
}), false)
|
}), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
@@ -57,7 +146,7 @@ func andOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
|
|||||||
return crossFunction(d, context, expressionNode, performBoolOp(
|
return crossFunction(d, context, expressionNode, performBoolOp(
|
||||||
func(b1 bool, b2 bool) bool {
|
func(b1 bool, b2 bool) bool {
|
||||||
return b1 && b2
|
return b1 && b2
|
||||||
}), false)
|
}), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
func notOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
|||||||
@@ -6,14 +6,30 @@ import (
|
|||||||
|
|
||||||
var booleanOperatorScenarios = []expressionScenario{
|
var booleanOperatorScenarios = []expressionScenario{
|
||||||
{
|
{
|
||||||
description: "OR example",
|
description: "`or` example",
|
||||||
expression: `true or false`,
|
expression: `true or false`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[], (!!bool)::true\n",
|
"D0, P[], (!!bool)::true\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "AND example",
|
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`,
|
expression: `true and false`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[], (!!bool)::false\n",
|
"D0, P[], (!!bool)::false\n",
|
||||||
@@ -27,6 +43,70 @@ var booleanOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n",
|
"D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "`any` returns true if any boolean in a given array is true",
|
||||||
|
document: `[false, true]`,
|
||||||
|
expression: "any",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "`any` returns false for an empty array",
|
||||||
|
document: `[]`,
|
||||||
|
expression: "any",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "`any_c` returns true if any element in the array is true for the given condition.",
|
||||||
|
document: "a: [rad, awesome]\nb: [meh, whatever]",
|
||||||
|
expression: `.[] |= any_c(. == "awesome")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (doc)::a: true\nb: false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[false, false]`,
|
||||||
|
expression: "any",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "`all` returns true if all booleans in a given array are true",
|
||||||
|
document: `[true, true]`,
|
||||||
|
expression: "all",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[false, true]`,
|
||||||
|
expression: "all",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "`all` returns true for an empty array",
|
||||||
|
document: `[]`,
|
||||||
|
expression: "all",
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "`all_c` returns true if all elements in the array are true for the given condition.",
|
||||||
|
document: "a: [rad, awesome]\nb: [meh, 12]",
|
||||||
|
expression: `.[] |= all_c(tag == "!!str")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (doc)::a: true\nb: false\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
expression: `false or false`,
|
expression: `false or false`,
|
||||||
|
|||||||
@@ -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.",
|
subdescription: "Note the use of `...` to ensure key nodes are included.",
|
||||||
document: "# hi\n\na: cat # comment\n\n# great\n\nb: # key comment",
|
document: "# hi\n\na: cat # comment\n\n# great\n\nb: # key comment",
|
||||||
expression: `... comments=""`,
|
expression: `... comments=""`,
|
||||||
|
|||||||
162
pkg/yqlib/operator_entries.go
Normal file
162
pkg/yqlib/operator_entries.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
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:
|
||||||
|
if candidateNode.Tag != "!!null" {
|
||||||
|
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{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//run expression against entries
|
||||||
|
// splat toEntries and pipe it into Rhs
|
||||||
|
splatted, err := splat(d, toEntries, traversePreferences{})
|
||||||
|
if err != 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{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
collected, err := collectOperator(d, result, expressionNode)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//from_entries on the result
|
||||||
|
return fromEntriesOperator(d, collected, expressionNode)
|
||||||
|
}
|
||||||
62
pkg/yqlib/operator_entries_test.go
Normal file
62
pkg/yqlib/operator_entries_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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: "to_entries null",
|
||||||
|
document: `null`,
|
||||||
|
expression: `to_entries`,
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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) {
|
func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
log.Debugf("-- equalsOperation")
|
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) {
|
func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||||
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||||
value := false
|
value := false
|
||||||
|
|
||||||
|
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)
|
lhsNode := unwrapDoc(lhs.Node)
|
||||||
rhsNode := unwrapDoc(rhs.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) {
|
func notEqualsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
log.Debugf("-- equalsOperation")
|
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",
|
"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) {
|
func TestEqualOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ list2:
|
|||||||
- "123"
|
- "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{
|
var multiplyOperatorScenarios = []expressionScenario{
|
||||||
{
|
{
|
||||||
description: "Multiply integers",
|
description: "Multiply integers",
|
||||||
@@ -245,6 +251,16 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[a], (!!seq)::[{name: fred, age: 34}, {name: bob, age: 32}]\n",
|
"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",
|
description: "Merge to prefix an element",
|
||||||
document: `{a: cat, b: dog}`,
|
document: `{a: cat, b: dog}`,
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ var selectOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[1], (!!str)::goat\n",
|
"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,
|
skipDoc: true,
|
||||||
document: `[{animal: cat, legs: {cool: true}}, {animal: fish}]`,
|
document: `[{animal: cat, legs: {cool: true}}, {animal: fish}]`,
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var styleOperatorScenarios = []expressionScenario{
|
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",
|
description: "Set tagged style",
|
||||||
document: `{a: cat, b: 5, c: 3.2, e: true}`,
|
document: `{a: cat, b: 5, c: 3.2, e: true}`,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type traversePreferences struct {
|
|||||||
IncludeMapKeys bool
|
IncludeMapKeys bool
|
||||||
DontAutoCreate bool // by default, we automatically create entries on the fly.
|
DontAutoCreate bool // by default, we automatically create entries on the fly.
|
||||||
DontIncludeMapValues bool
|
DontIncludeMapValues bool
|
||||||
|
OptionalTraverse bool // e.g. .adf?
|
||||||
}
|
}
|
||||||
|
|
||||||
func splat(d *dataTreeNavigator, context Context, prefs traversePreferences) (Context, error) {
|
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:
|
case yaml.SequenceNode:
|
||||||
log.Debug("its a sequence of %v things!", len(value.Content))
|
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:
|
case yaml.AliasNode:
|
||||||
log.Debug("its an alias!")
|
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 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, expressionNode.Rhs)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Context{}, err
|
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
|
var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Content
|
||||||
|
|
||||||
//now we traverse the result of the lhs against the indices we found
|
//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 {
|
if err != nil {
|
||||||
return Context{}, err
|
return Context{}, err
|
||||||
}
|
}
|
||||||
@@ -130,7 +136,7 @@ func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesT
|
|||||||
matchingNode.Node = node.Alias
|
matchingNode.Node = node.Alias
|
||||||
return traverseArrayIndices(context, matchingNode, indicesToTraverse, prefs)
|
return traverseArrayIndices(context, matchingNode, indicesToTraverse, prefs)
|
||||||
} else if node.Kind == yaml.SequenceNode {
|
} else if node.Kind == yaml.SequenceNode {
|
||||||
return traverseArrayWithIndices(matchingNode, indicesToTraverse)
|
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
||||||
} else if node.Kind == yaml.MappingNode {
|
} else if node.Kind == yaml.MappingNode {
|
||||||
return traverseMapWithIndices(context, matchingNode, indicesToTraverse, prefs)
|
return traverseMapWithIndices(context, matchingNode, indicesToTraverse, prefs)
|
||||||
} else if node.Kind == yaml.DocumentNode {
|
} else if node.Kind == yaml.DocumentNode {
|
||||||
@@ -159,7 +165,7 @@ func traverseMapWithIndices(context Context, candidate *CandidateNode, indices [
|
|||||||
return matchingNodeMap, nil
|
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")
|
log.Debug("traverseArrayWithIndices")
|
||||||
var newMatches = list.New()
|
var newMatches = list.New()
|
||||||
node := unwrapDoc(candidate.Node)
|
node := unwrapDoc(candidate.Node)
|
||||||
@@ -177,6 +183,9 @@ func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*
|
|||||||
for _, indexNode := range indices {
|
for _, indexNode := range indices {
|
||||||
log.Debug("traverseArrayWithIndices: '%v'", indexNode.Value)
|
log.Debug("traverseArrayWithIndices: '%v'", indexNode.Value)
|
||||||
index, err := strconv.ParseInt(indexNode.Value, 10, 64)
|
index, err := strconv.ParseInt(indexNode.Value, 10, 64)
|
||||||
|
if err != nil && prefs.OptionalTraverse {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Cannot index array with '%v' (%v)", indexNode.Value, err)
|
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
|
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)
|
log.Debug("operation Value %v", operation.Value)
|
||||||
indices := []*yaml.Node{&yaml.Node{Value: operation.StringValue}}
|
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",
|
"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",
|
description: "Special characters",
|
||||||
subdescription: "Use quotes with brackets around path elements with 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",
|
"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,
|
skipDoc: true,
|
||||||
document: ``,
|
document: ``,
|
||||||
|
|||||||
59
pkg/yqlib/operator_unique.go
Normal file
59
pkg/yqlib/operator_unique.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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.SingleChildContext(child), expressionNode.Rhs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
49
pkg/yqlib/operator_unique_by_test.go
Normal file
49
pkg/yqlib/operator_unique_by_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueOperatorScenarios(t *testing.T) {
|
||||||
|
for _, tt := range uniqueOperatorScenarios {
|
||||||
|
testScenario(t, &tt)
|
||||||
|
}
|
||||||
|
documentScenarios(t, "Unique", uniqueOperatorScenarios)
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
|
|||||||
t.Error(fmt.Errorf("%v: %v", err, s.expression))
|
t.Error(fmt.Errorf("%v: %v", err, s.expression))
|
||||||
return
|
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 {
|
func resultsToString(results *list.List) []string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: yq
|
name: yq
|
||||||
version: '4.7.1'
|
version: '4.9.0'
|
||||||
summary: A lightweight and portable command-line YAML processor
|
summary: A lightweight and portable command-line YAML processor
|
||||||
description: |
|
description: |
|
||||||
The aim of the project is to be the jq or sed of yaml files.
|
The aim of the project is to be the jq or sed of yaml files.
|
||||||
|
|||||||
Reference in New Issue
Block a user