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

Compare commits

...

63 Commits

Author SHA1 Message Date
Mike Farah
f91d30e46c Fixed variable precedence 2021-02-04 12:39:04 +11:00
Mike Farah
07b053f040 Fixing op precedences 2021-02-04 12:18:54 +11:00
Mike Farah
35c97e832e Fixing op precedences 2021-02-03 17:11:47 +11:00
Mike Farah
8a5c47906e Added variables 2021-02-03 15:51:26 +11:00
Mike Farah
a366cacad3 Dont create entries when selecting 2021-02-03 11:56:35 +11:00
Mike Farah
7c8d3b9e70 Pass context through operators
Allows more sophisticated functionality
2021-02-03 11:56:35 +11:00
Mike Farah
ca10642e23 Added funding button 2021-02-01 15:50:57 +11:00
zy
73518f3915 change version from master to latest(v4) 2021-01-27 10:09:32 +11:00
zy
348ddb7a59 fix: go install fails
Installation by go will result in an error:
module declares its path as: xxx,
but was required as: xxx.

Go install with master branch will fix it.

fix: #676
2021-01-27 10:09:32 +11:00
Mike Farah
f46fe384bd Fixed length of null to be zero 2021-01-26 10:21:16 +11:00
Mike Farah
071ec3c08c fixing docker pipeline for nextime 2021-01-21 21:15:21 +11:00
Mike Farah
c2ed3a3e6d attempt to fix dockre 2021-01-21 21:11:04 +11:00
Mike Farah
cc4c69bc89 Fixed bad docker version 2021-01-21 21:07:55 +11:00
Mike Farah
917fd0eb6a Bump version 2021-01-18 14:10:35 +11:00
Mike Farah
b2056a2056 Can add and merge append to null 2021-01-18 13:58:46 +11:00
evnp
ba59201217 Don't escape HTML chars when converting to json
json.Encoder and json.Marshal implicitly use HTMLEscape to convert
>, <, &, with \u003c, \u003e, \u0026. This behavior carries over
to yq, where chars will be escaped when outputting json but not when
outputting yaml.

This changeset disables this behavior via encoder.SetEscapeHTML(false).
Unfortunately there is no equivalent option for json.Marshal, so its
single usage has been replaced with an encoder (with escaping disabled).
2021-01-18 13:33:02 +11:00
Mike Farah
cfddee9101 Fixed cross-function combinatorial bug 2021-01-18 13:28:40 +11:00
Mike Farah
1941bb66a5 wip 2021-01-18 10:15:31 +11:00
Mike Farah
29af9e4c63 thoughts 2021-01-16 14:56:52 +11:00
Mike Farah
af3a9ae846 cross function fix wip 2021-01-16 14:09:49 +11:00
Mike Farah
5fb88627ca Fixed doker instructions 2021-01-15 09:44:02 +11:00
Mike Farah
4fc3793fcb Fixing multiply doc 2021-01-14 20:28:57 +11:00
Mike Farah
d612897e89 Incrementing version 2021-01-14 19:50:37 +11:00
Mike Farah
806f906041 Added keys operator 2021-01-14 15:45:07 +11:00
Mike Farah
e8b2f6e383 Added split string operator 2021-01-14 15:05:50 +11:00
Mike Farah
e419b5a39d Added join strings operator 2021-01-14 14:46:50 +11:00
Mike Farah
a5b6e08282 Split doc operator 2021-01-14 14:25:31 +11:00
Mike Farah
2417c0ee8f Fixing add,multiply,alternative operator precendences 2021-01-14 11:16:04 +11:00
Mike Farah
d3b2e37b66 Fixed remove comments example 2021-01-14 09:12:14 +11:00
Mike Farah
54723c1a36 Dont use pointer for env prefs (avoid nil) 2021-01-13 17:00:53 +11:00
Mike Farah
0318c80d33 Dont use pointer for recursive prefs (avoid nil) 2021-01-13 17:00:03 +11:00
Mike Farah
8980846632 Dont use pointer for multiply prefs (avoid nil) 2021-01-13 16:59:01 +11:00
Mike Farah
0fb62d3ae1 Dont use pointer for commment prefs (avoid nil) 2021-01-13 16:56:24 +11:00
Mike Farah
85398727ad Added merge if empty 2021-01-13 16:54:28 +11:00
Mike Farah
0bf3d781f6 Added operator level doc 2021-01-13 15:23:26 +11:00
Mike Farah
5c15936bf3 Incrementing version 2021-01-13 10:29:06 +11:00
Mike Farah
b0735d8152 Renaming pathtree to expression 2021-01-13 10:25:26 +11:00
Mike Farah
f17cbfd007 Removed global vars 2021-01-13 10:04:52 +11:00
Mike Farah
7b7ab70286 UnwrapDoc now private 2021-01-13 10:00:51 +11:00
Mike Farah
43165fa340 Moved eval function to eval interface 2021-01-13 09:35:57 +11:00
Mikhail Katychev
feda9f044d added lib_test.go 2021-01-13 09:22:40 +11:00
Mikhail Katychev
85629af59c added EvaluateNodes and EvaluateCandidateNodes to yqlib 2021-01-13 09:22:40 +11:00
Mike Farah
5fc26e9453 Merge now copies anchor names 2021-01-13 09:21:16 +11:00
Mike Farah
07a6fa4df5 Fixed creation of candidateNode in operators to include file metadata 2021-01-12 19:36:28 +11:00
Mike Farah
ec29ee7c14 Cleaning up exposed public api 2021-01-12 09:58:50 +11:00
Mike Farah
24538c0cc7 Fixed tag operator for top level node 2021-01-12 09:45:57 +11:00
Mike Farah
61398bfd3a Fixed equals operator for top level node 2021-01-12 09:40:37 +11:00
Mike Farah
f7d95021c1 Fixed has operator for top level node 2021-01-12 09:30:24 +11:00
Mike Farah
6bb8b1fb77 fixing exposed functions and interfaces 2021-01-11 17:13:48 +11:00
Mike Farah
5e2c19cc86 fixing exposed functions and interfaces 2021-01-11 16:46:28 +11:00
Mike Farah
30c269a66c Better add documentation 2021-01-11 15:52:06 +11:00
Mike Farah
70a1c60d7b Added scalar addition 2021-01-11 15:43:50 +11:00
Mike Farah
e4d48bbc0d Fixed collect at document level 2021-01-11 14:44:53 +11:00
Mike Farah
369ff94ad0 Better error handling will empty env 2021-01-11 14:38:53 +11:00
Mike Farah
1c7fb14631 Better recursive decent docs 2021-01-10 11:27:18 +11:00
Mike Farah
f4de5c4300 Better docIndex docs 2021-01-10 11:18:13 +11:00
Mike Farah
a72c14f06b Better env docs 2021-01-10 11:09:59 +11:00
Mike Farah
5ed52aca66 Merged env commands in :eye-roll: 2021-01-10 10:51:23 +11:00
Mike Farah
5a5ac0dfef Merge branch 'env_var' 2021-01-10 10:50:31 +11:00
Mike Farah
644063646e Env Ops! 2021-01-09 12:06:19 +11:00
Mike Farah
aabed1a237 strenv 2021-01-09 11:38:08 +11:00
Mike Farah
c12764dba8 wip 2021-01-08 21:09:43 +11:00
Mike Farah
1d5ecb244d wip 2021-01-08 21:07:46 +11:00
101 changed files with 2699 additions and 1324 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: mikefarah

View File

@@ -40,17 +40,7 @@ jobs:
IMAGE_NAME: mikefarah/yq IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get latest release tag - uses: actions/checkout@v2
uses: oprypin/find-latest-tag@v1
with:
repository: mikefarah/yq # The repository to scan.
releases-only: true # We know that all relevant tags have a GitHub release for them.
id: yq
- name: Clone source code
uses: actions/checkout@v2
with:
ref: ${{ steps.yq.outputs.tag }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1

View File

@@ -90,7 +90,7 @@ docker run --rm -v "${PWD}":/workdir mikefarah/yq <command> [flags] [expression
#### Run commands interactively: #### Run commands interactively:
```bash ```bash
docker run --rm -it -v "${PWD}":/workdir mikefarah/yq sh docker run --rm -it -v "${PWD}":/workdir --entrypoint sh mikefarah/yq
``` ```
It can be useful to have a bash function to avoid typing the whole docker command: It can be useful to have a bash function to avoid typing the whole docker command:
@@ -103,7 +103,7 @@ yq() {
### Go Get: ### Go Get:
``` ```
GO111MODULE=on go get github.com/mikefarah/yq GO111MODULE=on go get github.com/mikefarah/yq/v4
``` ```
## Community Supported Installation methods ## Community Supported Installation methods

View File

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

View File

@@ -1,4 +1,6 @@
a: simple # just the best a:
b: [1, 2] key1: "value1"
c: key2: 2.6
test: 1 ab:
key1: 6
key2: "h"

View File

@@ -1,4 +1,4 @@
FROM mikefarah/yq:4.3.0 FROM mikefarah/yq:4.4.1
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh

1
go.sum
View File

@@ -281,6 +281,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=

View File

@@ -1,29 +1,54 @@
package yqlib package yqlib
import "container/list" import (
"container/list"
/** yaml "gopkg.in/yaml.v3"
Loads all yaml documents of all files given into memory, then runs the given expression once. )
**/
// A yaml expression evaluator that runs the expression once against all files/nodes in memory.
type Evaluator interface { type Evaluator interface {
EvaluateFiles(expression string, filenames []string, printer Printer) error EvaluateFiles(expression string, filenames []string, printer Printer) error
// EvaluateNodes takes an expression and one or more yaml nodes, returning a list of matching candidate nodes
EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error)
// EvaluateCandidateNodes takes an expression and list of candidate nodes, returning a list of matching candidate nodes
EvaluateCandidateNodes(expression string, inputCandidateNodes *list.List) (*list.List, error)
} }
type allAtOnceEvaluator struct { type allAtOnceEvaluator struct {
treeNavigator DataTreeNavigator treeNavigator DataTreeNavigator
treeCreator PathTreeCreator treeCreator ExpressionParser
} }
func NewAllAtOnceEvaluator() Evaluator { func NewAllAtOnceEvaluator() Evaluator {
return &allAtOnceEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewPathTreeCreator()} return &allAtOnceEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewExpressionParser()}
}
func (e *allAtOnceEvaluator) EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error) {
inputCandidates := list.New()
for _, node := range nodes {
inputCandidates.PushBack(&CandidateNode{Node: node})
}
return e.EvaluateCandidateNodes(expression, inputCandidates)
}
func (e *allAtOnceEvaluator) EvaluateCandidateNodes(expression string, inputCandidates *list.List) (*list.List, error) {
node, err := e.treeCreator.ParseExpression(expression)
if err != nil {
return nil, err
}
context, err := e.treeNavigator.GetMatchingNodes(Context{MatchingNodes: inputCandidates}, node)
if err != nil {
return nil, err
}
return context.MatchingNodes, nil
} }
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error { func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
fileIndex := 0 fileIndex := 0
node, err := treeCreator.ParsePath(expression)
if err != nil {
return err
}
var allDocuments *list.List = list.New() var allDocuments *list.List = list.New()
for _, filename := range filenames { for _, filename := range filenames {
reader, err := readStream(filename) reader, err := readStream(filename)
@@ -37,7 +62,7 @@ func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string
allDocuments.PushBackList(fileDocuments) allDocuments.PushBackList(fileDocuments)
fileIndex = fileIndex + 1 fileIndex = fileIndex + 1
} }
matches, err := treeNavigator.GetMatchingNodes(allDocuments, node) matches, err := e.EvaluateCandidateNodes(expression, allDocuments)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -0,0 +1,40 @@
package yqlib
import (
"testing"
"github.com/mikefarah/yq/v4/test"
)
var evaluateNodesScenario = []expressionScenario{
{
document: `a: hello`,
expression: `.a`,
expected: []string{
"D0, P[a], (!!str)::hello\n",
},
},
{
document: `a: hello`,
expression: `.`,
expected: []string{
"D0, P[], (doc)::a: hello\n",
},
},
{
document: `- a: "yes"`,
expression: `.[] | has("a")`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
},
},
}
func TestAllAtOnceEvaluateNodes(t *testing.T) {
var evaluator = NewAllAtOnceEvaluator()
for _, tt := range evaluateNodesScenario {
node := test.ParseData(tt.document)
list, _ := evaluator.EvaluateNodes(tt.expression, &node)
test.AssertResultComplex(t, tt.expected, resultsToString(list))
}
}

View File

@@ -15,13 +15,32 @@ type CandidateNode struct {
Document uint // the document index of this node Document uint // the document index of this node
Filename string Filename string
FileIndex int FileIndex int
// when performing op against all nodes given, this will treat all the nodes as one
// (e.g. top level cross document merge). This property does not propegate to child nodes.
EvaluateTogether bool
} }
func (n *CandidateNode) GetKey() string { func (n *CandidateNode) GetKey() string {
return fmt.Sprintf("%v - %v", n.Document, n.Path) return fmt.Sprintf("%v - %v", n.Document, n.Path)
} }
func (n *CandidateNode) CreateChildPath(path interface{}) []interface{} { func (n *CandidateNode) CreateChild(path interface{}, node *yaml.Node) *CandidateNode {
return &CandidateNode{
Node: node,
Path: n.createChildPath(path),
Document: n.Document,
Filename: n.Filename,
FileIndex: n.FileIndex,
}
}
func (n *CandidateNode) createChildPath(path interface{}) []interface{} {
if path == nil {
newPath := make([]interface{}, len(n.Path))
copy(newPath, n.Path)
return newPath
}
//don't use append as they may actually modify the path of the orignal node! //don't use append as they may actually modify the path of the orignal node!
newPath := make([]interface{}, len(n.Path)+1) newPath := make([]interface{}, len(n.Path)+1)
copy(newPath, n.Path) copy(newPath, n.Path)
@@ -45,6 +64,7 @@ func (n *CandidateNode) UpdateFrom(other *CandidateNode) {
n.Node.Content = other.Node.Content n.Node.Content = other.Node.Content
n.Node.Value = other.Node.Value n.Node.Value = other.Node.Value
n.Node.Alias = other.Node.Alias n.Node.Alias = other.Node.Alias
n.Node.Anchor = other.Node.Anchor
} }
func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode) { func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode) {

View File

@@ -17,8 +17,8 @@ func format(attr color.Attribute) string {
return fmt.Sprintf("%s[%dm", escape, attr) return fmt.Sprintf("%s[%dm", escape, attr)
} }
func ColorizeAndPrint(bytes []byte, writer io.Writer) error { func colorizeAndPrint(yamlBytes []byte, writer io.Writer) error {
tokens := lexer.Tokenize(string(bytes)) tokens := lexer.Tokenize(string(yamlBytes))
var p printer.Printer var p printer.Printer
p.Bool = func() *printer.Property { p.Bool = func() *printer.Property {
return &printer.Property{ return &printer.Property{

44
pkg/yqlib/context.go Normal file
View File

@@ -0,0 +1,44 @@
package yqlib
import (
"container/list"
"github.com/jinzhu/copier"
)
type Context struct {
MatchingNodes *list.List
Variables map[string]*list.List
DontAutoCreate bool
}
func (n *Context) SingleChildContext(candidate *CandidateNode) Context {
list := list.New()
list.PushBack(candidate)
return n.ChildContext(list)
}
func (n *Context) GetVariable(name string) *list.List {
if n.Variables == nil {
return nil
}
return n.Variables[name]
}
func (n *Context) SetVariable(name string, value *list.List) {
if n.Variables == nil {
n.Variables = make(map[string]*list.List)
}
n.Variables[name] = value
}
func (n *Context) ChildContext(results *list.List) Context {
clone := Context{}
err := copier.Copy(&clone, n)
if err != nil {
log.Error("Error cloning context :(")
panic(err)
}
clone.MatchingNodes = results
return clone
}

View File

@@ -3,16 +3,14 @@ package yqlib
import ( import (
"fmt" "fmt"
"container/list"
logging "gopkg.in/op/go-logging.v1" logging "gopkg.in/op/go-logging.v1"
) )
type DataTreeNavigator interface { type DataTreeNavigator interface {
// given a list of CandidateEntities and a pathNode, // given the context and a expressionNode,
// this will process the list against the given pathNode and return // this will process the against the given expressionNode and return
// a new list of matching candidates // a new context of matching candidates
GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) GetMatchingNodes(context Context, expressionNode *ExpressionNode) (Context, error)
} }
type dataTreeNavigator struct { type dataTreeNavigator struct {
@@ -22,22 +20,22 @@ func NewDataTreeNavigator() DataTreeNavigator {
return &dataTreeNavigator{} return &dataTreeNavigator{}
} }
func (d *dataTreeNavigator) GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *ExpressionNode) (Context, error) {
if pathNode == nil { if expressionNode == nil {
log.Debugf("getMatchingNodes - nothing to do") log.Debugf("getMatchingNodes - nothing to do")
return matchingNodes, nil return context, nil
} }
log.Debugf("Processing Op: %v", pathNode.Operation.toString()) log.Debugf("Processing Op: %v", expressionNode.Operation.toString())
if log.IsEnabledFor(logging.DEBUG) { if log.IsEnabledFor(logging.DEBUG) {
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
log.Debug(NodeToString(el.Value.(*CandidateNode))) log.Debug(NodeToString(el.Value.(*CandidateNode)))
} }
} }
log.Debug(">>") log.Debug(">>")
handler := pathNode.Operation.OperationType.Handler handler := expressionNode.Operation.OperationType.Handler
if handler != nil { if handler != nil {
return handler(d, matchingNodes, pathNode) return handler(d, context, expressionNode)
} }
return nil, fmt.Errorf("Unknown operator %v", pathNode.Operation.OperationType) return Context{}, fmt.Errorf("Unknown operator %v", expressionNode.Operation.OperationType)
} }

View File

@@ -1,9 +1,9 @@
Add behaves differently according to the type of the LHS: Add behaves differently according to the type of the LHS:
- arrays: concatenate - arrays: concatenate
- number scalars: arithmetic addition (soon) - number scalars: arithmetic addition
- string scalars: concatenate (soon) - string scalars: concatenate
Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`. Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`.
## Concatenate and assign arrays ## Concatenate and assign arrays
Given a sample.yml file of: Given a sample.yml file of:
@@ -67,23 +67,19 @@ will output
- 2 - 2
``` ```
## Add object to array ## Add new object to array
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
a: a:
- 1 - dog: woof
- 2
c:
cat: meow
``` ```
then then
```bash ```bash
yq eval '.a + .c' sample.yml yq eval '.a + {"cat": "meow"}' sample.yml
``` ```
will output will output
```yaml ```yaml
- 1 - dog: woof
- 2
- cat: meow - cat: meow
``` ```
@@ -131,3 +127,97 @@ b:
- 4 - 4
``` ```
## String concatenation
Given a sample.yml file of:
```yaml
a: cat
b: meow
```
then
```bash
yq eval '.a = .a + .b' sample.yml
```
will output
```yaml
a: catmeow
b: meow
```
## Relative string concatenation
Given a sample.yml file of:
```yaml
a: cat
b: meow
```
then
```bash
yq eval '.a += .b' sample.yml
```
will output
```yaml
a: catmeow
b: meow
```
## Number addition - float
If the lhs or rhs are floats then the expression will be calculated with floats.
Given a sample.yml file of:
```yaml
a: 3
b: 4.9
```
then
```bash
yq eval '.a = .a + .b' sample.yml
```
will output
```yaml
a: 7.9
b: 4.9
```
## Number addition - int
If both the lhs and rhs are ints then the expression will be calculated with ints.
Given a sample.yml file of:
```yaml
a: 3
b: 4
```
then
```bash
yq eval '.a = .a + .b' sample.yml
```
will output
```yaml
a: 7
b: 4
```
## Increment number
Given a sample.yml file of:
```yaml
a: 3
```
then
```bash
yq eval '.a += 1' sample.yml
```
will output
```yaml
a: 4
```
## Add to null
Adding to null simply returns the rhs
Running
```bash
yq eval --null-input 'null + "cat"'
```
will output
```yaml
cat
```

View File

@@ -78,17 +78,22 @@ b: dog # leave this
``` ```
## Remove all comments ## Remove all comments
Note the use of `...` to ensure key nodes are included.
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
a: cat # comment a: cat # comment
# great
b: # key comment
``` ```
then then
```bash ```bash
yq eval '.. comments=""' sample.yml yq eval '... comments=""' sample.yml
``` ```
will output will output
```yaml ```yaml
a: cat a: cat
b:
``` ```
## Get line comment ## Get line comment

View File

@@ -1,4 +1,4 @@
Use the `documentIndex` operator to select nodes of a particular document. Use the `documentIndex` operator (or the `di` shorthand) to select nodes of a particular document.
## Retrieve a document index ## Retrieve a document index
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@@ -0,0 +1,78 @@
This operator is used to handle environment variables usage in path expressions. While environment variables can, of course, be passed in via your CLI with string interpolation, this often comes with complex quote escaping and can be tricky to write and read. Note that there are two forms, `env` which will parse the environment variable as a yaml (be it a map, array, string, number of boolean) and `strenv` which will always parse the argument as a string.
## Read string environment variable
Running
```bash
myenv="cat meow" yq eval --null-input '.a = env(myenv)'
```
will output
```yaml
a: cat meow
```
## Read boolean environment variable
Running
```bash
myenv="true" yq eval --null-input '.a = env(myenv)'
```
will output
```yaml
a: true
```
## Read numeric environment variable
Running
```bash
myenv="12" yq eval --null-input '.a = env(myenv)'
```
will output
```yaml
a: 12
```
## Read yaml environment variable
Running
```bash
myenv="{b: fish}" yq eval --null-input '.a = env(myenv)'
```
will output
```yaml
a: {b: fish}
```
## Read boolean environment variable as a string
Running
```bash
myenv="true" yq eval --null-input '.a = strenv(myenv)'
```
will output
```yaml
a: "true"
```
## Read numeric environment variable as a string
Running
```bash
myenv="12" yq eval --null-input '.a = strenv(myenv)'
```
will output
```yaml
a: "12"
```
## Dynamic key lookup with environment variable
Given a sample.yml file of:
```yaml
cat: meow
dog: woof
```
then
```bash
myenv="cat" yq eval '.[env(myenv)]' sample.yml
```
will output
```yaml
meow
```

35
pkg/yqlib/doc/Keys.md Normal file
View File

@@ -0,0 +1,35 @@
# Keys
Use the `keys` operator to return map keys or array indices.
## Map keys
Given a sample.yml file of:
```yaml
dog: woof
cat: meow
```
then
```bash
yq eval 'keys' sample.yml
```
will output
```yaml
- dog
- cat
```
## Array keys
Given a sample.yml file of:
```yaml
- apple
- banana
```
then
```bash
yq eval 'keys' sample.yml
```
will output
```yaml
- 0
- 1
```

View File

@@ -16,6 +16,20 @@ will output
3 3
``` ```
## null length
Given a sample.yml file of:
```yaml
a: null
```
then
```bash
yq eval '.a | length' sample.yml
```
will output
```yaml
0
```
## Map length ## Map length
returns number of entries returns number of entries

View File

@@ -1,13 +1,14 @@
Like the multiple operator in `jq`, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS. Like the multiple operator in jq, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS.
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings). To concatenate arrays when merging objects, use the *+ form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
To concatenate when merging objects, use the `*+` form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
To merge only existing fields, use the *? form. Note that this can be used with the concatenate arrays too *+?.
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below. Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
Multiplication of strings and numbers are not yet supported.
## Merging files ## Merging files
Note the use of eval-all to ensure all documents are loaded into memory. Note the use of `eval-all` to ensure all documents are loaded into memory.
```bash ```bash
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml
@@ -111,6 +112,26 @@ b:
- 5 - 5
``` ```
## Merge, only existing fields
Given a sample.yml file of:
```yaml
a:
thing: one
cat: frog
b:
missing: two
thing: two
```
then
```bash
yq eval '.a *? .b' sample.yml
```
will output
```yaml
thing: two
cat: frog
```
## Merge, appending arrays ## Merge, appending arrays
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@@ -143,6 +164,33 @@ array:
value: banana value: banana
``` ```
## Merge, only existing fields, appending arrays
Given a sample.yml file of:
```yaml
a:
thing:
- 1
- 2
b:
thing:
- 3
- 4
another:
- 1
```
then
```bash
yq eval '.a *?+ .b' sample.yml
```
will output
```yaml
thing:
- 1
- 2
- 3
- 4
```
## Merge to prefix an element ## Merge to prefix an element
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@@ -180,7 +228,7 @@ g: thongs
f: *cat f: *cat
``` ```
## Merge does not copy anchor names ## Merge copies anchor names
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
a: a:
@@ -197,7 +245,7 @@ yq eval '.c * .a' sample.yml
will output will output
```yaml ```yaml
g: thongs g: thongs
c: frog c: &cat frog
``` ```
## Merge with merge anchors ## Merge with merge anchors

View File

@@ -33,6 +33,8 @@ frog
``` ```
## Recursively find nodes with keys ## Recursively find nodes with keys
Note that this example has wrapped the expression in `[]` to show that there are two matches returned. You do not have to wrap in `[]` in your path expression.
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
a: a:
@@ -43,16 +45,16 @@ a:
``` ```
then then
```bash ```bash
yq eval '.. | select(has("name"))' sample.yml yq eval '[.. | select(has("name"))]' sample.yml
``` ```
will output will output
```yaml ```yaml
name: frog - name: frog
b: b:
name: blog name: blog
age: 12
- name: blog
age: 12 age: 12
name: blog
age: 12
``` ```
## Recursively find nodes with values ## Recursively find nodes with values

View File

@@ -0,0 +1,31 @@
# Split into Documents
This operator splits all matches into separate documents
## Split empty
Running
```bash
yq eval --null-input 'splitDoc'
```
will output
```yaml
```
## Split array
Given a sample.yml file of:
```yaml
- a: cat
- b: dog
```
then
```bash
yq eval '.[] | splitDoc' sample.yml
```
will output
```yaml
a: cat
---
b: dog
```

View File

@@ -0,0 +1,52 @@
# String Operators
## Join strings
Given a sample.yml file of:
```yaml
- cat
- meow
- 1
- null
- true
```
then
```bash
yq eval 'join("; ")' sample.yml
```
will output
```yaml
cat; meow; 1; ; true
```
## Split strings
Given a sample.yml file of:
```yaml
cat; meow; 1; ; true
```
then
```bash
yq eval 'split("; ")' sample.yml
```
will output
```yaml
- cat
- meow
- "1"
- ""
- "true"
```
## Split strings one match
Given a sample.yml file of:
```yaml
word
```
then
```bash
yq eval 'split("; ")' sample.yml
```
will output
```yaml
- word
```

View File

@@ -48,6 +48,24 @@ will output
frog frog
``` ```
## Dynamic keys
Expressions within [] can be used to dynamically lookup / calculate keys
Given a sample.yml file of:
```yaml
b: apple
apple: crispy yum
banana: soft yum
```
then
```bash
yq eval '.[.b]' sample.yml
```
will output
```yaml
crispy yum
```
## Children don't exist ## Children don't exist
Nodes are added dynamically while traversing Nodes are added dynamically while traversing

View File

@@ -0,0 +1,12 @@
# Operators
In `yq` expressions are made up of operators. Operators have 0-2 arguments and run against the current 'matching' nodes in the expression tree.
Lets look at a couple of examples.
The `length` operator take no arguments, and will simply return the length of _each_ matching node. So if there were 2 nodes, one string and one array, length will update the 'matching' nodes context to be two new numeric scalar nodes representing the lengths of the orignal 'matching' nodes.
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.
Please see the individual operator docs for more information and examples.

View File

@@ -1,6 +1,6 @@
Add behaves differently according to the type of the LHS: Add behaves differently according to the type of the LHS:
- arrays: concatenate - arrays: concatenate
- number scalars: arithmetic addition (soon) - number scalars: arithmetic addition
- string scalars: concatenate (soon) - string scalars: concatenate
Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`. Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`.

View File

@@ -1 +1 @@
Use the `documentIndex` operator to select nodes of a particular document. Use the `documentIndex` operator (or the `di` shorthand) to select nodes of a particular document.

View File

@@ -0,0 +1,2 @@
This operator is used to handle environment variables usage in path expressions. While environment variables can, of course, be passed in via your CLI with string interpolation, this often comes with complex quote escaping and can be tricky to write and read. Note that there are two forms, `env` which will parse the environment variable as a yaml (be it a map, array, string, number of boolean) and `strenv` which will always parse the argument as a string.

View File

@@ -0,0 +1,3 @@
# Keys
Use the `keys` operator to return map keys or array indices.

View File

@@ -1,13 +1,14 @@
Like the multiple operator in `jq`, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS. Like the multiple operator in jq, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS.
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings). To concatenate arrays when merging objects, use the *+ form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
To concatenate when merging objects, use the `*+` form (see examples below). This will recursively merge objects, appending arrays when it encounters them.
To merge only existing fields, use the *? form. Note that this can be used with the concatenate arrays too *+?.
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below. Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
Multiplication of strings and numbers are not yet supported.
## Merging files ## Merging files
Note the use of eval-all to ensure all documents are loaded into memory. Note the use of `eval-all` to ensure all documents are loaded into memory.
```bash ```bash
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml

