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

Compare commits

..

101 Commits

Author SHA1 Message Date
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
5a55869745 Bump version 2021-01-09 12:23:20 +11:00
Mike Farah
15e18bb98b Error when passing files and using null-input flag 2021-01-09 12:23:06 +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
Mike Farah
7798a141cf Bumped go yaml for comment hanlding fixes 2021-01-08 20:56:54 +11:00
Mike Farah
b7583a538d Added webi 2021-01-08 12:14:36 +11:00
Mike Farah
529e88b630 Added recurse examples 2021-01-08 12:11:29 +11:00
Mike Farah
658006eb93 Added another delete example 2021-01-08 11:59:49 +11:00
Mike Farah
2743a7e058 Can assign-update tag 2021-01-06 20:44:28 +11:00
Mike Farah
f95862fba3 Can assign-update style 2021-01-06 20:37:53 +11:00
Mike Farah
3f58c4bc38 Can assign-update aliases and anchors 2021-01-06 20:30:48 +11:00
Mike Farah
a7975df7cd Can assign-update comments 2021-01-06 20:22:50 +11:00
Mike Farah
3d5a5e0360 updating readme 2021-01-06 15:33:20 +11:00
Mike Farah
155755ae2f brew v3! 2021-01-06 15:22:29 +11:00
Mike Farah
601c13531c Updated collect objcet doc 2021-01-06 15:20:54 +11:00
Mike Farah
69d00c89df Added shorthand document index selection 2021-01-05 13:28:37 +11:00
Mike Farah
2faff7b05f Unwrap node in get tag to return proper tag at root level 2021-01-05 13:23:03 +11:00
Mike Farah
165949041d Added v3 snap instructions 2021-01-05 13:05:03 +11:00
Mike Farah
dbd7ab0f13 Refactored doc generation, add fi fileIndex alias 2021-01-02 10:49:33 +11:00
Mike Farah
6d512ad718 Fixed updating yaml from other files 2021-01-02 10:27:32 +11:00
Mike Farah
4fef4a7ab1 update issue template, instruct questions to be raised in disussion 2020-12-31 13:43:44 +11:00
Mike Farah
dcb17b51a9 fixed heading 2020-12-31 09:33:15 +11:00
Mike Farah
385417556d added tar.gz instructions 2020-12-31 09:30:59 +11:00
Mike Farah
edb5f213d7 updating readme 2020-12-31 09:22:22 +11:00
Mike Farah
5cfd9b05ee updating readme 2020-12-31 09:22:18 +11:00
jonatasrenan
0e764c59ce wget version var missing a 'v' prefix. 2020-12-31 08:59:38 +11:00
Mike Farah
1b887e23b3 scripts/check works for local and docker build 2020-12-30 10:40:41 +11:00
Chris Warth
a76b72e691 find golangci_lint through PATH 2020-12-30 10:30:53 +11:00
Mike Farah
9509831cff Updated docs 2020-12-29 22:35:57 +11:00
Mike Farah
e92180e89d updated release instructions 2020-12-29 09:59:16 +11:00
Mike Farah
a6ae33c3f1 Cleaning up release process, fixed github action version 2020-12-29 09:50:21 +11:00
Mike Farah
94a563dfd8 Updated docs 2020-12-28 11:57:20 +11:00
Mike Farah
0328cfd619 Added prettyPrint flag 2020-12-28 11:40:41 +11:00
Mike Farah
88663a6ce3 Added recurse keys operator 2020-12-28 11:24:42 +11:00
Mike Farah
b10a9ccfc6 Removed TraversePrefs 2020-12-28 10:29:43 +11:00
Mike Farah
9e9e15df73 More scenarios 2020-12-27 23:00:46 +11:00
Mike Farah
6cc6fdf322 Cleaning code 2020-12-27 22:56:15 +11:00
Mike Farah
a88c2dc5d3 Traverse Array Operator 2020-12-27 22:48:20 +11:00
Mike Farah
ea231006ed Refactoring traverse 2020-12-27 09:55:08 +11:00
Mike Farah
80f187f1a4 Refactoring traverse 2020-12-27 09:51:34 +11:00
Mike Farah
98e8b3479f Fixed nested array splat path 2020-12-25 12:49:05 +11:00
Mike Farah
eb539ff326 Fixed doc links 2020-12-23 10:37:51 +11:00
Mike Farah
c09f7aa707 Cleaning up docs 2020-12-23 10:30:13 +11:00
Mike Farah
7f5c380d16 v4.1.0 2020-12-23 10:23:54 +11:00
Mike Farah
6a05e517f1 Updated release instructions, remove gate for release 2020-12-23 10:23:09 +11:00
Mike Farah
8ee6f7dc1a fixing xcompile for git action 2020-12-22 22:50:01 +11:00
Mike Farah
8bd54cd603 fixing xcompile for git action 2020-12-22 22:31:28 +11:00
Mike Farah
f2f7b6db0f only tar executable files 2020-12-22 20:50:52 +11:00
Mike Farah
e082fee5d4 Fixed rhash call 2020-12-22 20:37:35 +11:00
Mike Farah
412911561f Fixed xcompile.sh 2020-12-22 20:23:13 +11:00
Mike Farah
52ae633cb7 trialing github release actions 2020-12-22 20:05:23 +11:00
Mike Farah
9356ca59bb trialing github release actions 2020-12-22 20:03:01 +11:00
Mike Farah
6dec167f74 trialing github release actions 2020-12-22 20:01:21 +11:00
Mike Farah
00f6981314 trialing github release actions 2020-12-22 19:56:56 +11:00
Mike Farah
4c60a2a967 trialing github release actions 2020-12-22 19:52:44 +11:00
Mike Farah
f4529614c4 trialing github release actions 2020-12-22 19:19:48 +11:00
Mike Farah
f059e13f94 automated docker releases! 2020-12-22 16:16:31 +11:00
Mike Farah
4dbf158505 playing with release action 2020-12-22 16:01:00 +11:00
Mike Farah
39a93a2836 playing with release action 2020-12-22 16:00:15 +11:00
Mike Farah
7c158fce26 playing with release action 2020-12-22 15:58:56 +11:00
Mike Farah
0bf43d2a55 playing with release action 2020-12-22 15:47:59 +11:00
Mike Farah
1b0bce5da6 Added alias operator;
alias, anchor and explode ops are now all documented together
2020-12-22 12:23:13 +11:00
Mike Farah
f112bde5fe Added anchor operator 2020-12-22 11:57:41 +11:00
Mike Farah
e5aa4a87a4 fixed test name 2020-12-22 11:47:58 +11:00
Mike Farah
f305e8fa12 Fixed delete full path 2020-12-22 11:45:51 +11:00
Mike Farah
d2d0c2c111 Added missing flag 2020-12-22 10:40:20 +11:00
Mike Farah
2aab79431c moved string space test to op values test cases 2020-12-22 10:38:52 +11:00
djajcevic
540d4953f5 #607 Fix string value with spaces error 2020-12-22 10:29:21 +11:00
Mike Farah
7849232255 tar files to keep permissions of exectuable 2020-12-22 10:25:15 +11:00
Mike Farah
57cd67f055 Added compressed binaries for download managers and better file size 2020-12-21 21:40:08 +11:00
Mike Farah
6b17fd4fc1 Added trivy to docker build, bumped alpine image 2020-12-21 15:48:25 +11:00
Mike Farah
ca8cd78616 Add now uses crossFunction 2020-12-21 11:54:03 +11:00
Mike Farah
9876b0ce8f Boolean operators now use the crossFunction util func 2020-12-21 11:42:35 +11:00
Mike Farah
a23272727d Added Alternative op 2020-12-21 11:32:34 +11:00
Mike Farah
1fb37785d6 Better readme 2020-12-20 13:34:58 +11:00
120 changed files with 3393 additions and 1528 deletions

View File

@@ -10,6 +10,8 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
Note that any how to questions should be posted in the discussion board and not raised as an issue.
Version of yq: 3.X.X
Operating system: mac/linux/windows/....
Installed via: docker/binary release/homebrew/snap/...

View File

@@ -10,6 +10,8 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
Note that any how to questions should be posted in the discussion board and not raised as an issue.
Version of yq: 4.X.X
Operating system: mac/linux/windows/....
Installed via: docker/binary release/homebrew/snap/...

View File

@@ -9,9 +9,11 @@ assignees: ''
**Please describe your feature request.**
A clear and concise description of what the request is and what it would solve.
Ex. I wish I could use yq to [...]
Eg. I wish I could use yq to [...]
Please note that V3 will no longer have any enhancements.
Note:
- how to questions should be posted in the discussion board and not raised as an issue.
- V3 will no longer have any enhancements.
**Describe the solution you'd like**
If we have data1.yml like:

View File

