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

Compare commits

...

67 Commits

Author SHA1 Message Date
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
76 changed files with 2060 additions and 786 deletions

View File

@@ -10,6 +10,8 @@ assignees: ''
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. 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 Version of yq: 3.X.X
Operating system: mac/linux/windows/.... Operating system: mac/linux/windows/....
Installed via: docker/binary release/homebrew/snap/... Installed via: docker/binary release/homebrew/snap/...

View File

@@ -10,6 +10,8 @@ assignees: ''
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. 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 Version of yq: 4.X.X
Operating system: mac/linux/windows/.... Operating system: mac/linux/windows/....
Installed via: docker/binary release/homebrew/snap/... Installed via: docker/binary release/homebrew/snap/...

View File

@@ -9,9 +9,11 @@ assignees: ''
**Please describe your feature request.** **Please describe your feature request.**
A clear and concise description of what the request is and what it would solve. 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** **Describe the solution you'd like**
If we have data1.yml like: If we have data1.yml like:

View File

@@ -1,11 +1,41 @@
name: Publish image to Dockerhub name: Release YQ
on: on:
release: push:
types: [released] tags:
- 'v*'
jobs: 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: env:
IMAGE_NAME: mikefarah/yq IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -38,11 +68,13 @@ jobs:
- name: Build and push image - name: Build and push image
run: | 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) SHORT_SHA1=$(git rev-parse --short HEAD)
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64" PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64"
echo "Building and pushing version ${IMAGE_VERSION} of image ${IMAGE_NAME}" echo "Building and pushing version ${IMAGE_VERSION} of image ${IMAGE_NAME}"
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 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 . --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 # Choose alpine as a base image to make this useful for CI, as many
# CI tools expect an interactive shell inside the container # 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 COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
RUN chmod +x /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 \ RUN set -e -x \
&& /opt/devtools.sh && /opt/devtools.sh
ENV PATH=/go/bin:$PATH
# install mkdocs # install mkdocs
RUN set -ex \ 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) ![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 ## Install
### [Download the latest binary](https://github.com/mikefarah/yq/releases/latest) ### [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/) Using [Homebrew](https://brew.sh/)
``` ```
brew install yq 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 snap install yq
``` ```
or, for the (deprecated) v3 version:
```
snap install yq --channel=v3/stable
```
#### Snap notes #### 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: `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 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 ### Run with Docker
#### Oneshot use: #### Oneshot use:
@@ -75,7 +103,7 @@ yq() {
### Go Get: ### Go Get:
``` ```
GO111MODULE=on go get github.com/mikefarah/yq/v4 GO111MODULE=on go get github.com/mikefarah/yq
``` ```
## Community Supported Installation methods ## Community Supported Installation methods
@@ -121,13 +149,13 @@ Supported by @rmescandon (https://launchpad.net/~rmescandon/+archive/ubuntu/yq)
- Colorized yaml output - Colorized yaml output
- [Deeply traverse yaml](https://mikefarah.gitbook.io/yq/v/v4.x/traverse) - [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) - [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) - [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) - [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) - 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) - [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) - [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/) ## [Usage](https://mikefarah.gitbook.io/yq/)
@@ -153,6 +181,7 @@ Flags:
-M, --no-colors force print with no colors -M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---) -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. -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. -j, --tojson output as json. Set indent to 0 to print json in one line.
-v, --verbose verbose mode -v, --verbose verbose mode
-V, --version Print version information and quit -V, --version Print version information and quit
@@ -166,15 +195,5 @@ Simple Example:
yq e '.a.b | length' f1.yml f2.yml 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 ## 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) - `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 nullInput = false
var verbose = false var verbose = false
var version = false var version = false
var prettyPrint = false
var completedSuccessfully = false var completedSuccessfully = false

View File

@@ -65,19 +65,19 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
switch len(args) { switch len(args) {
case 0: case 0:
if pipingStdIn { if pipingStdIn {
err = allAtOnceEvaluator.EvaluateFiles("", []string{"-"}, printer) err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
} else { } else {
cmd.Println(cmd.UsageString()) cmd.Println(cmd.UsageString())
return nil return nil
} }
case 1: case 1:
if nullInput { if nullInput {
err = yqlib.NewStreamEvaluator().EvaluateNew(args[0], printer) err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(args[0]), printer)
} else { } else {
err = allAtOnceEvaluator.EvaluateFiles("", []string{args[0]}, printer) err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
} }
default: default:
err = allAtOnceEvaluator.EvaluateFiles(args[0], args[1:], printer) err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer)
} }
completedSuccessfully = err == nil completedSuccessfully = err == nil

View File

@@ -33,6 +33,16 @@ yq e '.a.b = "cool"' -i file.yaml
} }
return cmdEvalSequence 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 { func evaluateSequence(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true cmd.SilenceUsage = true
// 0 args, read std in // 0 args, read std in
@@ -76,16 +86,16 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
switch len(args) { switch len(args) {
case 0: case 0:
if pipingStdIn { if pipingStdIn {
err = streamEvaluator.EvaluateFiles("", []string{"-"}, printer) err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
} else { } else {
cmd.Println(cmd.UsageString()) cmd.Println(cmd.UsageString())
return nil return nil
} }
case 1: case 1:
if nullInput { if nullInput {
err = streamEvaluator.EvaluateNew(args[0], printer) err = streamEvaluator.EvaluateNew(processExpression(args[0]), printer)
} else { } else {
err = streamEvaluator.EvaluateFiles("", []string{args[0]}, printer) err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
} }
default: default:
err = streamEvaluator.EvaluateFiles(args[0], args[1:], printer) 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.PersistentFlags().IntVarP(&indent, "indent", "I", 2, "sets indent level for output")
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit") 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(&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(&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") rootCmd.PersistentFlags().BoolVarP(&forceColor, "colors", "C", false, "force print with colors")

View File

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

View File

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

View File

@@ -21,6 +21,14 @@ func (n *CandidateNode) GetKey() string {
return fmt.Sprintf("%v - %v", n.Document, n.Path) return fmt.Sprintf("%v - %v", n.Document, n.Path)
} }
func (n *CandidateNode) CreateChildPath(path interface{}) []interface{} {
//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) { func (n *CandidateNode) Copy() (*CandidateNode, error) {
clone := &CandidateNode{} clone := &CandidateNode{}
err := copier.Copy(clone, n) err := copier.Copy(clone, n)
@@ -32,6 +40,7 @@ func (n *CandidateNode) Copy() (*CandidateNode, error) {
// updates this candidate from the given candidate node // updates this candidate from the given candidate node
func (n *CandidateNode) UpdateFrom(other *CandidateNode) { func (n *CandidateNode) UpdateFrom(other *CandidateNode) {
n.UpdateAttributesFrom(other) n.UpdateAttributesFrom(other)
n.Node.Content = other.Node.Content n.Node.Content = other.Node.Content
n.Node.Value = other.Node.Value n.Node.Value = other.Node.Value

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 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 ## Update node to be the sibling value
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@@ -112,7 +133,7 @@ a:
``` ```
then then
```bash ```bash
yq eval '(.a.[] | select(. == "apple")) = "frog"' sample.yml yq eval '(.a[] | select(. == "apple")) = "frog"' sample.yml
``` ```
will output will output
```yaml ```yaml

View File

@@ -13,6 +13,22 @@ will output
a: cat # single 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 ## Set head comment
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@@ -14,6 +14,23 @@ will output
a: cat 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 ## Delete entry in array
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@@ -31,6 +48,21 @@ will output
- 3 - 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 ## Delete no matches
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

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

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). 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 ## Merging files
Note the use of eval-all to ensure all documents are loaded into memory. Note the use of eval-all to ensure all documents are loaded into memory.
```bash ```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml yq eval-all 'select(fi == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
``` ```
## Get filename ## Get filename
Given a sample.yml file of: Given a sample.yml file of:
@@ -16,7 +18,7 @@ yq eval 'filename' sample.yml
``` ```
will output will output
```yaml ```yaml
sample.yaml sample.yml
``` ```
## Get file index ## Get file index
@@ -33,3 +35,17 @@ will output
0 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

@@ -1,8 +1,55 @@
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: 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. 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 ```bash
yq eval '.. style= "flow"' file.yaml 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
```
## 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 ## Aliases are not traversed
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@@ -40,6 +40,26 @@ c: "3.2"
e: "true" 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 ## Set single quote style
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
@@ -125,19 +145,19 @@ will output
{a: cat, b: 5, c: 3.2, e: true} {a: cat, b: 5, c: 3.2, e: true}
``` ```
## Pretty print ## Reset style - or pretty print
Set empty (default) quote style 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: Given a sample.yml file of:
```yaml ```yaml
a: cat a: cat
b: 5 "b": 5
c: 3.2 'c': 3.2
e: true "e": true
``` ```
then then
```bash ```bash
yq eval '.. style=""' sample.yml yq eval '... style=""' sample.yml
``` ```
will output will output
```yaml ```yaml
@@ -147,6 +167,22 @@ c: 3.2
e: true 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 ## Read style
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@@ -22,7 +22,21 @@ will output
!!seq !!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: Given a sample.yml file of:
```yaml ```yaml
a: cat a: cat

View File

@@ -106,7 +106,7 @@ b: *cat
``` ```
then then
```bash ```bash
yq eval '.b.[]' sample.yml yq eval '.b[]' sample.yml
``` ```
will output will output
```yaml ```yaml
@@ -290,7 +290,7 @@ foobar:
``` ```
then then
```bash ```bash
yq eval '.foobar.[]' sample.yml yq eval '.foobar[]' sample.yml
``` ```
will output will output
```yaml ```yaml
@@ -356,7 +356,7 @@ foobar:
``` ```
then then
```bash ```bash
yq eval '.foobarList.[]' sample.yml yq eval '.foobarList[]' sample.yml
``` ```
will output will output
```yaml ```yaml
@@ -366,3 +366,21 @@ bar_thing
foobarList_c 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

@@ -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 +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). 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 ## Merging files
Note the use of eval-all to ensure all documents are loaded into memory. Note the use of eval-all to ensure all documents are loaded into memory.
```bash ```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml yq eval-all 'select(fi == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
``` ```

View File

@@ -1,5 +1,19 @@
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: 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. 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 ```bash
yq eval '.. style= "flow"' file.yaml 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

@@ -19,7 +19,7 @@ type OperationType struct {
} }
// operators TODO: // operators TODO:
// - cookbook doc for common things // - keys operator for controlling key metadata (particularly anchors/aliases)
// - mergeEmpty (sets only if the document is empty, do I do that now?) // - mergeEmpty (sets only if the document is empty, do I do that now?)
var Or = &OperationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: OrOperator} var Or = &OperationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: OrOperator}
@@ -36,9 +36,12 @@ var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Pre
var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator} 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 AssignTag = &OperationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: AssignTagOperator}
var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator} var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator}
var AssignAnchor = &OperationType{Type: "ASSIGN_ANCHOR", NumArgs: 2, Precedence: 40, Handler: AssignAnchorOperator}
var AssignAlias = &OperationType{Type: "ASSIGN_ALIAS", NumArgs: 2, Precedence: 40, Handler: AssignAliasOperator}
var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: MultiplyOperator} var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: MultiplyOperator}
var Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator} var Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator}
var Alternative = &OperationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 45, Handler: AlternativeOperator}
var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator} var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator}
var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator} var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator}
@@ -50,6 +53,8 @@ var Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handle
var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator} 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 GetTag = &OperationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: GetTagOperator}
var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator} var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator}
var GetAnchor = &OperationType{Type: "GET_ANCHOR", NumArgs: 0, Precedence: 50, Handler: GetAnchorOperator}
var GetAlias = &OperationType{Type: "GET_ALIAS", NumArgs: 0, Precedence: 50, Handler: GetAliasOperator}
var GetDocumentIndex = &OperationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: GetDocumentIndexOperator} 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 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 GetFileIndex = &OperationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: GetFileIndexOperator}
@@ -60,6 +65,7 @@ var SortKeys = &OperationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Han
var CollectObject = &OperationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: CollectObjectOperator} 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 TraversePath = &OperationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator}
var TraverseArray = &OperationType{Type: "TRAVERSE_ARRAY", NumArgs: 1, Precedence: 50, Handler: TraverseArrayOperator}
var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator}
var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator} var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator}
@@ -72,6 +78,7 @@ var RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Pre
var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator} var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator}
var Has = &OperationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: HasOperator} var Has = &OperationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: HasOperator}
var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: DeleteChildOperator} var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: DeleteChildOperator}
var DeleteImmediateChild = &OperationType{Type: "DELETE_IMMEDIATE_CHILD", NumArgs: 1, Precedence: 40, Handler: DeleteImmediateChildOperator}
type Operation struct { type Operation struct {
OperationType *OperationType OperationType *OperationType
@@ -79,6 +86,7 @@ type Operation struct {
StringValue string StringValue string
CandidateNode *CandidateNode // used for Value Path elements CandidateNode *CandidateNode // used for Value Path elements
Preferences interface{} Preferences interface{}
UpdateAssign bool // used for assign ops, when true it means we evaluate the rhs given the lhs (instead of matching nodes)
} }
func CreateValueOperation(value interface{}, stringValue string) *Operation { func CreateValueOperation(value interface{}, stringValue string) *Operation {

View File

@@ -16,19 +16,13 @@ func createSelfAddOp(rhs *PathTreeNode) *PathTreeNode {
func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
assignmentOp := &Operation{OperationType: Assign} assignmentOp := &Operation{OperationType: Assign}
assignmentOp.Preferences = &AssignOpPreferences{true} assignmentOp.UpdateAssign = true
assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createSelfAddOp(pathNode.Rhs)} assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createSelfAddOp(pathNode.Rhs)}
return d.GetMatchingNodes(matchingNodes, assignmentOpNode) return d.GetMatchingNodes(matchingNodes, assignmentOpNode)
} }
func toNodes(candidates *list.List) []*yaml.Node { func toNodes(candidate *CandidateNode) []*yaml.Node {
if candidates.Len() == 0 {
return []*yaml.Node{}
}
candidate := candidates.Front().Value.(*CandidateNode)
if candidate.Node.Tag == "!!null" { if candidate.Node.Tag == "!!null" {
return []*yaml.Node{} return []*yaml.Node{}
} }
@@ -44,27 +38,21 @@ 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, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("Add operator") 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 crossFunction(d, matchingNodes, pathNode, add)
return nil, err
} }
for el := lhs.Front(); el != nil; el = el.Next() { func add(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhsCandidate := el.Value.(*CandidateNode) lhs.Node = UnwrapDoc(lhs.Node)
lhsNode := UnwrapDoc(lhsCandidate.Node) rhs.Node = UnwrapDoc(rhs.Node)
target := &CandidateNode{ target := &CandidateNode{
Path: lhsCandidate.Path, Path: lhs.Path,
Document: lhsCandidate.Document, Document: lhs.Document,
Filename: lhsCandidate.Filename, Filename: lhs.Filename,
Node: &yaml.Node{}, Node: &yaml.Node{},
} }
lhsNode := lhs.Node
switch lhsNode.Kind { switch lhsNode.Kind {
case yaml.MappingNode: case yaml.MappingNode:
@@ -74,10 +62,9 @@ func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
target.Node.Style = lhsNode.Style target.Node.Style = lhsNode.Style
target.Node.Tag = "!!seq" target.Node.Tag = "!!seq"
target.Node.Content = append(lhsNode.Content, toNodes(rhs)...) target.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
results.PushBack(target)
case yaml.ScalarNode: case yaml.ScalarNode:
return nil, fmt.Errorf("Scalars not yet supported for addition") return nil, fmt.Errorf("Scalars not yet supported for addition")
} }
}
return results, nil return target, nil
} }

View File

@@ -21,6 +21,14 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[a], (!!seq)::[1, 2, 3, 4]\n", "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", description: "Concatenate null to array",
document: `{a: [1,2]}`, document: `{a: [1,2]}`,

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, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- alternative")
return crossFunction(d, matchingNodes, pathNode, 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,9 +3,119 @@ package yqlib
import ( import (
"container/list" "container/list"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func AssignAliasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("AssignAlias operator!")
aliasName := ""
if !pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
aliasName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.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 pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.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, pathNode *PathTreeNode) (*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"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}
func AssignAnchorOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("AssignAnchor operator!")
anchorName := ""
if !pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
anchorName = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.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 pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.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, pathNode *PathTreeNode) (*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"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}
func ExplodeOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func ExplodeOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- ExplodeOperation") log.Debugf("-- ExplodeOperation")

View File

@@ -4,7 +4,55 @@ import (
"testing" "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", description: "Explode alias and anchor",
document: `{f : {a: &a cat, b: *a}}`, document: `{f : {a: &a cat, b: *a}}`,
@@ -82,9 +130,9 @@ foobar:
}, },
} }
func TestExplodeOperatorScenarios(t *testing.T) { func TestAnchorAliaseOperatorScenarios(t *testing.T) {
for _, tt := range explodeTest { for _, tt := range anchorOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Explode", explodeTest) documentScenarios(t, "Anchor and Alias Operators", anchorOperatorScenarios)
} }

View File

@@ -2,26 +2,20 @@ package yqlib
import "container/list" import "container/list"
type AssignOpPreferences struct {
UpdateAssign bool
}
func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
preferences := pathNode.Operation.Preferences.(*AssignOpPreferences)
var rhs *list.List var rhs *list.List
if !preferences.UpdateAssign { if !pathNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err = d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
} }
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
if preferences.UpdateAssign { if pathNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) rhs, err = d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
} }
@@ -33,7 +27,9 @@ func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNo
first := rhs.Front() first := rhs.Front()
if first != nil { 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 there was nothing given, perhaps we are creating a new yaml doc

View File

@@ -20,6 +20,16 @@ var assignOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: {g: foof}}\n", "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", description: "Update node to be the sibling value",
document: `{a: {b: child}, b: sibling}`, document: `{a: {b: child}, b: sibling}`,
@@ -80,6 +90,14 @@ var assignOperatorScenarios = []expressionScenario{
{ {
description: "Update selected results", description: "Update selected results",
document: `{a: {b: apple, c: cactus}}`, document: `{a: {b: apple, c: cactus}}`,
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"`, expression: `(.a.[] | select(. == "apple")) = "frog"`,
expected: []string{ expected: []string{
"D0, P[], (doc)::{a: {b: frog, c: cactus}}\n", "D0, P[], (doc)::{a: {b: frog, c: cactus}}\n",

View File

@@ -25,74 +25,39 @@ func isTruthy(c *CandidateNode) (bool, error) {
type boolOp func(bool, bool) bool type boolOp func(bool, bool) bool
func performBoolOp(results *list.List, lhs *list.List, rhs *list.List, op boolOp) error { func performBoolOp(op boolOp) func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
for lhsChild := lhs.Front(); lhsChild != nil; lhsChild = lhsChild.Next() { return func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhsCandidate := lhsChild.Value.(*CandidateNode) lhs.Node = UnwrapDoc(lhs.Node)
lhsTrue, errDecoding := isTruthy(lhsCandidate) rhs.Node = UnwrapDoc(rhs.Node)
lhsTrue, errDecoding := isTruthy(lhs)
if errDecoding != nil { if errDecoding != nil {
return errDecoding return nil, errDecoding
} }
for rhsChild := rhs.Front(); rhsChild != nil; rhsChild = rhsChild.Next() { rhsTrue, errDecoding := isTruthy(rhs)
rhsCandidate := rhsChild.Value.(*CandidateNode)
rhsTrue, errDecoding := isTruthy(rhsCandidate)
if errDecoding != nil { if errDecoding != nil {
return errDecoding return nil, errDecoding
}
boolResult := createBooleanCandidate(lhsCandidate, op(lhsTrue, rhsTrue))
results.PushBack(boolResult)
}
}
return nil
} }
func booleanOp(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, op boolOp) (*list.List, error) { return createBooleanCandidate(lhs, op(lhsTrue, rhsTrue)), nil
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, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- orOp") log.Debugf("-- orOp")
return booleanOp(d, matchingNodes, pathNode, func(b1 bool, b2 bool) bool { return crossFunction(d, matchingNodes, pathNode, performBoolOp(
func(b1 bool, b2 bool) bool {
return b1 || b2 return b1 || b2
}) }))
} }
func AndOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func AndOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- AndOp") log.Debugf("-- AndOp")
return booleanOp(d, matchingNodes, pathNode, func(b1 bool, b2 bool) bool { return crossFunction(d, matchingNodes, pathNode, performBoolOp(
func(b1 bool, b2 bool) bool {
return b1 && b2 return b1 && b2
}) }))
} }
func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {

View File

@@ -55,7 +55,7 @@ func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *
func createChildCandidate(candidate *CandidateNode, index int) *CandidateNode { func createChildCandidate(candidate *CandidateNode, index int) *CandidateNode {
return &CandidateNode{ return &CandidateNode{
Document: candidate.Document, Document: candidate.Document,
Path: append(candidate.Path, index), Path: candidate.CreateChildPath(index),
Filename: candidate.Filename, Filename: candidate.Filename,
Node: candidate.Node.Content[index], Node: candidate.Node.Content[index],
} }
@@ -67,7 +67,9 @@ func collect(d *dataTreeNavigator, aggregate *list.List, remainingMatches *list.
} }
candidate := remainingMatches.Remove(remainingMatches.Front()).(*CandidateNode) 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() { for splatEl := splatted.Front(); splatEl != nil; splatEl = splatEl.Next() {
splatEl.Value.(*CandidateNode).Path = nil splatEl.Value.(*CandidateNode).Path = nil

View File

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

View File

@@ -4,7 +4,7 @@ import (
"container/list" "container/list"
"strings" "strings"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type CommentOpPreferences struct { type CommentOpPreferences struct {
@@ -17,15 +17,6 @@ func AssignCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, path
log.Debugf("AssignComments operator!") log.Debugf("AssignComments operator!")
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
comment := ""
if rhs.Front() != nil {
comment = rhs.Front().Value.(*CandidateNode).Node.Value
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
@@ -34,8 +25,32 @@ func AssignCommentsOperator(d *dataTreeNavigator, matchingNodes *list.List, path
preferences := pathNode.Operation.Preferences.(*CommentOpPreferences) preferences := pathNode.Operation.Preferences.(*CommentOpPreferences)
comment := ""
if !pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
if rhs.Front() != nil {
comment = rhs.Front().Value.(*CandidateNode).Node.Value
}
}
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
if pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.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()) log.Debugf("Setting comment of : %v", candidate.GetKey())
if preferences.LineComment { if preferences.LineComment {
candidate.Node.LineComment = comment candidate.Node.LineComment = comment

View File

@@ -13,6 +13,39 @@ var commentOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::a: cat # single\n", "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", description: "Set head comment",
document: `a: cat`, document: `a: cat`,

View File

@@ -2,39 +2,67 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func DeleteChildOperator(d *dataTreeNavigator, 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.
for el := matchingNodes.Front(); el != nil; el = el.Next() { nodesToDelete, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
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 { if err != nil {
return nil, err return nil, err
} }
realNode := UnwrapDoc(candidate.Node) for el := nodesToDelete.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
if realNode.Kind == yaml.SequenceNode { deleteImmediateChildOp := &Operation{
deleteFromArray(candidate, nodesToDelete) OperationType: DeleteImmediateChild,
} else if realNode.Kind == yaml.MappingNode { Value: candidate.Path[len(candidate.Path)-1],
deleteFromMap(candidate, nodesToDelete) }
} else {
log.Debug("Cannot delete from node that's not a map or array %v", NodeToString(candidate)) deleteImmediateChildOpNode := &PathTreeNode{
Operation: deleteImmediateChildOp,
Rhs: createTraversalTree(candidate.Path[0 : len(candidate.Path)-1]),
}
_, err := d.GetMatchingNodes(matchingNodes, deleteImmediateChildOpNode)
if err != nil {
return nil, err
} }
} }
return matchingNodes, nil return matchingNodes, nil
} }
func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) { func DeleteImmediateChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
parents, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
childPath := pathNode.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") log.Debug("deleteFromMap")
node := UnwrapDoc(candidate.Node) node := UnwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
@@ -47,15 +75,10 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
childCandidate := &CandidateNode{ childCandidate := &CandidateNode{
Node: value, Node: value,
Document: candidate.Document, Document: candidate.Document,
Path: append(candidate.Path, key.Value), Path: candidate.CreateChildPath(key.Value),
} }
shouldDelete := false shouldDelete := key.Value == childPath
for el := nodesToDelete.Front(); el != nil && !shouldDelete; el = el.Next() {
if el.Value.(*CandidateNode).GetKey() == childCandidate.GetKey() {
shouldDelete = true
}
}
log.Debugf("shouldDelete %v ? %v", childCandidate.GetKey(), shouldDelete) log.Debugf("shouldDelete %v ? %v", childCandidate.GetKey(), shouldDelete)
@@ -66,7 +89,7 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
node.Content = newContents node.Content = newContents
} }
func deleteFromArray(candidate *CandidateNode, nodesToDelete *list.List) { func deleteFromArray(candidate *CandidateNode, childPath interface{}) {
log.Debug("deleteFromArray") log.Debug("deleteFromArray")
node := UnwrapDoc(candidate.Node) node := UnwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
@@ -75,18 +98,7 @@ func deleteFromArray(candidate *CandidateNode, nodesToDelete *list.List) {
for index := 0; index < len(contents); index = index + 1 { for index := 0; index < len(contents); index = index + 1 {
value := contents[index] value := contents[index]
childCandidate := &CandidateNode{ shouldDelete := fmt.Sprintf("%v", index) == fmt.Sprintf("%v", childPath)
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
}
}
if !shouldDelete { if !shouldDelete {
newContents = append(newContents, value) newContents = append(newContents, value)

View File

@@ -13,6 +13,22 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: cat}\n", "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", description: "Delete entry in array",
document: `[1,2,3]`, document: `[1,2,3]`,
@@ -21,6 +37,14 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::[1, 3]\n", "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", description: "Delete no matches",
document: `{a: cat, b: dog}`, document: `{a: cat, b: dog}`,

View File

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

View File

@@ -21,6 +21,14 @@ var fileOperatorScenarios = []expressionScenario{
"D0, P[], (!!int)::0\n", "D0, P[], (!!int)::0\n",
}, },
}, },
{
description: "Get file index alias",
document: `{a: cat}`,
expression: `fi`,
expected: []string{
"D0, P[], (!!int)::0\n",
},
},
} }
func TestFileOperatorsScenarios(t *testing.T) { func TestFileOperatorsScenarios(t *testing.T) {

View File

@@ -85,7 +85,9 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
var results = list.New() var results = list.New()
// shouldn't recurse arrays if appending // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -104,19 +106,6 @@ func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode,
return lhs, nil 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 { 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)) log.Debugf("merge - applyAssignment lhs %v, rhs: %v", NodeToString(lhs), NodeToString(rhs))
@@ -126,7 +115,7 @@ func applyAssignment(d *dataTreeNavigator, pathIndexToStartFrom int, lhs *Candid
assignmentOp := &Operation{OperationType: AssignAttributes} assignmentOp := &Operation{OperationType: AssignAttributes}
if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode { if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode {
assignmentOp.OperationType = Assign assignmentOp.OperationType = Assign
assignmentOp.Preferences = &AssignOpPreferences{false} assignmentOp.UpdateAssign = false
} else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode { } else if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode {
assignmentOp.OperationType = AddAssign assignmentOp.OperationType = AddAssign
} }

View File

@@ -13,6 +13,23 @@ var pathOperatorScenarios = []expressionScenario{
"D0, P[a b], (!!seq)::- a\n- b\n", "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", description: "Get map key",
document: `{a: {b: cat}}`, document: `{a: {b: cat}}`,

View File

@@ -6,10 +6,16 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type RecursiveDescentPreferences struct {
TraversePreferences *TraversePreferences
RecurseArray bool
}
func RecursiveDescentOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func RecursiveDescentOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
var results = list.New() var results = list.New()
err := recursiveDecent(d, results, matchMap, true) preferences := pathNode.Operation.Preferences.(*RecursiveDescentPreferences)
err := recursiveDecent(d, results, matchMap, preferences)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -17,7 +23,7 @@ func RecursiveDescentOperator(d *dataTreeNavigator, matchMap *list.List, pathNod
return results, nil 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() { for el := matchMap.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
@@ -27,14 +33,14 @@ func recursiveDecent(d *dataTreeNavigator, results *list.List, matchMap *list.Li
results.PushBack(candidate) results.PushBack(candidate)
if candidate.Node.Kind != yaml.AliasNode && len(candidate.Node.Content) > 0 && 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 { if err != nil {
return err return err
} }
err = recursiveDecent(d, results, children, recurseArray) err = recursiveDecent(d, results, children, preferences)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -13,6 +13,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::{}\n", "D0, P[], (!!map)::{}\n",
}, },
}, },
{
skipDoc: true,
document: `{}`,
expression: `...`,
expected: []string{
"D0, P[], (!!map)::{}\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: `[]`, document: `[]`,
@@ -21,6 +29,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[]\n", "D0, P[], (!!seq)::[]\n",
}, },
}, },
{
skipDoc: true,
document: `[]`,
expression: `...`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: `cat`, document: `cat`,
@@ -31,6 +47,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
{ {
skipDoc: true, skipDoc: true,
document: `cat`,
expression: `...`,
expected: []string{
"D0, P[], (!!str)::cat\n",
},
},
{
description: "Recurse map (values only)",
document: `{a: frog}`, document: `{a: frog}`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -38,6 +62,17 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[a], (!!str)::frog\n", "D0, P[a], (!!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, skipDoc: true,
document: `{a: {b: apple}}`, document: `{a: {b: apple}}`,
@@ -48,6 +83,18 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[a b], (!!str)::apple\n", "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, skipDoc: true,
document: `[1,2,3]`, document: `[1,2,3]`,
@@ -59,6 +106,17 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[2], (!!int)::3\n", "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, skipDoc: true,
document: `[{a: cat},2,true]`, document: `[{a: cat},2,true]`,
@@ -71,6 +129,19 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[2], (!!bool)::true\n", "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", description: "Aliases are not traversed",
document: `{a: &cat {c: frog}, b: *cat}`, document: `{a: &cat {c: frog}, b: *cat}`,
@@ -79,6 +150,20 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[a], (!!seq)::- {a: &cat {c: frog}, b: *cat}\n- &cat {c: frog}\n- frog\n- *cat\n", "D0, P[a], (!!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",
},
},
{ {
description: "Merge docs are not traversed", description: "Merge docs are not traversed",
document: mergeDocSample, document: mergeDocSample,
@@ -87,6 +172,14 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"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[foobar], (!!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[foobar], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- c\n- foobar_c\n- !!merge <<\n- *foo\n- thing\n- foobar_thing\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
@@ -100,6 +193,22 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
"D0, P[foobarList c], (!!str)::foobarList_c\n", "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) { func TestRecursiveDescentOperatorScenarios(t *testing.T) {

View File

@@ -4,39 +4,46 @@ import (
"container/list" "container/list"
"fmt" "fmt"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
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, pathNode *PathTreeNode) (*list.List, error) { func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("AssignStyleOperator: %v") log.Debugf("AssignStyleOperator: %v")
var style yaml.Style
if !pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
customStyle := ""
if rhs.Front() != nil { if rhs.Front() != nil {
customStyle = rhs.Front().Value.(*CandidateNode).Node.Value style, err = parseStyle(rhs.Front().Value.(*CandidateNode).Node.Value)
if err != nil {
return nil, err
}
}
} }
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)
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
@@ -46,6 +53,20 @@ func AssignStyleOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNod
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debugf("Setting style of : %v", candidate.GetKey()) log.Debugf("Setting style of : %v", candidate.GetKey())
if pathNode.Operation.UpdateAssign {
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.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 candidate.Node.Style = style
} }

View File

@@ -21,6 +21,22 @@ var styleOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::a: \"cat\"\nb: \"5\"\nc: \"3.2\"\ne: \"true\"\n", "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", description: "Set single quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`, document: `{a: cat, b: 5, c: 3.2, e: true}`,
@@ -70,14 +86,22 @@ e: >-
}, },
}, },
{ {
description: "Pretty print", description: "Reset style - or pretty print",
subdescription: "Set empty (default) quote style", 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}`, document: `{a: cat, "b": 5, 'c': 3.2, "e": true}`,
expression: `.. style=""`, expression: `... style=""`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::a: cat\nb: 5\nc: 3.2\ne: true\n", "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, skipDoc: true,
document: `{a: cat, b: double}`, document: `{a: cat, b: double}`,

View File

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

View File

@@ -19,13 +19,37 @@ 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",
},
},
{
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}`, document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `(.. | select(tag == "!!int")) tag= "!!str"`, expression: `(.. | select(tag == "!!int")) tag= "!!str"`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: cat, b: \"5\", c: 3.2, e: true}\n", "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) { func TestTagOperatorScenarios(t *testing.T) {

View File

@@ -1,46 +1,39 @@
package yqlib package yqlib
import ( import (
"fmt"
"container/list" "container/list"
"fmt"
"strconv"
"github.com/elliotchance/orderedmap" "github.com/elliotchance/orderedmap"
yaml "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3"
) )
type TraversePreferences struct { type TraversePreferences struct {
DontFollowAlias bool FollowAlias bool
IncludeMapKeys bool
} }
func Splat(d *dataTreeNavigator, matches *list.List) (*list.List, error) { func Splat(d *dataTreeNavigator, matches *list.List, prefs *TraversePreferences) (*list.List, error) {
preferences := &TraversePreferences{DontFollowAlias: true} return traverseNodesWithArrayIndices(matches, make([]*yaml.Node, 0), prefs)
splatOperation := &Operation{OperationType: TraversePath, Value: "[]", Preferences: preferences}
splatTreeNode := &PathTreeNode{Operation: splatOperation}
return TraversePathOperator(d, matches, splatTreeNode)
} }
func TraversePathOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func TraversePathOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- Traversing") log.Debugf("-- Traversing")
var matchingNodeMap = list.New() var matchingNodeMap = list.New()
var newNodes []*CandidateNode
var err error
for el := matchMap.Front(); el != nil; el = el.Next() { 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), pathNode.Operation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, n := range newNodes { matchingNodeMap.PushBackList(newNodes)
matchingNodeMap.PushBack(n)
}
} }
return matchingNodeMap, nil 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)) log.Debug("Traversing %v", NodeToString(matchingNode))
value := matchingNode.Node value := matchingNode.Node
@@ -61,34 +54,8 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
switch value.Kind { switch value.Kind {
case yaml.MappingNode: case yaml.MappingNode:
log.Debug("its a map with %v entries", len(value.Content)/2) log.Debug("its a map with %v entries", len(value.Content)/2)
var newMatches = orderedmap.NewOrderedMap() prefs := &TraversePreferences{FollowAlias: true}
err := traverseMap(newMatches, matchingNode, operation) return traverseMap(matchingNode, operation.StringValue, prefs, false)
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
case yaml.SequenceNode: case yaml.SequenceNode:
log.Debug("its a sequence of %v things!", len(value.Content)) log.Debug("its a sequence of %v things!", len(value.Content))
@@ -102,104 +69,117 @@ func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Oper
log.Debug("digging into doc node") log.Debug("digging into doc node")
return traverse(d, &CandidateNode{ return traverse(d, &CandidateNode{
Node: matchingNode.Node.Content[0], Node: matchingNode.Node.Content[0],
Filename: matchingNode.Filename,
FileIndex: matchingNode.FileIndex,
Document: matchingNode.Document}, operation) Document: matchingNode.Document}, operation)
default: default:
return nil, nil return list.New(), nil
} }
} }
func keyMatches(key *yaml.Node, pathNode *Operation) bool { func TraverseArrayOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
return pathNode.Value == "[]" || Match(key.Value, pathNode.StringValue) // rhs is a collect expression that will yield indexes to retreive of the arrays
}
func traverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, operation *Operation) error { rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
// 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
}
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" && followAlias {
log.Debug("Merge anchor")
err := traverseMergeAnchor(newMatches, candidate, value, operation)
if err != nil { if err != nil {
return err return nil, 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)
}
} }
return nil var indicesToTraverse = rhs.Front().Value.(*CandidateNode).Node.Content
prefs := &TraversePreferences{FollowAlias: true}
return traverseNodesWithArrayIndices(matchingNodes, indicesToTraverse, prefs)
} }
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, operation *Operation) error { func traverseNodesWithArrayIndices(matchingNodes *list.List, indicesToTraverse []*yaml.Node, prefs *TraversePreferences) (*list.List, error) {
switch value.Kind { var matchingNodeMap = list.New()
case yaml.AliasNode: for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidateNode := &CandidateNode{ candidate := el.Value.(*CandidateNode)
Node: value.Alias, newNodes, err := traverseArrayIndices(candidate, indicesToTraverse, prefs)
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 { if err != nil {
return err return nil, err
} }
} matchingNodeMap.PushBackList(newNodes)
}
return nil
} }
func traverseArray(candidate *CandidateNode, operation *Operation) ([]*CandidateNode, error) { return matchingNodeMap, nil
log.Debug("operation Value %v", operation.Value) }
if operation.Value == "[]" {
var contents = candidate.Node.Content func traverseArrayIndices(matchingNode *CandidateNode, indicesToTraverse []*yaml.Node, prefs *TraversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse
var newMatches = make([]*CandidateNode, len(contents)) 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
}
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(&CandidateNode{
Node: matchingNode.Node.Content[0],
Filename: matchingNode.Filename,
FileIndex: matchingNode.FileIndex,
Document: matchingNode.Document}, indicesToTraverse, prefs)
}
log.Debugf("OperatorArrayTraverse skipping %v as its a %v", matchingNode, node.Tag)
return list.New(), nil
}
func traverseMapWithIndices(candidate *CandidateNode, indices []*yaml.Node, prefs *TraversePreferences) (*list.List, error) {
if len(indices) == 0 {
return traverseMap(candidate, "", prefs, true)
}
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 var index int64
for index = 0; index < int64(len(contents)); index = index + 1 { for index = 0; index < int64(len(node.Content)); index = index + 1 {
newMatches[index] = &CandidateNode{
newMatches.PushBack(&CandidateNode{
Document: candidate.Document, Document: candidate.Document,
Path: append(candidate.Path, index), Path: candidate.CreateChildPath(index),
Node: contents[index], Node: node.Content[index],
} })
} }
return newMatches, nil return newMatches, nil
} }
switch operation.Value.(type) { for _, indexNode := range indices {
case int64: log.Debug("traverseArrayWithIndices: '%v'", indexNode.Value)
index := operation.Value.(int64) 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 indexToUse := index
contentLength := int64(len(candidate.Node.Content)) contentLength := int64(len(node.Content))
for contentLength <= index { for contentLength <= index {
candidate.Node.Content = append(candidate.Node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}) node.Content = append(node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"})
contentLength = int64(len(candidate.Node.Content)) contentLength = int64(len(node.Content))
} }
if indexToUse < 0 { if indexToUse < 0 {
@@ -210,14 +190,115 @@ func traverseArray(candidate *CandidateNode, operation *Operation) ([]*Candidate
return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength) return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength)
} }
return []*CandidateNode{&CandidateNode{ newMatches.PushBack(&CandidateNode{
Node: candidate.Node.Content[indexToUse], Node: node.Content[indexToUse],
Document: candidate.Document, Document: candidate.Document,
Path: append(candidate.Path, index), Path: candidate.CreateChildPath(index),
}}, nil })
default: }
log.Debug("argument not an int (%v), no array matches", operation.Value) return newMatches, nil
return nil, nil
} }
func keyMatches(key *yaml.Node, wantedKey string) bool {
return Match(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 := &CandidateNode{
Node: valueNode,
Path: append(matchingNode.Path, key),
Document: matchingNode.Document,
}
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 := &CandidateNode{
Node: key,
Path: candidate.CreateChildPath(key.Value),
Document: candidate.Document,
}
newMatches.Set(fmt.Sprintf("keyOf-%v", candidateNode.GetKey()), candidateNode)
}
candidateNode := &CandidateNode{
Node: value,
Path: candidate.CreateChildPath(key.Value),
Document: candidate.Document,
}
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 := &CandidateNode{
Node: value.Alias,
Path: originalCandidate.Path,
Document: originalCandidate.Document,
}
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

@@ -65,7 +65,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
{ {
skipDoc: true, skipDoc: true,
document: `{}`, document: ``,
expression: `.[1].a`, expression: `.[1].a`,
expected: []string{ expected: []string{
"D0, P[1 a], (!!null)::null\n", "D0, P[1 a], (!!null)::null\n",
@@ -137,6 +137,14 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Traversing aliases with splat", description: "Traversing aliases with splat",
document: `{a: &cat {c: frog}, b: *cat}`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b[]`,
expected: []string{
"D0, P[b c], (!!str)::frog\n",
},
},
{
skipDoc: true,
document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.[]`, expression: `.b.[]`,
expected: []string{ expected: []string{
"D0, P[b c], (!!str)::frog\n", "D0, P[b c], (!!str)::frog\n",
@@ -150,12 +158,6 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[b c], (!!str)::frog\n", "D0, P[b c], (!!str)::frog\n",
}, },
}, },
{
skipDoc: true,
document: `[1,2,3]`,
expression: `.b`,
expected: []string{},
},
{ {
description: "Traversing arrays by index", description: "Traversing arrays by index",
document: `[1,2,3]`, document: `[1,2,3]`,
@@ -215,6 +217,16 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Splatting merge anchors", description: "Splatting merge anchors",
document: mergeDocSample, document: mergeDocSample,
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.[]`, expression: `.foobar.[]`,
expected: []string{ expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n", "D0, P[foobar c], (!!str)::foo_c\n",
@@ -266,6 +278,17 @@ var traversePathOperatorScenarios = []expressionScenario{
{ {
description: "Splatting merge anchor lists", description: "Splatting merge anchor lists",
document: mergeDocSample, 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: mergeDocSample,
expression: `.foobarList.[]`, expression: `.foobarList.[]`,
expected: []string{ expected: []string{
"D0, P[foobarList b], (!!str)::bar_b\n", "D0, P[foobarList b], (!!str)::bar_b\n",
@@ -274,6 +297,112 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[foobarList c], (!!str)::foobarList_c\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) { func TestTraversePathOperatorScenarios(t *testing.T) {

View File

@@ -36,13 +36,22 @@ var valueOperatorScenarios = []expressionScenario{
expected: []string{ expected: []string{
"D0, P[], (!!float)::5e-10\n", "D0, P[], (!!float)::5e-10\n",
}, },
}, { },
{
document: ``, document: ``,
expression: `"cat"`, expression: `"cat"`,
expected: []string{ expected: []string{
"D0, P[], (!!str)::cat\n", "D0, P[], (!!str)::cat\n",
}, },
}, { },
{
document: ``,
expression: `"frog jumps"`,
expected: []string{
"D0, P[], (!!str)::frog jumps\n",
},
},
{
document: ``, document: ``,
expression: `"1.3"`, expression: `"1.3"`,
expected: []string{ expected: []string{

View File

@@ -2,6 +2,7 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -33,3 +34,15 @@ func nodeToMap(candidate *CandidateNode) *list.List {
elMap.PushBack(candidate) elMap.PushBack(candidate)
return elMap return elMap
} }
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:])}
}

View File

@@ -18,6 +18,7 @@ type expressionScenario struct {
description string description string
subdescription string subdescription string
document string document string
document2 string
expression string expression string
expected []string expected []string
skipDoc bool skipDoc bool
@@ -38,9 +39,17 @@ func testScenario(t *testing.T, s *expressionScenario) {
if s.document != "" { if s.document != "" {
inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml", 0) inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml", 0)
if err != nil { if err != nil {
t.Error(err, s.document) t.Error(err, s.document, s.expression)
return return
} }
if s.document2 != "" {
moreInputs, err := readDocuments(strings.NewReader(s.document2), "another.yml", 1)
if err != nil {
t.Error(err, s.document, s.expression)
return
}
inputs.PushBackList(moreInputs)
}
} else { } else {
candidateNode := &CandidateNode{ candidateNode := &CandidateNode{
Document: 0, Document: 0,
@@ -55,7 +64,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
results, err = treeNavigator.GetMatchingNodes(inputs, node) results, err = treeNavigator.GetMatchingNodes(inputs, node)
if err != nil { if err != nil {
t.Error(err) t.Error(fmt.Errorf("%v: %v", err, s.expression))
return return
} }
test.AssertResultComplexWithContext(t, s.expected, resultsToString(results), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document)) test.AssertResultComplexWithContext(t, s.expected, resultsToString(results), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document))
@@ -92,7 +101,7 @@ func copyFromHeader(title string, out *os.File) error {
return err return err
} }
func formatYaml(yaml string) string { func formatYaml(yaml string, filename string) string {
var output bytes.Buffer var output bytes.Buffer
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true) printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
@@ -101,7 +110,7 @@ func formatYaml(yaml string) string {
panic(err) panic(err)
} }
streamEvaluator := NewStreamEvaluator() streamEvaluator := NewStreamEvaluator()
err = streamEvaluator.Evaluate("sample.yaml", strings.NewReader(yaml), node, printer) err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -128,63 +137,116 @@ func documentScenarios(t *testing.T, title string, scenarios []expressionScenari
for _, s := range scenarios { for _, s := range scenarios {
if !s.skipDoc { if !s.skipDoc {
documentScenario(t, w, s)
}
}
w.Flush()
}
func documentScenario(t *testing.T, w *bufio.Writer, s expressionScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description)) writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" { if s.subdescription != "" {
writeOrPanic(w, s.subdescription) writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n") writeOrPanic(w, "\n\n")
} }
formattedDoc, formattedDoc2 := documentInput(w, s)
writeOrPanic(w, "will output\n")
documentOutput(t, w, s, formattedDoc, formattedDoc2)
}
func documentInput(w *bufio.Writer, s expressionScenario) (string, string) {
formattedDoc := "" formattedDoc := ""
formattedDoc2 := ""
command := "eval"
if s.document != "" { if s.document != "" {
if s.dontFormatInputForDoc { if s.dontFormatInputForDoc {
formattedDoc = s.document + "\n" formattedDoc = s.document + "\n"
} else { } else {
formattedDoc = formatYaml(s.document) formattedDoc = formatYaml(s.document, "sample.yml")
} }
//TODO: pretty here
writeOrPanic(w, "Given a sample.yml file of:\n")
writeOrPanic(w, "Given a sample.yml file of:\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n", formattedDoc)) writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n", formattedDoc))
files := "sample.yml"
if s.document2 != "" {
if s.dontFormatInputForDoc {
formattedDoc2 = s.document2 + "\n"
} else {
formattedDoc2 = formatYaml(s.document2, "another.yml")
}
writeOrPanic(w, "And another sample another.yml file of:\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n", formattedDoc2))
files = "sample.yml another.yml"
command = "eval-all"
}
writeOrPanic(w, "then\n") writeOrPanic(w, "then\n")
if s.expression != "" { if s.expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq eval '%v' sample.yml\n```\n", s.expression)) writeOrPanic(w, fmt.Sprintf("```bash\nyq %v '%v' %v\n```\n", command, s.expression, files))
} else { } else {
writeOrPanic(w, "```bash\nyq eval sample.yml\n```\n") writeOrPanic(w, fmt.Sprintf("```bash\nyq %v %v\n```\n", command, files))
} }
} else { } else {
writeOrPanic(w, "Running\n") writeOrPanic(w, "Running\n")
writeOrPanic(w, fmt.Sprintf("```bash\nyq eval --null-input '%v'\n```\n", s.expression)) writeOrPanic(w, fmt.Sprintf("```bash\nyq %v --null-input '%v'\n```\n", command, s.expression))
}
return formattedDoc, formattedDoc2
} }
writeOrPanic(w, "will output\n") func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formattedDoc string, formattedDoc2 string) {
var output bytes.Buffer var output bytes.Buffer
var err error var err error
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true) printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
streamEvaluator := NewStreamEvaluator()
if s.document != "" {
node, err := treeCreator.ParsePath(s.expression) node, err := treeCreator.ParsePath(s.expression)
if err != nil { if err != nil {
t.Error(err) t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err))
return
} }
err = streamEvaluator.Evaluate("sample.yaml", strings.NewReader(formattedDoc), node, printer)
inputs := list.New()
if s.document != "" {
inputs, err = readDocuments(strings.NewReader(formattedDoc), "sample.yml", 0)
if err != nil { if err != nil {
t.Error(err) t.Error(err, s.document, s.expression)
return
}
if s.document2 != "" {
moreInputs, err := readDocuments(strings.NewReader(formattedDoc2), "another.yml", 1)
if err != nil {
t.Error(err, s.document, s.expression)
return
}
inputs.PushBackList(moreInputs)
} }
} else { } else {
err = streamEvaluator.EvaluateNew(s.expression, printer) candidateNode := &CandidateNode{
if err != nil { Document: 0,
t.Error(err) Filename: "",
Node: &yaml.Node{Tag: "!!null"},
FileIndex: 0,
} }
inputs.PushBack(candidateNode)
}
results, err := treeNavigator.GetMatchingNodes(inputs, node)
if err != nil {
t.Error(err, s.expression)
}
err = printer.PrintResults(results)
if err != nil {
t.Error(err, s.expression)
} }
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", output.String())) writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", output.String()))
}
}
w.Flush()
} }