View File

@@ -0,0 +1,3 @@
# Split into Documents
This operator splits all matches into separate documents

View File

@@ -0,0 +1 @@
# String Operators

View File

@@ -50,7 +50,7 @@ func (ye *yamlEncoder) Encode(node *yaml.Node) error {
} }
if ye.colorise { if ye.colorise {
return ColorizeAndPrint(tempBuffer.Bytes(), ye.destination) return colorizeAndPrint(tempBuffer.Bytes(), ye.destination)
} }
return nil return nil
} }
@@ -76,6 +76,8 @@ func mapKeysToStrings(node *yaml.Node) {
func NewJsonEncoder(destination io.Writer, indent int) Encoder { func NewJsonEncoder(destination io.Writer, indent int) Encoder {
var encoder = json.NewEncoder(destination) var encoder = json.NewEncoder(destination)
encoder.SetEscapeHTML(false) // do not escape html chars e.g. &, <, >
var indentString = "" var indentString = ""
for index := 0; index < indent; index++ { for index := 0; index < indent; index++ {
@@ -153,11 +155,15 @@ func (o *orderedMap) UnmarshalJSON(data []byte) error {
} }
func (o orderedMap) MarshalJSON() ([]byte, error) { func (o orderedMap) MarshalJSON() ([]byte, error) {
if o.kv == nil {
return json.Marshal(o.altVal)
}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
enc := json.NewEncoder(buf) enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false) // do not escape html chars e.g. &, <, >
if o.kv == nil {
if err := enc.Encode(o.altVal); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
buf.WriteByte('{') buf.WriteByte('{')
for idx, el := range o.kv { for idx, el := range o.kv {
if err := enc.Encode(el.K); err != nil { if err := enc.Encode(el.K); err != nil {

View File

@@ -9,29 +9,11 @@ import (
"github.com/mikefarah/yq/v4/test" "github.com/mikefarah/yq/v4/test"
) )
var sampleYaml = `zabbix: winner func yamlToJson(sampleYaml string, indent int) string {
apple: great
banana:
- {cobra: kai, angus: bob}
`
var expectedJson = `{
"zabbix": "winner",
"apple": "great",
"banana": [
{
"cobra": "kai",
"angus": "bob"
}
]
}
`
func TestJsonEncoderPreservesObjectOrder(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
writer := bufio.NewWriter(&output) writer := bufio.NewWriter(&output)
var jsonEncoder = NewJsonEncoder(writer, 2) var jsonEncoder = NewJsonEncoder(writer, indent)
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0) inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -42,6 +24,33 @@ func TestJsonEncoderPreservesObjectOrder(t *testing.T) {
panic(err) panic(err)
} }
writer.Flush() writer.Flush()
test.AssertResult(t, expectedJson, output.String())
return strings.TrimSuffix(output.String(), "\n")
}
func TestJsonEncoderPreservesObjectOrder(t *testing.T) {
var sampleYaml = `zabbix: winner
apple: great
banana:
- {cobra: kai, angus: bob}
`
var expectedJson = `{
"zabbix": "winner",
"apple": "great",
"banana": [
{
"cobra": "kai",
"angus": "bob"
}
]
}`
var actualJson = yamlToJson(sampleYaml, 2)
test.AssertResult(t, expectedJson, actualJson)
}
func TestJsonEncoderDoesNotEscapeHTMLChars(t *testing.T) {
var sampleYaml = `build: "( ./lint && ./format && ./compile ) < src.code"`
var expectedJson = `{"build":"( ./lint && ./format && ./compile ) < src.code"}`
var actualJson = yamlToJson(sampleYaml, 0)
test.AssertResult(t, expectedJson, actualJson)
} }

View File

@@ -5,29 +5,28 @@ import (
"strings" "strings"
) )
var myPathTokeniser = NewPathTokeniser() var myPathTokeniser = newExpressionTokeniser()
var myPathPostfixer = NewPathPostFixer() var myPathPostfixer = newExpressionPostFixer()
type PathTreeNode struct { type ExpressionNode struct {
Operation *Operation Operation *Operation
Lhs *PathTreeNode Lhs *ExpressionNode
Rhs *PathTreeNode Rhs *ExpressionNode
} }
type PathTreeCreator interface { type ExpressionParser interface {
ParsePath(path string) (*PathTreeNode, error) ParseExpression(expression string) (*ExpressionNode, error)
CreatePathTree(postFixPath []*Operation) (*PathTreeNode, error)
} }
type pathTreeCreator struct { type expressionParserImpl struct {
} }
func NewPathTreeCreator() PathTreeCreator { func NewExpressionParser() ExpressionParser {
return &pathTreeCreator{} return &expressionParserImpl{}
} }
func (p *pathTreeCreator) ParsePath(path string) (*PathTreeNode, error) { func (p *expressionParserImpl) ParseExpression(expression string) (*ExpressionNode, error) {
tokens, err := myPathTokeniser.Tokenise(path) tokens, err := myPathTokeniser.Tokenise(expression)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -36,18 +35,18 @@ func (p *pathTreeCreator) ParsePath(path string) (*PathTreeNode, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return p.CreatePathTree(Operations) return p.createExpressionTree(Operations)
} }
func (p *pathTreeCreator) CreatePathTree(postFixPath []*Operation) (*PathTreeNode, error) { func (p *expressionParserImpl) createExpressionTree(postFixPath []*Operation) (*ExpressionNode, error) {
var stack = make([]*PathTreeNode, 0) var stack = make([]*ExpressionNode, 0)
if len(postFixPath) == 0 { if len(postFixPath) == 0 {
return nil, nil return nil, nil
} }
for _, Operation := range postFixPath { for _, Operation := range postFixPath {
var newNode = PathTreeNode{Operation: Operation} var newNode = ExpressionNode{Operation: Operation}
log.Debugf("pathTree %v ", Operation.toString()) log.Debugf("pathTree %v ", Operation.toString())
if Operation.OperationType.NumArgs > 0 { if Operation.OperationType.NumArgs > 0 {
numArgs := Operation.OperationType.NumArgs numArgs := Operation.OperationType.NumArgs
@@ -71,7 +70,7 @@ func (p *pathTreeCreator) CreatePathTree(postFixPath []*Operation) (*PathTreeNod
stack = append(stack, &newNode) stack = append(stack, &newNode)
} }
if len(stack) != 1 { if len(stack) != 1 {
return nil, fmt.Errorf("expected end of expression but found '%v', please check expression syntax", strings.TrimSpace(stack[1].Operation.StringValue)) return nil, fmt.Errorf("Bad expression, please check expression syntax")
} }
return stack[0], nil return stack[0], nil
} }

View File

@@ -7,36 +7,36 @@ import (
) )
func TestPathTreeNoArgsForTwoArgOp(t *testing.T) { func TestPathTreeNoArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("=") _, err := NewExpressionParser().ParseExpression("=")
test.AssertResultComplex(t, "'=' expects 2 args but there is 0", err.Error()) test.AssertResultComplex(t, "'=' expects 2 args but there is 0", err.Error())
} }
func TestPathTreeOneLhsArgsForTwoArgOp(t *testing.T) { func TestPathTreeOneLhsArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath(".a =") _, err := NewExpressionParser().ParseExpression(".a =")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error()) test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
} }
func TestPathTreeOneRhsArgsForTwoArgOp(t *testing.T) { func TestPathTreeOneRhsArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("= .a") _, err := NewExpressionParser().ParseExpression("= .a")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error()) test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
} }
func TestPathTreeTwoArgsForTwoArgOp(t *testing.T) { func TestPathTreeTwoArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath(".a = .b") _, err := NewExpressionParser().ParseExpression(".a = .b")
test.AssertResultComplex(t, nil, err) test.AssertResultComplex(t, nil, err)
} }
func TestPathTreeNoArgsForOneArgOp(t *testing.T) { func TestPathTreeNoArgsForOneArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("explode") _, err := NewExpressionParser().ParseExpression("explode")
test.AssertResultComplex(t, "'explode' expects 1 arg but received none", err.Error()) test.AssertResultComplex(t, "'explode' expects 1 arg but received none", err.Error())
} }
func TestPathTreeOneArgForOneArgOp(t *testing.T) { func TestPathTreeOneArgForOneArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("explode(.)") _, err := NewExpressionParser().ParseExpression("explode(.)")
test.AssertResultComplex(t, nil, err) test.AssertResultComplex(t, nil, err)
} }
func TestPathTreeExtraArgs(t *testing.T) { func TestPathTreeExtraArgs(t *testing.T) {
_, err := treeCreator.ParsePath("sortKeys(.) explode(.)") _, err := NewExpressionParser().ParseExpression("sortKeys(.) explode(.)")
test.AssertResultComplex(t, "expected end of expression but found 'explode', please check expression syntax", err.Error()) test.AssertResultComplex(t, "Bad expression, please check expression syntax", err.Error())
} }

View File

@@ -0,0 +1,95 @@
package yqlib
import (
"errors"
logging "gopkg.in/op/go-logging.v1"
)
type expressionPostFixer interface {
ConvertToPostfix([]*token) ([]*Operation, error)
}
type expressionPostFixerImpl struct {
}
func newExpressionPostFixer() expressionPostFixer {
return &expressionPostFixerImpl{}
}
func popOpToResult(opStack []*token, result []*Operation) ([]*token, []*Operation) {
var newOp *token
opStack, newOp = opStack[0:len(opStack)-1], opStack[len(opStack)-1]
log.Debugf("popped %v from opstack to results", newOp.toString(true))
return opStack, append(result, newOp.Operation)
}
func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Operation, error) {
var result []*Operation
// surround the whole thing with brackets
var opStack = []*token{{TokenType: openBracket}}
var tokens = append(infixTokens, &token{TokenType: closeBracket})
for _, currentToken := range tokens {
log.Debugf("postfix processing currentToken %v", currentToken.toString(true))
switch currentToken.TokenType {
case openBracket, openCollect, openCollectObject:
opStack = append(opStack, currentToken)
log.Debugf("put %v onto the opstack", currentToken.toString(true))
case closeCollect, closeCollectObject:
var opener tokenType = openCollect
var collectOperator *operationType = collectOpType
if currentToken.TokenType == closeCollectObject {
opener = openCollectObject
collectOperator = collectObjectOpType
}
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != opener {
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close collect brackets without matching opening bracket")
}
// now we should have [ as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
log.Debugf("deleteing open bracket from opstack")
//and append a collect to the opStack
result = append(result, &Operation{OperationType: collectOperator})
log.Debugf("put collect onto the result")
result = append(result, &Operation{OperationType: shortPipeOpType})
log.Debugf("put shortpipe onto the result")
case closeBracket:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket {
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close brackets without matching opening bracket")
}
// now we should have ( as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
default:
var currentPrecedence = currentToken.Operation.OperationType.Precedence
// pop off higher precedent operators onto the result
for len(opStack) > 0 &&
opStack[len(opStack)-1].TokenType == operationToken &&
opStack[len(opStack)-1].Operation.OperationType.Precedence > currentPrecedence {
opStack, result = popOpToResult(opStack, result)
}
// add this operator to the opStack
opStack = append(opStack, currentToken)
log.Debugf("put %v onto the opstack", currentToken.toString(true))
}
}
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("PostFix Result:")
for _, currentToken := range result {
log.Debugf("> %v", currentToken.toString())
}
}
return result, nil
}

View File