@@ -1,11 +1,41 @@
name: Publish image to Dockerhub
name: Release YQ
on:
release:
types: [released]
push:
tags:
- 'v*'
jobs:
build:
publishGitRelease:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.15'
- name: Cross compile
run: |
sudo apt-get install rhash -y
go get github.com/mitchellh/gox
./scripts/xcompile.sh
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
- uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/*
publishDocker:
environment: dockerhub
env:
IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest
@@ -38,11 +68,13 @@ jobs:
- name: Build and push image
run: |
IMAGE_VERSION="$(git describe --tags --abbrev=0)"
IMAGE_V_VERSION="$(git describe --tags --abbrev=0)"
IMAGE_VERSION=${IMAGE_V_VERSION:1}
SHORT_SHA1=$(git rev-parse --short HEAD)
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64"
echo "Building and pushing version ${IMAGE_VERSION} of image ${IMAGE_NAME}"
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker buildx build --platform "${PLATFORMS}" -t "${IMAGE_NAME}:${IMAGE_VERSION}" -t "${IMAGE_NAME}:latest" \
docker buildx build --platform "${PLATFORMS}" -t "${IMAGE_NAME}:${IMAGE_VERSION}" -t "${IMAGE_NAME}:latest" -t "${IMAGE_NAME}:4" \
--push .

View File

@@ -12,7 +12,7 @@ RUN CGO_ENABLED=0 make local build
# Choose alpine as a base image to make this useful for CI, as many
# CI tools expect an interactive shell inside the container
FROM alpine:3.12 as production
FROM alpine:3.12.3 as production
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
RUN chmod +x /usr/bin/yq

View File

@@ -4,6 +4,7 @@ COPY scripts/devtools.sh /opt/devtools.sh
RUN set -e -x \
&& /opt/devtools.sh
ENV PATH=/go/bin:$PATH
# install mkdocs
RUN set -ex \

View File

@@ -3,25 +3,64 @@
![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq)
a lightweight and portable command-line YAML processor
a lightweight and portable command-line YAML processor. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml files as well as json. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
The aim of the project is to be the [jq](https://github.com/stedolan/jq) or sed of yaml files.
yq is written in go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as docker, all listed below.
## V4 released!
V4 is now officially released, it's quite different from V3 (sorry for the migration), however it is much more similar to ```jq```, using a similar expression syntax and therefore support much more complex functionality!
If you've been using v3 and want/need to upgrade, checkout the [upgrade guide](https://mikefarah.gitbook.io/yq/v/v4.x/upgrading-from-v3).
Support for v3 will cease August 2021, until then, critical bug and security fixes will still get applied if required.
## Install
### [Download the latest binary](https://github.com/mikefarah/yq/releases/latest)
### MacOS:
### wget
Use wget to download the pre-compiled binaries:
#### Compressed via tar.gz
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
tar xz && mv ${BINARY} /usr/bin/yq
```
#### Plain binary
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/bin/yq &&\
chmod +x /usr/bin/yq
```
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
### MacOS / Linux via Homebrew:
Using [Homebrew](https://brew.sh/)
```
brew install yq
```
### Ubuntu and other Linux distros supporting `snap` packages:
or, for the (deprecated) v3 version:
```
brew install yq@3
```
Note that for v3, as it is a versioned brew it will not add the `yq` command to your path automatically. Please follow the instructions given by brew upon installation.
### Linux via snap:
```
snap install yq
```
or, for the (deprecated) v3 version:
```
snap install yq --channel=v3/stable
```
#### Snap notes
`yq` installs with [_strict confinement_](https://docs.snapcraft.io/snap-confinement/6233) in snap, this means it doesn't have direct access to root files. To read root files you can:
@@ -40,17 +79,6 @@ sudo mv /etc/myfile.tmp /etc/myfile
rm /etc/myfile.tmp
```
### wget
Use wget to download the pre-compiled binaries:
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/bin/yq &&\
chmod +x /usr/bin/yq
```
For instance, VERSION=4.0.0 and BINARY=yq_linux_amd64
### Run with Docker
#### Oneshot use:
@@ -75,12 +103,20 @@ yq() {
### Go Get:
```
GO111MODULE=on go get github.com/mikefarah/yq/v4
GO111MODULE=on go get github.com/mikefarah/yq
```
## Community Supported Installation methods
As these are supported by the community :heart: - however, they may be out of date with the officially supported releases.
# Webi
```
webi yq
```
See [webi](https://webinstall.dev/)
Supported by @adithyasunil26 (https://github.com/webinstall/webi-installers/tree/master/yq)
### Windows:
```
@@ -121,13 +157,13 @@ Supported by @rmescandon (https://launchpad.net/~rmescandon/+archive/ubuntu/yq)
- Colorized yaml output
- [Deeply traverse yaml](https://mikefarah.gitbook.io/yq/v/v4.x/traverse)
- [Sort yaml by keys](https://mikefarah.gitbook.io/yq/v/v4.x/sort-keys)
- Manipulate yaml [comments](https://app.gitbook.com/@mikefarah/s/yq/v/v4.x/comment-operators), [styling](https://app.gitbook.com/@mikefarah/s/yq/v/v4.x/style), [tags](https://app.gitbook.com/@mikefarah/s/yq/v/v4.x/tag) and [anchors](https://app.gitbook.com/@mikefarah/s/yq/v/v4.x/explode).
- Manipulate yaml [comments](https://mikefarah.gitbook.io/yq/comment-operators), [styling](https://mikefarah.gitbook.io/yq/style), [tags](https://mikefarah.gitbook.io/yq/tag) and [anchors and aliases](https://mikefarah.gitbook.io/yq/anchor-and-alias-operators).
- [Update yaml inplace](https://mikefarah.gitbook.io/yq/v/v4.x/commands/evaluate#flags)
- [Complex expressions to select and update](https://mikefarah.gitbook.io/yq/v/v4.x/select#select-and-update-matching-values-in-map)
- Keeps yaml formatting and comments when updating (though there are issues with whitespace)
- [Convert to/from json to yaml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
- [Pipe data in by using '-'](https://mikefarah.gitbook.io/yq/v/v4.x/commands/evaluate)
- General shell completion scripts (bash/zsh/fish/powershell) (https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion)
- [General shell completion scripts (bash/zsh/fish/powershell)](https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion)
## [Usage](https://mikefarah.gitbook.io/yq/)
@@ -153,6 +189,7 @@ Flags:
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating yaml docs from scratch.
-P, --prettyPrint pretty print, shorthand for '... style = ""'
-j, --tojson output as json. Set indent to 0 to print json in one line.
-v, --verbose verbose mode
-V, --version Print version information and quit
@@ -166,15 +203,5 @@ Simple Example:
yq e '.a.b | length' f1.yml f2.yml
```
## Upgrade from V3
If you've been using v3 and want/need to upgrade, checkout the [upgrade guide](https://mikefarah.gitbook.io/yq/v/v4.x/upgrading-from-v3).
## V4 is in development!
If you're keen - check out the alpha release [here](https://github.com/mikefarah/yq/releases/), or in docker `mikefarah/yq:4-beta1` and the docs (also in beta) [here](https://mikefarah.gitbook.io/yq/v/v4.x-alpha/).
V4 is quite different from V3 (sorry for the migration), however it will be much more similar to ```jq```, use a similar expression syntax and therefore support much more complex functionality!
For now - new features will be held off from V3 in anticipation of the V4 build. Critical fixes / issues will still be released.
## Known Issues / Missing Features
- `yq` attempts to preserve comment positions and whitespace as much as possible, but it does not handle all scenarios (see https://github.com/go-yaml/yaml/tree/v3 for details)

View File

@@ -14,5 +14,6 @@ var noDocSeparators = false
var nullInput = false
var verbose = false
var version = false
var prettyPrint = false
var completedSuccessfully = false

View File

@@ -59,25 +59,29 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
defer func() { writeInPlaceHandler.FinishWriteInPlace(completedSuccessfully) }()
}
if nullInput && len(args) > 1 {
return errors.New("Cannot pass files in when using null-input flag")
}
printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
allAtOnceEvaluator := yqlib.NewAllAtOnceEvaluator()
switch len(args) {
case 0:
if pipingStdIn {
err = allAtOnceEvaluator.EvaluateFiles("", []string{"-"}, printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
} else {
cmd.Println(cmd.UsageString())
return nil
}
case 1:
if nullInput {
err = yqlib.NewStreamEvaluator().EvaluateNew(args[0], printer)
err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(args[0]), printer)
} else {
err = allAtOnceEvaluator.EvaluateFiles("", []string{args[0]}, printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
}
default:
err = allAtOnceEvaluator.EvaluateFiles(args[0], args[1:], printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer)
}
completedSuccessfully = err == nil

View File

@@ -33,6 +33,16 @@ yq e '.a.b = "cool"' -i file.yaml
}
return cmdEvalSequence
}
func processExpression(expression string) string {
if prettyPrint && expression == "" {
return `... style=""`
} else if prettyPrint {
return fmt.Sprintf("%v | ... style= \"\"", expression)
}
return expression
}
func evaluateSequence(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
// 0 args, read std in
@@ -73,19 +83,23 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
streamEvaluator := yqlib.NewStreamEvaluator()
if nullInput && len(args) > 1 {
return errors.New("Cannot pass files in when using null-input flag")
}
switch len(args) {
case 0:
if pipingStdIn {
err = streamEvaluator.EvaluateFiles("", []string{"-"}, printer)
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
} else {
cmd.Println(cmd.UsageString())
return nil
}
case 1:
if nullInput {
err = streamEvaluator.EvaluateNew(args[0], printer)
err = streamEvaluator.EvaluateNew(processExpression(args[0]), printer)
} else {
err = streamEvaluator.EvaluateFiles("", []string{args[0]}, printer)
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
}
default:
err = streamEvaluator.EvaluateFiles(args[0], args[1:], printer)

View File

@@ -47,6 +47,8 @@ func New() *cobra.Command {
rootCmd.PersistentFlags().IntVarP(&indent, "indent", "I", 2, "sets indent level for output")
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
rootCmd.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace of first yaml file given.")
rootCmd.PersistentFlags().BoolVarP(&unwrapScalar, "unwrapScalar", "", true, "unwrap scalar, print the value with no quotes, colors or comments")
rootCmd.PersistentFlags().BoolVarP(&prettyPrint, "prettyPrint", "P", false, "pretty print, shorthand for '... style = \"\"'")
rootCmd.PersistentFlags().BoolVarP(&exitStatus, "exit-status", "e", false, "set exit status if there are no matches or null or false is returned")
rootCmd.PersistentFlags().BoolVarP(&forceColor, "colors", "C", false, "force print with colors")

View File

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

View File

@@ -1,4 +1,4 @@
FROM mikefarah/yq:3
FROM mikefarah/yq:4.3.2
COPY entrypoint.sh /entrypoint.sh

12
go.mod
View File

@@ -2,17 +2,15 @@ module github.com/mikefarah/yq/v4
require (
github.com/elliotchance/orderedmap v1.3.0
github.com/fatih/color v1.9.0
github.com/goccy/go-yaml v1.8.1
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/fatih/color v1.10.0
github.com/goccy/go-yaml v1.8.4
github.com/jinzhu/copier v0.1.0
github.com/spf13/cobra v1.1.1
github.com/timtadh/data-structures v0.5.3 // indirect
github.com/timtadh/lexmachine v0.2.2
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001 // indirect
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
go 1.15

43
go.sum
View File

@@ -39,19 +39,21 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/elliotchance/orderedmap v1.3.0 h1:k6m77/d0zCXTjsk12nX40TkEBkSICq8T4s6R6bpCqU0=
github.com/elliotchance/orderedmap v1.3.0/go.mod h1:8hdSl6jmveQw8ScByd3AaNHNk51RhbTazdqtTty+NFw=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-yaml v1.8.1 h1:JuZRFlqLM5cWF6A+waL8AKVuCcqvKOuhJtUQI+L3ez0=
github.com/goccy/go-yaml v1.8.1/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y=
github.com/goccy/go-yaml v1.8.4 h1:AOEdR7aQgbgwHznGe3BLkDQVujxCPUpHOZZcQcp8Y3M=
github.com/goccy/go-yaml v1.8.4/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -99,8 +101,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
github.com/jinzhu/copier v0.1.0 h1:Vh8xALtH3rrKGB/XIRe5d0yCTHPZFauWPLvdpDAbi88=
github.com/jinzhu/copier v0.1.0/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -118,15 +120,9 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -204,6 +200,7 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -251,22 +248,17 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001 h1:/dSxr6gT0FNI1MO5WLJo8mTmItROeOKTkDn+7OwWBos=
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -289,10 +281,9 @@ 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-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-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
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-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -322,8 +313,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
@@ -335,8 +324,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1,29 +1,50 @@
package yqlib
import "container/list"
import (
"container/list"
/**
Loads all yaml documents of all files given into memory, then runs the given expression once.
**/
yaml "gopkg.in/yaml.v3"
)
// A yaml expression evaluator that runs the expression once against all files/nodes in memory.
type Evaluator interface {
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 {
treeNavigator DataTreeNavigator
treeCreator PathTreeCreator
treeCreator ExpressionParser
}
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
}
return e.treeNavigator.GetMatchingNodes(inputCandidates, node)
}
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
fileIndex := 0
node, err := treeCreator.ParsePath(expression)
if err != nil {
return err
}
var allDocuments *list.List = list.New()
for _, filename := range filenames {
reader, err := readStream(filename)
@@ -37,7 +58,7 @@ func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string
allDocuments.PushBackList(fileDocuments)
fileIndex = fileIndex + 1
}
matches, err := treeNavigator.GetMatchingNodes(allDocuments, node)
matches, err := e.EvaluateCandidateNodes(expression, allDocuments)
if err != nil {
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

@@ -21,6 +21,30 @@ func (n *CandidateNode) GetKey() string {
return fmt.Sprintf("%v - %v", n.Document, n.Path)
}
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!
newPath := make([]interface{}, len(n.Path)+1)
copy(newPath, n.Path)
newPath[len(n.Path)] = path
return newPath
}
func (n *CandidateNode) Copy() (*CandidateNode, error) {
clone := &CandidateNode{}
err := copier.Copy(clone, n)
@@ -32,10 +56,12 @@ func (n *CandidateNode) Copy() (*CandidateNode, error) {
// updates this candidate from the given candidate node
func (n *CandidateNode) UpdateFrom(other *CandidateNode) {
n.UpdateAttributesFrom(other)
n.Node.Content = other.Node.Content
n.Node.Value = other.Node.Value
n.Node.Alias = other.Node.Alias
n.Node.Anchor = other.Node.Anchor
}
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)
}
func ColorizeAndPrint(bytes []byte, writer io.Writer) error {
tokens := lexer.Tokenize(string(bytes))
func colorizeAndPrint(yamlBytes []byte, writer io.Writer) error {
tokens := lexer.Tokenize(string(yamlBytes))
var p printer.Printer
p.Bool = func() *printer.Property {
return &printer.Property{

View File

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

View File

@@ -1,9 +1,9 @@
Add behaves differently according to the type of the LHS:
- arrays: concatenate
- number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon)
- number scalars: arithmetic addition
- 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
Given a sample.yml file of:
@@ -67,23 +67,19 @@ will output
- 2
```
## Add object to array
## Add new object to array
Given a sample.yml file of:
```yaml
a:
- 1
- 2
c:
cat: meow
- dog: woof
```
then
```bash
yq eval '.a + .c' sample.yml
yq eval '.a + {"cat": "meow"}' sample.yml
```
will output
```yaml
- 1
- 2
- dog: woof
- cat: meow
```
@@ -131,3 +127,85 @@ b:
- 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
```

View File

@@ -0,0 +1,73 @@
This operator is used to provide alternative (or default) values when a particular expression is either null or false.
## LHS is defined
Given a sample.yml file of:
```yaml
a: bridge
```
then
```bash
yq eval '.a // "hello"' sample.yml
```
will output
```yaml
bridge
```
## LHS is not defined
Given a sample.yml file of:
```yaml
{}
```
then
```bash
yq eval '.a // "hello"' sample.yml
```
will output
```yaml
hello
```
## LHS is null
Given a sample.yml file of:
```yaml
a: ~
```
then
```bash
yq eval '.a // "hello"' sample.yml
```
will output
```yaml
hello
```
## LHS is false
Given a sample.yml file of:
```yaml
a: false
```
then
```bash
yq eval '.a // "hello"' sample.yml
```
will output
```yaml
hello
```
## RHS is an expression
Given a sample.yml file of:
```yaml
a: false
b: cat
```
then
```bash
yq eval '.a // .b' sample.yml
```
will output
```yaml
cat
```

View File

@@ -0,0 +1,194 @@
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference aliases and remove anchor names).
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
## Get anchor
Given a sample.yml file of:
```yaml
a: &billyBob cat
```
then
```bash
yq eval '.a | anchor' sample.yml
```
will output
```yaml
billyBob
```
## Set anchor
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval '.a anchor = "foobar"' sample.yml
```
will output
```yaml
a: &foobar cat
```
## Set anchor relatively using assign-update
Given a sample.yml file of:
```yaml
a:
b: cat
```
then
```bash
yq eval '.a anchor |= .b' sample.yml
```
will output
```yaml
a: &cat
b: cat
```
## Get alias
Given a sample.yml file of:
```yaml
b: &billyBob meow
a: *billyBob
```
then
```bash
yq eval '.a | alias' sample.yml
```
will output
```yaml
billyBob
```
## Set alias
Given a sample.yml file of:
```yaml
b: &meow purr
a: cat
```
then
```bash
yq eval '.a alias = "meow"' sample.yml
```
will output
```yaml
b: &meow purr
a: *meow
```
## Set alias relatively using assign-update
Given a sample.yml file of:
```yaml
b: &meow purr
a:
f: meow
```
then
```bash
yq eval '.a alias |= .f' sample.yml
```
will output
```yaml
b: &meow purr
a: *meow
```
## Explode alias and anchor
Given a sample.yml file of:
```yaml
f:
a: &a cat
b: *a
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
b: cat
```
## Explode with no aliases or anchors
Given a sample.yml file of:
```yaml
a: mike
```
then
```bash
yq eval 'explode(.a)' sample.yml
```
will output
```yaml
a: mike
```
## Explode with alias keys
Given a sample.yml file of:
```yaml
f:
a: &a cat
*a: b
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
cat: b
```
## Explode with merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval 'explode(.)' sample.yml
```
will output
```yaml
foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: bar_b
a: foo_a
thing: bar_thing
c: foobarList_c
foobar:
c: foo_c
a: foo_a
thing: foobar_thing
```

View File

@@ -34,6 +34,27 @@ a:
g: foof
```
## Update node from another file
Note this will also work when the second file is a scalar (string/number)
Given a sample.yml file of:
```yaml
a: apples
```
And another sample another.yml file of:
```yaml
b: bob
```
then
```bash
yq eval-all 'select(fileIndex==0).a = select(fileIndex==1) | select(fileIndex==0)' sample.yml another.yml
```
will output
```yaml
a:
b: bob
```
## Update node to be the sibling value
Given a sample.yml file of:
```yaml
@@ -112,7 +133,7 @@ a:
```
then
```bash
yq eval '(.a.[] | select(. == "apple")) = "frog"' sample.yml
yq eval '(.a[] | select(. == "apple")) = "frog"' sample.yml
```
will output
```yaml

View File

@@ -13,6 +13,22 @@ will output
a: cat # single
```
## Use update assign to perform relative updates
Given a sample.yml file of:
```yaml
a: cat
b: dog
```
then
```bash
yq eval '.. lineComment |= .' sample.yml
```
will output
```yaml
a: cat # cat
b: dog # dog
```
## Set head comment
Given a sample.yml file of:
```yaml

View File

@@ -14,6 +14,23 @@ will output
a: cat
```
## Delete nested entry in map
Given a sample.yml file of:
```yaml
a:
a1: fred
a2: frood
```
then
```bash
yq eval 'del(.a.a1)' sample.yml
```
will output
```yaml
a:
a2: frood
```
## Delete entry in array
Given a sample.yml file of:
```yaml
@@ -31,6 +48,21 @@ will output
- 3
```
## Delete nested entry in array
Given a sample.yml file of:
```yaml
- a: cat
b: dog
```
then
```bash
yq eval 'del(.[0].a)' sample.yml
```
will output
```yaml
- b: dog
```
## Delete no matches
Given a sample.yml file of:
```yaml
@@ -63,3 +95,23 @@ will output
b: dog
```
## Recursively delete matching keys
Given a sample.yml file of:
```yaml
a:
name: frog
b:
name: blog
age: 12
```
then
```bash
yq eval 'del(.. | select(has("name")).name)' sample.yml
```
will output
```yaml
a:
b:
age: 12
```

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
Given a sample.yml file of:
```yaml
@@ -17,6 +17,24 @@ will output
1
```
## Retrieve a document index, shorthand
Given a sample.yml file of:
```yaml
a: cat
---
a: frog
```
then
```bash
yq eval '.a | di' sample.yml
```
will output
```yaml
0
---
1
```
## Filter by document index
Given a sample.yml file of:
```yaml
@@ -26,7 +44,23 @@ a: frog
```
then
```bash
yq eval 'select(. | documentIndex == 1)' sample.yml
yq eval 'select(documentIndex == 1)' sample.yml
```
will output
```yaml
a: frog
```
## Filter by document index shorthand
Given a sample.yml file of:
```yaml
a: cat
---
a: frog
```
then
```bash
yq eval 'select(di == 1)' sample.yml
```
will output
```yaml
@@ -42,7 +76,7 @@ a: frog
```
then
```bash
yq eval '.a | ({"match": ., "doc": (. | documentIndex)})' sample.yml
yq eval '.a | ({"match": ., "doc": documentIndex})' sample.yml
```
will output
```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
```

View File

@@ -1,98 +0,0 @@
Explodes (or dereferences) aliases and anchors.
## Explode alias and anchor
Given a sample.yml file of:
```yaml
f:
a: &a cat
b: *a
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
b: cat
```
## Explode with no aliases or anchors
Given a sample.yml file of:
```yaml
a: mike
```
then
```bash
yq eval 'explode(.a)' sample.yml
```
will output
```yaml
a: mike
```
## Explode with alias keys
Given a sample.yml file of:
```yaml
f:
a: &a cat
*a: b
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
cat: b
```
## Explode with merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval 'explode(.)' sample.yml
```
will output
```yaml
foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: bar_b
a: foo_a
thing: bar_thing
c: foobarList_c
foobar:
c: foo_c
a: foo_a
thing: foobar_thing
```

View File

@@ -1,9 +1,11 @@
File operators are most often used with merge when needing to merge specific files together. Note that when doing this, you will need to use `eval-all` to ensure all yaml documents are loaded into memory before performing the merge (as opposed to `eval` which runs the expression once per document).
Note that the `fileIndex` operator has a short alias of `fi`.
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
yq eval-all 'select(fi == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
```
## Get filename
Given a sample.yml file of:
@@ -16,7 +18,7 @@ yq eval 'filename' sample.yml
```
will output
```yaml
sample.yaml
sample.yml
```
## Get file index
@@ -33,3 +35,17 @@ will output
0
```
## Get file index alias
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval 'fi' sample.yml
```
will output
```yaml
0
```

View File

@@ -180,7 +180,7 @@ g: thongs
f: *cat
```
## Merge does not copy anchor names
## Merge copies anchor names
Given a sample.yml file of:
```yaml
a:
@@ -197,7 +197,7 @@ yq eval '.c * .a' sample.yml
will output
```yaml
g: thongs
c: frog
c: &cat frog
```
## Merge with merge anchors

View File

@@ -0,0 +1,154 @@
This operator recursively matches (or globs) all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches. It can be used in either the
## match values form `..`
This will, like the `jq` equivalent, recursively match all _value_ nodes. Use it to find/manipulate particular values.
For instance to set the `style` of all _value_ nodes in a yaml doc, excluding map keys:
```bash
yq eval '.. style= "flow"' file.yaml
```
## match values and map keys form `...`
The also includes map keys in the results set. This is particularly useful in YAML as unlike JSON, map keys can have their own styling, tags and use anchors and aliases.
For instance to set the `style` of all nodes in a yaml doc, including the map keys:
```bash
yq eval '... style= "flow"' file.yaml
```
## Recurse map (values only)
Given a sample.yml file of:
```yaml
a: frog
```
then
```bash
yq eval '..' sample.yml
```
will output
```yaml
a: frog
frog
```
## 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:
```yaml
a:
name: frog
b:
name: blog
age: 12
```
then
```bash
yq eval '[.. | select(has("name"))]' sample.yml
```
will output
```yaml
- name: frog
b:
name: blog
age: 12
- name: blog
age: 12
```
## Recursively find nodes with values
Given a sample.yml file of:
```yaml
a:
nameA: frog
b:
nameB: frog
age: 12
```
then
```bash
yq eval '.. | select(. == "frog")' sample.yml
```
will output
```yaml
frog
frog
```
## Recurse map (values and keys)
Note that the map key appears in the results
Given a sample.yml file of:
```yaml
a: frog
```
then
```bash
yq eval '...' sample.yml
```
will output
```yaml
a: frog
a
frog
```
## Aliases are not traversed
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '[..]' sample.yml
```
will output
```yaml
- a: &cat
c: frog
b: *cat
- &cat
c: frog
- frog
- *cat
```
## Merge docs are not traversed
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar | [..]' sample.yml
```
will output
```yaml
- c: foobar_c
!!merge <<: *foo
thing: foobar_thing
- foobar_c
- *foo
- foobar_thing
```

View File

@@ -1,63 +0,0 @@
This operator recursively matches all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches, for instance to set the `style` of all nodes in a yaml doc:
```bash
yq eval '.. style= "flow"' file.yaml
```
## Aliases are not traversed
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '[..]' sample.yml
```
will output
```yaml
- a: &cat
c: frog
b: *cat
- &cat
c: frog
- frog
- *cat
```
## Merge docs are not traversed
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar | [..]' sample.yml
```
will output
```yaml
- c: foobar_c
!!merge <<: *foo
thing: foobar_thing
- foobar_c
- *foo
- foobar_thing
```

View File

@@ -40,6 +40,26 @@ c: "3.2"
e: "true"
```
## Set double quote style on map keys too
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '... style="double"' sample.yml
```
will output
```yaml
"a": "cat"
"b": "5"
"c": "3.2"
"e": "true"
```
## Set single quote style
Given a sample.yml file of:
```yaml
@@ -125,19 +145,19 @@ will output
{a: cat, b: 5, c: 3.2, e: true}
```
## Pretty print
Set empty (default) quote style
## Reset style - or pretty print
Set empty (default) quote style, note the usage of `...` to match keys too. Note that there is a `--prettyPrint/-P` short flag for this.
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
"b": 5
'c': 3.2
"e": true
```
then
```bash
yq eval '.. style=""' sample.yml
yq eval '... style=""' sample.yml
```
will output
```yaml
@@ -147,6 +167,22 @@ c: 3.2
e: true
```
## Set style relatively with assign-update
Given a sample.yml file of:
```yaml
a: single
b: double
```
then
```bash
yq eval '.[] style |= .' sample.yml
```
will output
```yaml
a: 'single'
b: "double"
```
## Read style
Given a sample.yml file of:
```yaml

View File

@@ -22,7 +22,21 @@ will output
!!seq
```
## Convert numbers to strings
## Set custom tag
Given a sample.yml file of:
```yaml
a: str
```
then
```bash
yq eval '.a tag = "!!mikefarah"' sample.yml
```
will output
```yaml
a: !!mikefarah str
```
## Find numbers and convert them to strings
Given a sample.yml file of:
```yaml
a: cat

View File

@@ -48,6 +48,24 @@ will output
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
Nodes are added dynamically while traversing
@@ -106,7 +124,7 @@ b: *cat
```
then
```bash
yq eval '.b.[]' sample.yml
yq eval '.b[]' sample.yml
```
will output
```yaml
@@ -290,7 +308,7 @@ foobar:
```
then
```bash
yq eval '.foobar.[]' sample.yml
yq eval '.foobar[]' sample.yml
```
will output
```yaml
@@ -356,7 +374,7 @@ foobar:
```
then
```bash
yq eval '.foobarList.[]' sample.yml
yq eval '.foobarList[]' sample.yml
```
will output
```yaml
@@ -366,3 +384,21 @@ bar_thing
foobarList_c
```
## Select multiple indices
Given a sample.yml file of:
```yaml
a:
- a
- b
- c
```
then
```bash
yq eval '.a[0, 2]' sample.yml
```
will output
```yaml
a
c
```

View File

@@ -1,6 +1,6 @@
Add behaves differently according to the type of the LHS:
- arrays: concatenate
- number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon)
- number scalars: arithmetic addition
- 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

@@ -0,0 +1 @@
This operator is used to provide alternative (or default) values when a particular expression is either null or false.

View File

@@ -0,0 +1,4 @@
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference aliases and remove anchor names).
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.

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

@@ -1 +0,0 @@
Explodes (or dereferences) aliases and anchors.

View File

@@ -1,7 +1,9 @@
File operators are most often used with merge when needing to merge specific files together. Note that when doing this, you will need to use `eval-all` to ensure all yaml documents are loaded into memory before performing the merge (as opposed to `eval` which runs the expression once per document).
Note that the `fileIndex` operator has a short alias of `fi`.
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
yq eval-all 'select(fi == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
```

View File

@@ -0,0 +1,19 @@
This operator recursively matches (or globs) all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches. It can be used in either the
## match values form `..`
This will, like the `jq` equivalent, recursively match all _value_ nodes. Use it to find/manipulate particular values.
For instance to set the `style` of all _value_ nodes in a yaml doc, excluding map keys:
```bash
yq eval '.. style= "flow"' file.yaml
```
## match values and map keys form `...`
The also includes map keys in the results set. This is particularly useful in YAML as unlike JSON, map keys can have their own styling, tags and use anchors and aliases.
For instance to set the `style` of all nodes in a yaml doc, including the map keys:
```bash
yq eval '... style= "flow"' file.yaml
```

View File

@@ -1,5 +0,0 @@
This operator recursively matches all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches, for instance to set the `style` of all nodes in a yaml doc:
```bash
yq eval '.. style= "flow"' file.yaml
```

View File

@@ -50,7 +50,7 @@ func (ye *yamlEncoder) Encode(node *yaml.Node) error {
}
if ye.colorise {
return ColorizeAndPrint(tempBuffer.Bytes(), ye.destination)
return colorizeAndPrint(tempBuffer.Bytes(), ye.destination)
}
return nil
}

View File

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

View File

@@ -7,36 +7,36 @@ import (
)
func TestPathTreeNoArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("=")
_, err := NewExpressionParser().ParseExpression("=")
test.AssertResultComplex(t, "'=' expects 2 args but there is 0", err.Error())
}
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())
}
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())
}
func TestPathTreeTwoArgsForTwoArgOp(t *testing.T) {
_, err := treeCreator.ParsePath(".a = .b")
_, err := NewExpressionParser().ParseExpression(".a = .b")
test.AssertResultComplex(t, nil, err)
}
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())
}
func TestPathTreeOneArgForOneArgOp(t *testing.T) {
_, err := treeCreator.ParsePath("explode(.)")
_, err := NewExpressionParser().ParseExpression("explode(.)")
test.AssertResultComplex(t, nil, err)
}
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())
}