View File

@@ -17,20 +17,35 @@ var pathTests = []struct {
append(make([]interface{}, 0), "[", "]"), append(make([]interface{}, 0), "[", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE"), 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[]`, `.a[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]"), append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
}, },
{ {
`.a.[]`, `.a.[]`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]"), append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE"), 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`, `.a[].c`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]", "SHORT_PIPE", "c"), append(make([]interface{}, 0), "a", "SHORT_PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "c"),
append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE", "c", "SHORT_PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "c", "SHORT_PIPE"),
}, },
{ {
`[3]`, `[3]`,
@@ -44,18 +59,18 @@ var pathTests = []struct {
}, },
{ {
`.a | .[].b == "apple"`, `.a | .[].b == "apple"`,
append(make([]interface{}, 0), "a", "PIPE", "[]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"), append(make([]interface{}, 0), "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "[]", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "apple (string)", "EQUALS", "PIPE"),
}, },
{ {
`(.a | .[].b) == "apple"`, `(.a | .[].b) == "apple"`,
append(make([]interface{}, 0), "(", "a", "PIPE", "[]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"), append(make([]interface{}, 0), "(", "a", "PIPE", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "b", ")", "EQUALS", "apple (string)"),
append(make([]interface{}, 0), "a", "[]", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "b", "SHORT_PIPE", "PIPE", "apple (string)", "EQUALS"),
}, },
{ {
`.[] | select(. == "*at")`, `.[] | select(. == "*at")`,
append(make([]interface{}, 0), "[]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"), append(make([]interface{}, 0), "TRAVERSE_ARRAY", "[", "]", "PIPE", "SELECT", "(", "SELF", "EQUALS", "*at (string)", ")"),
append(make([]interface{}, 0), "[]", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"), append(make([]interface{}, 0), "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SELF", "*at (string)", "EQUALS", "SELECT", "PIPE"),
}, },
{ {
`[true]`, `[true]`,
@@ -89,8 +104,8 @@ var pathTests = []struct {
}, },
{ {
`{.a: .c, .b.[]: .f.g.[]}`, `{.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", "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", "[]", "SHORT_PIPE", "f", "g", "SHORT_PIPE", "[]", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"), 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)`, `explode(.a.b)`,
@@ -122,7 +137,11 @@ var pathTests = []struct {
append(make([]interface{}, 0), "SELF", "ASSIGN_COMMENT", "str (string)"), append(make([]interface{}, 0), "SELF", "ASSIGN_COMMENT", "str (string)"),
append(make([]interface{}, 0), "SELF", "str (string)", "ASSIGN_COMMENT"), 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"`, `.a.b tag="!!str"`,
append(make([]interface{}, 0), "a", "SHORT_PIPE", "b", "ASSIGN_TAG", "!!str (string)"), append(make([]interface{}, 0), "a", "SHORT_PIPE", "b", "ASSIGN_TAG", "!!str (string)"),