@@ -12,40 +12,65 @@ var pathTests = []struct {
expectedTokens []interface{} expectedTokens []interface{}
expectedPostFix []interface{} expectedPostFix []interface{}
}{ }{
{
`.a | .b | .c`,
append(make([]interface{}, 0), "a", "PIPE", "b", "PIPE", "c"),
append(make([]interface{}, 0), "a", "b", "c", "PIPE", "PIPE"),
},
{ {
`[]`, `[]`,
append(make([]interface{}, 0), "[", "]"), append(make([]interface{}, 0), "[", "EMPTY", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE"), append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE"),
}, },
{
`{}`,
append(make([]interface{}, 0), "{", "EMPTY", "}"),
append(make([]interface{}, 0), "EMPTY", "COLLECT_OBJECT", "SHORT_PIPE"),
},
{
`[{}]`,
append(make([]interface{}, 0), "[", "{", "EMPTY", "}", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT_OBJECT", "SHORT_PIPE", "COLLECT", "SHORT_PIPE"),
},
{
`.realnames as $names | $names["anon"]`,
append(make([]interface{}, 0), "realnames", "ASSIGN_VARIABLE", "GET_VARIABLE", "PIPE", "GET_VARIABLE", "TRAVERSE_ARRAY", "[", "anon (string)", "]"),
append(make([]interface{}, 0), "realnames", "GET_VARIABLE", "ASSIGN_VARIABLE", "GET_VARIABLE", "anon (string)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "PIPE"),
},
{
`.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), "TRAVERSE_ARRAY", "[", "]"), append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), append(make([]interface{}, 0), "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
}, },
{ {
`.a[]`, `.a[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"), append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
}, },
{ {
`.a.[]`, `.a.[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"), append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
}, },
{ {
`.a[0]`, `.a[0]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "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", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
}, },
{ {
`.a.[0]`, `.a.[0]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "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", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
}, },
{ {
`.a[].c`, `.a[].c`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "c"), append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "SHORT_PIPE", "c"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "c", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "c", "SHORT_PIPE"),
}, },
{ {
`[3]`, `[3]`,
@@ -53,24 +78,34 @@ var pathTests = []struct {
append(make([]interface{}, 0), "3 (int64)", "COLLECT", "SHORT_PIPE"), append(make([]interface{}, 0), "3 (int64)", "COLLECT", "SHORT_PIPE"),
}, },
{ {
`d0.a`, `.key.array + .key.array2`,
append(make([]interface{}, 0), "d0", "SHORT_PIPE", "a"), append(make([]interface{}, 0), "key", "SHORT_PIPE", "array", "ADD", "key", "SHORT_PIPE", "array2"),
append(make([]interface{}, 0), "d0", "a", "SHORT_PIPE"), append(make([]interface{}, 0), "key", "array", "SHORT_PIPE", "key", "array2", "SHORT_PIPE", "ADD"),
},
{
`.key.array * .key.array2`,
append(make([]interface{}, 0), "key", "SHORT_PIPE", "array", "MULTIPLY", "key", "SHORT_PIPE", "array2"),
append(make([]interface{}, 0), "key", "array", "SHORT_PIPE", "key", "array2", "SHORT_PIPE", "MULTIPLY"),
},
{
`.key.array // .key.array2`,
append(make([]interface{}, 0), "key", "SHORT_PIPE", "array", "ALTERNATIVE", "key", "SHORT_PIPE", "array2"),
append(make([]interface{}, 0), "key", "array", "SHORT_PIPE", "key", "array2", "SHORT_PIPE", "ALTERNATIVE"),
}, },
{ {
`.a | .[].b == "apple"`, `.a | .[].b == "apple"`,
append(make([]interface{}, 0), "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"), append(make([]interface{}, 0), "a", "PIPE", "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"), append(make([]interface{}, 0), "a", "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"),
}, },
{ {
`(.a | .[].b) == "apple"`, `(.a | .[].b) == "apple"`,
append(make([]interface{}, 0), "(", "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"), append(make([]interface{}, 0), "(", "a", "PIPE", "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"), append(make([]interface{}, 0), "a", "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"),
}, },
{ {
`.[] | select(. == "*at")`, `.[] | select(. == "*at")`,
append(make([]interface{}, 0), "TRAVERSE_ARRAY", "[", "]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"), append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"), append(make([]interface{}, 0), "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"),
}, },
{ {
`[true]`, `[true]`,
@@ -103,9 +138,9 @@ var pathTests = []struct {
append(make([]interface{}, 0), "a", "mike (string)", "CREATE_MAP", "COLLECT_OBJECT", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "mike (string)", "CREATE_MAP", "COLLECT_OBJECT", "SHORT_PIPE"),
}, },
{ {
`{.a: .c, .b.[]: .f.g.[]}`, `{.a: .c, .b.[]: .f.g[]}`,
append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]", "CREATE_MAP", "f", "SHORT_PIPE", "g", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]", "}"), append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "CREATE_MAP", "f", "SHORT_PIPE", "g", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "}"),
append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "f", "g", "SHORT_PIPE", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "f", "g", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"),
}, },
{ {
`explode(.a.b)`, `explode(.a.b)`,
@@ -157,15 +192,10 @@ var pathTests = []struct {
append(make([]interface{}, 0), "foo*", "PIPE", "(", "SELF", "ASSIGN_STYLE", "flow (string)", ")"), append(make([]interface{}, 0), "foo*", "PIPE", "(", "SELF", "ASSIGN_STYLE", "flow (string)", ")"),
append(make([]interface{}, 0), "foo*", "SELF", "flow (string)", "ASSIGN_STYLE", "PIPE"), append(make([]interface{}, 0), "foo*", "SELF", "flow (string)", "ASSIGN_STYLE", "PIPE"),
}, },
{
`{}`,
append(make([]interface{}, 0), "{", "}"),
append(make([]interface{}, 0), "EMPTY", "COLLECT_OBJECT", "SHORT_PIPE"),
},
} }
var tokeniser = NewPathTokeniser() var tokeniser = newExpressionTokeniser()
var postFixer = NewPathPostFixer() var postFixer = newExpressionPostFixer()
func TestPathParsing(t *testing.T) { func TestPathParsing(t *testing.T) {
for _, tt := range pathTests { for _, tt := range pathTests {
@@ -175,7 +205,7 @@ func TestPathParsing(t *testing.T) {
} }
var tokenValues []interface{} var tokenValues []interface{}
for _, token := range tokens { for _, token := range tokens {
tokenValues = append(tokenValues, token.toString()) tokenValues = append(tokenValues, token.toString(false))
} }
test.AssertResultComplexWithContext(t, tt.expectedTokens, tokenValues, fmt.Sprintf("tokenise: %v", tt.path)) test.AssertResultComplexWithContext(t, tt.expectedTokens, tokenValues, fmt.Sprintf("tokenise: %v", tt.path))

View File

@@ -0,0 +1,433 @@
package yqlib
import (
"fmt"
"strconv"
"strings"
lex "github.com/timtadh/lexmachine"
"github.com/timtadh/lexmachine/machines"
)
func skip(*lex.Scanner, *machines.Match) (interface{}, error) {
return nil, nil
}
type tokenType uint32
const (
operationToken = 1 << iota
openBracket
closeBracket
openCollect
closeCollect
openCollectObject
closeCollectObject
traverseArrayCollect
)
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
}
func (t *token) toString(detail bool) string {
if t.TokenType == operationToken {
if detail {
return fmt.Sprintf("%v (%v)", t.Operation.toString(), t.Operation.OperationType.Precedence)
}
return t.Operation.toString()
} else if t.TokenType == openBracket {
return "("
} else if t.TokenType == closeBracket {
return ")"
} else if t.TokenType == openCollect {
return "["
} else if t.TokenType == closeCollect {
return "]"
} else if t.TokenType == openCollectObject {
return "{"
} else if t.TokenType == closeCollectObject {
return "}"
} else if t.TokenType == traverseArrayCollect {
return ".["
} else {
return "NFI"
}
}
func pathToken(wrapped bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
value = value[1:]
if wrapped {
value = unwrap(value)
}
log.Debug("PathToken %v", value)
op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: traversePreferences{}}
return &token{TokenType: operationToken, Operation: op, CheckForPostTraverse: true}, nil
}
}
func opToken(op *operationType) lex.Action {
return opTokenWithPrefs(op, nil, nil)
}
func opAssignableToken(opType *operationType, assignOpType *operationType) lex.Action {
return opTokenWithPrefs(opType, assignOpType, nil)
}
func assignOpToken(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignOpToken %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{OperationType: assignOpType, Value: assignOpType.Type, StringValue: value, UpdateAssign: updateAssign}
return &token{TokenType: operationToken, Operation: op}, nil
}
}
func multiplyWithPrefs() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
prefs := multiplyPreferences{}
options := string(m.Bytes)
if strings.Contains(options, "+") {
prefs.AppendArrays = true
}
if strings.Contains(options, "?") {
prefs.TraversePrefs = traversePreferences{DontAutoCreate: true}
}
op := &Operation{OperationType: multiplyOpType, Value: multiplyOpType.Type, StringValue: options, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op}, nil
}
}
func opTokenWithPrefs(op *operationType, assignOpType *operationType, preferences interface{}) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("opTokenWithPrefs %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{OperationType: op, Value: op.Type, StringValue: value, Preferences: preferences}
var assign *Operation
if assignOpType != nil {
assign = &Operation{OperationType: assignOpType, Value: assignOpType.Type, StringValue: value, Preferences: preferences}
}
return &token{TokenType: operationToken, Operation: op, AssignOperation: assign}, nil
}
}
func assignAllCommentsOp(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignAllCommentsOp %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{
OperationType: assignCommentOpType,
Value: assignCommentOpType.Type,
StringValue: value,
UpdateAssign: updateAssign,
Preferences: commentOpPreferences{LineComment: true, HeadComment: true, FootComment: true},
}
return &token{TokenType: operationToken, Operation: op}, nil
}
}
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
}
}
func unwrap(value string) string {
return value[1 : len(value)-1]
}
func numberValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
var number, errParsingInt = strconv.ParseInt(numberString, 10, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
return &token{TokenType: operationToken, Operation: createValueOperation(number, numberString)}, nil
}
}
func floatValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
var number, errParsingInt = strconv.ParseFloat(numberString, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
return &token{TokenType: operationToken, Operation: createValueOperation(number, numberString)}, nil
}
}
func booleanValue(val bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return &token{TokenType: operationToken, Operation: createValueOperation(val, string(m.Bytes))}, nil
}
}
func stringValue(wrapped bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
if wrapped {
value = unwrap(value)
}
return &token{TokenType: operationToken, Operation: createValueOperation(value, value)}, nil
}
}
func getVariableOpToken() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
value = value[1 : len(value)-1]
getVarOperation := createValueOperation(value, value)
getVarOperation.OperationType = getVariableOpType
return &token{TokenType: operationToken, Operation: getVarOperation, CheckForPostTraverse: true}, nil
}
}
func envOp(strenv bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
preferences := envOpPreferences{}
if strenv {
// strenv( )
value = value[7 : len(value)-1]
preferences.StringValue = true
} else {
//env( )
value = value[4 : len(value)-1]
}
envOperation := createValueOperation(value, value)
envOperation.OperationType = envOpType
envOperation.Preferences = preferences
return &token{TokenType: operationToken, Operation: envOperation}, nil
}
}
func nullValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return &token{TokenType: operationToken, Operation: createValueOperation(nil, string(m.Bytes))}, nil
}
}
func selfToken() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
op := &Operation{OperationType: selfReferenceOpType}
return &token{TokenType: operationToken, Operation: op}, nil
}
}
func initLexer() (*lex.Lexer, error) {
lexer := lex.NewLexer()
lexer.Add([]byte(`\(`), literalToken(openBracket, false))
lexer.Add([]byte(`\)`), literalToken(closeBracket, true))
lexer.Add([]byte(`\.\[`), literalToken(traverseArrayCollect, false))
lexer.Add([]byte(`\.\.`), opTokenWithPrefs(recursiveDescentOpType, nil, recursiveDescentPreferences{RecurseArray: true,
TraversePreferences: traversePreferences{DontFollowAlias: true, IncludeMapKeys: false}}))
lexer.Add([]byte(`\.\.\.`), opTokenWithPrefs(recursiveDescentOpType, nil, recursiveDescentPreferences{RecurseArray: true,
TraversePreferences: traversePreferences{DontFollowAlias: true, IncludeMapKeys: true}}))
lexer.Add([]byte(`,`), opToken(unionOpType))
lexer.Add([]byte(`:\s*`), opToken(createMapOpType))
lexer.Add([]byte(`length`), opToken(lengthOpType))
lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
lexer.Add([]byte(`select`), opToken(selectOpType))
lexer.Add([]byte(`has`), opToken(hasOpType))
lexer.Add([]byte(`explode`), opToken(explodeOpType))
lexer.Add([]byte(`or`), opToken(orOpType))
lexer.Add([]byte(`and`), opToken(andOpType))
lexer.Add([]byte(`not`), opToken(notOpType))
lexer.Add([]byte(`\/\/`), opToken(alternativeOpType))
lexer.Add([]byte(`documentIndex`), opToken(getDocumentIndexOpType))
lexer.Add([]byte(`di`), opToken(getDocumentIndexOpType))
lexer.Add([]byte(`splitDoc`), opToken(splitDocumentOpType))
lexer.Add([]byte(`join`), opToken(joinStringOpType))
lexer.Add([]byte(`split`), opToken(splitStringOpType))
lexer.Add([]byte(`keys`), opToken(keysOpType))
lexer.Add([]byte(`style`), opAssignableToken(getStyleOpType, assignStyleOpType))
lexer.Add([]byte(`tag`), opAssignableToken(getTagOpType, assignTagOpType))
lexer.Add([]byte(`anchor`), opAssignableToken(getAnchorOpType, assignAnchorOpType))
lexer.Add([]byte(`alias`), opAssignableToken(getAliasOptype, assignAliasOpType))
lexer.Add([]byte(`filename`), opToken(getFilenameOpType))
lexer.Add([]byte(`fileIndex`), opToken(getFileIndexOpType))
lexer.Add([]byte(`fi`), opToken(getFileIndexOpType))
lexer.Add([]byte(`path`), opToken(getPathOpType))
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{LineComment: true}))
lexer.Add([]byte(`headComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{HeadComment: true}))
lexer.Add([]byte(`footComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{FootComment: true}))
lexer.Add([]byte(`comments\s*=`), assignAllCommentsOp(false))
lexer.Add([]byte(`comments\s*\|=`), assignAllCommentsOp(true))
lexer.Add([]byte(`collect`), opToken(collectOpType))
lexer.Add([]byte(`\s*==\s*`), opToken(equalsOpType))
lexer.Add([]byte(`\s*=\s*`), assignOpToken(false))
lexer.Add([]byte(`del`), opToken(deleteChildOpType))
lexer.Add([]byte(`\s*\|=\s*`), assignOpToken(true))
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
lexer.Add([]byte(`\."[^ "]+"`), pathToken(true))
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+`), pathToken(false))
lexer.Add([]byte(`\.`), selfToken())
lexer.Add([]byte(`\|`), opToken(pipeOpType))
lexer.Add([]byte(`-?\d+(\.\d+)`), floatValue())
lexer.Add([]byte(`-?[1-9](\.\d+)?[Ee][-+]?\d+`), floatValue())
lexer.Add([]byte(`-?\d+`), numberValue())
lexer.Add([]byte(`[Tt][Rr][Uu][Ee]`), booleanValue(true))
lexer.Add([]byte(`[Ff][Aa][Ll][Ss][Ee]`), booleanValue(false))
lexer.Add([]byte(`[Nn][Uu][Ll][Ll]`), nullValue())
lexer.Add([]byte(`~`), nullValue())
lexer.Add([]byte(`"[^"]*"`), stringValue(true))
lexer.Add([]byte(`strenv\([^\)]+\)`), envOp(true))
lexer.Add([]byte(`env\([^\)]+\)`), envOp(false))
lexer.Add([]byte(`\[`), literalToken(openCollect, false))
lexer.Add([]byte(`\]`), literalToken(closeCollect, true))
lexer.Add([]byte(`\{`), literalToken(openCollectObject, false))
lexer.Add([]byte(`\}`), literalToken(closeCollectObject, true))
lexer.Add([]byte(`\*[\+|\?]*`), multiplyWithPrefs())
lexer.Add([]byte(`\+`), opToken(addOpType))
lexer.Add([]byte(`\+=`), opToken(addAssignOpType))
lexer.Add([]byte(`\$[a-zA-Z_-0-9]+`), getVariableOpToken())
lexer.Add([]byte(`as`), opToken(assignVariableOpType))
err := lexer.Compile()
if err != nil {
return nil, err
}
return lexer, nil
}
type expressionTokeniser interface {
Tokenise(expression string) ([]*token, error)
}
type expressionTokeniserImpl struct {
lexer *lex.Lexer
}
func newExpressionTokeniser() expressionTokeniser {
var lexer, err = initLexer()
if err != nil {
panic(err)
}
return &expressionTokeniserImpl{lexer}
}
func (p *expressionTokeniserImpl) Tokenise(expression string) ([]*token, error) {
scanner, err := p.lexer.Scanner([]byte(expression))
if err != nil {
return nil, fmt.Errorf("Parsing expression: %v", err)
}
var tokens []*token
for tok, err, eof := scanner.Next(); !eof; tok, err, eof = scanner.Next() {
if tok != nil {
currentToken := tok.(*token)
log.Debugf("Tokenising %v", currentToken.toString(true))
tokens = append(tokens, currentToken)
}
if err != nil {
return nil, fmt.Errorf("Parsing expression: %v", err)
}
}
var postProcessedTokens = make([]*token, 0)
skipNextToken := false
for index := range tokens {
if skipNextToken {
skipNextToken = false
} else {
postProcessedTokens, skipNextToken = p.handleToken(tokens, index, postProcessedTokens)
}
}
return postProcessedTokens, nil
}
func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postProcessedTokens []*token) (tokensAccum []*token, skipNextToken bool) {
skipNextToken = false
currentToken := tokens[index]
if currentToken.TokenType == traverseArrayCollect {
//need to put a traverse array then a collect currentToken
// do this by adding traverse then converting currentToken to collect
if index == 0 || tokens[index-1].TokenType != operationToken ||
tokens[index-1].Operation.OperationType != traversePathOpType {
op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
op := &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
currentToken = &token{TokenType: openCollect}
}
if index != len(tokens)-1 && currentToken.AssignOperation != nil &&
tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == assignOpType {
currentToken.Operation = currentToken.AssignOperation
currentToken.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
skipNextToken = true
}
postProcessedTokens = append(postProcessedTokens, currentToken)
if index != len(tokens)-1 &&
((currentToken.TokenType == openCollect && tokens[index+1].TokenType == closeCollect) ||
(currentToken.TokenType == openCollectObject && tokens[index+1].TokenType == closeCollectObject)) {
op := &Operation{OperationType: emptyOpType, StringValue: "EMPTY"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == traversePathOpType {
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == openCollect {
op := &Operation{OperationType: traverseArrayOpType}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
return postProcessedTokens, skipNextToken
}

View File

@@ -1,3 +1,5 @@
// Use the top level Evaluator or StreamEvaluator to evaluate expressions and return matches.
//
package yqlib package yqlib
import ( import (
@@ -11,85 +13,91 @@ import (
var log = logging.MustGetLogger("yq-lib") var log = logging.MustGetLogger("yq-lib")
type OperationType struct { type operationType struct {
Type string Type string
NumArgs uint // number of arguments to the op NumArgs uint // number of arguments to the op
Precedence uint Precedence uint
Handler OperatorHandler Handler operatorHandler
} }
// operators TODO: // operators TODO:
// - keys operator for controlling key metadata (particularly anchors/aliases)
// - mergeEmpty (sets only if the document is empty, do I do that now?) // - mergeEmpty (sets only if the document is empty, do I do that now?)
var Or = &OperationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: OrOperator} var orOpType = &operationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: orOperator}
var And = &OperationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: AndOperator} var andOpType = &operationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: andOperator}
var Union = &OperationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: UnionOperator} var unionOpType = &operationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: unionOperator}
var Pipe = &OperationType{Type: "PIPE", NumArgs: 2, Precedence: 30, Handler: PipeOperator} var pipeOpType = &operationType{Type: "PIPE", NumArgs: 2, Precedence: 30, Handler: pipeOperator}
var Assign = &OperationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: AssignUpdateOperator} var assignOpType = &operationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: assignUpdateOperator}
var AddAssign = &OperationType{Type: "ADD_ASSIGN", NumArgs: 2, Precedence: 40, Handler: AddAssignOperator} var addAssignOpType = &operationType{Type: "ADD_ASSIGN", NumArgs: 2, Precedence: 40, Handler: addAssignOperator}
var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: AssignAttributesOperator} var assignAttributesOpType = &operationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: assignAttributesOperator}
var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator} var assignStyleOpType = &operationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: assignStyleOperator}
var AssignTag = &OperationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: AssignTagOperator} var assignVariableOpType = &operationType{Type: "ASSIGN_VARIABLE", NumArgs: 2, Precedence: 40, Handler: assignVariableOperator}
var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator} var assignTagOpType = &operationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: assignTagOperator}
var AssignAnchor = &OperationType{Type: "ASSIGN_ANCHOR", NumArgs: 2, Precedence: 40, Handler: AssignAnchorOperator} var assignCommentOpType = &operationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: assignCommentsOperator}
var AssignAlias = &OperationType{Type: "ASSIGN_ALIAS", NumArgs: 2, Precedence: 40, Handler: AssignAliasOperator} var assignAnchorOpType = &operationType{Type: "ASSIGN_ANCHOR", NumArgs: 2, Precedence: 40, Handler: assignAnchorOperator}
var assignAliasOpType = &operationType{Type: "ASSIGN_ALIAS", NumArgs: 2, Precedence: 40, Handler: assignAliasOperator}
var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: MultiplyOperator} var multiplyOpType = &operationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 42, Handler: multiplyOperator}
var Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator} var addOpType = &operationType{Type: "ADD", NumArgs: 2, Precedence: 42, Handler: addOperator}
var Alternative = &OperationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 45, Handler: AlternativeOperator} var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator}
var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator} var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: equalsOperator}
var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator} var createMapOpType = &operationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: createMapOperator}
var ShortPipe = &OperationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: 45, Handler: PipeOperator} var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: 45, Handler: pipeOperator}
var Length = &OperationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: LengthOperator} var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator}
var Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: CollectOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator}
var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator} var splitDocumentOpType = &operationType{Type: "SPLIT_DOC", NumArgs: 0, Precedence: 50, Handler: splitDocumentOperator}
var GetTag = &OperationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: GetTagOperator} var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator}
var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator} var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
var GetAnchor = &OperationType{Type: "GET_ANCHOR", NumArgs: 0, Precedence: 50, Handler: GetAnchorOperator} var getTagOpType = &operationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: getTagOperator}
var GetAlias = &OperationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 50, Handler: GetAliasOperator} var getCommentOpType = &operationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: getCommentsOperator}
var GetDocumentIndex = &OperationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: GetDocumentIndexOperator} var getAnchorOpType = &operationType{Type: "GET_ANCHOR", NumArgs: 0, Precedence: 50, Handler: getAnchorOperator}
var GetFilename = &OperationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: GetFilenameOperator} var getAliasOptype = &operationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 50, Handler: getAliasOperator}
var GetFileIndex = &OperationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: GetFileIndexOperator} var getDocumentIndexOpType = &operationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: getDocumentIndexOperator}
var GetPath = &OperationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: GetPathOperator} var getFilenameOpType = &operationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: getFilenameOperator}
var getFileIndexOpType = &operationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: getFileIndexOperator}
var getPathOpType = &operationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: getPathOperator}
var Explode = &OperationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: ExplodeOperator} var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: explodeOperator}
var SortKeys = &OperationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: SortKeysOperator} var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator}
var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator}
var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator}
var CollectObject = &OperationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: CollectObjectOperator} var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: keysOperator}
var TraversePath = &OperationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator}
var TraverseArray = &OperationType{Type: "TRAVERSE_ARRAY", NumArgs: 1, Precedence: 50, Handler: TraverseArrayOperator}
var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}
var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator} var traversePathOpType = &operationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 55, Handler: traversePathOperator}
var ValueOp = &OperationType{Type: "VALUE", NumArgs: 0, Precedence: 50, Handler: ValueOperator} var traverseArrayOpType = &operationType{Type: "TRAVERSE_ARRAY", NumArgs: 2, Precedence: 50, Handler: traverseArrayOperator}
var Not = &OperationType{Type: "NOT", NumArgs: 0, Precedence: 50, Handler: NotOperator}
var Empty = &OperationType{Type: "EMPTY", NumArgs: 50, Handler: EmptyOperator}
var RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: RecursiveDescentOperator} var selfReferenceOpType = &operationType{Type: "SELF", NumArgs: 0, Precedence: 55, Handler: selfOperator}
var valueOpType = &operationType{Type: "VALUE", NumArgs: 0, Precedence: 50, Handler: valueOperator}
var envOpType = &operationType{Type: "ENV", NumArgs: 0, Precedence: 50, Handler: envOperator}
var notOpType = &operationType{Type: "NOT", NumArgs: 0, Precedence: 50, Handler: notOperator}
var emptyOpType = &operationType{Type: "EMPTY", NumArgs: 50, Handler: emptyOperator}
var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator} var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: recursiveDescentOperator}
var Has = &OperationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: HasOperator}
var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: DeleteChildOperator} var selectOpType = &operationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: selectOperator}
var DeleteImmediateChild = &OperationType{Type: "DELETE_IMMEDIATE_CHILD", NumArgs: 1, Precedence: 40, Handler: DeleteImmediateChildOperator} var hasOpType = &operationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: hasOperator}
var deleteChildOpType = &operationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: deleteChildOperator}
var deleteImmediateChildOpType = &operationType{Type: "DELETE_IMMEDIATE_CHILD", NumArgs: 1, Precedence: 40, Handler: deleteImmediateChildOperator}
type Operation struct { type Operation struct {
OperationType *OperationType OperationType *operationType
Value interface{} Value interface{}
StringValue string StringValue string
CandidateNode *CandidateNode // used for Value Path elements CandidateNode *CandidateNode // used for Value Path elements
Preferences interface{} Preferences interface{}
UpdateAssign bool // used for assign ops, when true it means we evaluate the rhs given the lhs (instead of matching nodes) UpdateAssign bool // used for assign ops, when true it means we evaluate the rhs given the lhs
} }
func CreateValueOperation(value interface{}, stringValue string) *Operation { func createValueOperation(value interface{}, stringValue string) *Operation {
var node yaml.Node = yaml.Node{Kind: yaml.ScalarNode} var node yaml.Node = yaml.Node{Kind: yaml.ScalarNode}
node.Value = stringValue node.Value = stringValue
@@ -107,7 +115,7 @@ func CreateValueOperation(value interface{}, stringValue string) *Operation {
} }
return &Operation{ return &Operation{
OperationType: ValueOp, OperationType: valueOpType,
Value: value, Value: value,
StringValue: stringValue, StringValue: stringValue,
CandidateNode: &CandidateNode{Node: &node}, CandidateNode: &CandidateNode{Node: &node},
@@ -116,13 +124,11 @@ func CreateValueOperation(value interface{}, stringValue string) *Operation {
// debugging purposes only // debugging purposes only
func (p *Operation) toString() string { func (p *Operation) toString() string {
if p.OperationType == TraversePath { if p.OperationType == traversePathOpType {
return fmt.Sprintf("%v", p.Value) return fmt.Sprintf("%v", p.Value)
} else if p.OperationType == DocumentFilter { } else if p.OperationType == selfReferenceOpType {
return fmt.Sprintf("d%v", p.Value)
} else if p.OperationType == SelfReference {
return "SELF" return "SELF"
} else if p.OperationType == ValueOp { } else if p.OperationType == valueOpType {
return fmt.Sprintf("%v (%T)", p.Value, p.Value) return fmt.Sprintf("%v (%T)", p.Value, p.Value)
} else { } else {
return fmt.Sprintf("%v", p.OperationType.Type) return fmt.Sprintf("%v", p.OperationType.Type)

View File

@@ -1,6 +1,6 @@
package yqlib package yqlib
func Match(name string, pattern string) (matched bool) { func matchKey(name string, pattern string) (matched bool) {
if pattern == "" { if pattern == "" {
return name == pattern return name == pattern
} }

View File

@@ -3,23 +3,23 @@ package yqlib
import ( import (
"fmt" "fmt"
"container/list" "strconv"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func createSelfAddOp(rhs *PathTreeNode) *PathTreeNode { func createAddOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
return &PathTreeNode{Operation: &Operation{OperationType: Add}, return &ExpressionNode{Operation: &Operation{OperationType: addOpType},
Lhs: &PathTreeNode{Operation: &Operation{OperationType: SelfReference}}, Lhs: lhs,
Rhs: rhs} Rhs: rhs}
} }
func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func addAssignOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
assignmentOp := &Operation{OperationType: Assign} assignmentOp := &Operation{OperationType: assignOpType}
assignmentOp.UpdateAssign = true assignmentOp.UpdateAssign = false
assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createSelfAddOp(pathNode.Rhs)} assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: expressionNode.Lhs, Rhs: createAddOp(expressionNode.Lhs, expressionNode.Rhs)}
return d.GetMatchingNodes(matchingNodes, assignmentOpNode) return d.GetMatchingNodes(context, assignmentOpNode)
} }
func toNodes(candidate *CandidateNode) []*yaml.Node { func toNodes(candidate *CandidateNode) []*yaml.Node {
@@ -36,24 +36,24 @@ func toNodes(candidate *CandidateNode) []*yaml.Node {
} }
func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func addOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("Add operator") log.Debugf("Add operator")
return crossFunction(d, matchingNodes, pathNode, add) return crossFunction(d, context, expressionNode, add)
} }
func add(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func add(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node) lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node) rhs.Node = unwrapDoc(rhs.Node)
target := &CandidateNode{
Path: lhs.Path,
Document: lhs.Document,
Filename: lhs.Filename,
Node: &yaml.Node{},
}
lhsNode := lhs.Node lhsNode := lhs.Node
if lhsNode.Tag == "!!null" {
return lhs.CreateChild(nil, rhs.Node), nil
}
target := lhs.CreateChild(nil, &yaml.Node{})
switch lhsNode.Kind { switch lhsNode.Kind {
case yaml.MappingNode: case yaml.MappingNode:
return nil, fmt.Errorf("Maps not yet supported for addition") return nil, fmt.Errorf("Maps not yet supported for addition")
@@ -63,7 +63,48 @@ func add(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*Candida
target.Node.Tag = "!!seq" target.Node.Tag = "!!seq"
target.Node.Content = append(lhsNode.Content, toNodes(rhs)...) target.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
case yaml.ScalarNode: case yaml.ScalarNode:
return nil, fmt.Errorf("Scalars not yet supported for addition") if rhs.Node.Kind != yaml.ScalarNode {
return nil, fmt.Errorf("%v (%v) cannot be added to a %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag)
}
target.Node.Kind = yaml.ScalarNode
target.Node.Style = lhsNode.Style
return addScalars(target, lhsNode, rhs.Node)
}
return target, nil
}
func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*CandidateNode, error) {
if lhs.Tag == "!!str" {
target.Node.Tag = "!!str"
target.Node.Value = lhs.Value + rhs.Value
} else if lhs.Tag == "!!int" && rhs.Tag == "!!int" {
lhsNum, err := strconv.Atoi(lhs.Value)
if err != nil {
return nil, err
}
rhsNum, err := strconv.Atoi(rhs.Value)
if err != nil {
return nil, err
}
sum := lhsNum + rhsNum
target.Node.Tag = "!!int"
target.Node.Value = fmt.Sprintf("%v", sum)
} else if (lhs.Tag == "!!int" || lhs.Tag == "!!float") && (rhs.Tag == "!!int" || rhs.Tag == "!!float") {
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
if err != nil {
return nil, err
}
rhsNum, err := strconv.ParseFloat(rhs.Value, 64)
if err != nil {
return nil, err
}
sum := lhsNum + rhsNum
target.Node.Tag = "!!float"
target.Node.Value = fmt.Sprintf("%v", sum)
} else {
return nil, fmt.Errorf("%v cannot be added to %v", lhs.Tag, rhs.Tag)
} }
return target, nil return target, nil

View File

@@ -5,6 +5,15 @@ import (
) )
var addOperatorScenarios = []expressionScenario{ var addOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `[{a: foo, b: bar}, {a: 1, b: 2}]`,
expression: ".[] | .a + .b",
expected: []string{
"D0, P[0 a], (!!str)::foobar\n",
"D0, P[1 a], (!!int)::3\n",
},
},
{ {
description: "Concatenate and assign arrays", description: "Concatenate and assign arrays",
document: `{a: {val: thing, b: [cat,dog]}}`, document: `{a: {val: thing, b: [cat,dog]}}`,
@@ -38,11 +47,11 @@ var addOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "Add object to array", description: "Add new object to array",
document: `{a: [1,2], c: {cat: meow}}`, document: `a: [{dog: woof}]`,
expression: `.a + .c`, expression: `.a + {"cat": "meow"}`,
expected: []string{ expected: []string{
"D0, P[a], (!!seq)::[1, 2, {cat: meow}]\n", "D0, P[a], (!!seq)::[{dog: woof}, {cat: meow}]\n",
}, },
}, },
{ {
@@ -61,6 +70,56 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n", "D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n",
}, },
}, },
{
description: "String concatenation",
document: `{a: cat, b: meow}`,
expression: `.a = .a + .b`,
expected: []string{
"D0, P[], (doc)::{a: catmeow, b: meow}\n",
},
},
{
description: "Relative string concatenation",
document: `{a: cat, b: meow}`,
expression: `.a += .b`,
expected: []string{
"D0, P[], (doc)::{a: catmeow, b: meow}\n",
},
},
{
description: "Number addition - float",
subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",
document: `{a: 3, b: 4.9}`,
expression: `.a = .a + .b`,
expected: []string{
"D0, P[], (doc)::{a: 7.9, b: 4.9}\n",
},
},
{
description: "Number addition - int",
subdescription: "If both the lhs and rhs are ints then the expression will be calculated with ints.",
document: `{a: 3, b: 4}`,
expression: `.a = .a + .b`,
expected: []string{
"D0, P[], (doc)::{a: 7, b: 4}\n",
},
},
{
description: "Increment number",
document: `{a: 3}`,
expression: `.a += 1`,
expected: []string{
"D0, P[], (doc)::{a: 4}\n",
},
},
{
description: "Add to null",
subdescription: "Adding to null simply returns the rhs",
expression: `null + "cat"`,
expected: []string{
"D0, P[], (!!str)::cat\n",
},
},
} }
func TestAddOperatorScenarios(t *testing.T) { func TestAddOperatorScenarios(t *testing.T) {

View File

@@ -1,20 +1,16 @@
package yqlib package yqlib
import (
"container/list"
)
// corssFunction no matches // corssFunction no matches
// can boolean use crossfunction // can boolean use crossfunction
func AlternativeOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func alternativeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- alternative") log.Debugf("-- alternative")
return crossFunction(d, matchingNodes, pathNode, alternativeFunc) return crossFunction(d, context, expressionNode, alternativeFunc)
} }
func alternativeFunc(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func alternativeFunc(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node) lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node) rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Alternative LHS: %v", lhs.Node.Tag) log.Debugf("Alternative LHS: %v", lhs.Node.Tag)
log.Debugf("- RHS: %v", rhs.Node.Tag) log.Debugf("- RHS: %v", rhs.Node.Tag)

View File

@@ -6,146 +6,146 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func AssignAliasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignAlias operator!") log.Debugf("AssignAlias operator!")
aliasName := "" aliasName := ""
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
aliasName = rhs.Front().Value.(*CandidateNode).Node.Value aliasName = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting aliasName : %v", candidate.GetKey()) log.Debugf("Setting aliasName : %v", candidate.GetKey())
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
aliasName = rhs.Front().Value.(*CandidateNode).Node.Value aliasName = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
candidate.Node.Kind = yaml.AliasNode candidate.Node.Kind = yaml.AliasNode
candidate.Node.Value = aliasName candidate.Node.Value = aliasName
} }
return matchingNodes, nil return context, nil
} }
func GetAliasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetAlias operator!") log.Debugf("GetAlias operator!")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Node.Value, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Node.Value, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }
func AssignAnchorOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignAnchorOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignAnchor operator!") log.Debugf("AssignAnchor operator!")
anchorName := "" anchorName := ""
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
anchorName = rhs.Front().Value.(*CandidateNode).Node.Value anchorName = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting anchorName of : %v", candidate.GetKey()) log.Debugf("Setting anchorName of : %v", candidate.GetKey())
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
anchorName = rhs.Front().Value.(*CandidateNode).Node.Value anchorName = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
candidate.Node.Anchor = anchorName candidate.Node.Anchor = anchorName
} }
return matchingNodes, nil return context, nil
} }
func GetAnchorOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getAnchorOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetAnchor operator!") log.Debugf("GetAnchor operator!")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
anchor := candidate.Node.Anchor anchor := candidate.Node.Anchor
node := &yaml.Node{Kind: yaml.ScalarNode, Value: anchor, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: anchor, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }
func ExplodeOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func explodeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- ExplodeOperation") log.Debugf("-- ExplodeOperation")
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for childEl := rhs.Front(); childEl != nil; childEl = childEl.Next() { for childEl := rhs.MatchingNodes.Front(); childEl != nil; childEl = childEl.Next() {
err = explodeNode(childEl.Value.(*CandidateNode).Node) err = explodeNode(childEl.Value.(*CandidateNode).Node, context)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
} }
} }
return matchMap, nil return context, nil
} }
func explodeNode(node *yaml.Node) error { func explodeNode(node *yaml.Node, context Context) error {
node.Anchor = "" node.Anchor = ""
switch node.Kind { switch node.Kind {
case yaml.SequenceNode, yaml.DocumentNode: case yaml.SequenceNode, yaml.DocumentNode:
for index, contentNode := range node.Content { for index, contentNode := range node.Content {
log.Debugf("exploding index %v", index) log.Debugf("exploding index %v", index)
errorInContent := explodeNode(contentNode) errorInContent := explodeNode(contentNode, context)
if errorInContent != nil { if errorInContent != nil {
return errorInContent return errorInContent
} }
@@ -169,7 +169,7 @@ func explodeNode(node *yaml.Node) error {
valueNode := node.Content[index+1] valueNode := node.Content[index+1]
log.Debugf("traversing %v", keyNode.Value) log.Debugf("traversing %v", keyNode.Value)
if keyNode.Value != "<<" { if keyNode.Value != "<<" {
err := overrideEntry(node, keyNode, valueNode, index, newContent) err := overrideEntry(node, keyNode, valueNode, index, context.ChildContext(newContent))
if err != nil { if err != nil {
return err return err
} }
@@ -178,14 +178,14 @@ func explodeNode(node *yaml.Node) error {
log.Debugf("an alias merge list!") log.Debugf("an alias merge list!")
for index := 0; index < len(valueNode.Content); index = index + 1 { for index := 0; index < len(valueNode.Content); index = index + 1 {
aliasNode := valueNode.Content[index] aliasNode := valueNode.Content[index]
err := applyAlias(node, aliasNode.Alias, index, newContent) err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
if err != nil { if err != nil {
return err return err
} }
} }
} else { } else {
log.Debugf("an alias merge!") log.Debugf("an alias merge!")
err := applyAlias(node, valueNode.Alias, index, newContent) err := applyAlias(node, valueNode.Alias, index, context.ChildContext(newContent))
if err != nil { if err != nil {
return err return err
} }
@@ -205,7 +205,7 @@ func explodeNode(node *yaml.Node) error {
} }
} }
func applyAlias(node *yaml.Node, alias *yaml.Node, aliasIndex int, newContent *list.List) error { func applyAlias(node *yaml.Node, alias *yaml.Node, aliasIndex int, newContent Context) error {
if alias == nil { if alias == nil {
return nil return nil
} }
@@ -221,15 +221,15 @@ func applyAlias(node *yaml.Node, alias *yaml.Node, aliasIndex int, newContent *l
return nil return nil
} }
func overrideEntry(node *yaml.Node, key *yaml.Node, value *yaml.Node, startIndex int, newContent *list.List) error { func overrideEntry(node *yaml.Node, key *yaml.Node, value *yaml.Node, startIndex int, newContent Context) error {
err := explodeNode(value) err := explodeNode(value, newContent)
if err != nil { if err != nil {
return err return err
} }
for newEl := newContent.Front(); newEl != nil; newEl = newEl.Next() { for newEl := newContent.MatchingNodes.Front(); newEl != nil; newEl = newEl.Next() {
valueEl := newEl.Next() // move forward twice valueEl := newEl.Next() // move forward twice
keyNode := newEl.Value.(*yaml.Node) keyNode := newEl.Value.(*yaml.Node)
log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*yaml.Node).Value) log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*yaml.Node).Value)
@@ -250,12 +250,12 @@ func overrideEntry(node *yaml.Node, key *yaml.Node, value *yaml.Node, startIndex
} }
} }
err = explodeNode(key) err = explodeNode(key, newContent)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("adding %v:%v", key.Value, value.Value) log.Debugf("adding %v:%v", key.Value, value.Value)
newContent.PushBack(key) newContent.MatchingNodes.PushBack(key)
newContent.PushBack(value) newContent.MatchingNodes.PushBack(value)
return nil return nil
} }

View File

@@ -1,66 +1,60 @@
package yqlib package yqlib
import "container/list" func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
var rhs *list.List var rhs Context
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err = d.GetMatchingNodes(context, expressionNode.Rhs)
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err = d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
} }
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
// grab the first value // grab the first value
first := rhs.Front() first := rhs.MatchingNodes.Front()
if first != nil { if first != nil {
rhsCandidate := first.Value.(*CandidateNode) rhsCandidate := first.Value.(*CandidateNode)
rhsCandidate.Node = UnwrapDoc(rhsCandidate.Node) rhsCandidate.Node = unwrapDoc(rhsCandidate.Node)
candidate.UpdateFrom(rhsCandidate) candidate.UpdateFrom(rhsCandidate)
} }
} }
// // if there was nothing given, perhaps we are creating a new yaml doc
// if matchingNodes.Len() == 0 { return context, nil
// log.Debug("started with nothing, returning LHS, %v", lhs.Len())
// return lhs, nil
// }
return matchingNodes, nil
} }
// does not update content or values // does not update content or values
func AssignAttributesOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignAttributesOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
// grab the first value // grab the first value
first := rhs.Front() first := rhs.MatchingNodes.Front()
if first != nil { if first != nil {
candidate.UpdateAttributesFrom(first.Value.(*CandidateNode)) candidate.UpdateAttributesFrom(first.Value.(*CandidateNode))
} }
} }
return matchingNodes, nil return context, nil
} }