View File

@@ -6,40 +6,40 @@ import (
logging "gopkg.in/op/go-logging.v1"
)
type PathPostFixer interface {
ConvertToPostfix([]*Token) ([]*Operation, error)
type expressionPostFixer interface {
ConvertToPostfix([]*token) ([]*Operation, error)
}
type pathPostFixer struct {
type expressionPostFixerImpl struct {
}
func NewPathPostFixer() PathPostFixer {
return &pathPostFixer{}
func newExpressionPostFixer() expressionPostFixer {
return &expressionPostFixerImpl{}
}
func popOpToResult(opStack []*Token, result []*Operation) ([]*Token, []*Operation) {
var newOp *Token
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) {
func (p *expressionPostFixerImpl) 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})
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
for _, currentToken := range tokens {
log.Debugf("postfix processing currentToken %v, %v", currentToken.toString(), currentToken.Operation)
switch currentToken.TokenType {
case openBracket, openCollect, openCollectObject:
opStack = append(opStack, currentToken)
case closeCollect, closeCollectObject:
var opener tokenType = openCollect
var collectOperator *operationType = collectOpType
if currentToken.TokenType == closeCollectObject {
opener = openCollectObject
collectOperator = collectObjectOpType
}
itemsInMiddle := false
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != opener {
@@ -48,7 +48,7 @@ func (p *pathPostFixer) ConvertToPostfix(infixTokens []*Token) ([]*Operation, er
}
if !itemsInMiddle {
// must be an empty collection, add the empty object as a LHS parameter
result = append(result, &Operation{OperationType: Empty})
result = append(result, &Operation{OperationType: emptyOpType})
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close collect brackets without matching opening bracket")
@@ -56,10 +56,10 @@ func (p *pathPostFixer) ConvertToPostfix(infixTokens []*Token) ([]*Operation, er
// 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 = append(opStack, &token{TokenType: operationToken, Operation: &Operation{OperationType: shortPipeOpType}})
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 {
@@ -69,22 +69,22 @@ func (p *pathPostFixer) ConvertToPostfix(infixTokens []*Token) ([]*Operation, er
opStack = opStack[0 : len(opStack)-1]
default:
var currentPrecedence = token.Operation.OperationType.Precedence
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].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)
opStack = append(opStack, currentToken)
}
}
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("PostFix Result:")
for _, token := range result {
log.Debugf("> %v", token.toString())
for _, currentToken := range result {
log.Debugf("> %v", currentToken.toString())
}
}

View File

@@ -17,20 +17,35 @@ var pathTests = []struct {
append(make([]interface{}, 0), "[", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE"),
},
{
`.[]`,
append(make([]interface{}, 0), "TRAVERSE_ARRAY", "[", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
},
{
`.a[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE"),
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a.[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE"),
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a[0]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a.[0]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a[].c`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]", "SHORT_PIPE", "c"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE", "c", "SHORT_PIPE"),
append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "c"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "c", "SHORT_PIPE"),
},
{
`[3]`,
@@ -44,18 +59,18 @@ var pathTests = []struct {
},
{
`.a | .[].b == "apple"`,
append(make([]interface{}, 0), "a", "PIPE", "[]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "[]", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"),
append(make([]interface{}, 0), "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"),
},
{
`(.a | .[].b) == "apple"`,
append(make([]interface{}, 0), "(", "a", "PIPE", "[]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "[]", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"),
append(make([]interface{}, 0), "(", "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"),
},
{
`.[] | select(. == "*at")`,
append(make([]interface{}, 0), "[]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"),
append(make([]interface{}, 0), "[]", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"),
append(make([]interface{}, 0), "TRAVERSE_ARRAY", "[", "]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"),
},
{
`[true]`,
@@ -89,8 +104,8 @@ var pathTests = []struct {
},
{
`{.a: .c, .b.[]: .f.g.[]}`,
append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "SHORT_PIPE", "[]", "CREATE_MAP", "f", "SHORT_PIPE", "g", "SHORT_PIPE", "[]", "}"),
append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "[]", "SHORT_PIPE", "f", "g", "SHORT_PIPE", "[]", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"),
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", "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"),
},
{
`explode(.a.b)`,
@@ -122,7 +137,11 @@ var pathTests = []struct {
append(make([]interface{}, 0), "SELF", "ASSIGN_COMMENT", "str (string)"),
append(make([]interface{}, 0), "SELF", "str (string)", "ASSIGN_COMMENT"),
},
{
`. lineComment |= "str"`,
append(make([]interface{}, 0), "SELF", "ASSIGN_COMMENT", "str (string)"),
append(make([]interface{}, 0), "SELF", "str (string)", "ASSIGN_COMMENT"),
},
{
`.a.b tag="!!str"`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "b", "ASSIGN_TAG", "!!str (string)"),
@@ -145,8 +164,8 @@ var pathTests = []struct {
},
}
var tokeniser = NewPathTokeniser()
var postFixer = NewPathPostFixer()
var tokeniser = newExpressionTokeniser()
var postFixer = newExpressionPostFixer()
func TestPathParsing(t *testing.T) {
for _, tt := range pathTests {

View File

@@ -0,0 +1,407 @@
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: traversePathOpType, 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: documentFilterOpType, 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: assignOpType, Value: assignOpType.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: 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 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{FollowAlias: false, IncludeMapKeys: false}}))
lexer.Add([]byte(`\.\.\.`), opTokenWithPrefs(recursiveDescentOpType, nil, &recursiveDescentPreferences{RecurseArray: true,
TraversePreferences: &traversePreferences{FollowAlias: false, 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(`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(`d[0-9]+`), documentToken())
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(`\*`), opTokenWithPrefs(multiplyOpType, nil, &multiplyPreferences{AppendArrays: false}))
lexer.Add([]byte(`\*\+`), opTokenWithPrefs(multiplyOpType, nil, &multiplyPreferences{AppendArrays: true}))
lexer.Add([]byte(`\+`), opToken(addOpType))
lexer.Add([]byte(`\+=`), opToken(addAssignOpType))
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())
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
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.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: shortPipeOpType, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
op = &Operation{OperationType: traverseArrayOpType}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == traverseArrayCollect {
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
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
import (
@@ -11,77 +13,85 @@ import (
var log = logging.MustGetLogger("yq-lib")
type OperationType struct {
type operationType struct {
Type string
NumArgs uint // number of arguments to the op
Precedence uint
Handler OperatorHandler
Handler operatorHandler
}
// operators TODO:
// - cookbook doc for common things
// - 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 And = &OperationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: AndOperator}
var orOpType = &operationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: orOperator}
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 AddAssign = &OperationType{Type: "ADD_ASSIGN", NumArgs: 2, Precedence: 40, Handler: AddAssignOperator}
var assignOpType = &operationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: assignUpdateOperator}
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 AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator}
var AssignTag = &OperationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: AssignTagOperator}
var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator}
var assignAttributesOpType = &operationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: assignAttributesOperator}
var assignStyleOpType = &operationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: assignStyleOperator}
var assignTagOpType = &operationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: assignTagOperator}
var assignCommentOpType = &operationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: assignCommentsOperator}
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 Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator}
var multiplyOpType = &operationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: multiplyOperator}
var addOpType = &operationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: addOperator}
var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 45, Handler: alternativeOperator}
var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator}
var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator}
var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: equalsOperator}
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 Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: CollectOperator}
var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator}
var GetTag = &OperationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: GetTagOperator}
var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator}
var GetDocumentIndex = &OperationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: GetDocumentIndexOperator}
var GetFilename = &OperationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: GetFilenameOperator}
var GetFileIndex = &OperationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: GetFileIndexOperator}
var GetPath = &OperationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: GetPathOperator}
var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator}
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator}
var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
var getTagOpType = &operationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: getTagOperator}
var getCommentOpType = &operationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: getCommentsOperator}
var getAnchorOpType = &operationType{Type: "GET_ANCHOR", NumArgs: 0, Precedence: 50, Handler: getAnchorOperator}
var getAliasOptype = &operationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 50, Handler: getAliasOperator}
var getDocumentIndexOpType = &operationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: getDocumentIndexOperator}
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 SortKeys = &OperationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: SortKeysOperator}
var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: explodeOperator}
var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator}
var CollectObject = &OperationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: CollectObjectOperator}
var TraversePath = &OperationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator}
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}
var traversePathOpType = &operationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: traversePathOperator}
var traverseArrayOpType = &operationType{Type: "TRAVERSE_ARRAY", NumArgs: 1, Precedence: 50, Handler: traverseArrayOperator}
var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator}
var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator}
var ValueOp = &OperationType{Type: "VALUE", NumArgs: 0, Precedence: 50, Handler: ValueOperator}
var Not = &OperationType{Type: "NOT", NumArgs: 0, Precedence: 50, Handler: NotOperator}
var Empty = &OperationType{Type: "EMPTY", NumArgs: 50, Handler: EmptyOperator}
var documentFilterOpType = &operationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: traversePathOperator}
var selfReferenceOpType = &operationType{Type: "SELF", NumArgs: 0, Precedence: 50, 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 RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: RecursiveDescentOperator}
var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: recursiveDescentOperator}
var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator}
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 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 {
OperationType *OperationType
OperationType *operationType
Value interface{}
StringValue string
CandidateNode *CandidateNode // used for Value Path elements
Preferences interface{}
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}
node.Value = stringValue
@@ -99,7 +109,7 @@ func CreateValueOperation(value interface{}, stringValue string) *Operation {
}
return &Operation{
OperationType: ValueOp,
OperationType: valueOpType,
Value: value,
StringValue: stringValue,
CandidateNode: &CandidateNode{Node: &node},
@@ -108,13 +118,13 @@ func CreateValueOperation(value interface{}, stringValue string) *Operation {
// debugging purposes only
func (p *Operation) toString() string {
if p.OperationType == TraversePath {
if p.OperationType == traversePathOpType {
return fmt.Sprintf("%v", p.Value)
} else if p.OperationType == DocumentFilter {
} else if p.OperationType == documentFilterOpType {
return fmt.Sprintf("d%v", p.Value)
} else if p.OperationType == SelfReference {
} else if p.OperationType == selfReferenceOpType {
return "SELF"
} else if p.OperationType == ValueOp {
} else if p.OperationType == valueOpType {
return fmt.Sprintf("%v (%T)", p.Value, p.Value)
} else {
return fmt.Sprintf("%v", p.OperationType.Type)

View File

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

View File

@@ -4,31 +4,26 @@ import (
"fmt"
"container/list"
"strconv"
yaml "gopkg.in/yaml.v3"
)
func createSelfAddOp(rhs *PathTreeNode) *PathTreeNode {
return &PathTreeNode{Operation: &Operation{OperationType: Add},
Lhs: &PathTreeNode{Operation: &Operation{OperationType: SelfReference}},
func createAddOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
return &ExpressionNode{Operation: &Operation{OperationType: addOpType},
Lhs: lhs,
Rhs: rhs}
}
func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
assignmentOp := &Operation{OperationType: Assign}
assignmentOp.Preferences = &AssignOpPreferences{true}
func addAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
assignmentOp := &Operation{OperationType: assignOpType}
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)
}
func toNodes(candidates *list.List) []*yaml.Node {
if candidates.Len() == 0 {
return []*yaml.Node{}
}
candidate := candidates.Front().Value.(*CandidateNode)
func toNodes(candidate *CandidateNode) []*yaml.Node {
if candidate.Node.Tag == "!!null" {
return []*yaml.Node{}
}
@@ -42,42 +37,71 @@ func toNodes(candidates *list.List) []*yaml.Node {
}
func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func addOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("Add operator")
var results = list.New()
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil {
return nil, err
}
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode)
lhsNode := UnwrapDoc(lhsCandidate.Node)
target := &CandidateNode{
Path: lhsCandidate.Path,
Document: lhsCandidate.Document,
Filename: lhsCandidate.Filename,
Node: &yaml.Node{},
}
switch lhsNode.Kind {
case yaml.MappingNode:
return nil, fmt.Errorf("Maps not yet supported for addition")
case yaml.SequenceNode:
target.Node.Kind = yaml.SequenceNode
target.Node.Style = lhsNode.Style
target.Node.Tag = "!!seq"
target.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
results.PushBack(target)
case yaml.ScalarNode:
return nil, fmt.Errorf("Scalars not yet supported for addition")
}
}
return results, nil
return crossFunction(d, matchingNodes, expressionNode, add)
}
func add(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
target := lhs.CreateChild(nil, &yaml.Node{})
lhsNode := lhs.Node
switch lhsNode.Kind {
case yaml.MappingNode:
return nil, fmt.Errorf("Maps not yet supported for addition")
case yaml.SequenceNode:
target.Node.Kind = yaml.SequenceNode
target.Node.Style = lhsNode.Style
target.Node.Tag = "!!seq"
target.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
case yaml.ScalarNode:
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
}

View File

@@ -21,6 +21,14 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[a], (!!seq)::[1, 2, 3, 4]\n",
},
},
{
skipDoc: true,
expression: `[1] + ([2], [3])`,
expected: []string{
"D0, P[], (!!seq)::- 1\n- 2\n",
"D0, P[], (!!seq)::- 1\n- 3\n",
},
},
{
description: "Concatenate null to array",
document: `{a: [1,2]}`,
@@ -30,11 +38,11 @@ var addOperatorScenarios = []expressionScenario{
},
},
{
description: "Add object to array",
document: `{a: [1,2], c: {cat: meow}}`,
expression: `.a + .c`,
description: "Add new object to array",
document: `a: [{dog: woof}]`,
expression: `.a + {"cat": "meow"}`,
expected: []string{
"D0, P[a], (!!seq)::[1, 2, {cat: meow}]\n",
"D0, P[a], (!!seq)::[{dog: woof}, {cat: meow}]\n",
},
},
{
@@ -53,6 +61,48 @@ var addOperatorScenarios = []expressionScenario{
"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",
},
},
}
func TestAddOperatorScenarios(t *testing.T) {

View File

@@ -0,0 +1,28 @@
package yqlib
import (
"container/list"
)
// corssFunction no matches
// can boolean use crossfunction
func alternativeOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- alternative")
return crossFunction(d, matchingNodes, expressionNode, alternativeFunc)
}
func alternativeFunc(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Alternative LHS: %v", lhs.Node.Tag)
log.Debugf("- RHS: %v", rhs.Node.Tag)
isTrue, err := isTruthy(lhs)
if err != nil {
return nil, err
} else if isTrue {
return lhs, nil
}
return rhs, nil
}

View File

@@ -0,0 +1,62 @@
package yqlib
import (
"testing"
)
var alternativeOperatorScenarios = []expressionScenario{
{
description: "LHS is defined",
expression: `.a // "hello"`,
document: `{a: bridge}`,
expected: []string{
"D0, P[a], (!!str)::bridge\n",
},
},
{
description: "LHS is not defined",
expression: `.a // "hello"`,
document: `{}`,
expected: []string{
"D0, P[], (!!str)::hello\n",
},
},
{
description: "LHS is null",
expression: `.a // "hello"`,
document: `{a: ~}`,
expected: []string{
"D0, P[], (!!str)::hello\n",
},
},
{
description: "LHS is false",
expression: `.a // "hello"`,
document: `{a: false}`,
expected: []string{
"D0, P[], (!!str)::hello\n",
},
},
{
description: "RHS is an expression",
expression: `.a // .b`,
document: `{a: false, b: cat}`,
expected: []string{
"D0, P[b], (!!str)::cat\n",
},
},
{
skipDoc: true,
expression: `false // true`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
}
func TestAlternativeOperatorScenarios(t *testing.T) {
for _, tt := range alternativeOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Alternative (Default value)", alternativeOperatorScenarios)
}

View File

@@ -3,16 +3,126 @@ package yqlib
import (
"container/list"
"gopkg.in/yaml.v3"
yaml "gopkg.in/yaml.v3"
)
func ExplodeOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func assignAliasOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("AssignAlias operator!")
aliasName := ""
if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
aliasName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting aliasName : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
aliasName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
candidate.Node.Kind = yaml.AliasNode
candidate.Node.Value = aliasName
}
return matchingNodes, nil
}
func getAliasOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("GetAlias operator!")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Node.Value, Tag: "!!str"}
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil
}
func assignAnchorOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("AssignAnchor operator!")
anchorName := ""
if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
anchorName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting anchorName of : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
anchorName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
candidate.Node.Anchor = anchorName
}
return matchingNodes, nil
}
func getAnchorOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("GetAnchor operator!")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
anchor := candidate.Node.Anchor
node := &yaml.Node{Kind: yaml.ScalarNode, Value: anchor, Tag: "!!str"}
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil
}
func explodeOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- ExplodeOperation")
for el := matchMap.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err

View File

@@ -4,7 +4,55 @@ import (
"testing"
)
var explodeTest = []expressionScenario{
var anchorOperatorScenarios = []expressionScenario{
{
description: "Get anchor",
document: `a: &billyBob cat`,
expression: `.a | anchor`,
expected: []string{
"D0, P[a], (!!str)::billyBob\n",
},
},
{
description: "Set anchor",
document: `a: cat`,
expression: `.a anchor = "foobar"`,
expected: []string{
"D0, P[], (doc)::a: &foobar cat\n",
},
},
{
description: "Set anchor relatively using assign-update",
document: `a: {b: cat}`,
expression: `.a anchor |= .b`,
expected: []string{
"D0, P[], (doc)::a: &cat {b: cat}\n",
},
},
{
description: "Get alias",
document: `{b: &billyBob meow, a: *billyBob}`,
expression: `.a | alias`,
expected: []string{
"D0, P[a], (!!str)::billyBob\n",
},
},
{
description: "Set alias",
document: `{b: &meow purr, a: cat}`,
expression: `.a alias = "meow"`,
expected: []string{
"D0, P[], (doc)::{b: &meow purr, a: *meow}\n",
},
},
{
description: "Set alias relatively using assign-update",
document: `{b: &meow purr, a: {f: meow}}`,
expression: `.a alias |= .f`,
expected: []string{
"D0, P[], (doc)::{b: &meow purr, a: *meow}\n",
},
},
{
description: "Explode alias and anchor",
document: `{f : {a: &a cat, b: *a}}`,
@@ -82,9 +130,9 @@ foobar:
},
}
func TestExplodeOperatorScenarios(t *testing.T) {
for _, tt := range explodeTest {
func TestAnchorAliaseOperatorScenarios(t *testing.T) {
for _, tt := range anchorOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Explode", explodeTest)
documentScenarios(t, "Anchor and Alias Operators", anchorOperatorScenarios)
}

View File

@@ -2,27 +2,21 @@ package yqlib
import "container/list"
type AssignOpPreferences struct {
UpdateAssign bool
}
func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
func assignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
preferences := pathNode.Operation.Preferences.(*AssignOpPreferences)
var rhs *list.List
if !preferences.UpdateAssign {
rhs, err = d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if !expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
}
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
if preferences.UpdateAssign {
rhs, err = d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
if expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
}
if err != nil {
@@ -33,27 +27,25 @@ func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNo
first := rhs.Front()
if first != nil {
candidate.UpdateFrom(first.Value.(*CandidateNode))
rhsCandidate := first.Value.(*CandidateNode)
rhsCandidate.Node = unwrapDoc(rhsCandidate.Node)
candidate.UpdateFrom(rhsCandidate)
}
}
// // if there was nothing given, perhaps we are creating a new yaml doc
// if matchingNodes.Len() == 0 {
// log.Debug("started with nothing, returning LHS, %v", lhs.Len())
// return lhs, nil
// }
return matchingNodes, nil
}
// does not update content or values
func AssignAttributesOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
func assignAttributesOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err

View File

@@ -20,6 +20,16 @@ var assignOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: {g: foof}}\n",
},
},
{
description: "Update node from another file",
subdescription: "Note this will also work when the second file is a scalar (string/number)",
document: `{a: apples}`,
document2: "{b: bob}",
expression: `select(fileIndex==0).a = select(fileIndex==1) | select(fileIndex==0)`,
expected: []string{
"D0, P[], (doc)::{a: {b: bob}}\n",
},
},
{
description: "Update node to be the sibling value",
document: `{a: {b: child}, b: sibling}`,
@@ -80,7 +90,15 @@ var assignOperatorScenarios = []expressionScenario{
{
description: "Update selected results",
document: `{a: {b: apple, c: cactus}}`,
expression: `(.a.[] | select(. == "apple")) = "frog"`,
expression: `(.a[] | select(. == "apple")) = "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog, c: cactus}}\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple, c: cactus}}`,
expression: `(.a.[] | select(. == "apple")) = "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog, c: cactus}}\n",
},

View File

@@ -7,7 +7,7 @@ import (
)
func isTruthy(c *CandidateNode) (bool, error) {
node := UnwrapDoc(c.Node)
node := unwrapDoc(c.Node)
value := true
if node.Tag == "!!null" {
@@ -25,77 +25,42 @@ func isTruthy(c *CandidateNode) (bool, error) {
type boolOp func(bool, bool) bool
func performBoolOp(results *list.List, lhs *list.List, rhs *list.List, op boolOp) error {
for lhsChild := lhs.Front(); lhsChild != nil; lhsChild = lhsChild.Next() {
lhsCandidate := lhsChild.Value.(*CandidateNode)
lhsTrue, errDecoding := isTruthy(lhsCandidate)
func performBoolOp(op boolOp) func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
return func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
lhsTrue, errDecoding := isTruthy(lhs)
if errDecoding != nil {
return errDecoding
return nil, errDecoding
}
for rhsChild := rhs.Front(); rhsChild != nil; rhsChild = rhsChild.Next() {
rhsCandidate := rhsChild.Value.(*CandidateNode)
rhsTrue, errDecoding := isTruthy(rhsCandidate)
if errDecoding != nil {
return errDecoding
}
boolResult := createBooleanCandidate(lhsCandidate, op(lhsTrue, rhsTrue))
results.PushBack(boolResult)
rhsTrue, errDecoding := isTruthy(rhs)
if errDecoding != nil {
return nil, errDecoding
}
return createBooleanCandidate(lhs, op(lhsTrue, rhsTrue)), nil
}
return nil
}
func booleanOp(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, op boolOp) (*list.List, error) {
var results = list.New()
if matchingNodes.Len() == 0 {
lhs, err := d.GetMatchingNodes(list.New(), pathNode.Lhs)
if err != nil {
return nil, err
}
rhs, err := d.GetMatchingNodes(list.New(), pathNode.Rhs)
if err != nil {
return nil, err
}
return results, performBoolOp(results, lhs, rhs, op)
}
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
lhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Lhs)
if err != nil {
return nil, err
}
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
if err != nil {
return nil, err
}
err = performBoolOp(results, lhs, rhs, op)
if err != nil {
return nil, err
}
}
return results, nil
}
func OrOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func orOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- orOp")
return booleanOp(d, matchingNodes, pathNode, func(b1 bool, b2 bool) bool {
return b1 || b2
})
return crossFunction(d, matchingNodes, expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool {
return b1 || b2
}))
}
func AndOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func andOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- AndOp")
return booleanOp(d, matchingNodes, pathNode, func(b1 bool, b2 bool) bool {
return b1 && b2
})
return crossFunction(d, matchingNodes, expressionNode, performBoolOp(
func(b1 bool, b2 bool) bool {
return b1 && b2
}))
}
func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func notOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- notOperation")
var results = list.New()

View File

@@ -6,7 +6,7 @@ import (
yaml "gopkg.in/yaml.v3"
)
func CollectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func collectOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- collectOperation")
if matchMap.Len() == 0 {
@@ -18,21 +18,22 @@ func CollectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTr
var results = list.New()
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
var document uint = 0
var path []interface{}
var collectC *CandidateNode
if matchMap.Front() != nil {
collectC = matchMap.Front().Value.(*CandidateNode).CreateChild(nil, node)
if len(collectC.Path) > 0 {
collectC.Path = collectC.Path[:len(collectC.Path)-1]
}
} else {
collectC = &CandidateNode{Node: node}
}
for el := matchMap.Front(); el != nil; el = el.Next() {
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)
node.Content = append(node.Content, unwrapDoc(candidate.Node))
}
collectC := &CandidateNode{Node: node, Document: document, Path: path}
results.PushBack(collectC)
return results, nil

View File

@@ -17,7 +17,7 @@ import (
...
*/
func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func collectObjectOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- collectObjectOperation")
if matchMap.Len() == 0 {
@@ -35,7 +35,7 @@ func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *
for el := matchMap.Front(); el != nil; el = el.Next() {
candidateNode := el.Value.(*CandidateNode)
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]))
}
}
@@ -52,22 +52,15 @@ func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *
}
func createChildCandidate(candidate *CandidateNode, index int) *CandidateNode {
return &CandidateNode{
Document: candidate.Document,
Path: append(candidate.Path, 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 {
return aggregate, nil
}
candidate := remainingMatches.Remove(remainingMatches.Front()).(*CandidateNode)
splatted, err := Splat(d, nodeToMap(candidate))
splatted, err := splat(d, nodeToMap(candidate),
&traversePreferences{FollowAlias: false, IncludeMapKeys: false})
for splatEl := splatted.Front(); splatEl != nil; splatEl = splatEl.Next() {
splatEl.Value.(*CandidateNode).Path = nil
@@ -94,7 +87,7 @@ func collect(d *dataTreeNavigator, aggregate *list.List, remainingMatches *list.
newCandidate.Path = nil
newCandidate, err = multiply(&MultiplyPreferences{AppendArrays: false})(d, newCandidate, splatCandidate)
newCandidate, err = multiply(&multiplyPreferences{AppendArrays: false})(d, newCandidate, splatCandidate)
if err != nil {
return nil, err
}

View File

@@ -110,5 +110,5 @@ func TestCollectObjectOperatorScenarios(t *testing.T) {
for _, tt := range collectObjectOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Collect into Object", collectObjectOperatorScenarios)
documentScenarios(t, "Create, Collect into Object", collectObjectOperatorScenarios)
}

View File

@@ -13,6 +13,15 @@ var collectOperatorScenarios = []expressionScenario{
"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,
document: ``,

View File

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

View File

@@ -13,6 +13,39 @@ var commentOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::a: cat # single\n",
},
},
{
skipDoc: true,
document: "a: cat\nb: dog",
expression: `.a lineComment=.b`,
expected: []string{
"D0, P[], (doc)::a: cat # dog\nb: dog\n",
},
},
{
skipDoc: true,
document: "a: cat\n---\na: dog",
expression: `.a lineComment |= documentIndex`,
expected: []string{
"D0, P[], (doc)::a: cat # 0\n",
"D1, P[], (doc)::a: dog # 1\n",
},
},
{
description: "Use update assign to perform relative updates",
document: "a: cat\nb: dog",
expression: `.. lineComment |= .`,
expected: []string{
"D0, P[], (!!map)::a: cat # cat\nb: dog # dog\n",
},
},
{
skipDoc: true,
document: "a: cat\nb: dog",
expression: `.. comments |= .`,
expected: []string{
"D0, P[], (!!map)::a: cat # cat\n# cat\n\n# cat\nb: dog # dog\n# dog\n\n# dog\n",
},
},
{
description: "Set head comment",
document: `a: cat`,

View File

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

View File

@@ -2,41 +2,69 @@ package yqlib
import (
"container/list"
"fmt"
yaml "gopkg.in/yaml.v3"
)
func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
// for each lhs, splat the node,
// the intersect it against the rhs expression
// recreate the contents using only the intersection result.
func deleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
for el := matchingNodes.Front(); el != nil; el = el.Next() {
nodesToDelete, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
for el := nodesToDelete.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
elMap := list.New()
elMap.PushBack(candidate)
nodesToDelete, err := d.GetMatchingNodes(elMap, pathNode.Rhs)
log.Debug("nodesToDelete:\n%v", NodesToString(nodesToDelete))
if err != nil {
return nil, err
deleteImmediateChildOp := &Operation{
OperationType: deleteImmediateChildOpType,
Value: candidate.Path[len(candidate.Path)-1],
}
realNode := UnwrapDoc(candidate.Node)
deleteImmediateChildOpNode := &ExpressionNode{
Operation: deleteImmediateChildOp,
Rhs: createTraversalTree(candidate.Path[0 : len(candidate.Path)-1]),
}
if realNode.Kind == yaml.SequenceNode {
deleteFromArray(candidate, nodesToDelete)
} else if realNode.Kind == yaml.MappingNode {
deleteFromMap(candidate, nodesToDelete)
} else {
log.Debug("Cannot delete from node that's not a map or array %v", NodeToString(candidate))
_, err := d.GetMatchingNodes(matchingNodes, deleteImmediateChildOpNode)
if err != nil {
return nil, err
}
}
return matchingNodes, nil
}
func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
func deleteImmediateChildOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
parents, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
childPath := expressionNode.Operation.Value
log.Debug("childPath to remove %v", childPath)
for el := parents.Front(); el != nil; el = el.Next() {
parent := el.Value.(*CandidateNode)
parentNode := unwrapDoc(parent.Node)
if parentNode.Kind == yaml.MappingNode {
deleteFromMap(parent, childPath)
} else if parentNode.Kind == yaml.SequenceNode {
deleteFromArray(parent, childPath)
} else {
return nil, fmt.Errorf("Cannot delete nodes from parent of tag %v", parentNode.Tag)
}
}
return matchingNodes, nil
}
func deleteFromMap(candidate *CandidateNode, childPath interface{}) {
log.Debug("deleteFromMap")
node := UnwrapDoc(candidate.Node)
node := unwrapDoc(candidate.Node)
contents := node.Content
newContents := make([]*yaml.Node, 0)
@@ -44,18 +72,9 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
key := contents[index]
value := contents[index+1]
childCandidate := &CandidateNode{
Node: value,
Document: candidate.Document,
Path: append(candidate.Path, key.Value),
}
childCandidate := candidate.CreateChild(key.Value, value)
shouldDelete := false
for el := nodesToDelete.Front(); el != nil && !shouldDelete; el = el.Next() {
if el.Value.(*CandidateNode).GetKey() == childCandidate.GetKey() {
shouldDelete = true
}
}
shouldDelete := key.Value == childPath
log.Debugf("shouldDelete %v ? %v", childCandidate.GetKey(), shouldDelete)
@@ -66,27 +85,16 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
node.Content = newContents
}
func deleteFromArray(candidate *CandidateNode, nodesToDelete *list.List) {
func deleteFromArray(candidate *CandidateNode, childPath interface{}) {
log.Debug("deleteFromArray")
node := UnwrapDoc(candidate.Node)
node := unwrapDoc(candidate.Node)
contents := node.Content
newContents := make([]*yaml.Node, 0)
for index := 0; index < len(contents); index = index + 1 {
value := contents[index]
childCandidate := &CandidateNode{
Node: value,
Document: candidate.Document,
Path: append(candidate.Path, index),
}
shouldDelete := false
for el := nodesToDelete.Front(); el != nil && !shouldDelete; el = el.Next() {
if el.Value.(*CandidateNode).GetKey() == childCandidate.GetKey() {
shouldDelete = true
}
}
shouldDelete := fmt.Sprintf("%v", index) == fmt.Sprintf("%v", childPath)
if !shouldDelete {
newContents = append(newContents, value)

View File

@@ -13,6 +13,22 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: cat}\n",
},
},
{
description: "Delete nested entry in map",
document: `{a: {a1: fred, a2: frood}}`,
expression: `del(.a.a1)`,
expected: []string{
"D0, P[], (doc)::{a: {a2: frood}}\n",
},
},
{
skipDoc: true,
document: `{a: {a1: fred, a2: frood}}`,
expression: `del(.. | select(.=="frood"))`,
expected: []string{
"D0, P[], (!!map)::{a: {a1: fred}}\n",
},
},
{
description: "Delete entry in array",
document: `[1,2,3]`,
@@ -21,6 +37,14 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::[1, 3]\n",
},
},
{
description: "Delete nested entry in array",
document: `[{a: cat, b: dog}]`,
expression: `del(.[0].a)`,
expected: []string{
"D0, P[], (doc)::[{b: dog}]\n",
},
},
{
description: "Delete no matches",
document: `{a: cat, b: dog}`,
@@ -37,6 +61,14 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{b: dog}\n",
},
},
{
description: "Recursively delete matching keys",
document: `{a: {name: frog, b: {name: blog, age: 12}}}`,
expression: `del(.. | select(has("name")).name)`,
expected: []string{
"D0, P[], (!!map)::{a: {b: {age: 12}}}\n",
},
},
}
func TestDeleteOperatorScenarios(t *testing.T) {

View File

@@ -7,13 +7,13 @@ import (
"gopkg.in/yaml.v3"
)
func GetDocumentIndexOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func getDocumentIndexOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
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)
}
return results, nil

View File

@@ -14,10 +14,27 @@ var documentIndexScenarios = []expressionScenario{
"D1, P[a], (!!int)::1\n",
},
},
{
description: "Retrieve a document index, shorthand",
document: "a: cat\n---\na: frog\n",
expression: `.a | di`,
expected: []string{
"D0, P[a], (!!int)::0\n",
"D1, P[a], (!!int)::1\n",
},
},
{
description: "Filter by document index",
document: "a: cat\n---\na: frog\n",
expression: `select(. | documentIndex == 1)`,
expression: `select(documentIndex == 1)`,
expected: []string{
"D1, P[], (doc)::a: frog\n",
},
},
{
description: "Filter by document index shorthand",
document: "a: cat\n---\na: frog\n",
expression: `select(di == 1)`,
expected: []string{
"D1, P[], (doc)::a: frog\n",
},
@@ -25,7 +42,7 @@ var documentIndexScenarios = []expressionScenario{
{
description: "Print Document Index with matches",
document: "a: cat\n---\na: frog\n",
expression: `.a | ({"match": ., "doc": (. | documentIndex)})`,
expression: `.a | ({"match": ., "doc": documentIndex})`,
expected: []string{
"D0, P[], (!!map)::match: cat\ndoc: 0\n",
"D0, P[], (!!map)::match: frog\ndoc: 1\n",

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

@@ -0,0 +1,50 @@
package yqlib
import (
"container/list"
"fmt"
"os"
"strings"
yaml "gopkg.in/yaml.v3"
)
type envOpPreferences struct {
StringValue bool
}
func envOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, 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 nil, 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 nil, 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 nodeToMap(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

@@ -4,18 +4,21 @@ import (
"container/list"
)
func EqualsOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func equalsOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- equalsOperation")
return crossFunction(d, matchingNodes, pathNode, isEquals)
return crossFunction(d, matchingNodes, expressionNode, isEquals)
}
func isEquals(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
value := false
if lhs.Node.Tag == "!!null" {
value = (rhs.Node.Tag == "!!null")
lhsNode := unwrapDoc(lhs.Node)
rhsNode := unwrapDoc(rhs.Node)
if lhsNode.Tag == "!!null" {
value = (rhsNode.Tag == "!!null")
} 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)
return createBooleanCandidate(lhs, value), nil

View File

@@ -5,6 +5,15 @@ import (
)
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",
document: `[cat,goat,dog]`,

View File

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

View File

@@ -21,6 +21,22 @@ var fileOperatorScenarios = []expressionScenario{
"D0, P[], (!!int)::0\n",
},
},
{
description: "Get file index alias",
document: `{a: cat}`,
expression: `fi`,
expected: []string{
"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) {

View File

@@ -7,12 +7,12 @@ import (
yaml "gopkg.in/yaml.v3"
)
func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func hasOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- hasOperation")
var results = list.New()
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
wanted := rhs.Front().Value.(*CandidateNode).Node
wantedKey := wanted.Value
@@ -24,8 +24,9 @@ func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
candidate := el.Value.(*CandidateNode)
// grab the first value
var contents = candidate.Node.Content
switch candidate.Node.Kind {
candidateNode := unwrapDoc(candidate.Node)
var contents = candidateNode.Content
switch candidateNode.Kind {
case yaml.MappingNode:
candidateHasKey := false
for index := 0; index < len(contents) && !candidateHasKey; index = index + 2 {

View File

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

View File

@@ -7,13 +7,13 @@ import (
yaml "gopkg.in/yaml.v3"
)
func LengthOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func lengthOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- lengthOperation")
var results = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
targetNode := UnwrapDoc(candidate.Node)
targetNode := unwrapDoc(candidate.Node)
var length int
switch targetNode.Kind {
case yaml.ScalarNode:
@@ -27,8 +27,8 @@ func LengthOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTre
}
node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", length), Tag: "!!int"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil

View File

@@ -8,16 +8,16 @@ import (
yaml "gopkg.in/yaml.v3"
)
type CrossFunctionCalculation func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error)
type crossFunctionCalculation func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error)
func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, calculation CrossFunctionCalculation) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode, calculation crossFunctionCalculation) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
log.Debugf("crossFunction LHS len: %v", lhs.Len())
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
@@ -43,19 +43,19 @@ func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, pathNode *Pat
return results, nil
}
type MultiplyPreferences struct {
type multiplyPreferences struct {
AppendArrays bool
}
func MultiplyOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func multiplyOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- MultiplyOperator")
return crossFunction(d, matchingNodes, pathNode, multiply(pathNode.Operation.Preferences.(*MultiplyPreferences)))
return crossFunction(d, matchingNodes, 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, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
return func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node)
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Multipling LHS: %v", lhs.Node.Tag)
log.Debugf("- RHS: %v", rhs.Node.Tag)
@@ -64,12 +64,7 @@ func multiply(preferences *MultiplyPreferences) func(d *dataTreeNavigator, lhs *
if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode ||
(lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) {
var newBlank = &CandidateNode{
Path: lhs.Path,
Document: lhs.Document,
Filename: lhs.Filename,
Node: &yaml.Node{},
}
var newBlank = lhs.CreateChild(nil, &yaml.Node{})
var newThing, err = mergeObjects(d, newBlank, lhs, false)
if err != nil {
return nil, err
@@ -85,7 +80,9 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
var results = list.New()
// shouldn't recurse arrays if appending
err := recursiveDecent(d, results, nodeToMap(rhs), !shouldAppendArrays)
prefs := &recursiveDescentPreferences{RecurseArray: !shouldAppendArrays,
TraversePreferences: &traversePreferences{FollowAlias: false}}
err := recursiveDecent(d, results, nodeToMap(rhs), prefs)
if err != nil {
return nil, err
}
@@ -104,35 +101,22 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
return lhs, nil
}
func createTraversalTree(path []interface{}) *PathTreeNode {
if len(path) == 0 {
return &PathTreeNode{Operation: &Operation{OperationType: SelfReference}}
} else if len(path) == 1 {
return &PathTreeNode{Operation: &Operation{OperationType: TraversePath, Value: path[0], StringValue: fmt.Sprintf("%v", path[0])}}
}
return &PathTreeNode{
Operation: &Operation{OperationType: ShortPipe},
Lhs: createTraversalTree(path[0:1]),
Rhs: createTraversalTree(path[1:])}
}
func applyAssignment(d *dataTreeNavigator, pathIndexToStartFrom int, lhs *CandidateNode, rhs *CandidateNode, shouldAppendArrays bool) error {
log.Debugf("merge - applyAssignment lhs %v, rhs: %v", NodeToString(lhs), NodeToString(rhs))
lhsPath := rhs.Path[pathIndexToStartFrom:]
assignmentOp := &Operation{OperationType: AssignAttributes}
assignmentOp := &Operation{OperationType: assignAttributesOpType}
if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode {
assignmentOp.OperationType = Assign
assignmentOp.Preferences = &AssignOpPreferences{false}
assignmentOp.OperationType = assignOpType
assignmentOp.UpdateAssign = false
} 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), Rhs: &ExpressionNode{Operation: rhsOp}}
_, err := d.GetMatchingNodes(nodeToMap(lhs), assignmentOpNode)

View File

@@ -132,11 +132,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}}`,
expression: `.c * .a`,
expected: []string{
"D0, P[c], (!!map)::{g: thongs, c: frog}\n",
"D0, P[c], (!!map)::{g: thongs, c: &cat frog}\n",
},
},
{

View File

@@ -16,7 +16,7 @@ func createPathNodeFor(pathElement interface{}) *yaml.Node {
}
}
func GetPathOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func getPathOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("GetPath")
var results = list.New()
@@ -31,8 +31,8 @@ func GetPathOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *P
content[pathIndex] = createPathNodeFor(path)
}
node.Content = content
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil

View File

@@ -13,6 +13,23 @@ var pathOperatorScenarios = []expressionScenario{
"D0, P[a b], (!!seq)::- a\n- b\n",
},
},
{
skipDoc: true,
document: `a:
b:
c:
- 0
- 1
- 2
- 3`,
expression: `.a.b.c.[]`,
expected: []string{
"D0, P[a b c 0], (!!int)::0\n",
"D0, P[a b c 1], (!!int)::1\n",
"D0, P[a b c 2], (!!int)::2\n",
"D0, P[a b c 3], (!!int)::3\n",
},
},
{
description: "Get map key",
document: `{a: {b: cat}}`,

View File

@@ -2,10 +2,10 @@ package yqlib
import "container/list"
func PipeOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
func pipeOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
}
return d.GetMatchingNodes(lhs, pathNode.Rhs)
return d.GetMatchingNodes(lhs, expressionNode.Rhs)
}

