mirror of
https://github.com/mikefarah/yq.git
synced 2024-12-19 20:19:04 +00:00
Added pick operator
This commit is contained in:
parent
33a29817d7
commit
58be9829f9
5
pkg/yqlib/doc/operators/headers/pick.md
Normal file
5
pkg/yqlib/doc/operators/headers/pick.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Pick
|
||||||
|
|
||||||
|
Filter a map by the specified list of keys. Map is returned with the key in the order of the pick list.
|
||||||
|
|
||||||
|
Similarly, you can filter a map by the specified list of indices.
|
53
pkg/yqlib/doc/operators/pick.md
Normal file
53
pkg/yqlib/doc/operators/pick.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Pick
|
||||||
|
|
||||||
|
Filter a map by the specified list of keys. Map is returned with the key in the order of the pick list.
|
||||||
|
|
||||||
|
Similarly, you can filter a map by the specified list of indices.
|
||||||
|
|
||||||
|
{% hint style="warning" %}
|
||||||
|
Note that versions prior to 4.18 require the 'eval/e' command to be specified. 
|
||||||
|
|
||||||
|
`yq e <exp> <file>`
|
||||||
|
{% endhint %}
|
||||||
|
|
||||||
|
## Pick keys from map
|
||||||
|
Note that the order of the keys matches the pick order and non existent keys are skipped.
|
||||||
|
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
myMap:
|
||||||
|
cat: meow
|
||||||
|
dog: bark
|
||||||
|
thing: hamster
|
||||||
|
hamster: squeek
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq '.myMap |= pick(["hamster", "cat", "goat"])' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
myMap:
|
||||||
|
hamster: squeek
|
||||||
|
cat: meow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pick indices from array
|
||||||
|
Note that the order of the indexes matches the pick order and non existent indexes are skipped.
|
||||||
|
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
- cat
|
||||||
|
- leopard
|
||||||
|
- lion
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq 'pick([2, 0, 734, -5])' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
- lion
|
||||||
|
- cat
|
||||||
|
```
|
||||||
|
|
@ -322,6 +322,7 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
|
|
||||||
lexer.Add([]byte(`map`), opToken(mapOpType))
|
lexer.Add([]byte(`map`), opToken(mapOpType))
|
||||||
lexer.Add([]byte(`map_values`), opToken(mapValuesOpType))
|
lexer.Add([]byte(`map_values`), opToken(mapValuesOpType))
|
||||||
|
lexer.Add([]byte(`pick`), opToken(pickOpType))
|
||||||
|
|
||||||
lexer.Add([]byte(`flatten\([0-9]+\)`), flattenWithDepth())
|
lexer.Add([]byte(`flatten\([0-9]+\)`), flattenWithDepth())
|
||||||
lexer.Add([]byte(`flatten`), opTokenWithPrefs(flattenOpType, nil, flattenPreferences{depth: -1}))
|
lexer.Add([]byte(`flatten`), opTokenWithPrefs(flattenOpType, nil, flattenPreferences{depth: -1}))
|
||||||
|
@ -85,6 +85,7 @@ var columnOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Hand
|
|||||||
|
|
||||||
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator}
|
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator}
|
||||||
var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator}
|
var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator}
|
||||||
|
var pickOpType = &operationType{Type: "PICK", NumArgs: 1, Precedence: 50, Handler: pickOperator}
|
||||||
var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator}
|
var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator}
|
||||||
var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator}
|
var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator}
|
||||||
|
|
||||||
@ -197,6 +198,16 @@ func findInArray(array *yaml.Node, item *yaml.Node) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findKeyInMap(array *yaml.Node, item *yaml.Node) int {
|
||||||
|
|
||||||
|
for index := 0; index < len(array.Content); index = index + 2 {
|
||||||
|
if recursiveNodeEqual(array.Content[index], item) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
func recurseNodeObjectEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
|
func recurseNodeObjectEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
|
||||||
if len(lhs.Content) != len(rhs.Content) {
|
if len(lhs.Content) != len(rhs.Content) {
|
||||||
return false
|
return false
|
||||||
@ -274,11 +285,21 @@ func deepCloneContent(content []*yaml.Node) []*yaml.Node {
|
|||||||
return clonedContent
|
return clonedContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deepCloneNoContent(node *yaml.Node) *yaml.Node {
|
||||||
|
return deepCloneWithOptions(node, false)
|
||||||
|
}
|
||||||
func deepClone(node *yaml.Node) *yaml.Node {
|
func deepClone(node *yaml.Node) *yaml.Node {
|
||||||
|
return deepCloneWithOptions(node, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepCloneWithOptions(node *yaml.Node, cloneContent bool) *yaml.Node {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
clonedContent := deepCloneContent(node.Content)
|
var clonedContent []*yaml.Node
|
||||||
|
if cloneContent {
|
||||||
|
clonedContent = deepCloneContent(node.Content)
|
||||||
|
}
|
||||||
return &yaml.Node{
|
return &yaml.Node{
|
||||||
Content: clonedContent,
|
Content: clonedContent,
|
||||||
Kind: node.Kind,
|
Kind: node.Kind,
|
||||||
|
86
pkg/yqlib/operator_pick.go
Normal file
86
pkg/yqlib/operator_pick.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pickMap(original *yaml.Node, indices *yaml.Node) *yaml.Node {
|
||||||
|
|
||||||
|
filteredContent := make([]*yaml.Node, 0)
|
||||||
|
for index := 0; index < len(indices.Content); index = index + 1 {
|
||||||
|
keyToFind := indices.Content[index]
|
||||||
|
|
||||||
|
indexInMap := findKeyInMap(original, keyToFind)
|
||||||
|
if indexInMap > -1 {
|
||||||
|
clonedKey := deepClone(original.Content[indexInMap])
|
||||||
|
clonedValue := deepClone(original.Content[indexInMap+1])
|
||||||
|
filteredContent = append(filteredContent, clonedKey, clonedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode := deepCloneNoContent(original)
|
||||||
|
newNode.Content = filteredContent
|
||||||
|
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickSequence(original *yaml.Node, indices *yaml.Node) (*yaml.Node, error) {
|
||||||
|
|
||||||
|
filteredContent := make([]*yaml.Node, 0)
|
||||||
|
for index := 0; index < len(indices.Content); index = index + 1 {
|
||||||
|
_, indexInArray, err := parseInt(indices.Content[index].Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot index array with %v", indices.Content[index].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(indexInArray) > -1 && int(indexInArray) < len(original.Content) {
|
||||||
|
filteredContent = append(filteredContent, deepClone(original.Content[indexInArray]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode := deepCloneNoContent(original)
|
||||||
|
newNode.Content = filteredContent
|
||||||
|
|
||||||
|
return newNode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
log.Debugf("Pick")
|
||||||
|
|
||||||
|
contextIndicesToPick, err := d.GetMatchingNodes(context, expressionNode.RHS)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
indicesToPick := &yaml.Node{}
|
||||||
|
if contextIndicesToPick.MatchingNodes.Len() > 0 {
|
||||||
|
indicesToPick = contextIndicesToPick.MatchingNodes.Front().Value.(*CandidateNode).Node
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = list.New()
|
||||||
|
|
||||||
|
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||||
|
candidate := el.Value.(*CandidateNode)
|
||||||
|
node := unwrapDoc(candidate.Node)
|
||||||
|
|
||||||
|
var replacement *yaml.Node
|
||||||
|
if node.Tag == "!!map" {
|
||||||
|
replacement = pickMap(node, indicesToPick)
|
||||||
|
} else if node.Tag == "!!seq" {
|
||||||
|
replacement, err = pickSequence(node, indicesToPick)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return Context{}, fmt.Errorf("cannot pick indicies from type %v (%v)", node.Tag, candidate.GetNicePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
results.PushBack(candidate.CreateReplacement(replacement))
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.ChildContext(results), nil
|
||||||
|
}
|
33
pkg/yqlib/operator_pick_test.go
Normal file
33
pkg/yqlib/operator_pick_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pickOperatorScenarios = []expressionScenario{
|
||||||
|
{
|
||||||
|
description: "Pick keys from map",
|
||||||
|
subdescription: "Note that the order of the keys matches the pick order and non existent keys are skipped.",
|
||||||
|
document: "myMap: {cat: meow, dog: bark, thing: hamster, hamster: squeek}\n",
|
||||||
|
expression: `.myMap |= pick(["hamster", "cat", "goat"])`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (doc)::myMap: {hamster: squeek, cat: meow}\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Pick indices from array",
|
||||||
|
subdescription: "Note that the order of the indexes matches the pick order and non existent indexes are skipped.",
|
||||||
|
document: `[cat, leopard, lion]`,
|
||||||
|
expression: `pick([2, 0, 734, -5])`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!seq)::[lion, cat]\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickOperatorScenarios(t *testing.T) {
|
||||||
|
for _, tt := range pickOperatorScenarios {
|
||||||
|
testScenario(t, &tt)
|
||||||
|
}
|
||||||
|
documentOperatorScenarios(t, "pick", pickOperatorScenarios)
|
||||||
|
}
|
@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mikefarah/yq/v4/test"
|
"github.com/mikefarah/yq/v4/test"
|
||||||
"gopkg.in/op/go-logging.v1"
|
logging "gopkg.in/op/go-logging.v1"
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user