View File

@@ -7,7 +7,7 @@ import (
) )
func isTruthy(c *CandidateNode) (bool, error) { func isTruthy(c *CandidateNode) (bool, error) {
node := UnwrapDoc(c.Node) node := unwrapDoc(c.Node)
value := true value := true
if node.Tag == "!!null" { if node.Tag == "!!null" {
@@ -25,10 +25,10 @@ func isTruthy(c *CandidateNode) (bool, error) {
type boolOp func(bool, bool) bool type boolOp func(bool, bool) bool
func performBoolOp(op boolOp) func(d *dataTreeNavigator, 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, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node) lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node) rhs.Node = unwrapDoc(rhs.Node)
lhsTrue, errDecoding := isTruthy(lhs) lhsTrue, errDecoding := isTruthy(lhs)
if errDecoding != nil { if errDecoding != nil {
@@ -44,35 +44,35 @@ func performBoolOp(op boolOp) func(d *dataTreeNavigator, lhs *CandidateNode, rhs
} }
} }
func OrOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- orOp") log.Debugf("-- orOp")
return crossFunction(d, matchingNodes, pathNode, 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
})) }))
} }
func AndOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func andOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- AndOp") log.Debugf("-- AndOp")
return crossFunction(d, matchingNodes, pathNode, 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
})) }))
} }
func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func notOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- notOperation") log.Debugf("-- notOperation")
var results = list.New() var results = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debug("notOperation checking %v", candidate) log.Debug("notOperation checking %v", candidate)
truthy, errDecoding := isTruthy(candidate) truthy, errDecoding := isTruthy(candidate)
if errDecoding != nil { if errDecoding != nil {
return nil, errDecoding return Context{}, errDecoding
} }
result := createBooleanCandidate(candidate, !truthy) result := createBooleanCandidate(candidate, !truthy)
results.PushBack(result) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -6,34 +6,35 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func CollectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func collectOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- collectOperation") log.Debugf("-- collectOperation")
if matchMap.Len() == 0 { if context.MatchingNodes.Len() == 0 {
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Value: "[]"} node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Value: "[]"}
candidate := &CandidateNode{Node: node} candidate := &CandidateNode{Node: node}
return nodeToMap(candidate), nil return context.SingleChildContext(candidate), nil
} }
var results = list.New() var results = list.New()
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
var collectC *CandidateNode
var document uint = 0 if context.MatchingNodes.Front() != nil {
var path []interface{} collectC = context.MatchingNodes.Front().Value.(*CandidateNode).CreateChild(nil, node)
if len(collectC.Path) > 0 {
for el := matchMap.Front(); el != nil; el = el.Next() { collectC.Path = collectC.Path[:len(collectC.Path)-1]
candidate := el.Value.(*CandidateNode)
log.Debugf("Collecting %v", NodeToString(candidate))
if path == nil && candidate.Path != nil && len(candidate.Path) > 1 {
path = candidate.Path[:len(candidate.Path)-1]
document = candidate.Document
} }
node.Content = append(node.Content, candidate.Node) } else {
collectC = &CandidateNode{Node: node}
}
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Collecting %v", NodeToString(candidate))
node.Content = append(node.Content, unwrapDoc(candidate.Node))
} }
collectC := &CandidateNode{Node: node, Document: document, Path: path}
results.PushBack(collectC) results.PushBack(collectC)
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -17,92 +17,83 @@ import (
... ...
*/ */
func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func collectObjectOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- collectObjectOperation") log.Debugf("-- collectObjectOperation")
if matchMap.Len() == 0 { if context.MatchingNodes.Len() == 0 {
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"} node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"}
candidate := &CandidateNode{Node: node} candidate := &CandidateNode{Node: node}
return nodeToMap(candidate), nil return context.SingleChildContext(candidate), nil
} }
first := matchMap.Front().Value.(*CandidateNode) first := context.MatchingNodes.Front().Value.(*CandidateNode)
var rotated []*list.List = make([]*list.List, len(first.Node.Content)) var rotated []*list.List = make([]*list.List, len(first.Node.Content))
for i := 0; i < len(first.Node.Content); i++ { for i := 0; i < len(first.Node.Content); i++ {
rotated[i] = list.New() rotated[i] = list.New()
} }
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidateNode := el.Value.(*CandidateNode) candidateNode := el.Value.(*CandidateNode)
for i := 0; i < len(first.Node.Content); i++ { for i := 0; i < len(first.Node.Content); i++ {
rotated[i].PushBack(createChildCandidate(candidateNode, i)) rotated[i].PushBack(candidateNode.CreateChild(i, candidateNode.Node.Content[i]))
} }
} }
newObject := list.New() newObject := list.New()
for i := 0; i < len(first.Node.Content); i++ { for i := 0; i < len(first.Node.Content); i++ {
additions, err := collect(d, list.New(), rotated[i]) additions, err := collect(d, context.ChildContext(list.New()), rotated[i])
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
newObject.PushBackList(additions) newObject.PushBackList(additions.MatchingNodes)
} }
return newObject, nil return context.ChildContext(newObject), nil
} }
func createChildCandidate(candidate *CandidateNode, index int) *CandidateNode { func collect(d *dataTreeNavigator, context Context, remainingMatches *list.List) (Context, error) {
return &CandidateNode{
Document: candidate.Document,
Path: candidate.CreateChildPath(index),
Filename: candidate.Filename,
Node: candidate.Node.Content[index],
}
}
func collect(d *dataTreeNavigator, aggregate *list.List, remainingMatches *list.List) (*list.List, error) {
if remainingMatches.Len() == 0 { if remainingMatches.Len() == 0 {
return aggregate, nil return context, nil
} }
candidate := remainingMatches.Remove(remainingMatches.Front()).(*CandidateNode) candidate := remainingMatches.Remove(remainingMatches.Front()).(*CandidateNode)
splatted, err := Splat(d, nodeToMap(candidate), splatted, err := splat(d, context.SingleChildContext(candidate),
&TraversePreferences{FollowAlias: false, IncludeMapKeys: false}) traversePreferences{DontFollowAlias: true, IncludeMapKeys: false})
for splatEl := splatted.Front(); splatEl != nil; splatEl = splatEl.Next() { for splatEl := splatted.MatchingNodes.Front(); splatEl != nil; splatEl = splatEl.Next() {
splatEl.Value.(*CandidateNode).Path = nil splatEl.Value.(*CandidateNode).Path = nil
} }
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if aggregate.Len() == 0 { if context.MatchingNodes.Len() == 0 {
return collect(d, splatted, remainingMatches) return collect(d, splatted, remainingMatches)
} }
newAgg := list.New() newAgg := list.New()
for el := aggregate.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
aggCandidate := el.Value.(*CandidateNode) aggCandidate := el.Value.(*CandidateNode)
for splatEl := splatted.Front(); splatEl != nil; splatEl = splatEl.Next() { for splatEl := splatted.MatchingNodes.Front(); splatEl != nil; splatEl = splatEl.Next() {
splatCandidate := splatEl.Value.(*CandidateNode) splatCandidate := splatEl.Value.(*CandidateNode)
newCandidate, err := aggCandidate.Copy() newCandidate, err := aggCandidate.Copy()
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
newCandidate.Path = nil newCandidate.Path = nil
newCandidate, err = multiply(&MultiplyPreferences{AppendArrays: false})(d, newCandidate, splatCandidate) newCandidate, err = multiply(multiplyPreferences{AppendArrays: false})(d, context, newCandidate, splatCandidate)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
newAgg.PushBack(newCandidate) newAgg.PushBack(newCandidate)
} }
} }
return collect(d, newAgg, remainingMatches) return collect(d, context.ChildContext(newAgg), remainingMatches)
} }

View File