View File

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

View File

@@ -13,6 +13,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::{}\n",
},
},
{
skipDoc: true,
document: `{}`,
expression: `...`,
expected: []string{
"D0, P[], (!!map)::{}\n",
},
},
{
skipDoc: true,
document: `[]`,
@@ -21,6 +29,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[]\n",
},
},
{
skipDoc: true,
document: `[]`,
expression: `...`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
{
skipDoc: true,
document: `cat`,
@@ -31,13 +47,50 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
},
{
skipDoc: true,
document: `{a: frog}`,
expression: `..`,
document: `cat`,
expression: `...`,
expected: []string{
"D0, P[], (!!str)::cat\n",
},
},
{
description: "Recurse map (values only)",
document: `{a: frog}`,
expression: `..`,
expected: []string{
"D0, P[], (!!map)::{a: frog}\n",
"D0, P[a], (!!str)::frog\n",
},
},
{
description: "Recursively find nodes with keys",
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.",
document: `{a: {name: frog, b: {name: blog, age: 12}}}`,
expression: `[.. | select(has("name"))]`,
expected: []string{
"D0, P[], (!!seq)::- {name: frog, b: {name: blog, age: 12}}\n- {name: blog, age: 12}\n",
},
},
{
description: "Recursively find nodes with values",
document: `{a: {nameA: frog, b: {nameB: frog, age: 12}}}`,
expression: `.. | select(. == "frog")`,
expected: []string{
"D0, P[a nameA], (!!str)::frog\n",
"D0, P[a b nameB], (!!str)::frog\n",
},
},
{
description: "Recurse map (values and keys)",
subdescription: "Note that the map key appears in the results",
document: `{a: frog}`,
expression: `...`,
expected: []string{
"D0, P[], (!!map)::{a: frog}\n",
"D0, P[a], (!!str)::a\n",
"D0, P[a], (!!str)::frog\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple}}`,
@@ -48,6 +101,18 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[a b], (!!str)::apple\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple}}`,
expression: `...`,
expected: []string{
"D0, P[], (!!map)::{a: {b: apple}}\n",
"D0, P[a], (!!str)::a\n",
"D0, P[a], (!!map)::{b: apple}\n",
"D0, P[a b], (!!str)::b\n",
"D0, P[a b], (!!str)::apple\n",
},
},
{
skipDoc: true,
document: `[1,2,3]`,
@@ -59,6 +124,17 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[2], (!!int)::3\n",
},
},
{
skipDoc: true,
document: `[1,2,3]`,
expression: `...`,
expected: []string{
"D0, P[], (!!seq)::[1, 2, 3]\n",
"D0, P[0], (!!int)::1\n",
"D0, P[1], (!!int)::2\n",
"D0, P[2], (!!int)::3\n",
},
},
{
skipDoc: true,
document: `[{a: cat},2,true]`,
@@ -71,12 +147,39 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[2], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `[{a: cat},2,true]`,
expression: `...`,
expected: []string{
"D0, P[], (!!seq)::[{a: cat}, 2, true]\n",
"D0, P[0], (!!map)::{a: cat}\n",
"D0, P[0 a], (!!str)::a\n",
"D0, P[0 a], (!!str)::cat\n",
"D0, P[1], (!!int)::2\n",
"D0, P[2], (!!bool)::true\n",
},
},
{
description: "Aliases are not traversed",
document: `{a: &cat {c: frog}, b: *cat}`,
expression: `[..]`,
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",
},
},
{
skipDoc: true,
document: `{a: &cat {c: frog}, b: *cat}`,
expression: `...`,
expected: []string{
"D0, P[], (!!map)::{a: &cat {c: frog}, b: *cat}\n",
"D0, P[a], (!!str)::a\n",
"D0, P[a], (!!map)::&cat {c: frog}\n",
"D0, P[a c], (!!str)::c\n",
"D0, P[a c], (!!str)::frog\n",
"D0, P[b], (!!str)::b\n",
"D0, P[b], (alias)::*cat\n",
},
},
{
@@ -84,7 +187,15 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
document: mergeDocSample,
expression: `.foobar | [..]`,
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",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foobar | [...]`,
expected: []string{
"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",
},
},
{
@@ -100,11 +211,27 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[foobarList c], (!!str)::foobarList_c\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foobarList | ...`,
expected: []string{
"D0, P[foobarList], (!!map)::b: foobarList_b\n!!merge <<: [*foo, *bar]\nc: foobarList_c\n",
"D0, P[foobarList b], (!!str)::b\n",
"D0, P[foobarList b], (!!str)::foobarList_b\n",
"D0, P[foobarList <<], (!!merge)::!!merge <<\n",
"D0, P[foobarList <<], (!!seq)::[*foo, *bar]\n",
"D0, P[foobarList << 0], (alias)::*foo\n",
"D0, P[foobarList << 1], (alias)::*bar\n",
"D0, P[foobarList c], (!!str)::c\n",
"D0, P[foobarList c], (!!str)::foobarList_c\n",
},
},
}
func TestRecursiveDescentOperatorScenarios(t *testing.T) {
for _, tt := range recursiveDescentOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Recursive Descent", recursiveDescentOperatorScenarios)
documentScenarios(t, "Recursive Descent (Glob)", recursiveDescentOperatorScenarios)
}

View File

@@ -4,7 +4,7 @@ import (
"container/list"
)
func SelectOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func selectOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- selectOperation")
var results = list.New()
@@ -12,7 +12,7 @@ func SelectOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *Pa
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err

View File

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

View File

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

View File

@@ -4,40 +4,47 @@ import (
"container/list"
"fmt"
"gopkg.in/yaml.v3"
yaml "gopkg.in/yaml.v3"
)
func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func parseStyle(customStyle string) (yaml.Style, error) {
if customStyle == "tagged" {
return yaml.TaggedStyle, nil
} else if customStyle == "double" {
return yaml.DoubleQuotedStyle, nil
} else if customStyle == "single" {
return yaml.SingleQuotedStyle, nil
} else if customStyle == "literal" {
return yaml.LiteralStyle, nil
} else if customStyle == "folded" {
return yaml.FoldedStyle, nil
} else if customStyle == "flow" {
return yaml.FlowStyle, nil
} else if customStyle != "" {
return 0, fmt.Errorf("Unknown style %v", customStyle)
}
return 0, nil
}
func assignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("AssignStyleOperator: %v")
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
customStyle := ""
if rhs.Front() != nil {
customStyle = rhs.Front().Value.(*CandidateNode).Node.Value
}
var style yaml.Style
if customStyle == "tagged" {
style = yaml.TaggedStyle
} else if customStyle == "double" {
style = yaml.DoubleQuotedStyle
} else if customStyle == "single" {
style = yaml.SingleQuotedStyle
} else if customStyle == "literal" {
style = yaml.LiteralStyle
} else if customStyle == "folded" {
style = yaml.FoldedStyle
} else if customStyle == "flow" {
style = yaml.FlowStyle
} else if customStyle != "" {
return nil, fmt.Errorf("Unknown style %v", customStyle)
if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
style, err = parseStyle(rhs.Front().Value.(*CandidateNode).Node.Value)
if err != nil {
return nil, err
}
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
@@ -46,13 +53,27 @@ func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNod
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting style of : %v", candidate.GetKey())
if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
style, err = parseStyle(rhs.Front().Value.(*CandidateNode).Node.Value)
if err != nil {
return nil, err
}
}
}
candidate.Node.Style = style
}
return matchingNodes, nil
}
func GetStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func getStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("GetStyleOperator")
var results = list.New()
@@ -79,8 +100,8 @@ func GetStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *
style = "<unknown>"
}
node := &yaml.Node{Kind: yaml.ScalarNode, Value: style, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil

View File

@@ -21,6 +21,22 @@ var styleOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::a: \"cat\"\nb: \"5\"\nc: \"3.2\"\ne: \"true\"\n",
},
},
{
description: "Set double quote style on map keys too",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `... style="double"`,
expected: []string{
"D0, P[], (!!map)::\"a\": \"cat\"\n\"b\": \"5\"\n\"c\": \"3.2\"\n\"e\": \"true\"\n",
},
},
{
skipDoc: true,
document: "bing: &foo frog\na:\n c: cat\n <<: [*foo]",
expression: `(... | select(tag=="!!str")) style="single"`,
expected: []string{
"D0, P[], (!!map)::'bing': &foo 'frog'\n'a':\n 'c': 'cat'\n !!merge <<: [*foo]\n",
},
},
{
description: "Set single quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
@@ -70,14 +86,22 @@ e: >-
},
},
{
description: "Pretty print",
subdescription: "Set empty (default) quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style=""`,
description: "Reset style - or pretty print",
subdescription: "Set empty (default) quote style, note the usage of `...` to match keys too. Note that there is a `--prettyPrint/-P` short flag for this.",
document: `{a: cat, "b": 5, 'c': 3.2, "e": true}`,
expression: `... style=""`,
expected: []string{
"D0, P[], (!!map)::a: cat\nb: 5\nc: 3.2\ne: true\n",
},
},
{
description: "Set style relatively with assign-update",
document: `{a: single, b: double}`,
expression: `.[] style |= .`,
expected: []string{
"D0, P[], (doc)::{a: 'single', b: \"double\"}\n",
},
},
{
skipDoc: true,
document: `{a: cat, b: double}`,

View File

@@ -3,24 +3,26 @@ package yqlib
import (
"container/list"
"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, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("AssignTagOperator: %v")
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
tag := ""
if rhs.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value
if !expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
lhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Lhs)
if err != nil {
return nil, err
@@ -29,22 +31,32 @@ func AssignTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting tag of : %v", candidate.GetKey())
candidate.Node.Tag = tag
if expressionNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), expressionNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
unwrapDoc(candidate.Node).Tag = tag
}
return matchingNodes, nil
}
func GetTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
func getTagOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("GetTagOperator")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Node.Tag, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: unwrapDoc(candidate.Node).Tag, Tag: "!!str"}
result := candidate.CreateChild(nil, node)
results.PushBack(result)
}
return results, nil