View File

@@ -22,7 +22,7 @@ const (
CloseCollect CloseCollect
OpenCollectObject OpenCollectObject
CloseCollectObject CloseCollectObject
SplatOrEmptyCollect TraverseArrayCollect
) )
type Token struct { type Token struct {
@@ -49,8 +49,9 @@ func (t *Token) toString() string {
return "{" return "{"
} else if t.TokenType == CloseCollectObject { } else if t.TokenType == CloseCollectObject {
return "}" return "}"
} else if t.TokenType == SplatOrEmptyCollect { } else if t.TokenType == TraverseArrayCollect {
return "[]?" return ".["
} else { } else {
return "NFI" return "NFI"
} }
@@ -91,6 +92,15 @@ func opAssignableToken(opType *OperationType, assignOpType *OperationType) lex.A
return opTokenWithPrefs(opType, assignOpType, nil) return opTokenWithPrefs(opType, assignOpType, nil)
} }
func assignOpToken(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignOpToken %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{OperationType: Assign, Value: Assign.Type, StringValue: value, UpdateAssign: updateAssign}
return &Token{TokenType: OperationToken, Operation: op}, nil
}
}
func opTokenWithPrefs(op *OperationType, assignOpType *OperationType, preferences interface{}) lex.Action { func opTokenWithPrefs(op *OperationType, assignOpType *OperationType, preferences interface{}) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("opTokenWithPrefs %v", string(m.Bytes)) log.Debug("opTokenWithPrefs %v", string(m.Bytes))
@@ -104,6 +114,21 @@ func opTokenWithPrefs(op *OperationType, assignOpType *OperationType, preference
} }
} }
func assignAllCommentsOp(updateAssign bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("assignAllCommentsOp %v", string(m.Bytes))
value := string(m.Bytes)
op := &Operation{
OperationType: AssignComment,
Value: AssignComment.Type,
StringValue: value,
UpdateAssign: updateAssign,
Preferences: &CommentOpPreferences{LineComment: true, HeadComment: true, FootComment: true},
}
return &Token{TokenType: OperationToken, Operation: op}, nil
}
}
func literalToken(pType TokenType, checkForPost bool) lex.Action { func literalToken(pType TokenType, checkForPost bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
return &Token{TokenType: pType, CheckForPostTraverse: checkForPost}, nil return &Token{TokenType: pType, CheckForPostTraverse: checkForPost}, nil
@@ -114,23 +139,6 @@ func unwrap(value string) string {
return value[1 : len(value)-1] return value[1 : len(value)-1]
} }
func arrayIndextoken(precedingDot bool) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
startIndex := 1
if precedingDot {
startIndex = 2
}
numberString = numberString[startIndex : len(numberString)-1]
var number, errParsingInt = strconv.ParseInt(numberString, 10, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
op := &Operation{OperationType: TraversePath, Value: number, StringValue: numberString}
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
}
}
func numberValue() lex.Action { func numberValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes) var numberString = string(m.Bytes)
@@ -188,8 +196,12 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\(`), literalToken(OpenBracket, false)) lexer.Add([]byte(`\(`), literalToken(OpenBracket, false))
lexer.Add([]byte(`\)`), literalToken(CloseBracket, true)) lexer.Add([]byte(`\)`), literalToken(CloseBracket, true))
lexer.Add([]byte(`\.\[\]`), pathToken(false)) lexer.Add([]byte(`\.\[`), literalToken(TraverseArrayCollect, false))
lexer.Add([]byte(`\.\.`), opToken(RecursiveDescent)) lexer.Add([]byte(`\.\.`), opTokenWithPrefs(RecursiveDescent, nil, &RecursiveDescentPreferences{RecurseArray: true,
TraversePreferences: &TraversePreferences{FollowAlias: false, IncludeMapKeys: false}}))
lexer.Add([]byte(`\.\.\.`), opTokenWithPrefs(RecursiveDescent, nil, &RecursiveDescentPreferences{RecurseArray: true,
TraversePreferences: &TraversePreferences{FollowAlias: false, IncludeMapKeys: true}}))
lexer.Add([]byte(`,`), opToken(Union)) lexer.Add([]byte(`,`), opToken(Union))
lexer.Add([]byte(`:\s*`), opToken(CreateMap)) lexer.Add([]byte(`:\s*`), opToken(CreateMap))
@@ -201,14 +213,19 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`or`), opToken(Or)) lexer.Add([]byte(`or`), opToken(Or))
lexer.Add([]byte(`and`), opToken(And)) lexer.Add([]byte(`and`), opToken(And))
lexer.Add([]byte(`not`), opToken(Not)) lexer.Add([]byte(`not`), opToken(Not))
lexer.Add([]byte(`\/\/`), opToken(Alternative))
lexer.Add([]byte(`documentIndex`), opToken(GetDocumentIndex)) lexer.Add([]byte(`documentIndex`), opToken(GetDocumentIndex))
lexer.Add([]byte(`di`), opToken(GetDocumentIndex))
lexer.Add([]byte(`style`), opAssignableToken(GetStyle, AssignStyle)) lexer.Add([]byte(`style`), opAssignableToken(GetStyle, AssignStyle))
lexer.Add([]byte(`tag`), opAssignableToken(GetTag, AssignTag)) lexer.Add([]byte(`tag`), opAssignableToken(GetTag, AssignTag))
lexer.Add([]byte(`anchor`), opAssignableToken(GetAnchor, AssignAnchor))
lexer.Add([]byte(`alias`), opAssignableToken(GetAlias, AssignAlias))
lexer.Add([]byte(`filename`), opToken(GetFilename)) lexer.Add([]byte(`filename`), opToken(GetFilename))
lexer.Add([]byte(`fileIndex`), opToken(GetFileIndex)) lexer.Add([]byte(`fileIndex`), opToken(GetFileIndex))
lexer.Add([]byte(`fi`), opToken(GetFileIndex))
lexer.Add([]byte(`path`), opToken(GetPath)) lexer.Add([]byte(`path`), opToken(GetPath))
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{LineComment: true})) lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{LineComment: true}))
@@ -217,18 +234,17 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`footComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{FootComment: true})) lexer.Add([]byte(`footComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{FootComment: true}))
lexer.Add([]byte(`comments\s*=`), opTokenWithPrefs(AssignComment, nil, &CommentOpPreferences{LineComment: true, HeadComment: true, FootComment: true})) lexer.Add([]byte(`comments\s*=`), assignAllCommentsOp(false))
lexer.Add([]byte(`comments\s*\|=`), assignAllCommentsOp(true))
lexer.Add([]byte(`collect`), opToken(Collect)) lexer.Add([]byte(`collect`), opToken(Collect))
lexer.Add([]byte(`\s*==\s*`), opToken(Equals)) lexer.Add([]byte(`\s*==\s*`), opToken(Equals))
lexer.Add([]byte(`\s*=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{false})) lexer.Add([]byte(`\s*=\s*`), assignOpToken(false))
lexer.Add([]byte(`del`), opToken(DeleteChild)) lexer.Add([]byte(`del`), opToken(DeleteChild))
lexer.Add([]byte(`\s*\|=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{true})) lexer.Add([]byte(`\s*\|=\s*`), assignOpToken(true))
lexer.Add([]byte(`\.\[-?[0-9]+\]`), arrayIndextoken(true))
lexer.Add([]byte("( |\t|\n|\r)+"), skip) lexer.Add([]byte("( |\t|\n|\r)+"), skip)
@@ -251,8 +267,6 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`"[^"]*"`), stringValue(true)) lexer.Add([]byte(`"[^"]*"`), stringValue(true))
lexer.Add([]byte(`\[\]`), literalToken(SplatOrEmptyCollect, true))
lexer.Add([]byte(`\[`), literalToken(OpenCollect, false)) lexer.Add([]byte(`\[`), literalToken(OpenCollect, false))
lexer.Add([]byte(`\]`), literalToken(CloseCollect, true)) lexer.Add([]byte(`\]`), literalToken(CloseCollect, true))
lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false)) lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false))
@@ -321,30 +335,23 @@ func (p *pathTokeniser) Tokenise(path string) ([]*Token, error) {
func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTokens []*Token) (tokensAccum []*Token, skipNextToken bool) { func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTokens []*Token) (tokensAccum []*Token, skipNextToken bool) {
skipNextToken = false skipNextToken = false
token := tokens[index] token := tokens[index]
if token.TokenType == SplatOrEmptyCollect {
if index > 0 && tokens[index-1].TokenType == OperationToken &&
tokens[index-1].Operation.OperationType == TraversePath {
// must be a splat without a preceding dot , e.g. .a[]
// lets put a pipe in front of it, and convert it to a traverse "[]" token
pipeOp := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: pipeOp}) if token.TokenType == TraverseArrayCollect {
//need to put a traverse array then a collect token
// do this by adding traverse then converting token to collect
traverseOp := &Operation{OperationType: TraversePath, Value: "[]", StringValue: "[]"} op := &Operation{OperationType: TraverseArray, StringValue: "TRAVERSE_ARRAY"}
token = &Token{TokenType: OperationToken, Operation: traverseOp, CheckForPostTraverse: true} postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
token = &Token{TokenType: OpenCollect}
} else {
// gotta be a collect empty array, we need to split this into two tokens
// one OpenCollect, the other CloseCollect
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OpenCollect})
token = &Token{TokenType: CloseCollect, CheckForPostTraverse: true}
}
} }
if index != len(tokens)-1 && token.AssignOperation != nil && if index != len(tokens)-1 && token.AssignOperation != nil &&
tokens[index+1].TokenType == OperationToken && tokens[index+1].TokenType == OperationToken &&
tokens[index+1].Operation.OperationType == Assign { tokens[index+1].Operation.OperationType == Assign {
token.Operation = token.AssignOperation token.Operation = token.AssignOperation
token.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
skipNextToken = true skipNextToken = true
} }
@@ -356,5 +363,21 @@ func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTok
op := &Operation{OperationType: ShortPipe, Value: "PIPE"} op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op}) postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
} }
if index != len(tokens)-1 && token.CheckForPostTraverse &&
tokens[index+1].TokenType == OpenCollect {
op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
op = &Operation{OperationType: TraverseArray}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
if index != len(tokens)-1 && token.CheckForPostTraverse &&
tokens[index+1].TokenType == TraverseArrayCollect {
op := &Operation{OperationType: ShortPipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
return postProcessedTokens, skipNextToken return postProcessedTokens, skipNextToken
} }

View File

@@ -1,16 +1,11 @@
- increment version in version.go - increment version in version.go
- increment version in snapcraft.yaml - increment version in snapcraft.yaml
- commit - increment version in github-action/Dockerfile
- tag git with same version number
- make sure local build passes - make sure local build passes
- push tag to git - tag git with same version number
- 3.4.0, v3 - commit vX tag - this will trigger github actions
- git push --tags - use github actions to publish docker and make github release
- make local xcompile (builds binaries for all platforms) - check github updated yq action in marketplace
- git release
./scripts/release.sh
./scripts/upload.sh
- snapcraft - snapcraft
- will auto create a candidate, test it works then promote - will auto create a candidate, test it works then promote

View File

@@ -3,7 +3,12 @@
set -o errexit set -o errexit
set -o pipefail set -o pipefail
if command -v golangci-lint &> /dev/null
then
golangci-lint run --timeout=5m
else
./bin/golangci-lint run --timeout=5m ./bin/golangci-lint run --timeout=5m
fi
# ./bin/golangci-lint \ # ./bin/golangci-lint \
# --tests \ # --tests \

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -ex
VERSION="$(git describe --tags --abbrev=0)"
docker build \
--target production \
--build-arg VERSION=${VERSION} \
-t mikefarah/yq:latest \
-t mikefarah/yq:${VERSION} \
.

View File

@@ -1,21 +0,0 @@
#!/bin/bash
set -ex
GITHUB_TOKEN="${GITHUB_TOKEN:?missing required input \'GITHUB_TOKEN\'}"
CURRENT="$(git describe --tags --abbrev=0)"
PREVIOUS="$(git describe --tags --abbrev=0 --always "${CURRENT}"^)"
OWNER="mikefarah"
REPO="yq"
release() {
github-release release \
--user "$OWNER" \
--draft \
--repo "$REPO" \
--tag "$CURRENT"
}
release

View File

@@ -1,28 +0,0 @@
#!/bin/bash
set -ex
GITHUB_TOKEN="${GITHUB_TOKEN:?missing required input \'GITHUB_TOKEN\'}"
CURRENT="$(git describe --tags --abbrev=0)"
PREVIOUS="$(git describe --tags --abbrev=0 --always "${CURRENT}"^)"
OWNER="mikefarah"
REPO="yq"
upload() {
mkdir -p ./build-done
while IFS= read -r -d $'\0'; do
file=$REPLY
BINARY=$(basename "${file}")
echo "--> ${BINARY}"
github-release upload \
--replace \
--user "$OWNER" \
--repo "$REPO" \
--tag "$CURRENT" \
--name "${BINARY}" \
--file "$file"
mv "$file" "./build-done/${BINARY}"
done < <(find ./build -mindepth 1 -maxdepth 1 -print0)
}
upload

View File

@@ -1,14 +1,24 @@
#!/bin/bash #!/bin/bash
set -e
# This assumes that gonative and gox is installed as per the 'one time setup' instructions # This assumes that gonative and gox is installed as per the 'one time setup' instructions
# at https://github.com/inconshreveable/gonative # at https://github.com/inconshreveable/gonative
CGO_ENABLED=0 gox -ldflags "${LDFLAGS}" -output="build/yq_{{.OS}}_{{.Arch}}" CGO_ENABLED=0 gox -ldflags "${LDFLAGS}" -output="build/yq_{{.OS}}_{{.Arch}}" --osarch="darwin/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/s390x netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 windows/386 windows/amd64"
# include non-default linux builds too
CGO_ENABLED=0 gox -ldflags "${LDFLAGS}" -os=linux -output="build/yq_{{.OS}}_{{.Arch}}"
cd build cd build
rhash -r -a . -P -o checksums rhash -r -a . -o checksums
rhash --list-hashes > checksums_hashes_order rhash --list-hashes > checksums_hashes_order
find . -executable -type f | xargs -I {} tar czvf {}.tar.gz {}
# just in case find thinks this is executable...
rm -f checksums_hashes_order.tar.gz
rm -f checksums.tar.gz
rm yq_windows_386.exe.tar.gz
rm yq_windows_amd64.exe.tar.gz
zip yq_windows_386.zip yq_windows_386.exe
zip yq_windows_amd64.zip yq_windows_amd64.exe

View File

@@ -1,5 +1,5 @@
name: yq name: yq
version: '4.0.0' version: '4.2.1'
summary: A lightweight and portable command-line YAML processor summary: A lightweight and portable command-line YAML processor
description: | description: |
The aim of the project is to be the jq or sed of yaml files. The aim of the project is to be the jq or sed of yaml files.

BIN
yq_linux_amd64.tar.gz Normal file

Binary file not shown.

View File

@@ -1,60 +0,0 @@
package main
// import (
// "fmt"
// "runtime"
// "testing"
// "github.com/mikefarah/yq/v2/pkg/marshal"
// "github.com/mikefarah/yq/v2/test"
// )
// func TestMultilineString(t *testing.T) {
// testString := `
// abcd
// efg`
// formattedResult, _ := marshal.NewYamlConverter().YamlToString(testString, false)
// test.AssertResult(t, testString, formattedResult)
// }
// func TestNewYaml(t *testing.T) {
// result, _ := newYaml([]string{"b.c", "3"})
// formattedResult := fmt.Sprintf("%v", result)
// test.AssertResult(t,
// "[{b [{c 3}]}]",
// formattedResult)
// }
// func TestNewYamlArray(t *testing.T) {
// result, _ := newYaml([]string{"[0].cat", "meow"})
// formattedResult := fmt.Sprintf("%v", result)
// test.AssertResult(t,
// "[[{cat meow}]]",
// formattedResult)
// }
// func TestNewYaml_WithScript(t *testing.T) {
// writeScript = "examples/instruction_sample.yaml"
// expectedResult := `b:
// c: cat
// e:
// - name: Mike Farah`
// result, _ := newYaml([]string{""})
// actualResult, _ := marshal.NewYamlConverter().YamlToString(result, true)
// test.AssertResult(t, expectedResult, actualResult)
// }
// func TestNewYaml_WithUnknownScript(t *testing.T) {
// writeScript = "fake-unknown"
// _, err := newYaml([]string{""})
// if err == nil {
// t.Error("Expected error due to unknown file")
// }
// var expectedOutput string
// if runtime.GOOS == "windows" {
// expectedOutput = `open fake-unknown: The system cannot find the file specified.`
// } else {
// expectedOutput = `open fake-unknown: no such file or directory`
// }
// test.AssertResult(t, expectedOutput, err.Error())
// }

BIN
yqt Executable file

Binary file not shown.