@@ -13,6 +13,15 @@ var collectOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[]\n", "D0, P[], (!!seq)::[]\n",
}, },
}, },
{
skipDoc: true,
document: "{a: apple}\n---\n{b: frog}",
expression: `[.]`,
expected: []string{
"D0, P[], (!!seq)::- {a: apple}\n- {b: frog}\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: ``, document: ``,

View File

@@ -7,47 +7,47 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type CommentOpPreferences struct { type commentOpPreferences struct {
LineComment bool LineComment bool
HeadComment bool HeadComment bool
FootComment bool FootComment bool
} }
func AssignCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignCommentsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignComments operator!") log.Debugf("AssignComments operator!")
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
preferences := pathNode.Operation.Preferences.(*CommentOpPreferences) preferences := expressionNode.Operation.Preferences.(commentOpPreferences)
comment := "" comment := ""
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
comment = rhs.Front().Value.(*CandidateNode).Node.Value comment = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
comment = rhs.Front().Value.(*CandidateNode).Node.Value comment = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
@@ -63,15 +63,15 @@ func AssignCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, path
} }
} }
return matchingNodes, nil return context, nil
} }
func GetCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getCommentsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
preferences := pathNode.Operation.Preferences.(*CommentOpPreferences) preferences := expressionNode.Operation.Preferences.(commentOpPreferences)
log.Debugf("GetComments operator!") log.Debugf("GetComments operator!")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
comment := "" comment := ""
if preferences.LineComment { if preferences.LineComment {
@@ -84,8 +84,8 @@ func GetCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNod
comment = strings.Replace(comment, "# ", "", 1) comment = strings.Replace(comment, "# ", "", 1)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: comment, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: comment, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -71,11 +71,12 @@ var commentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "Remove all comments", description: "Remove all comments",
document: "# hi\n\na: cat # comment\n\n# great\n", subdescription: "Note the use of `...` to ensure key nodes are included.",
expression: `.. comments=""`, document: "# hi\n\na: cat # comment\n\n# great\n\nb: # key comment",
expression: `... comments=""`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::a: cat\n", "D0, P[], (!!map)::a: cat\nb:\n",
}, },
}, },
{ {

View File

@@ -6,7 +6,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func CreateMapOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func createMapOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- createMapOperation") log.Debugf("-- createMapOperation")
//each matchingNodes entry should turn into a sequence of keys to create. //each matchingNodes entry should turn into a sequence of keys to create.
@@ -18,29 +18,29 @@ func CreateMapOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode
sequences := list.New() sequences := list.New()
if matchingNodes.Len() > 0 { if context.MatchingNodes.Len() > 0 {
for matchingNodeEl := matchingNodes.Front(); matchingNodeEl != nil; matchingNodeEl = matchingNodeEl.Next() { for matchingNodeEl := context.MatchingNodes.Front(); matchingNodeEl != nil; matchingNodeEl = matchingNodeEl.Next() {
matchingNode := matchingNodeEl.Value.(*CandidateNode) matchingNode := matchingNodeEl.Value.(*CandidateNode)
sequenceNode, err := sequenceFor(d, matchingNode, pathNode) sequenceNode, err := sequenceFor(d, context, matchingNode, expressionNode)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
sequences.PushBack(sequenceNode) sequences.PushBack(sequenceNode)
} }
} else { } else {
sequenceNode, err := sequenceFor(d, nil, pathNode) sequenceNode, err := sequenceFor(d, context, nil, expressionNode)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
sequences.PushBack(sequenceNode) sequences.PushBack(sequenceNode)
} }
return nodeToMap(&CandidateNode{Node: listToNodeSeq(sequences), Document: document, Path: path}), nil return context.SingleChildContext(&CandidateNode{Node: listToNodeSeq(sequences), Document: document, Path: path}), nil
} }
func sequenceFor(d *dataTreeNavigator, matchingNode *CandidateNode, pathNode *PathTreeNode) (*CandidateNode, error) { func sequenceFor(d *dataTreeNavigator, context Context, matchingNode *CandidateNode, expressionNode *ExpressionNode) (*CandidateNode, error) {
var path []interface{} var path []interface{}
var document uint = 0 var document uint = 0
var matches = list.New() var matches = list.New()
@@ -48,17 +48,17 @@ func sequenceFor(d *dataTreeNavigator, matchingNode *CandidateNode, pathNode *Pa
if matchingNode != nil { if matchingNode != nil {
path = matchingNode.Path path = matchingNode.Path
document = matchingNode.Document document = matchingNode.Document
matches = nodeToMap(matchingNode) matches.PushBack(matchingNode)
} }
mapPairs, err := crossFunction(d, matches, pathNode, mapPairs, err := crossFunction(d, context.ChildContext(matches), expressionNode,
func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
node := yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} node := yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
log.Debugf("LHS:", NodeToString(lhs)) log.Debugf("LHS:", NodeToString(lhs))
log.Debugf("RHS:", NodeToString(rhs)) log.Debugf("RHS:", NodeToString(rhs))
node.Content = []*yaml.Node{ node.Content = []*yaml.Node{
UnwrapDoc(lhs.Node), unwrapDoc(lhs.Node),
UnwrapDoc(rhs.Node), unwrapDoc(rhs.Node),
} }
return &CandidateNode{Node: &node, Document: document, Path: path}, nil return &CandidateNode{Node: &node, Document: document, Path: path}, nil
@@ -67,7 +67,7 @@ func sequenceFor(d *dataTreeNavigator, matchingNode *CandidateNode, pathNode *Pa
if err != nil { if err != nil {
return nil, err return nil, err
} }
innerList := listToNodeSeq(mapPairs) innerList := listToNodeSeq(mapPairs.MatchingNodes)
innerList.Style = yaml.FlowStyle innerList.Style = yaml.FlowStyle
return &CandidateNode{Node: innerList, Document: document, Path: path}, nil return &CandidateNode{Node: innerList, Document: document, Path: path}, nil
} }

View File

@@ -1,70 +1,69 @@
package yqlib package yqlib
import ( import (
"container/list"
"fmt" "fmt"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func deleteChildOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
nodesToDelete, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) nodesToDelete, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := nodesToDelete.Front(); el != nil; el = el.Next() { for el := nodesToDelete.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
deleteImmediateChildOp := &Operation{ deleteImmediateChildOp := &Operation{
OperationType: DeleteImmediateChild, OperationType: deleteImmediateChildOpType,
Value: candidate.Path[len(candidate.Path)-1], Value: candidate.Path[len(candidate.Path)-1],
} }
deleteImmediateChildOpNode := &PathTreeNode{ deleteImmediateChildOpNode := &ExpressionNode{
Operation: deleteImmediateChildOp, Operation: deleteImmediateChildOp,
Rhs: createTraversalTree(candidate.Path[0 : len(candidate.Path)-1]), Rhs: createTraversalTree(candidate.Path[0:len(candidate.Path)-1], traversePreferences{}),
} }
_, err := d.GetMatchingNodes(matchingNodes, deleteImmediateChildOpNode) _, err := d.GetMatchingNodes(context, deleteImmediateChildOpNode)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
} }
return matchingNodes, nil return context, nil
} }
func DeleteImmediateChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func deleteImmediateChildOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
parents, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) parents, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
childPath := pathNode.Operation.Value childPath := expressionNode.Operation.Value
log.Debug("childPath to remove %v", childPath) log.Debug("childPath to remove %v", childPath)
for el := parents.Front(); el != nil; el = el.Next() { for el := parents.MatchingNodes.Front(); el != nil; el = el.Next() {
parent := el.Value.(*CandidateNode) parent := el.Value.(*CandidateNode)
parentNode := UnwrapDoc(parent.Node) parentNode := unwrapDoc(parent.Node)
if parentNode.Kind == yaml.MappingNode { if parentNode.Kind == yaml.MappingNode {
deleteFromMap(parent, childPath) deleteFromMap(parent, childPath)
} else if parentNode.Kind == yaml.SequenceNode { } else if parentNode.Kind == yaml.SequenceNode {
deleteFromArray(parent, childPath) deleteFromArray(parent, childPath)
} else { } else {
return nil, fmt.Errorf("Cannot delete nodes from parent of tag %v", parentNode.Tag) return Context{}, fmt.Errorf("Cannot delete nodes from parent of tag %v", parentNode.Tag)
} }
} }
return matchingNodes, nil return context, nil
} }
func deleteFromMap(candidate *CandidateNode, childPath interface{}) { func deleteFromMap(candidate *CandidateNode, childPath interface{}) {
log.Debug("deleteFromMap") log.Debug("deleteFromMap")
node := UnwrapDoc(candidate.Node) node := unwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
newContents := make([]*yaml.Node, 0) newContents := make([]*yaml.Node, 0)
@@ -72,11 +71,7 @@ func deleteFromMap(candidate *CandidateNode, childPath interface{}) {
key := contents[index] key := contents[index]
value := contents[index+1] value := contents[index+1]
childCandidate := &CandidateNode{ childCandidate := candidate.CreateChild(key.Value, value)
Node: value,
Document: candidate.Document,
Path: candidate.CreateChildPath(key.Value),
}
shouldDelete := key.Value == childPath shouldDelete := key.Value == childPath
@@ -91,7 +86,7 @@ func deleteFromMap(candidate *CandidateNode, childPath interface{}) {
func deleteFromArray(candidate *CandidateNode, childPath interface{}) { func deleteFromArray(candidate *CandidateNode, childPath interface{}) {
log.Debug("deleteFromArray") log.Debug("deleteFromArray")
node := UnwrapDoc(candidate.Node) node := unwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
newContents := make([]*yaml.Node, 0) newContents := make([]*yaml.Node, 0)

View File

@@ -7,14 +7,14 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func GetDocumentIndexOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getDocumentIndexOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.Document), Tag: "!!int"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.Document), Tag: "!!int"}
scalar := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} scalar := candidate.CreateChild(nil, node)
results.PushBack(scalar) results.PushBack(scalar)
} }
return results, nil return context.ChildContext(results), nil
} }

49
pkg/yqlib/operator_env.go Normal file
View File

@@ -0,0 +1,49 @@
package yqlib
import (
"fmt"
"os"
"strings"
yaml "gopkg.in/yaml.v3"
)
type envOpPreferences struct {
StringValue bool
}
func envOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
envName := expressionNode.Operation.CandidateNode.Node.Value
log.Debug("EnvOperator, env name:", envName)
rawValue := os.Getenv(envName)
preferences := expressionNode.Operation.Preferences.(envOpPreferences)
var node *yaml.Node
if preferences.StringValue {
node = &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: rawValue,
}
} else if rawValue == "" {
return Context{}, fmt.Errorf("Value for env variable '%v' not provided in env()", envName)
} else {
var dataBucket yaml.Node
decoder := yaml.NewDecoder(strings.NewReader(rawValue))
errorReading := decoder.Decode(&dataBucket)
if errorReading != nil {
return Context{}, errorReading
}
//first node is a doc
node = unwrapDoc(&dataBucket)
}
log.Debug("ENV tag", node.Tag)
log.Debug("ENV value", node.Value)
log.Debug("ENV Kind", node.Kind)
target := &CandidateNode{Node: node}
return context.SingleChildContext(target), nil
}

View File

@@ -0,0 +1,72 @@
package yqlib
import (
"testing"
)
var envOperatorScenarios = []expressionScenario{
{
description: "Read string environment variable",
environmentVariable: "cat meow",
expression: `.a = env(myenv)`,
expected: []string{
"D0, P[], ()::a: cat meow\n",
},
},
{
description: "Read boolean environment variable",
environmentVariable: "true",
expression: `.a = env(myenv)`,
expected: []string{
"D0, P[], ()::a: true\n",
},
},
{
description: "Read numeric environment variable",
environmentVariable: "12",
expression: `.a = env(myenv)`,
expected: []string{
"D0, P[], ()::a: 12\n",
},
},
{
description: "Read yaml environment variable",
environmentVariable: "{b: fish}",
expression: `.a = env(myenv)`,
expected: []string{
"D0, P[], ()::a: {b: fish}\n",
},
},
{
description: "Read boolean environment variable as a string",
environmentVariable: "true",
expression: `.a = strenv(myenv)`,
expected: []string{
"D0, P[], ()::a: \"true\"\n",
},
},
{
description: "Read numeric environment variable as a string",
environmentVariable: "12",
expression: `.a = strenv(myenv)`,
expected: []string{
"D0, P[], ()::a: \"12\"\n",
},
},
{
description: "Dynamic key lookup with environment variable",
environmentVariable: "cat",
document: `{cat: meow, dog: woof}`,
expression: `.[env(myenv)]`,
expected: []string{
"D0, P[cat], (!!str)::meow\n",
},
},
}
func TestEnvOperatorScenarios(t *testing.T) {
for _, tt := range envOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Env Variable Operators", envOperatorScenarios)
}

View File

@@ -1,21 +1,20 @@
package yqlib package yqlib
import ( func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
"container/list"
)
func EqualsOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- equalsOperation") log.Debugf("-- equalsOperation")
return crossFunction(d, matchingNodes, pathNode, isEquals) return crossFunction(d, context, expressionNode, isEquals)
} }
func isEquals(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func isEquals(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
value := false value := false
if lhs.Node.Tag == "!!null" { lhsNode := unwrapDoc(lhs.Node)
value = (rhs.Node.Tag == "!!null") rhsNode := unwrapDoc(rhs.Node)
if lhsNode.Tag == "!!null" {
value = (rhsNode.Tag == "!!null")
} else { } else {
value = Match(lhs.Node.Value, rhs.Node.Value) value = matchKey(lhsNode.Value, rhsNode.Value)
} }
log.Debugf("%v == %v ? %v", NodeToString(lhs), NodeToString(rhs), value) log.Debugf("%v == %v ? %v", NodeToString(lhs), NodeToString(rhs), value)
return createBooleanCandidate(lhs, value), nil return createBooleanCandidate(lhs, value), nil

View File

@@ -5,6 +5,15 @@ import (
) )
var equalsOperatorScenarios = []expressionScenario{ var equalsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: "cat",
document2: "dog",
expression: "select(fi==0) == select(fi==1)",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{ {
description: "Match string", description: "Match string",
document: `[cat,goat,dog]`, document: `[cat,goat,dog]`,

View File

@@ -7,32 +7,32 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func GetFilenameOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getFilenameOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetFilename") log.Debugf("GetFilename")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Filename, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Filename, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }
func GetFileIndexOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getFileIndexOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetFileIndex") log.Debugf("GetFileIndex")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.FileIndex), Tag: "!!int"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.FileIndex), Tag: "!!int"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -29,6 +29,14 @@ var fileOperatorScenarios = []expressionScenario{
"D0, P[], (!!int)::0\n", "D0, P[], (!!int)::0\n",
}, },
}, },
{
skipDoc: true,
document: "a: cat\nb: dog",
expression: `.. lineComment |= filename`,
expected: []string{
"D0, P[], (!!map)::a: cat # sample.yml\nb: dog # sample.yml\n",
},
},
} }
func TestFileOperatorsScenarios(t *testing.T) { func TestFileOperatorsScenarios(t *testing.T) {

View File

@@ -7,25 +7,26 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func hasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- hasOperation") log.Debugf("-- hasOperation")
var results = list.New() var results = list.New()
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
wanted := rhs.Front().Value.(*CandidateNode).Node wanted := rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
wantedKey := wanted.Value wantedKey := wanted.Value
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
// grab the first value // grab the first value
var contents = candidate.Node.Content candidateNode := unwrapDoc(candidate.Node)
switch candidate.Node.Kind { var contents = candidateNode.Content
switch candidateNode.Kind {
case yaml.MappingNode: case yaml.MappingNode:
candidateHasKey := false candidateHasKey := false
for index := 0; index < len(contents) && !candidateHasKey; index = index + 2 { for index := 0; index < len(contents) && !candidateHasKey; index = index + 2 {
@@ -40,7 +41,7 @@ func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
if wanted.Tag == "!!int" { if wanted.Tag == "!!int" {
var number, errParsingInt = strconv.ParseInt(wantedKey, 10, 64) // nolint var number, errParsingInt = strconv.ParseInt(wantedKey, 10, 64) // nolint
if errParsingInt != nil { if errParsingInt != nil {
return nil, errParsingInt return Context{}, errParsingInt
} }
candidateHasKey = int64(len(contents)) > number candidateHasKey = int64(len(contents)) > number
} }
@@ -49,5 +50,5 @@ func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
results.PushBack(createBooleanCandidate(candidate, false)) results.PushBack(createBooleanCandidate(candidate, false))
} }
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -5,6 +5,14 @@ import (
) )
var hasOperatorScenarios = []expressionScenario{ var hasOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `a: hello`,
expression: `has("a")`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{ {
description: "Has map key", description: "Has map key",
document: `- a: "yes" document: `- a: "yes"

View File

@@ -0,0 +1,54 @@
package yqlib
import (
"container/list"
"fmt"
"gopkg.in/yaml.v3"
)
func keysOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- keysOperator")
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
var targetNode *yaml.Node
if node.Kind == yaml.MappingNode {
targetNode = getMapKeys(node)
} else if node.Kind == yaml.SequenceNode {
targetNode = getIndicies(node)
} else {
return Context{}, fmt.Errorf("Cannot get keys of %v, keys only works for maps and arrays", node.Tag)
}
result := candidate.CreateChild(nil, targetNode)
results.PushBack(result)
}
return context.ChildContext(results), nil
}
func getMapKeys(node *yaml.Node) *yaml.Node {
contents := make([]*yaml.Node, 0)
for index := 0; index < len(node.Content); index = index + 2 {
contents = append(contents, node.Content[index])
}
return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: contents}
}
func getIndicies(node *yaml.Node) *yaml.Node {
var contents = make([]*yaml.Node, len(node.Content))
for index := range node.Content {
contents[index] = &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!int",
Value: fmt.Sprintf("%v", index),
}
}
return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: contents}
}

View File

@@ -0,0 +1,47 @@
package yqlib
import (
"testing"
)
var keysOperatorScenarios = []expressionScenario{
{
description: "Map keys",
document: `{dog: woof, cat: meow}`,
expression: `keys`,
expected: []string{
"D0, P[], (!!seq)::- dog\n- cat\n",
},
},
{
skipDoc: true,
document: `{}`,
expression: `keys`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
{
description: "Array keys",
document: `[apple, banana]`,
expression: `keys`,
expected: []string{
"D0, P[], (!!seq)::- 0\n- 1\n",
},
},
{
skipDoc: true,
document: `[]`,
expression: `keys`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
}
func TestKeysOperatorScenarios(t *testing.T) {
for _, tt := range keysOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Keys", keysOperatorScenarios)
}

View File

@@ -7,17 +7,21 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func LengthOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func lengthOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- lengthOperation") log.Debugf("-- lengthOperation")
var results = list.New() var results = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
targetNode := UnwrapDoc(candidate.Node) targetNode := unwrapDoc(candidate.Node)
var length int var length int
switch targetNode.Kind { switch targetNode.Kind {
case yaml.ScalarNode: case yaml.ScalarNode:
length = len(targetNode.Value) if targetNode.Tag == "!!null" {
length = 0
} else {
length = len(targetNode.Value)
}
case yaml.MappingNode: case yaml.MappingNode:
length = len(targetNode.Content) / 2 length = len(targetNode.Content) / 2
case yaml.SequenceNode: case yaml.SequenceNode:
@@ -27,9 +31,9 @@ func LengthOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTre
} }
node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", length), Tag: "!!int"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", length), Tag: "!!int"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -14,6 +14,30 @@ var lengthOperatorScenarios = []expressionScenario{
"D0, P[a], (!!int)::3\n", "D0, P[a], (!!int)::3\n",
}, },
}, },
{
description: "null length",
document: `{a: null}`,
expression: `.a | length`,
expected: []string{
"D0, P[a], (!!int)::0\n",
},
},
{
skipDoc: true,
document: `{a: ~}`,
expression: `.a | length`,
expected: []string{
"D0, P[a], (!!int)::0\n",
},
},
{
skipDoc: true,
document: `{a: key no exist}`,
expression: `.b | length`,
expected: []string{
"D0, P[b], (!!int)::0\n",
},
},
{ {
description: "Map length", description: "Map length",
subdescription: "returns number of entries", subdescription: "returns number of entries",

View File

@@ -2,92 +2,72 @@ package yqlib
import ( import (
"fmt" "fmt"
"strconv"
"container/list" "container/list"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type CrossFunctionCalculation func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) type multiplyPreferences struct {
AppendArrays bool
func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, calculation CrossFunctionCalculation) (*list.List, error) { TraversePrefs traversePreferences
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil {
return nil, err
}
log.Debugf("crossFunction LHS len: %v", lhs.Len())
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
log.Debugf("crossFunction RHS len: %v", rhs.Len())
var results = list.New()
for el := lhs.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode)
for rightEl := rhs.Front(); rightEl != nil; rightEl = rightEl.Next() {
log.Debugf("Applying calc")
rhsCandidate := rightEl.Value.(*CandidateNode)
resultCandidate, err := calculation(d, lhsCandidate, rhsCandidate)
if err != nil {
return nil, err
}
results.PushBack(resultCandidate)
}
}
return results, nil
} }
type MultiplyPreferences struct { func multiplyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
AppendArrays bool
}
func MultiplyOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- MultiplyOperator") log.Debugf("-- MultiplyOperator")
return crossFunction(d, matchingNodes, pathNode, multiply(pathNode.Operation.Preferences.(*MultiplyPreferences))) return crossFunction(d, context, expressionNode, multiply(expressionNode.Operation.Preferences.(multiplyPreferences)))
} }
func multiply(preferences *MultiplyPreferences) func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func multiply(preferences multiplyPreferences) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
return func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node) lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node) rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Multipling LHS: %v", lhs.Node.Tag) log.Debugf("Multipling LHS: %v", lhs.Node.Tag)
log.Debugf("- RHS: %v", rhs.Node.Tag) log.Debugf("- RHS: %v", rhs.Node.Tag)
shouldAppendArrays := preferences.AppendArrays
if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode || if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode ||
(lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) { (lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) {
var newBlank = &CandidateNode{ var newBlank = lhs.CreateChild(nil, &yaml.Node{})
Path: lhs.Path, var newThing, err = mergeObjects(d, context, newBlank, lhs, multiplyPreferences{})
Document: lhs.Document,
Filename: lhs.Filename,
Node: &yaml.Node{},
}
var newThing, err = mergeObjects(d, newBlank, lhs, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return mergeObjects(d, newThing, rhs, shouldAppendArrays) return mergeObjects(d, context, newThing, rhs, preferences)
} else if lhs.Node.Tag == "!!int" && rhs.Node.Tag == "!!int" {
return multiplyIntegers(lhs, rhs)
} }
return nil, fmt.Errorf("Cannot multiply %v with %v", lhs.Node.Tag, rhs.Node.Tag) return nil, fmt.Errorf("Cannot multiply %v with %v", lhs.Node.Tag, rhs.Node.Tag)
} }
} }
func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode, shouldAppendArrays bool) (*CandidateNode, error) { func multiplyIntegers(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
target := lhs.CreateChild(nil, &yaml.Node{})
target.Node.Kind = yaml.ScalarNode
target.Node.Style = lhs.Node.Style
target.Node.Tag = "!!int"
lhsNum, err := strconv.Atoi(lhs.Node.Value)
if err != nil {
return nil, err
}
rhsNum, err := strconv.Atoi(rhs.Node.Value)
if err != nil {
return nil, err
}
target.Node.Value = fmt.Sprintf("%v", lhsNum*rhsNum)
return target, nil
}
func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode, preferences multiplyPreferences) (*CandidateNode, error) {
shouldAppendArrays := preferences.AppendArrays
var results = list.New() var results = list.New()
// shouldn't recurse arrays if appending // shouldn't recurse arrays if appending
prefs := &RecursiveDescentPreferences{RecurseArray: !shouldAppendArrays, prefs := recursiveDescentPreferences{RecurseArray: !shouldAppendArrays,
TraversePreferences: &TraversePreferences{FollowAlias: false}} TraversePreferences: traversePreferences{DontFollowAlias: true}}
err := recursiveDecent(d, results, nodeToMap(rhs), prefs) err := recursiveDecent(d, results, context.SingleChildContext(rhs), prefs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -98,7 +78,7 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
} }
for el := results.Front(); el != nil; el = el.Next() { for el := results.Front(); el != nil; el = el.Next() {
err := applyAssignment(d, pathIndexToStartFrom, lhs, el.Value.(*CandidateNode), shouldAppendArrays) err := applyAssignment(d, context, pathIndexToStartFrom, lhs, el.Value.(*CandidateNode), preferences)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -106,24 +86,24 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
return lhs, nil return lhs, nil
} }
func applyAssignment(d *dataTreeNavigator, pathIndexToStartFrom int, lhs *CandidateNode, rhs *CandidateNode, shouldAppendArrays bool) error { func applyAssignment(d *dataTreeNavigator, context Context, pathIndexToStartFrom int, lhs *CandidateNode, rhs *CandidateNode, preferences multiplyPreferences) error {
shouldAppendArrays := preferences.AppendArrays
log.Debugf("merge - applyAssignment lhs %v, rhs: %v", NodeToString(lhs), NodeToString(rhs)) log.Debugf("merge - applyAssignment lhs %v, rhs: %v", NodeToString(lhs), NodeToString(rhs))
lhsPath := rhs.Path[pathIndexToStartFrom:] lhsPath := rhs.Path[pathIndexToStartFrom:]
assignmentOp := &Operation{OperationType: AssignAttributes} assignmentOp := &Operation{OperationType: assignAttributesOpType}
if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode { if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode {
assignmentOp.OperationType = Assign assignmentOp.OperationType = assignOpType
assignmentOp.UpdateAssign = false assignmentOp.UpdateAssign = false
} else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode { } else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode {
assignmentOp.OperationType = AddAssign assignmentOp.OperationType = addAssignOpType
} }
rhsOp := &Operation{OperationType: ValueOp, CandidateNode: rhs} rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhs}
assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: createTraversalTree(lhsPath), Rhs: &PathTreeNode{Operation: rhsOp}} assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: createTraversalTree(lhsPath, preferences.TraversePrefs), Rhs: &ExpressionNode{Operation: rhsOp}}
_, err := d.GetMatchingNodes(nodeToMap(lhs), assignmentOpNode) _, err := d.GetMatchingNodes(context.SingleChildContext(lhs), assignmentOpNode)
return err return err
} }