View File

@@ -19,13 +19,45 @@ var tagOperatorScenarios = []expressionScenario{
},
},
{
description: "Convert numbers to strings",
skipDoc: true,
document: `{a: cat, b: 5, c: 3.2, e: true, f: []}`,
expression: `tag`,
expected: []string{
"D0, P[], (!!str)::'!!map'\n",
},
},
{
skipDoc: true,
document: `32`,
expression: `. tag= "!!str"`,
expected: []string{
"D0, P[], (doc)::\"32\"\n",
},
},
{
description: "Set custom tag",
document: `{a: str}`,
expression: `.a tag = "!!mikefarah"`,
expected: []string{
"D0, P[], (doc)::{a: !!mikefarah str}\n",
},
},
{
description: "Find numbers and convert them to strings",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `(.. | select(tag == "!!int")) tag= "!!str"`,
expected: []string{
"D0, P[], (!!map)::{a: cat, b: \"5\", c: 3.2, e: true}\n",
},
},
{
skipDoc: true,
document: `{a: "!!frog", b: "!!customTag"}`,
expression: `.[] tag |= .`,
expected: []string{
"D0, P[], (doc)::{a: !!frog \"!!frog\", b: !!customTag \"!!customTag\"}\n",
},
},
}
func TestTagOperatorScenarios(t *testing.T) {

View File

@@ -1,46 +1,39 @@
package yqlib
import (
"fmt"
"container/list"
"fmt"
"strconv"
"github.com/elliotchance/orderedmap"
"gopkg.in/yaml.v3"
yaml "gopkg.in/yaml.v3"
)
type TraversePreferences struct {
DontFollowAlias bool
type traversePreferences struct {
FollowAlias bool
IncludeMapKeys bool
}
func Splat(d *dataTreeNavigator, matches *list.List) (*list.List, error) {
preferences := &TraversePreferences{DontFollowAlias: true}
splatOperation := &Operation{OperationType: TraversePath, Value: "[]", Preferences: preferences}
splatTreeNode := &PathTreeNode{Operation: splatOperation}
return TraversePathOperator(d, matches, splatTreeNode)
func splat(d *dataTreeNavigator, matches *list.List, prefs *traversePreferences) (*list.List, error) {
return traverseNodesWithArrayIndices(matches, make([]*yaml.Node, 0), prefs)
}
func TraversePathOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
func traversePathOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) {
log.Debugf("-- Traversing")
var matchingNodeMap = list.New()
var newNodes []*CandidateNode
var err error
for el := matchMap.Front(); el != nil; el = el.Next() {
newNodes, err = traverse(d, el.Value.(*CandidateNode), pathNode.Operation)
newNodes, err := traverse(d, el.Value.(*CandidateNode), expressionNode.Operation)
if err != nil {
return nil, err
}
for _, n := range newNodes {
matchingNodeMap.PushBack(n)
}
matchingNodeMap.PushBackList(newNodes)
}
return matchingNodeMap, nil
}
func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Operation) ([]*CandidateNode, error) {
func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
log.Debug("Traversing %v", NodeToString(matchingNode))
value := matchingNode.Node
@@ -61,34 +54,8 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
switch value.Kind {
case yaml.MappingNode:
log.Debug("its a map with %v entries", len(value.Content)/2)
var newMatches = orderedmap.NewOrderedMap()
err := traverseMap(newMatches, matchingNode, operation)
if err != nil {
return nil, err
}
if newMatches.Len() == 0 {
//no matches, create one automagically
valueNode := &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}
node := matchingNode.Node
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: operation.StringValue}, valueNode)
candidateNode := &CandidateNode{
Node: valueNode,
Path: append(matchingNode.Path, operation.StringValue),
Document: matchingNode.Document,
}
newMatches.Set(candidateNode.GetKey(), candidateNode)
}
arrayMatches := make([]*CandidateNode, newMatches.Len())
i := 0
for el := newMatches.Front(); el != nil; el = el.Next() {
arrayMatches[i] = el.Value.(*CandidateNode)
i++
}
return arrayMatches, nil
prefs := &traversePreferences{FollowAlias: true}
return traverseMap(matchingNode, operation.StringValue, prefs, false)
case yaml.SequenceNode:
log.Debug("its a sequence of %v things!", len(value.Content))
@@ -100,106 +67,108 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
return traverse(d, matchingNode, operation)
case yaml.DocumentNode:
log.Debug("digging into doc node")
return traverse(d, &CandidateNode{
Node: matchingNode.Node.Content[0],
Document: matchingNode.Document}, operation)
return traverse(d, matchingNode.CreateChild(nil, matchingNode.Node.Content[0]), operation)
default:
return nil, nil
return list.New(), nil
}
}
func keyMatches(key *yaml.Node, pathNode *Operation) bool {
return pathNode.Value == "[]" || Match(key.Value, pathNode.StringValue)
}
func traverseArrayOperator(d *dataTreeNavigator, matchingNodes *list.List, expressionNode *ExpressionNode) (*list.List, error) {
// rhs is a collect expression that will yield indexes to retreive of the arrays
func traverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, operation *Operation) error {
// value.Content is a concatenated array of key, value,
// so keys are in the even indexes, values in odd.
// merge aliases are defined first, but we only want to traverse them
// if we don't find a match directly on this node first.
//TODO ALIASES, auto creation?
node := candidate.Node
followAlias := true
if operation.Preferences != nil {
followAlias = !operation.Preferences.(*TraversePreferences).DontFollowAlias
rhs, err := d.GetMatchingNodes(matchingNodes, expressionNode.Rhs)
if err != nil {
return nil, err
}
var contents = node.Content
for index := 0; index < len(contents); index = index + 2 {
key := contents[index]
value := contents[index+1]
var indicesToTraverse = rhs.Front().Value.(*CandidateNode).Node.Content
prefs := &traversePreferences{FollowAlias: true}
return traverseNodesWithArrayIndices(matchingNodes, indicesToTraverse, prefs)
}
log.Debug("checking %v (%v)", key.Value, key.Tag)
//skip the 'merge' tag, find a direct match first
if key.Tag == "!!merge" && followAlias {
log.Debug("Merge anchor")
err := traverseMergeAnchor(newMatches, candidate, value, operation)
if err != nil {
return err
}
} else if keyMatches(key, operation) {
log.Debug("MATCHED")
candidateNode := &CandidateNode{
Node: value,
Path: append(candidate.Path, key.Value),
Document: candidate.Document,
}
newMatches.Set(candidateNode.GetKey(), candidateNode)
func traverseNodesWithArrayIndices(matchingNodes *list.List, indicesToTraverse []*yaml.Node, prefs *traversePreferences) (*list.List, error) {
var matchingNodeMap = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
newNodes, err := traverseArrayIndices(candidate, indicesToTraverse, prefs)
if err != nil {
return nil, err
}
matchingNodeMap.PushBackList(newNodes)
}
return nil
return matchingNodeMap, nil
}
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, operation *Operation) error {
switch value.Kind {
case yaml.AliasNode:
candidateNode := &CandidateNode{
Node: value.Alias,
Path: originalCandidate.Path,
Document: originalCandidate.Document,
}
return traverseMap(newMatches, candidateNode, operation)
case yaml.SequenceNode:
for _, childValue := range value.Content {
err := traverseMergeAnchor(newMatches, originalCandidate, childValue, operation)
if err != nil {
return err
}
}
func traverseArrayIndices(matchingNode *CandidateNode, indicesToTraverse []*yaml.Node, prefs *traversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse
node := matchingNode.Node
if node.Tag == "!!null" {
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
// auto vivification, make it into an empty array
node.Tag = ""
node.Kind = yaml.SequenceNode
}
return nil
if node.Kind == yaml.AliasNode {
matchingNode.Node = node.Alias
return traverseArrayIndices(matchingNode, indicesToTraverse, prefs)
} else if node.Kind == yaml.SequenceNode {
return traverseArrayWithIndices(matchingNode, indicesToTraverse)
} else if node.Kind == yaml.MappingNode {
return traverseMapWithIndices(matchingNode, indicesToTraverse, prefs)
} else if node.Kind == yaml.DocumentNode {
return traverseArrayIndices(matchingNode.CreateChild(nil, matchingNode.Node.Content[0]), indicesToTraverse, prefs)
}
log.Debugf("OperatorArrayTraverse skipping %v as its a %v", matchingNode, node.Tag)
return list.New(), nil
}
func traverseArray(candidate *CandidateNode, operation *Operation) ([]*CandidateNode, error) {
log.Debug("operation Value %v", operation.Value)
if operation.Value == "[]" {
func traverseMapWithIndices(candidate *CandidateNode, indices []*yaml.Node, prefs *traversePreferences) (*list.List, error) {
if len(indices) == 0 {
return traverseMap(candidate, "", prefs, true)
}
var contents = candidate.Node.Content
var newMatches = make([]*CandidateNode, len(contents))
var matchingNodeMap = list.New()
for _, indexNode := range indices {
log.Debug("traverseMapWithIndices: %v", indexNode.Value)
newNodes, err := traverseMap(candidate, indexNode.Value, prefs, false)
if err != nil {
return nil, err
}
matchingNodeMap.PushBackList(newNodes)
}
return matchingNodeMap, nil
}
func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*list.List, error) {
log.Debug("traverseArrayWithIndices")
var newMatches = list.New()
node := unwrapDoc(candidate.Node)
if len(indices) == 0 {
log.Debug("splatting")
var index int64
for index = 0; index < int64(len(contents)); index = index + 1 {
newMatches[index] = &CandidateNode{
Document: candidate.Document,
Path: append(candidate.Path, index),
Node: contents[index],
}
for index = 0; index < int64(len(node.Content)); index = index + 1 {
newMatches.PushBack(candidate.CreateChild(index, node.Content[index]))
}
return newMatches, nil
}
switch operation.Value.(type) {
case int64:
index := operation.Value.(int64)
for _, indexNode := range indices {
log.Debug("traverseArrayWithIndices: '%v'", indexNode.Value)
index, err := strconv.ParseInt(indexNode.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("Cannot index array with '%v' (%v)", indexNode.Value, err)
}
indexToUse := index
contentLength := int64(len(candidate.Node.Content))
contentLength := int64(len(node.Content))
for contentLength <= index {
candidate.Node.Content = append(candidate.Node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"})
contentLength = int64(len(candidate.Node.Content))
node.Content = append(node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"})
contentLength = int64(len(node.Content))
}
if indexToUse < 0 {
@@ -210,14 +179,94 @@ func traverseArray(candidate *CandidateNode, operation *Operation) ([]*Candidate
return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength)
}
return []*CandidateNode{&CandidateNode{
Node: candidate.Node.Content[indexToUse],
Document: candidate.Document,
Path: append(candidate.Path, index),
}}, nil
default:
log.Debug("argument not an int (%v), no array matches", operation.Value)
return nil, nil
newMatches.PushBack(candidate.CreateChild(index, node.Content[indexToUse]))
}
return newMatches, nil
}
func keyMatches(key *yaml.Node, wantedKey string) bool {
return matchKey(key.Value, wantedKey)
}
func traverseMap(matchingNode *CandidateNode, key string, prefs *traversePreferences, splat bool) (*list.List, error) {
var newMatches = orderedmap.NewOrderedMap()
err := doTraverseMap(newMatches, matchingNode, key, prefs, splat)
if err != nil {
return nil, err
}
if newMatches.Len() == 0 {
//no matches, create one automagically
valueNode := &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}
node := matchingNode.Node
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, valueNode)
candidateNode := matchingNode.CreateChild(key, valueNode)
newMatches.Set(candidateNode.GetKey(), candidateNode)
}
results := list.New()
i := 0
for el := newMatches.Front(); el != nil; el = el.Next() {
results.PushBack(el.Value)
i++
}
return results, nil
}
func doTraverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, wantedKey string, prefs *traversePreferences, splat bool) error {
// value.Content is a concatenated array of key, value,
// so keys are in the even indexes, values in odd.
// merge aliases are defined first, but we only want to traverse them
// if we don't find a match directly on this node first.
node := candidate.Node
var contents = node.Content
for index := 0; index < len(contents); index = index + 2 {
key := contents[index]
value := contents[index+1]
log.Debug("checking %v (%v)", key.Value, key.Tag)
//skip the 'merge' tag, find a direct match first
if key.Tag == "!!merge" && prefs.FollowAlias {
log.Debug("Merge anchor")
err := traverseMergeAnchor(newMatches, candidate, value, wantedKey, prefs, splat)
if err != nil {
return err
}
} else if splat || keyMatches(key, wantedKey) {
log.Debug("MATCHED")
if prefs.IncludeMapKeys {
candidateNode := candidate.CreateChild(key.Value, key)
newMatches.Set(fmt.Sprintf("keyOf-%v", candidateNode.GetKey()), candidateNode)
}
candidateNode := candidate.CreateChild(key.Value, value)
newMatches.Set(candidateNode.GetKey(), candidateNode)
}
}
return nil
}
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, wantedKey string, prefs *traversePreferences, splat bool) error {
switch value.Kind {
case yaml.AliasNode:
candidateNode := originalCandidate.CreateChild(nil, value.Alias)
return doTraverseMap(newMatches, candidateNode, wantedKey, prefs, splat)
case yaml.SequenceNode:
for _, childValue := range value.Content {
err := traverseMergeAnchor(newMatches, originalCandidate, childValue, wantedKey, prefs, splat)
if err != nil {
return err
}
}
}
return nil
}
func traverseArray(candidate *CandidateNode, operation *Operation) (*list.List, error) {
log.Debug("operation Value %v", operation.Value)
indices := []*yaml.Node{&yaml.Node{Value: operation.StringValue}}
return traverseArrayWithIndices(candidate, indices)
}

View File

@@ -54,6 +54,15 @@ var traversePathOperatorScenarios = []expressionScenario{
"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",
},
},
{
description: "Children don't exist",
subdescription: "Nodes are added dynamically while traversing",
@@ -65,7 +74,7 @@ var traversePathOperatorScenarios = []expressionScenario{
},
{
skipDoc: true,
document: `{}`,
document: ``,
expression: `.[1].a`,
expected: []string{
"D0, P[1 a], (!!null)::null\n",
@@ -137,7 +146,15 @@ var traversePathOperatorScenarios = []expressionScenario{
{
description: "Traversing aliases with splat",
document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.[]`,
expression: `.b[]`,
expected: []string{
"D0, P[b c], (!!str)::frog\n",
},
},
{
skipDoc: true,
document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.[]`,
expected: []string{
"D0, P[b c], (!!str)::frog\n",
},
@@ -150,12 +167,6 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[b c], (!!str)::frog\n",
},
},
{
skipDoc: true,
document: `[1,2,3]`,
expression: `.b`,
expected: []string{},
},
{
description: "Traversing arrays by index",
document: `[1,2,3]`,
@@ -215,7 +226,17 @@ var traversePathOperatorScenarios = []expressionScenario{
{
description: "Splatting merge anchors",
document: mergeDocSample,
expression: `.foobar.[]`,
expression: `.foobar[]`,
expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n",
"D0, P[foobar a], (!!str)::foo_a\n",
"D0, P[foobar thing], (!!str)::foobar_thing\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foobar.[]`,
expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n",
"D0, P[foobar a], (!!str)::foo_a\n",
@@ -266,7 +287,7 @@ var traversePathOperatorScenarios = []expressionScenario{
{
description: "Splatting merge anchor lists",
document: mergeDocSample,
expression: `.foobarList.[]`,
expression: `.foobarList[]`,
expected: []string{
"D0, P[foobarList b], (!!str)::bar_b\n",
"D0, P[foobarList a], (!!str)::foo_a\n",
@@ -274,6 +295,123 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[foobarList c], (!!str)::foobarList_c\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foobarList.[]`,
expected: []string{
"D0, P[foobarList b], (!!str)::bar_b\n",
"D0, P[foobarList a], (!!str)::foo_a\n",
"D0, P[foobarList thing], (!!str)::bar_thing\n",
"D0, P[foobarList c], (!!str)::foobarList_c\n",
},
},
{
skipDoc: true,
document: `[a,b,c]`,
expression: `.[]`,
expected: []string{
"D0, P[0], (!!str)::a\n",
"D0, P[1], (!!str)::b\n",
"D0, P[2], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `[a,b,c]`,
expression: `[]`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a[0]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
},
},
{
description: "Select multiple indices",
document: `{a: [a,b,c]}`,
expression: `.a[0, 2]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
"D0, P[a 2], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a.[0, 2]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
"D0, P[a 2], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a[-1]`,
expected: []string{
"D0, P[a -1], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a.[-1]`,
expected: []string{
"D0, P[a -1], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a[-2]`,
expected: []string{
"D0, P[a -2], (!!str)::b\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a.[-2]`,
expected: []string{
"D0, P[a -2], (!!str)::b\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a[]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
"D0, P[a 1], (!!str)::b\n",
"D0, P[a 2], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a.[]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
"D0, P[a 1], (!!str)::b\n",
"D0, P[a 2], (!!str)::c\n",
},
},
{
skipDoc: true,
document: `{a: [a,b,c]}`,
expression: `.a | .[]`,
expected: []string{
"D0, P[a 0], (!!str)::a\n",
"D0, P[a 1], (!!str)::b\n",
"D0, P[a 2], (!!str)::c\n",
},
},
}
func TestTraversePathOperatorScenarios(t *testing.T) {

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