View File

@@ -13,6 +13,27 @@ var multiplyOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::{a: {also: me}, b: {also: me}}\n", "D0, P[], (!!map)::{a: {also: me}, b: {also: me}}\n",
}, },
}, },
{
skipDoc: true,
document: `{a: 2, b: 5}`,
document2: `{a: 3, b: 10}`,
expression: `.a * .b`,
expected: []string{
"D0, P[a], (!!int)::10\n",
"D0, P[a], (!!int)::20\n",
"D0, P[a], (!!int)::15\n",
"D0, P[a], (!!int)::30\n",
},
},
{
skipDoc: true,
document: `{a: 2}`,
document2: `{b: 10}`,
expression: `select(fi ==0) * select(fi==1)`,
expected: []string{
"D0, P[], (!!map)::{a: 2, b: 10}\n",
},
},
{ {
skipDoc: true, skipDoc: true,
expression: `{} * {"cat":"dog"}`, expression: `{} * {"cat":"dog"}`,
@@ -107,6 +128,30 @@ b:
"D0, P[a], (!!seq)::[1, 2]\n", "D0, P[a], (!!seq)::[1, 2]\n",
}, },
}, },
{
description: "Merge, only existing fields",
document: `{a: {thing: one, cat: frog}, b: {missing: two, thing: two}}`,
expression: `.a *? .b`,
expected: []string{
"D0, P[a], (!!map)::{thing: two, cat: frog}\n",
},
},
{
skipDoc: true,
document: `{a: [{thing: one}], b: [{missing: two, thing: two}]}`,
expression: `.a *? .b`,
expected: []string{
"D0, P[a], (!!seq)::[{thing: two}]\n",
},
},
{
skipDoc: true,
document: `{a: {array: [1]}, b: {}}`,
expression: `.b *+ .a`,
expected: []string{
"D0, P[b], (!!map)::{array: [1]}\n",
},
},
{ {
description: "Merge, appending arrays", description: "Merge, appending arrays",
document: `{a: {array: [1, 2, animal: dog], value: coconut}, b: {array: [3, 4, animal: cat], value: banana}}`, document: `{a: {array: [1, 2, animal: dog], value: coconut}, b: {array: [3, 4, animal: cat], value: banana}}`,
@@ -115,6 +160,14 @@ b:
"D0, P[a], (!!map)::{array: [1, 2, {animal: dog}, 3, 4, {animal: cat}], value: banana}\n", "D0, P[a], (!!map)::{array: [1, 2, {animal: dog}, 3, 4, {animal: cat}], value: banana}\n",
}, },
}, },
{
description: "Merge, only existing fields, appending arrays",
document: `{a: {thing: [1,2]}, b: {thing: [3,4], another: [1]}}`,
expression: `.a *?+ .b`,
expected: []string{
"D0, P[a], (!!map)::{thing: [1, 2, 3, 4]}\n",
},
},
{ {
description: "Merge to prefix an element", description: "Merge to prefix an element",
document: `{a: cat, b: dog}`, document: `{a: cat, b: dog}`,
@@ -132,11 +185,11 @@ b:
}, },
}, },
{ {
description: "Merge does not copy anchor names", description: "Merge copies anchor names",
document: `{a: {c: &cat frog}, b: {f: *cat}, c: {g: thongs}}`, document: `{a: {c: &cat frog}, b: {f: *cat}, c: {g: thongs}}`,
expression: `.c * .a`, expression: `.c * .a`,
expected: []string{ expected: []string{
"D0, P[c], (!!map)::{g: thongs, c: frog}\n", "D0, P[c], (!!map)::{g: thongs, c: &cat frog}\n",
}, },
}, },
{ {

View File

@@ -16,12 +16,12 @@ func createPathNodeFor(pathElement interface{}) *yaml.Node {
} }
} }
func GetPathOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getPathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetPath") log.Debugf("GetPath")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
@@ -31,9 +31,9 @@ func GetPathOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *P
content[pathIndex] = createPathNodeFor(path) content[pathIndex] = createPathNodeFor(path)
} }
node.Content = content node.Content = content
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -1,11 +1,18 @@
package yqlib package yqlib
import "container/list" func pipeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
func PipeOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { //lhs may update the variable context, we should pass that into the RHS
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) // BUT we still return the original context back (see jq)
// https://stedolan.github.io/jq/manual/#Variable/SymbolicBindingOperator:...as$identifier|...
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
return d.GetMatchingNodes(lhs, pathNode.Rhs) rhs, err := d.GetMatchingNodes(lhs, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
return context.ChildContext(rhs.MatchingNodes), nil
} }

View File

@@ -6,28 +6,28 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type RecursiveDescentPreferences struct { type recursiveDescentPreferences struct {
TraversePreferences *TraversePreferences TraversePreferences traversePreferences
RecurseArray bool RecurseArray bool
} }
func RecursiveDescentOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func recursiveDescentOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
var results = list.New() var results = list.New()
preferences := pathNode.Operation.Preferences.(*RecursiveDescentPreferences) preferences := expressionNode.Operation.Preferences.(recursiveDescentPreferences)
err := recursiveDecent(d, results, matchMap, preferences) err := recursiveDecent(d, results, context, preferences)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
return results, nil return context.ChildContext(results), nil
} }
func recursiveDecent(d *dataTreeNavigator, results *list.List, matchMap *list.List, preferences *RecursiveDescentPreferences) error { func recursiveDecent(d *dataTreeNavigator, results *list.List, context Context, preferences recursiveDescentPreferences) error {
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
candidate.Node = UnwrapDoc(candidate.Node) candidate.Node = unwrapDoc(candidate.Node)
log.Debugf("Recursive Decent, added %v", NodeToString(candidate)) log.Debugf("Recursive Decent, added %v", NodeToString(candidate))
results.PushBack(candidate) results.PushBack(candidate)
@@ -35,7 +35,7 @@ func recursiveDecent(d *dataTreeNavigator, results *list.List, matchMap *list.Li
if candidate.Node.Kind != yaml.AliasNode && len(candidate.Node.Content) > 0 && if candidate.Node.Kind != yaml.AliasNode && len(candidate.Node.Content) > 0 &&
(preferences.RecurseArray || candidate.Node.Kind != yaml.SequenceNode) { (preferences.RecurseArray || candidate.Node.Kind != yaml.SequenceNode) {
children, err := Splat(d, nodeToMap(candidate), preferences.TraversePreferences) children, err := splat(d, context.SingleChildContext(candidate), preferences.TraversePreferences)
if err != nil { if err != nil {
return err return err

View File

@@ -63,12 +63,12 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "Recursively find nodes with keys", description: "Recursively find nodes with keys",
document: `{a: {name: frog, b: {name: blog, age: 12}}}`, subdescription: "Note that this example has wrapped the expression in `[]` to show that there are two matches returned. You do not have to wrap in `[]` in your path expression.",
expression: `.. | select(has("name"))`, document: `{a: {name: frog, b: {name: blog, age: 12}}}`,
expression: `[.. | select(has("name"))]`,
expected: []string{ expected: []string{
"D0, P[a], (!!map)::{name: frog, b: {name: blog, age: 12}}\n", "D0, P[], (!!seq)::- {name: frog, b: {name: blog, age: 12}}\n- {name: blog, age: 12}\n",
"D0, P[a b], (!!map)::{name: blog, age: 12}\n",
}, },
}, },
{ {
@@ -165,7 +165,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
document: `{a: &cat {c: frog}, b: *cat}`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `[..]`, expression: `[..]`,
expected: []string{ expected: []string{
"D0, P[a], (!!seq)::- {a: &cat {c: frog}, b: *cat}\n- &cat {c: frog}\n- frog\n- *cat\n", "D0, P[], (!!seq)::- {a: &cat {c: frog}, b: *cat}\n- &cat {c: frog}\n- frog\n- *cat\n",
}, },
}, },
{ {
@@ -187,7 +187,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
document: mergeDocSample, document: mergeDocSample,
expression: `.foobar | [..]`, expression: `.foobar | [..]`,
expected: []string{ expected: []string{
"D0, P[foobar], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- foobar_c\n- *foo\n- foobar_thing\n", "D0, P[], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- foobar_c\n- *foo\n- foobar_thing\n",
}, },
}, },
{ {
@@ -195,7 +195,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
document: mergeDocSample, document: mergeDocSample,
expression: `.foobar | [...]`, expression: `.foobar | [...]`,
expected: []string{ expected: []string{
"D0, P[foobar], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- c\n- foobar_c\n- !!merge <<\n- *foo\n- thing\n- foobar_thing\n", "D0, P[], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- c\n- foobar_c\n- !!merge <<\n- *foo\n- thing\n- foobar_thing\n",
}, },
}, },
{ {
@@ -233,5 +233,5 @@ func TestRecursiveDescentOperatorScenarios(t *testing.T) {
for _, tt := range recursiveDescentOperatorScenarios { for _, tt := range recursiveDescentOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Recursive Descent", recursiveDescentOperatorScenarios) documentScenarios(t, "Recursive Descent (Glob)", recursiveDescentOperatorScenarios)
} }

View File

@@ -4,28 +4,29 @@ import (
"container/list" "container/list"
) )
func SelectOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func selectOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- selectOperation") log.Debugf("-- selectOperation")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
childContext := context.SingleChildContext(candidate)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) childContext.DontAutoCreate = true
rhs, err := d.GetMatchingNodes(childContext, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
// grab the first value // grab the first value
first := rhs.Front() first := rhs.MatchingNodes.Front()
if first != nil { if first != nil {
result := first.Value.(*CandidateNode) result := first.Value.(*CandidateNode)
includeResult, errDecoding := isTruthy(result) includeResult, errDecoding := isTruthy(result)
if errDecoding != nil { if errDecoding != nil {
return nil, errDecoding return Context{}, errDecoding
} }
if includeResult { if includeResult {
@@ -33,5 +34,5 @@ func SelectOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *Pa
} }
} }
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -14,6 +14,14 @@ var selectOperatorScenarios = []expressionScenario{
"D0, P[1], (!!str)::goat\n", "D0, P[1], (!!str)::goat\n",
}, },
}, },
{
skipDoc: true,
document: `[{animal: cat, legs: {cool: true}}, {animal: fish}]`,
expression: `(.[] | select(.legs.cool == true).canWalk) = true | (.[] | .alive.things) = "yes"`,
expected: []string{
"D0, P[], (doc)::[{animal: cat, legs: {cool: true}, canWalk: true, alive: {things: yes}}, {animal: fish, alive: {things: yes}}]\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: `[hot, fot, dog]`, document: `[hot, fot, dog]`,

View File

@@ -1,7 +1,5 @@
package yqlib package yqlib
import "container/list" func selfOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
return context, nil
func SelfOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
return matchMap, nil
} }

View File

@@ -1,33 +1,32 @@
package yqlib package yqlib
import ( import (
"container/list"
"sort" "sort"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func SortKeysOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func sortKeysOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for childEl := rhs.Front(); childEl != nil; childEl = childEl.Next() { for childEl := rhs.MatchingNodes.Front(); childEl != nil; childEl = childEl.Next() {
node := UnwrapDoc(childEl.Value.(*CandidateNode).Node) node := unwrapDoc(childEl.Value.(*CandidateNode).Node)
if node.Kind == yaml.MappingNode { if node.Kind == yaml.MappingNode {
sortKeys(node) sortKeys(node)
} }
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
} }
} }
return matchingNodes, nil return context, nil
} }
func sortKeys(node *yaml.Node) { func sortKeys(node *yaml.Node) {

View File

@@ -0,0 +1,14 @@
package yqlib
func splitDocumentOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- splitDocumentOperator")
var index uint = 0
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
candidate.Document = index
index = index + 1
}
return context, nil
}

View File

@@ -0,0 +1,32 @@
package yqlib
import (
"testing"
)
var splitDocOperatorScenarios = []expressionScenario{
{
description: "Split empty",
document: ``,
expression: `splitDoc`,
expected: []string{
"D0, P[], (!!null)::\n",
},
},
{
description: "Split array",
document: `[{a: cat}, {b: dog}]`,
expression: `.[] | splitDoc`,
expected: []string{
"D0, P[0], (!!map)::{a: cat}\n",
"D1, P[1], (!!map)::{b: dog}\n",
},
},
}
func TestSplitDocOperatorScenarios(t *testing.T) {
for _, tt := range splitDocOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Split into Documents", splitDocOperatorScenarios)
}

View File

@@ -0,0 +1,96 @@
package yqlib
import (
"container/list"
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- joinStringOperator")
joinStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
if rhs.MatchingNodes.Front() != nil {
joinStr = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
}
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
if node.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("Cannot join with %v, can only join arrays of scalars", node.Tag)
}
targetNode := join(node.Content, joinStr)
result := candidate.CreateChild(nil, targetNode)
results.PushBack(result)
}
return context.ChildContext(results), nil
}
func join(content []*yaml.Node, joinStr string) *yaml.Node {
var stringsToJoin []string
for _, node := range content {
str := node.Value
if node.Tag == "!!null" {
str = ""
}
stringsToJoin = append(stringsToJoin, str)
}
return &yaml.Node{Kind: yaml.ScalarNode, Value: strings.Join(stringsToJoin, joinStr), Tag: "!!str"}
}
func splitStringOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- splitStringOperator")
splitStr := ""
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
if rhs.MatchingNodes.Front() != nil {
splitStr = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
}
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
if node.Tag == "!!null" {
continue
}
if node.Tag != "!!str" {
return Context{}, fmt.Errorf("Cannot split %v, can only split strings", node.Tag)
}
targetNode := split(node.Value, splitStr)
result := candidate.CreateChild(nil, targetNode)
results.PushBack(result)
}
return context.ChildContext(results), nil
}
func split(value string, spltStr string) *yaml.Node {
var contents []*yaml.Node
if value != "" {
var newStrings = strings.Split(value, spltStr)
contents = make([]*yaml.Node, len(newStrings))
for index, str := range newStrings {
contents[index] = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: str}
}
}
return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: contents}
}

View File

@@ -0,0 +1,52 @@
package yqlib
import (
"testing"
)
var stringsOperatorScenarios = []expressionScenario{
{
description: "Join strings",
document: `[cat, meow, 1, null, true]`,
expression: `join("; ")`,
expected: []string{
"D0, P[], (!!str)::cat; meow; 1; ; true\n",
},
},
{
description: "Split strings",
document: `"cat; meow; 1; ; true"`,
expression: `split("; ")`,
expected: []string{
"D0, P[], (!!seq)::- cat\n- meow\n- \"1\"\n- \"\"\n- \"true\"\n",
},
},
{
description: "Split strings one match",
document: `"word"`,
expression: `split("; ")`,
expected: []string{
"D0, P[], (!!seq)::- word\n",
},
},
{
skipDoc: true,
document: `""`,
expression: `split("; ")`,
expected: []string{
"D0, P[], (!!seq)::[]\n", // dont actually want this, just not to error
},
},
{
skipDoc: true,
expression: `split("; ")`,
expected: []string{},
},
}
func TestStringsOperatorScenarios(t *testing.T) {
for _, tt := range stringsOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "String Operators", stringsOperatorScenarios)
}

View File

@@ -26,43 +26,43 @@ func parseStyle(customStyle string) (yaml.Style, error) {
return 0, nil return 0, nil
} }
func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignStyleOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignStyleOperator: %v") log.Debugf("AssignStyleOperator: %v")
var style yaml.Style var style yaml.Style
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
style, err = parseStyle(rhs.Front().Value.(*CandidateNode).Node.Value) style, err = parseStyle(rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
} }
} }
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting style of : %v", candidate.GetKey()) log.Debugf("Setting style of : %v", candidate.GetKey())
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
style, err = parseStyle(rhs.Front().Value.(*CandidateNode).Node.Value) style, err = parseStyle(rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
} }
} }
@@ -70,15 +70,15 @@ func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNod
candidate.Node.Style = style candidate.Node.Style = style
} }
return matchingNodes, nil return context, nil
} }
func GetStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getStyleOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetStyleOperator") log.Debugf("GetStyleOperator")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
var style string var style string
switch candidate.Node.Style { switch candidate.Node.Style {
@@ -100,9 +100,9 @@ func GetStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *
style = "<unknown>" style = "<unknown>"
} }
node := &yaml.Node{Kind: yaml.ScalarNode, Value: style, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: style, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -6,58 +6,58 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func AssignTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func assignTagOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignTagOperator: %v") log.Debugf("AssignTagOperator: %v")
tag := "" tag := ""
if !pathNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value tag = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting tag of : %v", candidate.GetKey()) log.Debugf("Setting tag of : %v", candidate.GetKey())
if pathNode.Operation.UpdateAssign { if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
if rhs.Front() != nil { if rhs.MatchingNodes.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value tag = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
} }
} }
candidate.Node.Tag = tag unwrapDoc(candidate.Node).Tag = tag
} }
return matchingNodes, nil return context, nil
} }
func GetTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func getTagOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("GetTagOperator") log.Debugf("GetTagOperator")
var results = list.New() var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: UnwrapDoc(candidate.Node).Tag, Tag: "!!str"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: unwrapDoc(candidate.Node).Tag, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} result := candidate.CreateChild(nil, node)
results.PushBack(lengthCand) results.PushBack(result)
} }
return results, nil return context.ChildContext(results), nil
} }

View File

@@ -26,6 +26,14 @@ var tagOperatorScenarios = []expressionScenario{
"D0, P[], (!!str)::'!!map'\n", "D0, P[], (!!str)::'!!map'\n",
}, },
}, },
{
skipDoc: true,
document: `32`,
expression: `. tag= "!!str"`,
expected: []string{
"D0, P[], (doc)::\"32\"\n",
},
},
{ {
description: "Set custom tag", description: "Set custom tag",
document: `{a: str}`, document: `{a: str}`,

View File

@@ -9,31 +9,32 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type TraversePreferences struct { type traversePreferences struct {
FollowAlias bool DontFollowAlias bool
IncludeMapKeys bool IncludeMapKeys bool
DontAutoCreate bool // by default, we automatically create entries on the fly.
} }
func Splat(d *dataTreeNavigator, matches *list.List, prefs *TraversePreferences) (*list.List, error) { func splat(d *dataTreeNavigator, context Context, prefs traversePreferences) (Context, error) {
return traverseNodesWithArrayIndices(matches, make([]*yaml.Node, 0), prefs) return traverseNodesWithArrayIndices(context, make([]*yaml.Node, 0), prefs)
} }
func TraversePathOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func traversePathOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- Traversing") log.Debugf("-- traversePathOperator")
var matchingNodeMap = list.New() var matches = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
newNodes, err := traverse(d, el.Value.(*CandidateNode), pathNode.Operation) newNodes, err := traverse(d, context, el.Value.(*CandidateNode), expressionNode.Operation)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
matchingNodeMap.PushBackList(newNodes) matches.PushBackList(newNodes)
} }
return matchingNodeMap, nil return context.ChildContext(matches), nil
} }
func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Operation) (*list.List, error) { func traverse(d *dataTreeNavigator, context Context, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
log.Debug("Traversing %v", NodeToString(matchingNode)) log.Debug("Traversing %v", NodeToString(matchingNode))
value := matchingNode.Node value := matchingNode.Node
@@ -54,8 +55,7 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
switch value.Kind { switch value.Kind {
case yaml.MappingNode: case yaml.MappingNode:
log.Debug("its a map with %v entries", len(value.Content)/2) log.Debug("its a map with %v entries", len(value.Content)/2)
prefs := &TraversePreferences{FollowAlias: true} return traverseMap(context, matchingNode, operation.StringValue, operation.Preferences.(traversePreferences), false)
return traverseMap(matchingNode, operation.StringValue, prefs, false)
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))
@@ -64,47 +64,59 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
case yaml.AliasNode: case yaml.AliasNode:
log.Debug("its an alias!") log.Debug("its an alias!")
matchingNode.Node = matchingNode.Node.Alias matchingNode.Node = matchingNode.Node.Alias
return traverse(d, matchingNode, operation) return traverse(d, context, matchingNode, operation)
case yaml.DocumentNode: case yaml.DocumentNode:
log.Debug("digging into doc node") log.Debug("digging into doc node")
return traverse(d, &CandidateNode{
Node: matchingNode.Node.Content[0], return traverse(d, context, matchingNode.CreateChild(nil, matchingNode.Node.Content[0]), operation)
Filename: matchingNode.Filename,
FileIndex: matchingNode.FileIndex,
Document: matchingNode.Document}, operation)
default: default:
return list.New(), nil return list.New(), nil
} }
} }
func TraverseArrayOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
// rhs is a collect expression that will yield indexes to retreive of the arrays
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) //lhs may update the variable context, we should pass that into the RHS
// BUT we still return the original context back (see jq)
// https://stedolan.github.io/jq/manual/#Variable/SymbolicBindingOperator:...as$identifier|...
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
var indicesToTraverse = rhs.Front().Value.(*CandidateNode).Node.Content // rhs is a collect expression that will yield indexes to retreive of the arrays
prefs := &TraversePreferences{FollowAlias: true}
return traverseNodesWithArrayIndices(matchingNodes, indicesToTraverse, prefs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
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{})
if err != nil {
return Context{}, err
}
return context.ChildContext(result.MatchingNodes), nil
} }
func traverseNodesWithArrayIndices(matchingNodes *list.List, indicesToTraverse []*yaml.Node, prefs *TraversePreferences) (*list.List, error) { func traverseNodesWithArrayIndices(context Context, indicesToTraverse []*yaml.Node, prefs traversePreferences) (Context, error) {
var matchingNodeMap = list.New() var matchingNodeMap = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
newNodes, err := traverseArrayIndices(candidate, indicesToTraverse, prefs) newNodes, err := traverseArrayIndices(context, candidate, indicesToTraverse, prefs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
matchingNodeMap.PushBackList(newNodes) matchingNodeMap.PushBackList(newNodes)
} }
return matchingNodeMap, nil return context.ChildContext(matchingNodeMap), nil
} }
func traverseArrayIndices(matchingNode *CandidateNode, indicesToTraverse []*yaml.Node, prefs *TraversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesToTraverse []*yaml.Node, prefs traversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse
node := matchingNode.Node node := matchingNode.Node
if node.Tag == "!!null" { if node.Tag == "!!null" {
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array") log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
@@ -115,32 +127,28 @@ func traverseArrayIndices(matchingNode *CandidateNode, indicesToTraverse []*yaml
if node.Kind == yaml.AliasNode { if node.Kind == yaml.AliasNode {
matchingNode.Node = node.Alias matchingNode.Node = node.Alias
return traverseArrayIndices(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)
} else if node.Kind == yaml.MappingNode { } else if node.Kind == yaml.MappingNode {
return traverseMapWithIndices(matchingNode, indicesToTraverse, prefs) return traverseMapWithIndices(context, matchingNode, indicesToTraverse, prefs)
} else if node.Kind == yaml.DocumentNode { } else if node.Kind == yaml.DocumentNode {
return traverseArrayIndices(&CandidateNode{ return traverseArrayIndices(context, matchingNode.CreateChild(nil, matchingNode.Node.Content[0]), indicesToTraverse, prefs)
Node: matchingNode.Node.Content[0],
Filename: matchingNode.Filename,
FileIndex: matchingNode.FileIndex,
Document: matchingNode.Document}, indicesToTraverse, prefs)
} }
log.Debugf("OperatorArrayTraverse skipping %v as its a %v", matchingNode, node.Tag) log.Debugf("OperatorArrayTraverse skipping %v as its a %v", matchingNode, node.Tag)
return list.New(), nil return list.New(), nil
} }
func traverseMapWithIndices(candidate *CandidateNode, indices []*yaml.Node, prefs *TraversePreferences) (*list.List, error) { func traverseMapWithIndices(context Context, candidate *CandidateNode, indices []*yaml.Node, prefs traversePreferences) (*list.List, error) {
if len(indices) == 0 { if len(indices) == 0 {
return traverseMap(candidate, "", prefs, true) return traverseMap(context, candidate, "", prefs, true)
} }
var matchingNodeMap = list.New() var matchingNodeMap = list.New()
for _, indexNode := range indices { for _, indexNode := range indices {
log.Debug("traverseMapWithIndices: %v", indexNode.Value) log.Debug("traverseMapWithIndices: %v", indexNode.Value)
newNodes, err := traverseMap(candidate, indexNode.Value, prefs, false) newNodes, err := traverseMap(context, candidate, indexNode.Value, prefs, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -153,17 +161,13 @@ func traverseMapWithIndices(candidate *CandidateNode, indices []*yaml.Node, pref
func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*list.List, error) { func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*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)
if len(indices) == 0 { if len(indices) == 0 {
log.Debug("splatting") log.Debug("splatting")
var index int64 var index int64
for index = 0; index < int64(len(node.Content)); index = index + 1 { for index = 0; index < int64(len(node.Content)); index = index + 1 {
newMatches.PushBack(&CandidateNode{ newMatches.PushBack(candidate.CreateChild(index, node.Content[index]))
Document: candidate.Document,
Path: candidate.CreateChildPath(index),
Node: node.Content[index],
})
} }
return newMatches, nil return newMatches, nil
@@ -190,20 +194,16 @@ func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*
return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength) return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength)
} }
newMatches.PushBack(&CandidateNode{ newMatches.PushBack(candidate.CreateChild(index, node.Content[indexToUse]))
Node: node.Content[indexToUse],
Document: candidate.Document,
Path: candidate.CreateChildPath(index),
})
} }
return newMatches, nil return newMatches, nil
} }
func keyMatches(key *yaml.Node, wantedKey string) bool { func keyMatches(key *yaml.Node, wantedKey string) bool {
return Match(key.Value, wantedKey) return matchKey(key.Value, wantedKey)
} }
func traverseMap(matchingNode *CandidateNode, key string, prefs *TraversePreferences, splat bool) (*list.List, error) { func traverseMap(context Context, matchingNode *CandidateNode, key string, prefs traversePreferences, splat bool) (*list.List, error) {
var newMatches = orderedmap.NewOrderedMap() var newMatches = orderedmap.NewOrderedMap()
err := doTraverseMap(newMatches, matchingNode, key, prefs, splat) err := doTraverseMap(newMatches, matchingNode, key, prefs, splat)
@@ -211,18 +211,13 @@ func traverseMap(matchingNode *CandidateNode, key string, prefs *TraversePrefere
return nil, err return nil, err
} }
if newMatches.Len() == 0 { if !prefs.DontAutoCreate && !context.DontAutoCreate && newMatches.Len() == 0 {
//no matches, create one automagically //no matches, create one automagically
valueNode := &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"} valueNode := &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}
node := matchingNode.Node node := matchingNode.Node
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, valueNode) node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, valueNode)
candidateNode := &CandidateNode{ candidateNode := matchingNode.CreateChild(key, valueNode)
Node: valueNode,
Path: append(matchingNode.Path, key),
Document: matchingNode.Document,
}
newMatches.Set(candidateNode.GetKey(), candidateNode) newMatches.Set(candidateNode.GetKey(), candidateNode)
} }
results := list.New() results := list.New()
@@ -234,7 +229,7 @@ func traverseMap(matchingNode *CandidateNode, key string, prefs *TraversePrefere
return results, nil return results, nil
} }
func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, wantedKey string, prefs *TraversePreferences, splat bool) error { func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, wantedKey string, prefs traversePreferences, splat bool) error {
// value.Content is a concatenated array of key, value, // value.Content is a concatenated array of key, value,
// so keys are in the even indexes, values in odd. // so keys are in the even indexes, values in odd.
// merge aliases are defined first, but we only want to traverse them // merge aliases are defined first, but we only want to traverse them
@@ -249,7 +244,7 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode,
log.Debug("checking %v (%v)", key.Value, key.Tag) log.Debug("checking %v (%v)", key.Value, key.Tag)
//skip the 'merge' tag, find a direct match first //skip the 'merge' tag, find a direct match first
if key.Tag == "!!merge" && prefs.FollowAlias { if key.Tag == "!!merge" && !prefs.DontFollowAlias {
log.Debug("Merge anchor") log.Debug("Merge anchor")
err := traverseMergeAnchor(newMatches, candidate, value, wantedKey, prefs, splat) err := traverseMergeAnchor(newMatches, candidate, value, wantedKey, prefs, splat)
if err != nil { if err != nil {
@@ -258,18 +253,10 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode,
} else if splat || keyMatches(key, wantedKey) { } else if splat || keyMatches(key, wantedKey) {
log.Debug("MATCHED") log.Debug("MATCHED")
if prefs.IncludeMapKeys { if prefs.IncludeMapKeys {
candidateNode := &CandidateNode{ candidateNode := candidate.CreateChild(key.Value, key)
Node: key,
Path: candidate.CreateChildPath(key.Value),
Document: candidate.Document,
}
newMatches.Set(fmt.Sprintf("keyOf-%v", candidateNode.GetKey()), candidateNode) newMatches.Set(fmt.Sprintf("keyOf-%v", candidateNode.GetKey()), candidateNode)
} }
candidateNode := &CandidateNode{ candidateNode := candidate.CreateChild(key.Value, value)
Node: value,
Path: candidate.CreateChildPath(key.Value),
Document: candidate.Document,
}
newMatches.Set(candidateNode.GetKey(), candidateNode) newMatches.Set(candidateNode.GetKey(), candidateNode)
} }
} }
@@ -277,14 +264,10 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode,
return nil return nil
} }
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, wantedKey string, prefs *TraversePreferences, splat bool) error { func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, wantedKey string, prefs traversePreferences, splat bool) error {
switch value.Kind { switch value.Kind {
case yaml.AliasNode: case yaml.AliasNode:
candidateNode := &CandidateNode{ candidateNode := originalCandidate.CreateChild(nil, value.Alias)
Node: value.Alias,
Path: originalCandidate.Path,
Document: originalCandidate.Document,
}
return doTraverseMap(newMatches, candidateNode, wantedKey, prefs, splat) return doTraverseMap(newMatches, candidateNode, wantedKey, prefs, splat)
case yaml.SequenceNode: case yaml.SequenceNode:
for _, childValue := range value.Content { for _, childValue := range value.Content {

View File

@@ -54,6 +54,23 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[{}], (!!str)::frog\n", "D0, P[{}], (!!str)::frog\n",
}, },
}, },
{
description: "Dynamic keys",
subdescription: `Expressions within [] can be used to dynamically lookup / calculate keys`,
document: `{b: apple, apple: crispy yum, banana: soft yum}`,
expression: `.[.b]`,
expected: []string{
"D0, P[apple], (!!str)::crispy yum\n",
},
},
{
skipDoc: true,
document: `{b: apple, fruit: {apple: yum, banana: smooth}}`,
expression: `.fruit[.b]`,
expected: []string{
"D0, P[fruit apple], (!!str)::yum\n",
},
},
{ {
description: "Children don't exist", description: "Children don't exist",
subdescription: "Nodes are added dynamically while traversing", subdescription: "Nodes are added dynamically while traversing",
@@ -74,7 +91,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{}`, document: `{}`,
expression: `.a.[1]`, expression: `.a[1]`,
expected: []string{ expected: []string{
"D0, P[a 1], (!!null)::null\n", "D0, P[a 1], (!!null)::null\n",
}, },
@@ -145,7 +162,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{a: &cat {c: frog}, b: *cat}`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.[]`, expression: `.b[]`,
expected: []string{ expected: []string{
"D0, P[b c], (!!str)::frog\n", "D0, P[b c], (!!str)::frog\n",
}, },
@@ -227,7 +244,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobar.[]`, expression: `.foobar[]`,
expected: []string{ expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n", "D0, P[foobar c], (!!str)::foo_c\n",
"D0, P[foobar a], (!!str)::foo_a\n", "D0, P[foobar a], (!!str)::foo_a\n",
@@ -289,7 +306,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList.[]`, expression: `.foobarList[]`,
expected: []string{ expected: []string{
"D0, P[foobarList b], (!!str)::bar_b\n", "D0, P[foobarList b], (!!str)::bar_b\n",
"D0, P[foobarList a], (!!str)::foo_a\n", "D0, P[foobarList a], (!!str)::foo_a\n",
@@ -335,7 +352,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{a: [a,b,c]}`, document: `{a: [a,b,c]}`,
expression: `.a.[0, 2]`, expression: `.a[0, 2]`,
expected: []string{ expected: []string{
"D0, P[a 0], (!!str)::a\n", "D0, P[a 0], (!!str)::a\n",
"D0, P[a 2], (!!str)::c\n", "D0, P[a 2], (!!str)::c\n",
@@ -352,7 +369,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{a: [a,b,c]}`, document: `{a: [a,b,c]}`,
expression: `.a.[-1]`, expression: `.a[-1]`,
expected: []string{ expected: []string{
"D0, P[a -1], (!!str)::c\n", "D0, P[a -1], (!!str)::c\n",
}, },
@@ -368,7 +385,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{a: [a,b,c]}`, document: `{a: [a,b,c]}`,
expression: `.a.[-2]`, expression: `.a[-2]`,
expected: []string{ expected: []string{
"D0, P[a -2], (!!str)::b\n", "D0, P[a -2], (!!str)::b\n",
}, },
@@ -386,7 +403,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
skipDoc: true, skipDoc: true,
document: `{a: [a,b,c]}`, document: `{a: [a,b,c]}`,
expression: `.a.[]`, expression: `.a[]`,
expected: []string{ expected: []string{
"D0, P[a 0], (!!str)::a\n", "D0, P[a 0], (!!str)::a\n",
"D0, P[a 1], (!!str)::b\n", "D0, P[a 1], (!!str)::b\n",

View File

@@ -1,19 +1,17 @@
package yqlib package yqlib
import "container/list" func unionOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
func UnionOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil { if err != nil {
return nil, err return Context{}, err
} }
for el := rhs.Front(); el != nil; el = el.Next() { for el := rhs.MatchingNodes.Front(); el != nil; el = el.Next() {
node := el.Value.(*CandidateNode) node := el.Value.(*CandidateNode)
lhs.PushBack(node) lhs.MatchingNodes.PushBack(node)
} }
return lhs, nil return lhs, nil
} }

View File

@@ -1,8 +1,6 @@
package yqlib package yqlib
import "container/list" func valueOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debug("value = %v", expressionNode.Operation.CandidateNode.Node.Value)
func ValueOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { return context.SingleChildContext(expressionNode.Operation.CandidateNode), nil
log.Debug("value = %v", pathNode.Operation.CandidateNode.Node.Value)
return nodeToMap(pathNode.Operation.CandidateNode), nil
} }

View File

@@ -0,0 +1,29 @@
package yqlib
import (
"container/list"
"fmt"
)
func getVariableOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
variableName := expressionNode.Operation.StringValue
log.Debug("getVariableOperator %v", variableName)
result := context.GetVariable(variableName)
if result == nil {
result = list.New()
}
return context.ChildContext(result), nil
}
func assignVariableOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil {
return Context{}, nil
}
if expressionNode.Rhs.Operation.OperationType.Type != "GET_VARIABLE" {
return Context{}, fmt.Errorf("RHS of 'as' operator must be a variable name e.g. $foo")
}
variableName := expressionNode.Rhs.Operation.StringValue
context.SetVariable(variableName, lhs.MatchingNodes)
return context, nil
}

View File

@@ -0,0 +1,43 @@
package yqlib
import (
"testing"
)
var variableOperatorScenarios = []expressionScenario{
{
description: "Single value variable",
document: `a: cat`,
expression: `.a as $foo | $foo`,
expected: []string{
"D0, P[a], (!!str)::cat\n",
},
},
{
description: "Multi value variable",
document: `[cat, dog]`,
expression: `.[] as $foo | $foo`,
expected: []string{
"D0, P[0], (!!str)::cat\n",
"D0, P[1], (!!str)::dog\n",
},
},
{
description: "Using variables as a lookup",
document: `{"posts": [{"title": "Frist psot", "author": "anon"},
{"title": "A well-written article", "author": "person1"}],
"realnames": {"anon": "Anonymous Coward",
"person1": "Person McPherson"}}`,
expression: `.realnames as $names | .posts[] | {"title":.title, "author": $names[.author]}`,
expected: []string{
"D0, P[], (!!map)::title: \"Frist psot\"\nauthor: \"Anonymous Coward\"\n",
"D0, P[], (!!map)::title: \"A well-written article\"\nauthor: \"Person McPherson\"\n",
},
},
}
func TestVariableOperatorScenarios(t *testing.T) {
for _, tt := range variableOperatorScenarios {
testScenario(t, &tt)
}
}

View File

@@ -7,17 +7,76 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type OperatorHandler func(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) type operatorHandler func(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error)
func UnwrapDoc(node *yaml.Node) *yaml.Node { func unwrapDoc(node *yaml.Node) *yaml.Node {
if node.Kind == yaml.DocumentNode { if node.Kind == yaml.DocumentNode {
return node.Content[0] return node.Content[0]
} }
return node return node
} }
func EmptyOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func emptyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
return list.New(), nil context.MatchingNodes = list.New()
return context, nil
}
type crossFunctionCalculation func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error)
func doCrossFunc(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, calculation crossFunctionCalculation) (Context, error) {
var results = list.New()
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil {
return Context{}, err
}
log.Debugf("crossFunction LHS len: %v", lhs.MatchingNodes.Len())
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode)
for rightEl := rhs.MatchingNodes.Front(); rightEl != nil; rightEl = rightEl.Next() {
log.Debugf("Applying calc")
rhsCandidate := rightEl.Value.(*CandidateNode)
resultCandidate, err := calculation(d, context, lhsCandidate, rhsCandidate)
if err != nil {
return Context{}, err
}
results.PushBack(resultCandidate)
}
}
return context.ChildContext(results), nil
}
func crossFunction(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, calculation crossFunctionCalculation) (Context, error) {
var results = list.New()
var evaluateAllTogether = true
for matchEl := context.MatchingNodes.Front(); matchEl != nil; matchEl = matchEl.Next() {
evaluateAllTogether = evaluateAllTogether && matchEl.Value.(*CandidateNode).EvaluateTogether
if !evaluateAllTogether {
break
}
}
if evaluateAllTogether {
return doCrossFunc(d, context, expressionNode, calculation)
}
for matchEl := context.MatchingNodes.Front(); matchEl != nil; matchEl = matchEl.Next() {
innerResults, err := doCrossFunc(d, context.SingleChildContext(matchEl.Value.(*CandidateNode)), expressionNode, calculation)
if err != nil {
return Context{}, err
}
results.PushBackList(innerResults.MatchingNodes)
}
return context.ChildContext(results), nil
} }
func createBooleanCandidate(owner *CandidateNode, value bool) *CandidateNode { func createBooleanCandidate(owner *CandidateNode, value bool) *CandidateNode {
@@ -26,23 +85,19 @@ func createBooleanCandidate(owner *CandidateNode, value bool) *CandidateNode {
valString = "false" valString = "false"
} }
node := &yaml.Node{Kind: yaml.ScalarNode, Value: valString, Tag: "!!bool"} node := &yaml.Node{Kind: yaml.ScalarNode, Value: valString, Tag: "!!bool"}
return &CandidateNode{Node: node, Document: owner.Document, Path: owner.Path} return owner.CreateChild(nil, node)
} }
func nodeToMap(candidate *CandidateNode) *list.List { func createTraversalTree(path []interface{}, traversePrefs traversePreferences) *ExpressionNode {
elMap := list.New()
elMap.PushBack(candidate)
return elMap
}
func createTraversalTree(path []interface{}) *PathTreeNode {
if len(path) == 0 { if len(path) == 0 {
return &PathTreeNode{Operation: &Operation{OperationType: SelfReference}} return &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}}
} else if len(path) == 1 { } else if len(path) == 1 {
return &PathTreeNode{Operation: &Operation{OperationType: TraversePath, Value: path[0], StringValue: fmt.Sprintf("%v", path[0])}} return &ExpressionNode{Operation: &Operation{OperationType: traversePathOpType, Preferences: traversePrefs, Value: path[0], StringValue: fmt.Sprintf("%v", path[0])}}
}
return &ExpressionNode{
Operation: &Operation{OperationType: shortPipeOpType},
Lhs: createTraversalTree(path[0:1], traversePrefs),
Rhs: createTraversalTree(path[1:], traversePrefs),
} }
return &PathTreeNode{
Operation: &Operation{OperationType: ShortPipe},
Lhs: createTraversalTree(path[0:1]),
Rhs: createTraversalTree(path[1:])}
} }

View File

@@ -17,6 +17,7 @@ import (
type expressionScenario struct { type expressionScenario struct {
description string description string
subdescription string subdescription string
environmentVariable string
document string document string
document2 string document2 string
expression string expression string
@@ -26,10 +27,9 @@ type expressionScenario struct {
} }
func testScenario(t *testing.T, s *expressionScenario) { func testScenario(t *testing.T, s *expressionScenario) {
var results *list.List
var err error var err error
node, err := treeCreator.ParsePath(s.expression) node, err := NewExpressionParser().ParseExpression(s.expression)
if err != nil { if err != nil {
t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err)) t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err))
return return
@@ -54,20 +54,24 @@ func testScenario(t *testing.T, s *expressionScenario) {
candidateNode := &CandidateNode{ candidateNode := &CandidateNode{
Document: 0, Document: 0,
Filename: "", Filename: "",
Node: &yaml.Node{Tag: "!!null"}, Node: &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode},
FileIndex: 0, FileIndex: 0,
} }
inputs.PushBack(candidateNode) inputs.PushBack(candidateNode)
} }
results, err = treeNavigator.GetMatchingNodes(inputs, node) if s.environmentVariable != "" {
os.Setenv("myenv", s.environmentVariable)
}
context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
if err != nil { if err != nil {
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(results), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document)) test.AssertResultComplexWithContext(t, s.expected, resultsToString(context.MatchingNodes), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document))
} }
func resultsToString(results *list.List) []string { func resultsToString(results *list.List) []string {
@@ -105,7 +109,7 @@ func formatYaml(yaml string, filename string) string {
var output bytes.Buffer var output bytes.Buffer
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true) printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
node, err := treeCreator.ParsePath(".. style= \"\"") node, err := NewExpressionParser().ParseExpression(".. style= \"\"")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -162,6 +166,14 @@ func documentInput(w *bufio.Writer, s expressionScenario) (string, string) {
formattedDoc := "" formattedDoc := ""
formattedDoc2 := "" formattedDoc2 := ""
command := "eval" command := "eval"
envCommand := ""
if s.environmentVariable != "" {
envCommand = fmt.Sprintf("myenv=\"%v\" ", s.environmentVariable)
os.Setenv("myenv", s.environmentVariable)
}
if s.document != "" { if s.document != "" {
if s.dontFormatInputForDoc { if s.dontFormatInputForDoc {
formattedDoc = s.document + "\n" formattedDoc = s.document + "\n"
@@ -188,14 +200,15 @@ func documentInput(w *bufio.Writer, s expressionScenario) (string, string) {
} }
writeOrPanic(w, "then\n") writeOrPanic(w, "then\n")
if s.expression != "" { if s.expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq %v '%v' %v\n```\n", command, s.expression, files)) writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v '%v' %v\n```\n", envCommand, command, s.expression, files))
} else { } else {
writeOrPanic(w, fmt.Sprintf("```bash\nyq %v %v\n```\n", command, files)) writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v %v\n```\n", envCommand, command, files))
} }
} else { } else {
writeOrPanic(w, "Running\n") writeOrPanic(w, "Running\n")
writeOrPanic(w, fmt.Sprintf("```bash\nyq %v --null-input '%v'\n```\n", command, s.expression)) writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v --null-input '%v'\n```\n", envCommand, command, s.expression))
} }
return formattedDoc, formattedDoc2 return formattedDoc, formattedDoc2
} }
@@ -205,7 +218,7 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
var err error var err error
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true) printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
node, err := treeCreator.ParsePath(s.expression) node, err := NewExpressionParser().ParseExpression(s.expression)
if err != nil { if err != nil {
t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err)) t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err))
return return
@@ -231,19 +244,19 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
candidateNode := &CandidateNode{ candidateNode := &CandidateNode{
Document: 0, Document: 0,
Filename: "", Filename: "",
Node: &yaml.Node{Tag: "!!null"}, Node: &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode},
FileIndex: 0, FileIndex: 0,
} }
inputs.PushBack(candidateNode) inputs.PushBack(candidateNode)
} }
results, err := treeNavigator.GetMatchingNodes(inputs, node) context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
if err != nil { if err != nil {
t.Error(err, s.expression) t.Error(err, s.expression)
} }
err = printer.PrintResults(results) err = printer.PrintResults(context.MatchingNodes)
if err != nil { if err != nil {
t.Error(err, s.expression) t.Error(err, s.expression)
} }

View File

@@ -1,92 +0,0 @@
package yqlib
import (
"errors"
logging "gopkg.in/op/go-logging.v1"
)
type PathPostFixer interface {
ConvertToPostfix([]*Token) ([]*Operation, error)
}
type pathPostFixer struct {
}
func NewPathPostFixer() PathPostFixer {
return &pathPostFixer{}
}
func popOpToResult(opStack []*Token, result []*Operation) ([]*Token, []*Operation) {
var newOp *Token
opStack, newOp = opStack[0:len(opStack)-1], opStack[len(opStack)-1]
return opStack, append(result, newOp.Operation)
}
func (p *pathPostFixer) ConvertToPostfix(infixTokens []*Token) ([]*Operation, error) {
var result []*Operation
// surround the whole thing with quotes
var opStack = []*Token{&Token{TokenType: OpenBracket}}
var tokens = append(infixTokens, &Token{TokenType: CloseBracket})
for _, token := range tokens {
log.Debugf("postfix processing token %v, %v", token.toString(), token.Operation)
switch token.TokenType {
case OpenBracket, OpenCollect, OpenCollectObject:
opStack = append(opStack, token)
case CloseCollect, CloseCollectObject:
var opener TokenType = OpenCollect
var collectOperator *OperationType = Collect
if token.TokenType == CloseCollectObject {
opener = OpenCollectObject
collectOperator = CollectObject
}
itemsInMiddle := false
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != opener {
opStack, result = popOpToResult(opStack, result)
itemsInMiddle = true
}
if !itemsInMiddle {
// must be an empty collection, add the empty object as a LHS parameter
result = append(result, &Operation{OperationType: Empty})
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close collect brackets without matching opening bracket")
}
// now we should have [] as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
//and append a collect to the opStack
opStack = append(opStack, &Token{TokenType: OperationToken, Operation: &Operation{OperationType: ShortPipe}})
opStack = append(opStack, &Token{TokenType: OperationToken, Operation: &Operation{OperationType: collectOperator}})
case CloseBracket:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != OpenBracket {
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close brackets without matching opening bracket")
}
// now we should have ( as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
default:
var currentPrecedence = token.Operation.OperationType.Precedence
// pop off higher precedent operators onto the result
for len(opStack) > 0 &&
opStack[len(opStack)-1].TokenType == OperationToken &&
opStack[len(opStack)-1].Operation.OperationType.Precedence >= currentPrecedence {
opStack, result = popOpToResult(opStack, result)
}
// add this operator to the opStack
opStack = append(opStack, token)
}
}
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("PostFix Result:")
for _, token := range result {
log.Debugf("> %v", token.toString())
}
}
return result, nil
}

View File

@@ -1,383 +0,0 @@
package yqlib
import (
"fmt"
"strconv"
lex "github.com/timtadh/lexmachine"
"github.com/timtadh/lexmachine/machines"
)
func skip(*lex.Scanner, *machines.Match) (interface{}, error) {
return nil, nil
}
type TokenType uint32
const (
OperationToken = 1 << iota
OpenBracket
CloseBracket
OpenCollect
CloseCollect
OpenCollectObject
CloseCollectObject
TraverseArrayCollect
)
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
}
func (t *Token) toString() string {
if t.TokenType == OperationToken {
log.Debug("toString, its an op")
return t.Operation.toString()
} else if t.TokenType == OpenBracket {
return "("
} else if t.TokenType == CloseBracket {
return ")"
} else if t.TokenType == OpenCollect {
return "["
} else if t.TokenType == CloseCollect {
return "]"
} else if t.TokenType == OpenCollectObject {
return "{"
} else if t.TokenType == CloseCollectObject {
return "}"
} else if t.TokenType == TraverseArrayCollect {
return ".["
} else {
return "NFI"
}
}
func pathToken(wrapped bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
value = value[1:]
if wrapped {
value = unwrap(value)
}
log.Debug("PathToken %v", value)
op := &Operation{OperationType: TraversePath, Value: value, StringValue: value}
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
}
}
func documentToken() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
numberString = numberString[1:]
var number, errParsingInt = strconv.ParseInt(numberString, 10, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
log.Debug("documentToken %v", string(m.Bytes))
op := &Operation{OperationType: DocumentFilter, Value: number, StringValue: numberString}
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
}
}
func opToken(op *OperationType) lex.Action {
return opTokenWithPrefs(op, nil, nil)
}
func opAssignableToken(opType *OperationType, assignOpType *OperationType) lex.Action {
return opTokenWithPrefs(opType, assignOpType, nil)
}
func assignOpToken(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignOpToken %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{OperationType: Assign, Value: Assign.Type, StringValue: value, UpdateAssign: updateAssign}
return &Token{TokenType: OperationToken, Operation: op}, nil
}
}
func opTokenWithPrefs(op *OperationType, assignOpType *OperationType, preferences interface{}) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("opTokenWithPrefs %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{OperationType: op, Value: op.Type, StringValue: value, Preferences: preferences}
var assign *Operation
if assignOpType != nil {
assign = &Operation{OperationType: assignOpType, Value: assignOpType.Type, StringValue: value, Preferences: preferences}
}
return &Token{TokenType: OperationToken, Operation: op, AssignOperation: assign}, nil
}
}
func assignAllCommentsOp(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignAllCommentsOp %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{
OperationType: AssignComment,
Value: AssignComment.Type,
StringValue: value,
UpdateAssign: updateAssign,
Preferences: &CommentOpPreferences{LineComment: true, HeadComment: true, FootComment: true},
}
return &Token{TokenType: OperationToken, Operation: op}, nil
}
}
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
}
}
func unwrap(value string) string {
return value[1 : len(value)-1]
}
func numberValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
var number, errParsingInt = strconv.ParseInt(numberString, 10, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
return &Token{TokenType: OperationToken, Operation: CreateValueOperation(number, numberString)}, nil
}
}
func floatValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
var number, errParsingInt = strconv.ParseFloat(numberString, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
return &Token{TokenType: OperationToken, Operation: CreateValueOperation(number, numberString)}, nil
}
}
func booleanValue(val bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return &Token{TokenType: OperationToken, Operation: CreateValueOperation(val, string(m.Bytes))}, nil
}
}
func stringValue(wrapped bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
value := string(m.Bytes)
if wrapped {
value = unwrap(value)
}
return &Token{TokenType: OperationToken, Operation: CreateValueOperation(value, value)}, nil
}
}
func nullValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return &Token{TokenType: OperationToken, Operation: CreateValueOperation(nil, string(m.Bytes))}, nil
}
}
func selfToken() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
op := &Operation{OperationType: SelfReference}
return &Token{TokenType: OperationToken, Operation: op}, nil
}
}
func initLexer() (*lex.Lexer, error) {
lexer := lex.NewLexer()
lexer.Add([]byte(`\(`), literalToken(OpenBracket, false))
lexer.Add([]byte(`\)`), literalToken(CloseBracket, true))
lexer.Add([]byte(`\.\[`), literalToken(TraverseArrayCollect, false))
lexer.Add([]byte(`\.\.`), opTokenWithPrefs(RecursiveDescent, nil, &RecursiveDescentPreferences{RecurseArray: true,
TraversePreferences: &TraversePreferences{FollowAlias: false, IncludeMapKeys: false}}))
lexer.Add([]byte(`\.\.\.`), opTokenWithPrefs(RecursiveDescent, nil, &RecursiveDescentPreferences{RecurseArray: true,
TraversePreferences: &TraversePreferences{FollowAlias: false, IncludeMapKeys: true}}))
lexer.Add([]byte(`,`), opToken(Union))
lexer.Add([]byte(`:\s*`), opToken(CreateMap))
lexer.Add([]byte(`length`), opToken(Length))
lexer.Add([]byte(`sortKeys`), opToken(SortKeys))
lexer.Add([]byte(`select`), opToken(Select))
lexer.Add([]byte(`has`), opToken(Has))
lexer.Add([]byte(`explode`), opToken(Explode))
lexer.Add([]byte(`or`), opToken(Or))
lexer.Add([]byte(`and`), opToken(And))
lexer.Add([]byte(`not`), opToken(Not))
lexer.Add([]byte(`\/\/`), opToken(Alternative))
lexer.Add([]byte(`documentIndex`), opToken(GetDocumentIndex))
lexer.Add([]byte(`di`), opToken(GetDocumentIndex))
lexer.Add([]byte(`style`), opAssignableToken(GetStyle, AssignStyle))
lexer.Add([]byte(`tag`), opAssignableToken(GetTag, AssignTag))
lexer.Add([]byte(`anchor`), opAssignableToken(GetAnchor, AssignAnchor))
lexer.Add([]byte(`alias`), opAssignableToken(GetAlias, AssignAlias))
lexer.Add([]byte(`filename`), opToken(GetFilename))
lexer.Add([]byte(`fileIndex`), opToken(GetFileIndex))
lexer.Add([]byte(`fi`), opToken(GetFileIndex))
lexer.Add([]byte(`path`), opToken(GetPath))
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{LineComment: true}))
lexer.Add([]byte(`headComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{HeadComment: true}))
lexer.Add([]byte(`footComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{FootComment: true}))
lexer.Add([]byte(`comments\s*=`), assignAllCommentsOp(false))
lexer.Add([]byte(`comments\s*\|=`), assignAllCommentsOp(true))
lexer.Add([]byte(`collect`), opToken(Collect))
lexer.Add([]byte(`\s*==\s*`), opToken(Equals))
lexer.Add([]byte(`\s*=\s*`), assignOpToken(false))
lexer.Add([]byte(`del`), opToken(DeleteChild))
lexer.Add([]byte(`\s*\|=\s*`), assignOpToken(true))
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
lexer.Add([]byte(`d[0-9]+`), documentToken())
lexer.Add([]byte(`\."[^ "]+"`), pathToken(true))
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+`), pathToken(false))
lexer.Add([]byte(`\.`), selfToken())
lexer.Add([]byte(`\|`), opToken(Pipe))
lexer.Add([]byte(`-?\d+(\.\d+)`), floatValue())
lexer.Add([]byte(`-?[1-9](\.\d+)?[Ee][-+]?\d+`), floatValue())
lexer.Add([]byte(`-?\d+`), numberValue())
lexer.Add([]byte(`[Tt][Rr][Uu][Ee]`), booleanValue(true))
lexer.Add([]byte(`[Ff][Aa][Ll][Ss][Ee]`), booleanValue(false))
lexer.Add([]byte(`[Nn][Uu][Ll][Ll]`), nullValue())
lexer.Add([]byte(`~`), nullValue())
lexer.Add([]byte(`"[^"]*"`), stringValue(true))
lexer.Add([]byte(`\[`), literalToken(OpenCollect, false))
lexer.Add([]byte(`\]`), literalToken(CloseCollect, true))
lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false))
lexer.Add([]byte(`\}`), literalToken(CloseCollectObject, true))
lexer.Add([]byte(`\*`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: false}))
lexer.Add([]byte(`\*\+`), opTokenWithPrefs(Multiply, nil, &MultiplyPreferences{AppendArrays: true}))
lexer.Add([]byte(`\+`), opToken(Add))
lexer.Add([]byte(`\+=`), opToken(AddAssign))
err := lexer.Compile()
if err != nil {
return nil, err
}
return lexer, nil
}
type PathTokeniser interface {
Tokenise(path string) ([]*Token, error)
}
type pathTokeniser struct {
lexer *lex.Lexer
}
func NewPathTokeniser() PathTokeniser {
var lexer, err = initLexer()
if err != nil {
panic(err)
}
return &pathTokeniser{lexer}
}
func (p *pathTokeniser) Tokenise(path string) ([]*Token, error) {
scanner, err := p.lexer.Scanner([]byte(path))
if err != nil {
return nil, fmt.Errorf("Parsing expression: %v", err)
}
var tokens []*Token
for tok, err, eof := scanner.Next(); !eof; tok, err, eof = scanner.Next() {
if tok != nil {
token := tok.(*Token)
log.Debugf("Tokenising %v", token.toString())
tokens = append(tokens, token)
}
if err != nil {
return nil, fmt.Errorf("Parsing expression: %v", err)
}
}
var postProcessedTokens = make([]*Token, 0)
skipNextToken := false
for index := range tokens {
if skipNextToken {
skipNextToken = false
} else {
postProcessedTokens, skipNextToken = p.handleToken(tokens, index, postProcessedTokens)
}
}
return postProcessedTokens, nil
}
func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTokens []*Token) (tokensAccum []*Token, skipNextToken bool) {
skipNextToken = false
token := tokens[index]
if token.TokenType == TraverseArrayCollect {
//need to put a traverse array then a collect token
// do this by adding traverse then converting token to collect
op := &Operation{OperationType: TraverseArray, StringValue: "TRAVERSE_ARRAY"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
token = &Token{TokenType: OpenCollect}
}
if index != len(tokens)-1 && token.AssignOperation != nil &&
tokens[index+1].TokenType == OperationToken &&
tokens[index+1].Operation.OperationType == Assign {
token.Operation = token.AssignOperation
token.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
skipNextToken = true
}
postProcessedTokens = append(postProcessedTokens, token)
if index != len(tokens)-1 && token.CheckForPostTraverse &&
tokens[index+1].TokenType == OperationToken &&
tokens[index+1].Operation.OperationType == TraversePath {
op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
if index != len(tokens)-1 && token.CheckForPostTraverse &&
tokens[index+1].TokenType == OpenCollect {
op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
op = &Operation{OperationType: TraverseArray}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
if index != len(tokens)-1 && token.CheckForPostTraverse &&
tokens[index+1].TokenType == TraverseArrayCollect {
op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
return postProcessedTokens, skipNextToken
}

View File

@@ -24,6 +24,7 @@ type resultsPrinter struct {
previousDocIndex uint previousDocIndex uint
previousFileIndex int previousFileIndex int
printedMatches bool printedMatches bool
treeNavigator DataTreeNavigator
} }
func NewPrinter(writer io.Writer, outputToJSON bool, unwrapScalar bool, colorsEnabled bool, indent int, printDocSeparators bool) Printer { func NewPrinter(writer io.Writer, outputToJSON bool, unwrapScalar bool, colorsEnabled bool, indent int, printDocSeparators bool) Printer {
@@ -35,6 +36,7 @@ func NewPrinter(writer io.Writer, outputToJSON bool, unwrapScalar bool, colorsEn
indent: indent, indent: indent,
printDocSeparators: printDocSeparators, printDocSeparators: printDocSeparators,
firstTimePrinting: true, firstTimePrinting: true,
treeNavigator: NewDataTreeNavigator(),
} }
} }
@@ -72,14 +74,14 @@ func (p *resultsPrinter) safelyFlush(writer *bufio.Writer) {
func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error { func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error {
log.Debug("PrintResults for %v matches", matchingNodes.Len()) log.Debug("PrintResults for %v matches", matchingNodes.Len())
var err error
if p.outputToJSON { if p.outputToJSON {
explodeOp := Operation{OperationType: Explode} explodeOp := Operation{OperationType: explodeOpType}
explodeNode := PathTreeNode{Operation: &explodeOp} explodeNode := ExpressionNode{Operation: &explodeOp}
matchingNodes, err = treeNavigator.GetMatchingNodes(matchingNodes, &explodeNode) context, err := p.treeNavigator.GetMatchingNodes(Context{MatchingNodes: matchingNodes}, &explodeNode)
if err != nil { if err != nil {
return err return err
} }
matchingNodes = context.MatchingNodes
} }
bufferedWriter := bufio.NewWriter(p.writer) bufferedWriter := bufio.NewWriter(p.writer)

View File

@@ -3,6 +3,7 @@ package yqlib
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"container/list"
"strings" "strings"
"testing" "testing"
@@ -16,6 +17,12 @@ a: apple
a: coconut a: coconut
` `
func nodeToList(candidate *CandidateNode) *list.List {
elMap := list.New()
elMap.PushBack(candidate)
return elMap
}
func TestPrinterMultipleDocsInSequence(t *testing.T) { func TestPrinterMultipleDocsInSequence(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
var writer = bufio.NewWriter(&output) var writer = bufio.NewWriter(&output)
@@ -27,13 +34,13 @@ func TestPrinterMultipleDocsInSequence(t *testing.T) {
} }
el := inputs.Front() el := inputs.Front()
sample1 := nodeToMap(el.Value.(*CandidateNode)) sample1 := nodeToList(el.Value.(*CandidateNode))
el = el.Next() el = el.Next()
sample2 := nodeToMap(el.Value.(*CandidateNode)) sample2 := nodeToList(el.Value.(*CandidateNode))
el = el.Next() el = el.Next()
sample3 := nodeToMap(el.Value.(*CandidateNode)) sample3 := nodeToList(el.Value.(*CandidateNode))
err = printer.PrintResults(sample1) err = printer.PrintResults(sample1)
if err != nil { if err != nil {
@@ -68,19 +75,19 @@ func TestPrinterMultipleFilesInSequence(t *testing.T) {
elNode := el.Value.(*CandidateNode) elNode := el.Value.(*CandidateNode)
elNode.Document = 0 elNode.Document = 0
elNode.FileIndex = 0 elNode.FileIndex = 0
sample1 := nodeToMap(elNode) sample1 := nodeToList(elNode)
el = el.Next() el = el.Next()
elNode = el.Value.(*CandidateNode) elNode = el.Value.(*CandidateNode)
elNode.Document = 0 elNode.Document = 0
elNode.FileIndex = 1 elNode.FileIndex = 1
sample2 := nodeToMap(elNode) sample2 := nodeToList(elNode)
el = el.Next() el = el.Next()
elNode = el.Value.(*CandidateNode) elNode = el.Value.(*CandidateNode)
elNode.Document = 0 elNode.Document = 0
elNode.FileIndex = 2 elNode.FileIndex = 2
sample3 := nodeToMap(elNode) sample3 := nodeToList(elNode)
err = printer.PrintResults(sample1) err = printer.PrintResults(sample1)
if err != nil { if err != nil {

View File

@@ -8,46 +8,49 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
// A yaml expression evaluator that runs the expression multiple times for each given yaml document.
// Uses less memory than loading all documents and running the expression once, but this cannot process
// cross document expressions.
type StreamEvaluator interface { type StreamEvaluator interface {
Evaluate(filename string, reader io.Reader, node *PathTreeNode, printer Printer) error Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer) error
EvaluateFiles(expression string, filenames []string, printer Printer) error EvaluateFiles(expression string, filenames []string, printer Printer) error
EvaluateNew(expression string, printer Printer) error EvaluateNew(expression string, printer Printer) error
} }
type streamEvaluator struct { type streamEvaluator struct {
treeNavigator DataTreeNavigator treeNavigator DataTreeNavigator
treeCreator PathTreeCreator treeCreator ExpressionParser
fileIndex int fileIndex int
} }
func NewStreamEvaluator() StreamEvaluator { func NewStreamEvaluator() StreamEvaluator {
return &streamEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewPathTreeCreator()} return &streamEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewExpressionParser()}
} }
func (s *streamEvaluator) EvaluateNew(expression string, printer Printer) error { func (s *streamEvaluator) EvaluateNew(expression string, printer Printer) error {
node, err := treeCreator.ParsePath(expression) node, err := s.treeCreator.ParseExpression(expression)
if err != nil { if err != nil {
return err return err
} }
candidateNode := &CandidateNode{ candidateNode := &CandidateNode{
Document: 0, Document: 0,
Filename: "", Filename: "",
Node: &yaml.Node{Tag: "!!null"}, Node: &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode},
FileIndex: 0, FileIndex: 0,
} }
inputList := list.New() inputList := list.New()
inputList.PushBack(candidateNode) inputList.PushBack(candidateNode)
matches, errorParsing := treeNavigator.GetMatchingNodes(inputList, node) result, errorParsing := s.treeNavigator.GetMatchingNodes(Context{MatchingNodes: inputList}, node)
if errorParsing != nil { if errorParsing != nil {
return errorParsing return errorParsing
} }
return printer.PrintResults(matches) return printer.PrintResults(result.MatchingNodes)
} }
func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error { func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
node, err := treeCreator.ParsePath(expression) node, err := s.treeCreator.ParseExpression(expression)
if err != nil { if err != nil {
return err return err
} }
@@ -70,7 +73,7 @@ func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, p
return nil return nil
} }
func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *PathTreeNode, printer Printer) error { func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer) error {
var currentIndex uint var currentIndex uint
@@ -94,11 +97,11 @@ func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *Path
inputList := list.New() inputList := list.New()
inputList.PushBack(candidateNode) inputList.PushBack(candidateNode)
matches, errorParsing := treeNavigator.GetMatchingNodes(inputList, node) result, errorParsing := s.treeNavigator.GetMatchingNodes(Context{MatchingNodes: inputList}, node)
if errorParsing != nil { if errorParsing != nil {
return errorParsing return errorParsing
} }
err := printer.PrintResults(matches) err := printer.PrintResults(result.MatchingNodes)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -9,9 +9,6 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
var treeNavigator = NewDataTreeNavigator()
var treeCreator = NewPathTreeCreator()
func readStream(filename string) (io.Reader, error) { func readStream(filename string) (io.Reader, error) {
if filename == "-" { if filename == "-" {
return bufio.NewReader(os.Stdin), nil return bufio.NewReader(os.Stdin), nil
@@ -39,10 +36,11 @@ func readDocuments(reader io.Reader, filename string, fileIndex int) (*list.List
return nil, errorReading return nil, errorReading
} }
candidateNode := &CandidateNode{ candidateNode := &CandidateNode{
Document: currentIndex, Document: currentIndex,
Filename: filename, Filename: filename,
Node: &dataBucket, Node: &dataBucket,
FileIndex: fileIndex, FileIndex: fileIndex,
EvaluateTogether: true,
} }
inputList.PushBack(candidateNode) inputList.PushBack(candidateNode)

View File

@@ -5,22 +5,22 @@ import (
"os" "os"
) )
type WriteInPlaceHandler interface { type writeInPlaceHandler interface {
CreateTempFile() (*os.File, error) CreateTempFile() (*os.File, error)
FinishWriteInPlace(evaluatedSuccessfully bool) FinishWriteInPlace(evaluatedSuccessfully bool)
} }
type writeInPlaceHandler struct { type writeInPlaceHandlerImpl struct {
inputFilename string inputFilename string
tempFile *os.File tempFile *os.File
} }
func NewWriteInPlaceHandler(inputFile string) WriteInPlaceHandler { func NewWriteInPlaceHandler(inputFile string) writeInPlaceHandler {
return &writeInPlaceHandler{inputFile, nil} return &writeInPlaceHandlerImpl{inputFile, nil}
} }
func (w *writeInPlaceHandler) CreateTempFile() (*os.File, error) { func (w *writeInPlaceHandlerImpl) CreateTempFile() (*os.File, error) {
info, err := os.Stat(w.inputFilename) info, err := os.Stat(w.inputFilename)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -46,7 +46,7 @@ func (w *writeInPlaceHandler) CreateTempFile() (*os.File, error) {
return file, err return file, err
} }
func (w *writeInPlaceHandler) FinishWriteInPlace(evaluatedSuccessfully bool) { func (w *writeInPlaceHandlerImpl) FinishWriteInPlace(evaluatedSuccessfully bool) {
log.Debug("Going to write-inplace, evaluatedSuccessfully=%v, target=%v", evaluatedSuccessfully, w.inputFilename) log.Debug("Going to write-inplace, evaluatedSuccessfully=%v, target=%v", evaluatedSuccessfully, w.inputFilename)
safelyCloseFile(w.tempFile) safelyCloseFile(w.tempFile)
if evaluatedSuccessfully { if evaluatedSuccessfully {

Some files were not shown because too many files have changed in this diff Show More