Compare commits

..
Author SHA1 Message Date
Mike Farah 9c98d3effe Man page release workflow wip 2021-10-20 18:45:14 +11:00
Mike Farah ee376fdfd2 Man page release workflow wip 2021-10-20 15:01:08 +11:00
Mike Farah e5f389d0c2 Added test release flow 2021-10-20 11:11:52 +11:00
Mike Farah 7b92aa0fcf Bumping go-lang, docker versions 2021-10-20 10:58:54 +11:00
Mike Farah 5819dd0853 Fixed expression parsing bug #970 2021-10-18 11:47:03 +11:00
Mike Farah bc04873292 Man page 2021-10-18 11:03:53 +11:00
Mike Farah 7fdd205858 Updated Readme 2021-10-15 11:47:54 +11:00
Mike Farah 67541a2fe9 Keep flag, it is needed in corner cases 2021-10-15 11:47:00 +11:00
Mike Farah 0064b68ed9 Revert "Removed leadingContentPreProcessing flag - header preprocessing is stable"
This reverts commit 4ce9433468.
2021-10-15 11:47:00 +11:00
Mike Farah e741e6d52f Man page wip 2021-10-15 10:39:33 +11:00
Mike Farah 948399f163 Man page wip 2021-10-15 09:59:08 +11:00
Mike Farah 0436b77d91 Man page wip 2021-10-15 09:51:42 +11:00
Mike Farah 4ce9433468 Removed leadingContentPreProcessing flag - header preprocessing is stable 2021-10-15 08:53:00 +11:00
Peter MatseykanetsandMike Farah dd259b4957 Make deepMatch report in linear time
The current implementation of the deepMatch() has the exponential runtime.
Given the long enough input and the pattern with multiple wildcards
it takes a while if ever to complete which can potentially be used
maliciously to cause a denial of service (cpu and memory consumption).

E.g. running this in the root of this repository
time yq eval '.jobs.publishDocker.steps.[] | select (.run == "****outputs")' .github/workflows/release.yml
gives on my laptop
25.11s user 0.06s system 99% cpu 25.182 total

Whereas the updated implementation gives
0.01s user 0.01s system 36% cpu 0.049 total

There are numerous similar CVEs reported for glob evaluation in
different shells/ftp-servers/libraries.

The replacement implementation with the linear runtime is shamelessly taken
verbatim from the briliant article by Russ Cox https://research.swtch.com/glob
2021-10-14 18:45:25 +11:00
Chris KerrandMike Farah 2da2001651 Update README with recently added / changed options
In particular, remove reference to the deprecated `--tojson` option.
2021-10-14 18:40:58 +11:00
Mike Farah d363963f98 Bump version 2021-10-11 14:50:17 +11:00
Mike Farah ed4d888bfa Speed up multiply 2021-10-11 14:46:46 +11:00
Mike Farah 79bc1f95cb Slight performance improvement to context.ChildContext 2021-10-11 12:56:27 +11:00
Mike Farah 4a4aae00af Sped up explode operator 2021-10-11 10:41:15 +11:00
Mike Farah 839f795710 Fixed select bug (#958) 2021-10-07 15:04:36 +11:00
Emmanuel BourgandMike Farah e87808683a Skip the tests if the nocheck Debian build option is specified 2021-10-07 13:44:16 +11:00
Emmanuel BourgandMike Farah bec30e910f Fix a typo in root.go 2021-10-07 13:43:57 +11:00
Mike Farah b8aeb76f29 Bump version 2021-10-02 15:14:45 +10:00
Mike Farah 989b11764d Updating to go 1.17 to fix CVE #944 2021-10-02 15:12:57 +10:00
Mike Farah e052ff8025 New release with docker build fixes 2021-09-20 11:58:21 +10:00
Mike Farah 111c1a0e4a Fixed docker timeout - simplify docker builds 2021-09-20 11:57:05 +10:00
Mike Farah 222bfd3d17 Bumping version 2021-09-20 10:40:27 +10:00
Mike Farah 4123669efe Help text tweak 2021-09-20 09:57:41 +10:00
Mike Farah 7caefc8521 minor readme cleanup 2021-09-20 09:55:35 +10:00
Roberto Mier EscandonandMike Farah 91018683bf Bump deb to version 4.13.0 2021-09-17 08:44:13 +10:00
Mike Farah 282d047dc2 Added STDIN example to the top 2021-09-17 08:38:10 +10:00
Mike Farah b68e6adce9 Added STDIN example to the top 2021-09-17 08:37:28 +10:00
Mike Farah 43e40a6dda Updating with documentation 2021-09-16 10:55:26 +10:00
Mike Farah 2292f0ffb4 Fixed with semicolon space issue 2021-09-15 22:24:03 +10:00
Mike Farah 3339562aa2 Bumping version 2021-09-15 15:19:19 +10:00
Mike Farah 2db8140d7f Added contains operator 2021-09-15 15:18:10 +10:00
Mike Farah 5f154eb1b6 Update release notes 2021-09-12 21:59:57 +10:00
Mike Farah 084a0f6f1e Merge branch 'subtract' 2021-09-12 21:59:07 +10:00
Mike Farah 457c977e61 Update release notes 2021-09-12 21:53:50 +10:00
Mike Farah 6002604251 Added with operator 2021-09-12 21:52:02 +10:00
Mike Farah 0c3334d838 Draft release notes 2021-09-12 17:01:11 +10:00
Mike Farah b2ee131a4c Updated var to work like jq #934 2021-09-12 16:55:55 +10:00
Mike Farah 1cfbbde796 Added missing closing bracket error check 2021-09-12 15:49:50 +10:00
YABATANIEN, MurichadukeandMike Farah 25ba763b08 supports multiline output 2021-09-12 13:09:54 +10:00
Mike Farah 13c42db238 Adding subtraction support for arrays 2021-09-07 16:58:34 +10:00
Mike Farah 6ba8dc75a6 Bumping version 2021-09-05 11:11:51 +10:00
Mike Farah 3543a2dbdc Fixed union infinite loop #930 2021-09-05 11:07:40 +10:00
Mike Farah 8941573c1a Fixing alternative op bug #930 2021-09-02 15:36:23 +10:00
Mike Farah eea2c97cd8 Can process hex numbers 2021-09-02 15:26:44 +10:00
Mike Farah f848e334ee Clarifying readme 2021-08-27 12:30:22 +10:00
Mike Farah b72d225092 Bumping version 2021-08-27 09:33:57 +10:00
Mike Farah cb95ab1494 Fixed merge comment issue #919 2021-08-26 16:31:26 +10:00
Mike Farah 063d40de25 Fixing pipeline 2021-08-25 10:09:44 +10:00
Mike Farah fe12407c93 Bumping version 2021-08-20 16:03:59 +10:00
Mike Farah d18aa3e9e0 Added shorthand output formats 2021-08-20 15:46:33 +10:00
Mike Farah 10d4eb3381 Updated to use -o for output format 2021-08-20 13:35:57 +10:00
Mike Farah 043e9128d7 Pretty Print tests 2021-08-20 12:23:35 +10:00
Mike Farah 43f47c4ed9 wip - pretty print 1.1 compat mode 2021-08-20 12:23:35 +10:00
Mike Farah 4ba96d902b Fixed document header/footer comment handling when merging 2021-08-17 09:54:12 +10:00
Mike Farah d2e89f7c72 Updated README to include instructions on docker 2021-08-15 21:22:39 +10:00
Mike Farah 9de0d0aae1 Adding string readme 2021-08-01 18:47:04 +10:00
Mike Farah 669f6cf127 Added properties encoder test 2021-07-27 21:51:27 +10:00
Mike Farah 8c1a96d121 Properties encoder wip 2021-07-25 18:08:33 +10:00
Mike Farah b64982a487 Properties encoder wip 2021-07-25 11:43:51 +10:00
Mike Farah 9fd467590f Add github action docs to readme 2021-07-25 10:51:13 +10:00
Mike Farah 39090fcf58 updating readme 2021-07-25 10:47:53 +10:00
Mike Farah f89a133558 Version bump 2021-07-24 15:08:30 +10:00
Mike Farah d079c5709e bad github action now fails properly 2021-07-24 15:07:05 +10:00
Mike Farah 192a5ed0d2 testing bad github action fails 2021-07-24 15:06:23 +10:00
Mike Farah bdf4ad4432 testing bad github action fails 2021-07-24 15:05:42 +10:00
Mike Farah eeac03a437 Fixed merging arrays with merge anchors #899 2021-07-22 20:58:58 +10:00
Mike Farah 9ec20f8ba2 Added more tests 2021-07-20 11:14:54 +10:00
Mike Farah b2380399a8 Added more tests 2021-07-20 11:12:41 +10:00
Mike Farah 66b248ac18 Version bump 2021-07-20 11:05:22 +10:00
Mike Farah 3b91ad5764 Handle leading comment with no new-line 2021-07-20 11:01:56 +10:00
Mike Farah 8508d3309b More efficient front matter processor 2021-07-20 10:38:42 +10:00
Mike Farah 4e628327c4 Better way of processing leading content 2021-07-20 10:19:55 +10:00
Mike Farah bbebebe30c Fixed for most cases, except strip comments 2021-07-20 09:18:40 +10:00
Mike Farah 38dd4175fb Added more tests 2021-07-19 20:18:42 +10:00
Mike Farah dafa114e65 Added printer tests 2021-07-19 20:12:04 +10:00
Mike Farah 519cf1dcd7 wip 2021-07-19 19:58:47 +10:00
Mike Farah 3a6f73e836 wip 2021-07-19 19:52:51 +10:00
Mike Farah 85bbbbeed4 shellcheck 2021-07-18 17:05:12 +10:00
Mike Farah 7474ac62ef Now using shunit2 for acceptance tests 2021-07-18 16:55:08 +10:00
Mike Farah 4c288e8d90 Removed blank file disclaimer 2021-07-18 15:39:28 +10:00
Mike Farah 102e7e7ab0 Version bump 2021-07-18 13:45:21 +10:00
Mike Farah 9c8253b582 Front matter processor seems to be working! 2021-07-18 13:17:35 +10:00
Mike Farah 555ad0762c Added front-matter handler 2021-07-18 12:28:46 +10:00
Mike Farah f6e2ab5cef Remember comments in empty files 2021-07-16 22:08:22 +10:00
Mike Farah 28a8721f49 Handle empty files 2021-07-16 21:08:20 +10:00
Mike Farah a13617407e Fixed EOF bug when processing empty files 2021-07-16 20:56:22 +10:00
Mike Farah 171ca2e053 Improving reg ex doc 2021-07-16 10:18:25 +10:00
Mike Farah 2f0fe8aa13 Increment version 2021-07-16 10:09:12 +10:00
Mike Farah b76c9f6e57 added more readme disclaimers 2021-07-16 10:06:28 +10:00
Mike Farah 8e72f65ddc fixing github flow 2021-07-16 09:59:47 +10:00
Mike Farah 19baeec5c9 set github default user 2021-07-16 09:58:40 +10:00
Mike Farah 19fb1044b7 testing github action 2021-07-16 09:44:50 +10:00
Mike Farah d74bb8e28d Adding magic to detect leading seperators 2021-07-15 11:34:49 +10:00
Mike Farah 5c0a5bd9d3 Added capture regex operator 2021-07-11 11:08:18 +10:00
Mike Farah 3394feee0d Added test for #878 2021-07-09 15:58:33 +10:00
Mike Farah b9d01f1e95 Added test operator 2021-07-09 15:54:56 +10:00
Mike Farah 69c45ff64a Added match operator 2021-07-09 15:33:41 +10:00
Mike Farah 573618e4ce (wip) regex match op 2021-07-09 14:58:31 +10:00
Mike Farah cc7ea83506 (wip) regex match op 2021-07-09 14:58:31 +10:00
Mike Farah cae3d95fdc Clean up cli help doc, add github and doc links 2021-07-09 14:58:09 +10:00
Mike Farah 21e2fdaea2 Bump version 2021-07-08 10:27:57 +10:00
Mike Farah bae319ab7f Revert usage of filepath.Clean (azure compatability issues) 2021-07-08 10:26:35 +10:00
Mike Farah 38ce8618e1 Revert "Fix for gosec rule G304 - sanitize filepaths"
This reverts commit ffb6cbf769.
2021-07-08 10:14:01 +10:00
Mike Farah 90708c010c Updated docker instructions 2021-07-08 10:02:23 +10:00
Mike Farah b4ce50b705 Fixed secure script to run against all files 2021-07-07 20:06:14 +10:00
Mike Farah aba950a751 Bump version 2021-07-07 20:01:41 +10:00
Mike Farah cce3af001a Extract out compound assign logic, use it add and subtract ops 2021-07-07 20:00:46 +10:00
Mike Farah b4b2e1217a Added another |= example 2021-07-07 19:53:33 +10:00
Mike Farah b0074f5eba Fixes #870 2021-07-07 19:22:55 +10:00
Mike Farah 542efd8928 Update append array docs 2021-07-07 15:36:43 +10:00
Mike Farah ce3e347157 Fixed append array issue https://github.com/mikefarah/yq/issues/874 2021-07-07 15:30:45 +10:00
Mike Farah d1b6a6fdd9 Fixed merge bug https://github.com/mikefarah/yq/issues/880 2021-07-07 14:29:24 +10:00
Roberto Mier EscandonandMike Farah f285a684ec Update debian pkg files to version 4.9.6 2021-07-07 13:52:42 +10:00
Monica DebbelerandMike Farah ffb6cbf769 Fix for gosec rule G304 - sanitize filepaths 2021-07-07 13:52:11 +10:00
Monica DebbelerandMike Farah f67a83a5bc Fix for gosec rule G104 - handle errors 2021-07-07 13:50:04 +10:00
Mike Farah 6658a93e7d powershell issues 2021-06-25 13:20:13 +10:00
Andrew StribblehillandMike Farah d61b9cab65 Checksum archives as well as binaries
Often, checksums are based on the archive files. This gives some measure of confidence that the file won't exploit a tar or zip vulnerability.
2021-06-22 08:47:43 +10:00
Daniel VystrcilandMike Farah 5df0e49b1e Update Dockerfile
Adding group:user to fix (CIS_Docker_v1.2.0 - 4.1) Image should be created with a non-root user
2021-06-18 12:18:47 +10:00
Mike Farah 4a08ca6e29 Updating release instructions 2021-06-18 12:13:15 +10:00
Mike Farah 4bb2d04d43 Increment version 2021-06-12 16:40:42 +10:00
Alec ThomasandMike Farah 9e61f88dba Add darwin/arm64 builds 2021-06-12 16:39:06 +10:00
Mike Farah da47318f42 Fixed newline issue https://github.com/mikefarah/yq/issues/855 2021-06-12 08:26:27 +10:00
Mike Farah 88bee2809f Fixed issue on creating objects using [] 2021-06-11 14:27:44 +10:00
Daniel VystrcilandMike Farah 01267d062b Update Dockerfile
Alpine 3.15.5 has fixes for CVE-2021-30139, CVE-2021-28831, CVE-2021-23840, and CVE-2021-3450
2021-06-10 10:56:43 +10:00
Mike Farah cb912ca188 Added more tests 2021-06-10 09:53:50 +10:00
Mike Farah 58c63ed121 Bumping version 2021-06-10 09:37:08 +10:00
Mike Farah d1548ead71 Fixed alternative operator 2021-06-10 09:35:07 +10:00
Mike Farah 09c5ed24be Updating docs 2021-06-10 08:35:41 +10:00
Mike Farah 78af68f436 Added nested traversal examples 2021-06-10 08:31:26 +10:00
Mike Farah b6e2a96ead Increment version 2021-06-09 08:59:27 +10:00
Mike Farah 5f5c832570 Moved GithubAction docs to gitbook 2021-06-09 08:57:42 +10:00
Chris GruelandMike Farah 1a49f5222b outputs support in the action 2021-06-09 08:49:04 +10:00
Mike Farah 01e478e81d Fixing entries test 2021-06-08 21:50:14 +10:00
Curtis VogtandMike Farah ed377a032d Add with_entries example using filtering 2021-06-08 21:38:54 +10:00
Mike Farah fae2b2643c Added gofmt to format command 2021-06-01 10:52:14 +10:00
Mike Farah dd86b5e7f2 Fixing doc 2021-05-28 17:00:25 +10:00
Mike Farah f1f75683c1 Fixed nil RHS bug in alternative operator #838 2021-05-28 16:59:02 +10:00
118 changed files with 5990 additions and 417 deletions
+94
View File
@@ -0,0 +1,94 @@
name: Test Release YQ
on:
push:
tags:
- 'releaseTest-*'
jobs:
publishGitRelease:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.17.2'
- name: Compile man page markup
id: gen-man-page-md
run: |
./scripts/generate-man-page-md.sh
echo "::set-output name=man-page-md::man.md"
- name: Generate man page
uses: docker://pandoc/latex:2.9
id: gen-man-page
env:
MAN_HEADER: yq (https://github.com/mikefarah/yq/) version ${{ github.ref }}
with:
args: >-
--standalone
--to man
--variable=title:"YQ"
--variable=section:"1"
--variable=header:"${MAN_HEADER}"
--variable=author:"Mike Farah"
--output=yq.1
${{ steps.gen-man-page-md.outputs.man-page-md }}
- name: Cross compile
run: |
sudo apt-get install rhash -y
go get github.com/mitchellh/gox
mkdir -p build
pwd
ls
cp yq.1 build/yq.1
./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: TEST ${{ github.ref }}
draft: true
prerelease: false
- uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/*
# publishDocker:
# environment: dockerhub
# env:
# IMAGE_NAME: mikefarah/yq
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v1
# with:
# platforms: all
# - name: Set up Docker Buildx
# id: buildx
# uses: docker/setup-buildx-action@v1
# with:
# version: latest
# - name: Available platforms
# run: echo ${{ steps.buildx.outputs.platforms }} && docker version
# - name: Build image
# run: |
# IMAGE_V_VERSION="$(git describe --tags --abbrev=0)"
# IMAGE_VERSION=${IMAGE_V_VERSION:1}
# SHORT_SHA1=$(git rev-parse --short HEAD)
# PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64"
# echo "Building version ${IMAGE_VERSION} of image ${IMAGE_NAME}"
# docker buildx build --platform "${PLATFORMS}" .
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.15'
go-version: '^1.17.2'
- name: Cross compile
run: |
sudo apt-get install rhash -y
+19
View File
@@ -0,0 +1,19 @@
name: Test Yq Action
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get test
uses: mikefarah/yq@master
with:
cmd: yq eval '.a' examples/data1.yaml
- name: Write inplace test
id: lookupSdkVersion
uses: mikefarah/yq@master
with:
cmd: yq eval -i '.a.b = 5' examples/data1.yaml
+7
View File
@@ -38,3 +38,10 @@ parts/
prime/
.snapcraft/
yq*.snap
test.yml
test2.yml
# man page
man.md
yq.1
+12 -7
View File
@@ -1,18 +1,21 @@
FROM golang:1.15 as builder
FROM golang:1.17 as builder
WORKDIR /go/src/mikefarah/yq
# cache devtools
COPY ./scripts/devtools.sh /go/src/mikefarah/yq/scripts/devtools.sh
RUN ./scripts/devtools.sh
COPY . /go/src/mikefarah/yq
RUN CGO_ENABLED=0 make local build
RUN CGO_ENABLED=0 go build .
RUN ./scripts/test.sh
RUN ./scripts/acceptance.sh
# Choose alpine as a base image to make this useful for CI, as many
# CI tools expect an interactive shell inside the container
FROM alpine:3.12.3 as production
FROM alpine:3 as production
RUN mkdir /home/yq/
RUN addgroup -g 1000 yq && \
adduser -u 1000 -G yq -s /bin/bash -h /home/yq -D yq
RUN chown -R yq:yq /home/yq/
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
RUN chmod +x /usr/bin/yq
@@ -20,6 +23,8 @@ RUN chmod +x /usr/bin/yq
ARG VERSION=none
LABEL version=${VERSION}
USER yq
WORKDIR /workdir
ENTRYPOINT ["/usr/bin/yq"]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.15
FROM golang:1.17
COPY scripts/devtools.sh /opt/devtools.sh
+96 -25
View File
@@ -7,12 +7,43 @@ a lightweight and portable command-line YAML processor. `yq` uses [jq](https://g
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!
## Quick Usage Guide
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).
Read a value:
```bash
yq e '.a.b[0].c' file.yaml
```
Support for v3 will cease August 2021, until then, critical bug and security fixes will still get applied if required.
Pipe from STDIN:
```bash
cat file.yaml | yq e '.a.b[0].c' -
```
Update a yaml file, inplace
```bash
yq e -i '.a.b[0].c = "cool"' file.yaml
```
Update using environment variables
```bash
NAME=mike yq e -i '.a.b[0].c = strenv(NAME)' file.yaml
```
Merge multiple files
```
yq ea '. as $item ireduce ({}; . * $item )' path/to/*.yml
```
Multiple updates to a yaml file
```bash
yq e -i '
.a.b[0].c = "cool" |
.x.y.z = "foobar" |
.person.name = strenv(NAME)
' file.yaml
```
See the [documentation](https://mikefarah.gitbook.io/yq/) for more.
## Install
@@ -87,6 +118,14 @@ rm /etc/myfile.tmp
docker run --rm -v "${PWD}":/workdir mikefarah/yq <command> [flags] [expression ]FILE...
```
#### Pipe in via STDIN:
You'll need to pass the `-i\--interactive` flag to docker:
```bash
cat myfile.yml | docker run -i --rm mikefarah/yq e . -
```
#### Run commands interactively:
```bash
@@ -101,6 +140,36 @@ yq() {
}
```
#### Running as root:
`yq`'s docker image no longer runs under root (https://github.com/mikefarah/yq/pull/860). If you'd like to install more things in the docker image, or you're having permissions issues when attempting to read/write files you'll need to either:
```
docker run --user="root" -it --entrypoint sh mikefarah/yq
```
Or, in your docker file:
```
FROM mikefarah/yq
USER root
RUN apk add bash
USER yq
```
### GitHub Action
```
- name: Set foobar to cool
uses: mikefarah/yq@master
with:
cmd: yq eval -i '.foo.bar = "cool"' 'config.yml'
```
See https://mikefarah.gitbook.io/yq/usage/github-action for more.
### Go Get:
```
GO111MODULE=on go get github.com/mikefarah/yq/v4
@@ -157,6 +226,7 @@ Supported by @rmescandon (https://launchpad.net/~rmescandon/+archive/ubuntu/yq)
- Written in portable go, so you can download a lovely dependency free binary
- Uses similar syntax as `jq` but works with YAML and JSON files
- Fully supports multi document yaml files
- Supports yaml [front matter](https://mikefarah.gitbook.io/yq/usage/front-matter) blocks (e.g. jekyll/assemble)
- Colorized yaml output
- [Deeply traverse yaml](https://mikefarah.gitbook.io/yq/operators/traverse-read)
- [Sort yaml by keys](https://mikefarah.gitbook.io/yq/operators/sort-keys)
@@ -164,10 +234,12 @@ Supported by @rmescandon (https://launchpad.net/~rmescandon/+archive/ubuntu/yq)
- [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/operators/select#select-and-update-matching-values-in-map)
- Keeps yaml formatting and comments when updating (though there are issues with whitespace)
- [Convert to/from json to yaml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
- [Convert to/from json](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
- [Convert to properties](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties)
- [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)
- [Reduce](https://mikefarah.gitbook.io/yq/operators/reduce) to merge multiple files or sum an array or other fancy things.
- [Github Action](https://mikefarah.gitbook.io/yq/usage/github-action) to use in your automated pipeline (thanks @devorbitus)
## [Usage](https://mikefarah.gitbook.io/yq/)
@@ -179,33 +251,32 @@ Usage:
yq [command]
Available Commands:
eval Apply expression to each document in each yaml file given in sequence
eval Apply the expression to each document in each yaml file in sequence
eval-all Loads _all_ yaml documents of _all_ yaml files and runs expression once
help Help about any command
shell-completion Generate completion script
Flags:
-C, --colors force print with colors
-e, --exit-status set exit status if there are no matches or null or false is returned
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the yaml file inplace of first yaml file given.
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating yaml docs from scratch.
-P, --prettyPrint pretty print, shorthand for '... style = ""'
-j, --tojson output as json. Set indent to 0 to print json in one line.
-v, --verbose verbose mode
-V, --version Print version information and quit
-C, --colors force print with colors
-e, --exit-status set exit status if there are no matches or null or false is returned
-f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact
--header-preprocess Slurp any header comments and seperators before processing expression. This is a workaround for go-yaml to persist header content (default true)
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the yaml file inplace of first yaml file given.
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating yaml docs from scratch.
-o, --output-format string [yaml|y|json|j|props|p] output format type. (default "yaml")
-P, --prettyPrint pretty print, shorthand for '... style = ""'
--unwrapScalar unwrap scalar, print the value with no quotes, colors or comments (default true)
-v, --verbose verbose mode
-V, --version Print version information and quit
Use "yq [command] --help" for more information about a command.
```
Simple Example:
```bash
yq e '.a.b | length' f1.yml f2.yml
```
## 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)
- Powershell has its own...opinions: https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell
See [tips and tricks](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks) for more common problems and solutions.
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
setUp() {
rm -f test.yml
}
testBasicEvalRoundTrip() {
./yq e -n ".a = 123" > test.yml
X=$(./yq e '.a' test.yml)
assertEquals 123 "$X"
}
testBasicUpdateInPlaceSequence() {
cat >test.yml <<EOL
a: 0
EOL
./yq e -i ".a = 10" test.yml
X=$(./yq e '.a' test.yml)
assertEquals "10" "$X"
}
testBasicUpdateInPlaceSequenceEvalAll() {
cat >test.yml <<EOL
a: 0
EOL
./yq ea -i ".a = 10" test.yml
X=$(./yq e '.a' test.yml)
assertEquals "10" "$X"
}
testBasicNoExitStatus() {
echo "a: cat" > test.yml
X=$(./yq e '.z' test.yml)
assertEquals "null" "$X"
}
testBasicExitStatus() {
echo "a: cat" > test.yml
X=$(./yq e -e '.z' test.yml 2&>/dev/null)
assertEquals 1 "$?"
}
testBasicExtractFieldWithSeperator() {
cat >test.yml <<EOL
---
name: chart-name
version: 1.2.3
EOL
X=$(./yq e '.name' test.yml)
assertEquals "chart-name" "$X"
}
testBasicExtractMultipleFieldWithSeperator() {
cat >test.yml <<EOL
---
name: chart-name
version: 1.2.3
---
name: thing
version: 1.2.3
EOL
read -r -d '' expected << EOM
chart-name
---
thing
EOM
X=$(./yq e '.name' test.yml)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
setUp() {
cat >test.yml <<EOL
# comment
EOL
}
testEmptyEval() {
X=$(./yq e test.yml)
expected=$(cat test.yml)
assertEquals 0 $?
assertEquals "$expected" "$X"
}
testEmptyEvalNoNewLine() {
echo -n "#comment" >test.yml
X=$(./yq e test.yml)
expected=$(cat test.yml)
assertEquals 0 $?
assertEquals "$expected" "$X"
}
testEmptyEvalNoNewLineWithExpression() {
echo -n "# comment" >test.yml
X=$(./yq e '.apple = "tree"' test.yml)
read -r -d '' expected << EOM
# comment
apple: tree
EOM
assertEquals "$expected" "$X"
}
testEmptyEvalPipe() {
X=$(./yq e - < test.yml)
assertEquals 0 $?
}
testEmptyCommentsWithExpressionEval() {
read -r -d '' expected << EOM
# comment
apple: tree
EOM
X=$(./yq e '.apple="tree"' test.yml)
assertEquals "$expected" "$X"
}
testEmptyCommentsWithExpressionEvalAll() {
read -r -d '' expected << EOM
# comment
apple: tree
EOM
X=$(./yq ea '.apple="tree"' test.yml)
assertEquals "$expected" "$X"
}
testEmptyWithExpressionEval() {
rm test.yml
touch test.yml
expected="apple: tree"
X=$(./yq e '.apple="tree"' test.yml)
assertEquals "$expected" "$X"
}
testEmptyWithExpressionEvalAll() {
rm test.yml
touch test.yml
expected="apple: tree"
X=$(./yq ea '.apple="tree"' test.yml)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
setUp() {
cat >test.yml <<EOL
---
a: apple
b: cat
---
not yaml
c: at
EOL
}
testFrontMatterProcessEval() {
read -r -d '' expected << EOM
---
a: apple
b: dog
---
not yaml
c: at
EOM
./yq e --front-matter="process" '.b = "dog"' test.yml -i
assertEquals "$expected" "$(cat test.yml)"
}
testFrontMatterProcessEvalAll() {
read -r -d '' expected << EOM
---
a: apple
b: dog
---
not yaml
c: at
EOM
./yq ea --front-matter="process" '.b = "dog"' test.yml -i
assertEquals "$expected" "$(cat test.yml)"
}
testFrontMatterExtractEval() {
cat >test.yml <<EOL
a: apple
b: cat
---
not yaml
c: at
EOL
read -r -d '' expected << EOM
a: apple
b: dog
EOM
./yq e --front-matter="extract" '.b = "dog"' test.yml -i
assertEquals "$expected" "$(cat test.yml)"
}
testFrontMatterExtractEvalAll() {
cat >test.yml <<EOL
a: apple
b: cat
---
not yaml
c: at
EOL
read -r -d '' expected << EOM
a: apple
b: dog
EOM
./yq ea --front-matter="extract" '.b = "dog"' test.yml -i
assertEquals "$expected" "$(cat test.yml)"
}
source ./scripts/shunit2
+449
View File
@@ -0,0 +1,449 @@
#!/bin/bash
setUp() {
cat >test.yml <<EOL
---
a: test
EOL
}
testLeadingSeperatorWithDoc() {
cat >test.yml <<EOL
# hi peeps
# cool
---
a: test
---
b: cool
EOL
read -r -d '' expected << EOM
# hi peeps
# cool
---
a: thing
---
b: cool
EOM
X=$(./yq e '(select(di == 0) | .a) = "thing"' - < test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorPipeIntoEvalSeq() {
X=$(./yq e - < test.yml)
expected=$(cat test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorExtractField() {
X=$(./yq e '.a' - < test.yml)
assertEquals "test" "$X"
}
testLeadingSeperatorExtractFieldWithCommentsAfterSep() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
EOL
X=$(./yq e '.a' test.yml)
assertEquals "test" "$X"
}
testLeadingSeperatorExtractFieldWithCommentsBeforeSep() {
cat >test.yml <<EOL
# hi peeps
# cool
---
a: test
EOL
X=$(./yq e '.a' test.yml)
assertEquals "test" "$X"
}
testLeadingSeperatorExtractFieldMultiDoc() {
cat >test.yml <<EOL
---
a: test
---
a: test2
EOL
read -r -d '' expected << EOM
test
---
test2
EOM
X=$(./yq e '.a' test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorExtractFieldMultiDocWithComments() {
cat >test.yml <<EOL
# here
---
# there
a: test
# whereever
---
# you are
a: test2
# woop
EOL
read -r -d '' expected << EOM
test
---
test2
EOM
X=$(./yq e '.a' test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorEvalSeq() {
X=$(./yq e test.yml)
expected=$(cat test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorPipeIntoEvalAll() {
X=$(./yq ea - < test.yml)
expected=$(cat test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorEvalAll() {
X=$(./yq ea test.yml)
expected=$(cat test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalSimple() {
read -r -d '' expected << EOM
---
a: test
---
version: 3
application: MyApp
EOM
X=$(./yq e '.' test.yml examples/order.yaml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocInOneFile() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
---
b: things
EOL
expected=$(cat test.yml)
X=$(./yq e '.' test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocInOneFileEvalAll() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
---
b: things
EOL
expected=$(cat test.yml)
X=$(./yq ea '.' test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalComments() {
cat >test.yml <<EOL
# hi peeps
# cool
a: test
EOL
cat >test2.yml <<EOL
# this is another doc
# great
b: sane
EOL
read -r -d '' expected << EOM
# hi peeps
# cool
a: test
---
# this is another doc
# great
b: sane
EOM
X=$(./yq e '.' test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalCommentsTrailingSep() {
cat >test.yml <<EOL
# hi peeps
# cool
---
a: test
EOL
cat >test2.yml <<EOL
# this is another doc
# great
---
b: sane
EOL
read -r -d '' expected << EOM
# hi peeps
# cool
---
a: test
---
# this is another doc
# great
---
b: sane
EOM
X=$(./yq e '.' test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiMultiDocEvalCommentsTrailingSep() {
cat >test.yml <<EOL
# hi peeps
# cool
---
a: test
---
a1: test2
EOL
cat >test2.yml <<EOL
# this is another doc
# great
---
b: sane
---
b2: cool
EOL
read -r -d '' expected << EOM
# hi peeps
# cool
---
a: test
---
a1: test2
---
# this is another doc
# great
---
b: sane
---
b2: cool
EOM
X=$(./yq e '.' test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalCommentsLeadingSep() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
EOL
cat >test2.yml <<EOL
---
# this is another doc
# great
b: sane
EOL
read -r -d '' expected << EOM
---
# hi peeps
# cool
a: test
---
# this is another doc
# great
b: sane
EOM
X=$(./yq e '.' test.yml test2.yml)
assertEquals "$expected" "$X"
}
# https://github.com/mikefarah/yq/issues/919
testLeadingSeparatorDoesNotBreakCommentsOnOtherFiles() {
cat >test.yml <<EOL
# a1
a: 1
# a2
EOL
cat >test2.yml <<EOL
# b1
b: 2
# b2
EOL
read -r -d '' expected << EOM
# a1
a: 1
# a2
# b1
b: 2
# b2
EOM
X=$(./yq ea 'select(fi == 0) * select(fi == 1)' test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalCommentsStripComments() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
---
# this is another doc
# great
b: sane
EOL
# it will be hard to remove that top level separator
read -r -d '' expected << EOM
a: test
---
b: sane
EOM
X=$(./yq e '... comments=""' test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalCommentsLeadingSepNoDocFlag() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
---
# this is another doc
# great
b: sane
EOL
read -r -d '' expected << EOM
# hi peeps
# cool
a: test
# this is another doc
# great
b: sane
EOM
X=$(./yq e '.' --no-doc test.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalJsonFlag() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
EOL
cat >test2.yml <<EOL
---
# this is another doc
# great
b: sane
EOL
read -r -d '' expected << EOM
{
"a": "test"
}
{
"b": "sane"
}
EOM
X=$(./yq e '.' -j test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalAllJsonFlag() {
cat >test.yml <<EOL
---
# hi peeps
# cool
a: test
EOL
cat >test2.yml <<EOL
---
# this is another doc
# great
b: sane
EOL
read -r -d '' expected << EOM
{
"a": "test"
}
{
"b": "sane"
}
EOM
X=$(./yq ea '.' -j test.yml test2.yml)
assertEquals "$expected" "$X"
}
testLeadingSeperatorMultiDocEvalAll() {
read -r -d '' expected << EOM
---
a: test
---
version: 3
application: MyApp
EOM
X=$(./yq ea '.' test.yml examples/order.yaml)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
testOutputJsonDeprecated() {
cat >test.yml <<EOL
a: {b: ["cat"]}
EOL
read -r -d '' expected << EOM
{
"a": {
"b": [
"cat"
]
}
}
EOM
X=$(./yq e -j test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea -j test.yml)
assertEquals "$expected" "$X"
}
testOutputJson() {
cat >test.yml <<EOL
a: {b: ["cat"]}
EOL
read -r -d '' expected << EOM
{
"a": {
"b": [
"cat"
]
}
}
EOM
X=$(./yq e --output-format=json test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --output-format=json test.yml)
assertEquals "$expected" "$X"
}
testOutputJsonShort() {
cat >test.yml <<EOL
a: {b: ["cat"]}
EOL
read -r -d '' expected << EOM
{
"a": {
"b": [
"cat"
]
}
}
EOM
X=$(./yq e -o=j test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea -o=j test.yml)
assertEquals "$expected" "$X"
}
testOutputProperties() {
cat >test.yml <<EOL
a: {b: {c: ["cat"]}}
EOL
read -r -d '' expected << EOM
a.b.c.0 = cat
EOM
X=$(./yq e --output-format=props test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --output-format=props test.yml)
assertEquals "$expected" "$X"
}
testOutputPropertiesShort() {
cat >test.yml <<EOL
a: {b: {c: ["cat"]}}
EOL
read -r -d '' expected << EOM
a.b.c.0 = cat
EOM
X=$(./yq e -o=p test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea -o=p test.yml)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
+173
View File
@@ -0,0 +1,173 @@
#!/bin/bash
testPrettyPrintWithBooleans() {
cat >test.yml <<EOL
leaveUnquoted: [yes, no, on, off, y, n, true, false]
leaveQuoted: ["yes", "no", "on", "off", "y", "n", "true", "false"]
EOL
read -r -d '' expected << EOM
leaveUnquoted:
- yes
- no
- on
- off
- y
- n
- true
- false
leaveQuoted:
- "yes"
- "no"
- "on"
- "off"
- "y"
- "n"
- "true"
- "false"
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintWithBooleansCapitals() {
cat >test.yml <<EOL
leaveUnquoted: [YES, NO, ON, OFF, Y, N, TRUE, FALSE]
leaveQuoted: ["YES", "NO", "ON", "OFF", "Y", "N", "TRUE", "FALSE"]
EOL
read -r -d '' expected << EOM
leaveUnquoted:
- YES
- NO
- ON
- OFF
- Y
- N
- TRUE
- FALSE
leaveQuoted:
- "YES"
- "NO"
- "ON"
- "OFF"
- "Y"
- "N"
- "TRUE"
- "FALSE"
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintOtherStringValues() {
cat >test.yml <<EOL
leaveUnquoted: [yesSir, hellno, bonapite]
makeUnquoted: ["yesSir", "hellno", "bonapite"]
EOL
read -r -d '' expected << EOM
leaveUnquoted:
- yesSir
- hellno
- bonapite
makeUnquoted:
- yesSir
- hellno
- bonapite
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintKeys() {
cat >test.yml <<EOL
"removeQuotes": "please"
EOL
read -r -d '' expected << EOM
removeQuotes: please
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintOtherStringValues() {
cat >test.yml <<EOL
leaveUnquoted: [yesSir, hellno, bonapite]
makeUnquoted: ["yesSir", "hellno", "bonapite"]
EOL
read -r -d '' expected << EOM
leaveUnquoted:
- yesSir
- hellno
- bonapite
makeUnquoted:
- yesSir
- hellno
- bonapite
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintStringBlocks() {
cat >test.yml <<EOL
"removeQuotes": |
"please"
EOL
read -r -d '' expected << EOM
removeQuotes: |
"please"
EOM
X=$(./yq e --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
testPrettyPrintWithExpression() {
cat >test.yml <<EOL
a: {b: {c: ["cat"]}}
EOL
read -r -d '' expected << EOM
b:
c:
- cat
EOM
X=$(./yq e '.a' --prettyPrint test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea '.a' --prettyPrint test.yml)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
+6 -2
View File
@@ -1,11 +1,15 @@
name: 'yq - portable yaml processor'
description: 'create, read, update, delete, merge, validate and do more with yaml'
icon: command
color: gray-dark
branding:
icon: command
color: gray-dark
inputs:
cmd:
description: 'The Command which should be run'
required: true
outputs:
result:
description: "The complete result from the yq command being run"
runs:
using: 'docker'
image: 'github-action/Dockerfile'
+5
View File
@@ -1,9 +1,11 @@
package cmd
var leadingContentPreProcessing = true
var unwrapScalar = true
var writeInplace = false
var outputToJSON = false
var outputFormat = "yaml"
var exitStatus = false
var forceColor = false
@@ -16,4 +18,7 @@ var verbose = false
var version = false
var prettyPrint = false
// can be either "" (off), "extract" or "process"
var frontMatter = ""
var completedSuccessfully = false
+48 -9
View File
@@ -15,13 +15,27 @@ func createEvaluateAllCommand() *cobra.Command {
Aliases: []string{"ea"},
Short: "Loads _all_ yaml documents of _all_ yaml files and runs expression once",
Example: `
# merges f2.yml into f1.yml (inplace)
yq eval-all --inplace 'select(fileIndex == 0) * select(fileIndex == 1)' f1.yml f2.yml
# Merge f2.yml into f1.yml (inplace)
yq eval-all --inplace 'select(fileIndex == 0) * select(fileIndex == 1)' f1.yml f2.yml
## the same command and expression using shortened names:
yq ea -i 'select(fi == 0) * select(fi == 1)' f1.yml f2.yml
# use '-' as a filename to read from STDIN
# Merge all given files
yq ea '. as $item ireduce ({}; . * $item )' file1.yml file2.yml ...
# Pipe from STDIN
## use '-' as a filename to pipe from STDIN
cat file2.yml | yq ea '.a.b' file1.yml - file3.yml
`,
Long: "Evaluate All:\nUseful when you need to run an expression across several yaml documents or files. Consumes more memory than eval",
Long: `yq is a portable command-line YAML processor (https://github.com/mikefarah/yq/)
See https://mikefarah.gitbook.io/yq/ for detailed documentation and examples.
## Evaluate All ##
This command loads _all_ yaml documents of _all_ yaml files and runs expression once
Useful when you need to run an expression across several yaml documents or files (like merge).
Note that it consumes more memory than eval.
`,
RunE: evaluateAll,
}
return cmdEvalAll
@@ -72,26 +86,51 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
if nullInput && len(args) > 1 {
return errors.New("Cannot pass files in when using null-input flag")
}
// backwards compatibilty
if outputToJSON {
outputFormat = "json"
}
printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
format, err := yqlib.OutputFormatFromString(outputFormat)
if err != nil {
return err
}
printer := yqlib.NewPrinter(out, format, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
if frontMatter != "" {
frontMatterHandler := yqlib.NewFrontMatterHandler(args[firstFileIndex])
err = frontMatterHandler.Split()
if err != nil {
return err
}
args[firstFileIndex] = frontMatterHandler.GetYamlFrontMatterFilename()
if frontMatter == "process" {
reader := frontMatterHandler.GetContentReader()
printer.SetAppendix(reader)
defer yqlib.SafelyCloseReader(reader)
}
defer frontMatterHandler.CleanUp()
}
allAtOnceEvaluator := yqlib.NewAllAtOnceEvaluator()
switch len(args) {
case 0:
if pipingStdIn {
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing)
} else {
cmd.Println(cmd.UsageString())
return nil
}
case 1:
if nullInput {
err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(args[0]), printer)
err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(args[0]), printer, "")
} else {
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing)
}
default:
err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer)
err = allAtOnceEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing)
}
completedSuccessfully = err == nil
+49 -16
View File
@@ -13,35 +13,42 @@ func createEvaluateSequenceCommand() *cobra.Command {
var cmdEvalSequence = &cobra.Command{
Use: "eval [expression] [yaml_file1]...",
Aliases: []string{"e"},
Short: "Apply expression to each document in each yaml file given in sequence",
Short: "Apply the expression to each document in each yaml file in sequence",
Example: `
# runs the expression against each file, in series
yq e '.a.b | length' f1.yml f2.yml
# Reads field under the given path for each file
yq e '.a.b' f1.yml f2.yml
# prints out the file
# Prints out the file
yq e sample.yaml
# use '-' as a filename to read from STDIN
# Pipe from STDIN
## use '-' as a filename to pipe from STDIN
cat file2.yml | yq e '.a.b' file1.yml - file3.yml
# prints a new yaml document
# Creates a new yaml document
## Note that editing an empty file does not work.
yq e -n '.a.b.c = "cat"'
# updates file.yaml directly
# Update a file inplace
yq e '.a.b = "cool"' -i file.yaml
`,
Long: "Evaluate Sequence:\nIterate over each yaml document, apply the expression and print the results, in sequence.",
Long: `yq is a portable command-line YAML processor (https://github.com/mikefarah/yq/)
See https://mikefarah.gitbook.io/yq/ for detailed documentation and examples.
## Evaluate Sequence ##
This command iterates over each yaml document from each given file, applies the
expression and prints the result in sequence.`,
RunE: evaluateSequence,
}
return cmdEvalSequence
}
func processExpression(expression string) string {
var prettyPrintExp = `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`
if prettyPrint && expression == "" {
return `... style=""`
return prettyPrintExp
} else if prettyPrint {
return fmt.Sprintf("%v | ... style= \"\"", expression)
return fmt.Sprintf("%v | %v", expression, prettyPrintExp)
}
return expression
}
@@ -89,7 +96,17 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
defer func() { writeInPlaceHandler.FinishWriteInPlace(completedSuccessfully) }()
}
printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
// backwards compatibilty
if outputToJSON {
outputFormat = "json"
}
format, err := yqlib.OutputFormatFromString(outputFormat)
if err != nil {
return err
}
printer := yqlib.NewPrinter(out, format, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
streamEvaluator := yqlib.NewStreamEvaluator()
@@ -97,22 +114,38 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
return errors.New("Cannot pass files in when using null-input flag")
}
if frontMatter != "" {
frontMatterHandler := yqlib.NewFrontMatterHandler(args[firstFileIndex])
err = frontMatterHandler.Split()
if err != nil {
return err
}
args[firstFileIndex] = frontMatterHandler.GetYamlFrontMatterFilename()
if frontMatter == "process" {
reader := frontMatterHandler.GetContentReader()
printer.SetAppendix(reader)
defer yqlib.SafelyCloseReader(reader)
}
defer frontMatterHandler.CleanUp()
}
switch len(args) {
case 0:
if pipingStdIn {
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer)
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{"-"}, printer, leadingContentPreProcessing)
} else {
cmd.Println(cmd.UsageString())
return nil
}
case 1:
if nullInput {
err = streamEvaluator.EvaluateNew(processExpression(args[0]), printer)
err = streamEvaluator.EvaluateNew(processExpression(args[0]), printer, "")
} else {
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer)
err = streamEvaluator.EvaluateFiles(processExpression(""), []string{args[0]}, printer, leadingContentPreProcessing)
}
default:
err = streamEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer)
err = streamEvaluator.EvaluateFiles(processExpression(args[0]), args[1:], printer, leadingContentPreProcessing)
}
completedSuccessfully = err == nil
+12 -2
View File
@@ -11,7 +11,8 @@ func New() *cobra.Command {
var rootCmd = &cobra.Command{
Use: "yq",
Short: "yq is a lightweight and portable command-line YAML processor.",
Long: `yq is a lightweight and portable command-line YAML processor. It aims to be the jq or sed of yaml files.`,
Long: `yq is a portable command-line YAML processor (https://github.com/mikefarah/yq/)
See https://mikefarah.gitbook.io/yq/ for detailed documentation and examples.`,
RunE: func(cmd *cobra.Command, args []string) error {
if version {
cmd.Print(GetVersionDisplay())
@@ -40,7 +41,14 @@ func New() *cobra.Command {
}
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json. Set indent to 0 to print json in one line.")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "(deprecated) output as json. Set indent to 0 to print json in one line.")
err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead")
if err != nil {
panic(err)
}
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "yaml", "[yaml|y|json|j|props|p] output format type.")
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating yaml docs from scratch.")
rootCmd.PersistentFlags().BoolVarP(&noDocSeparators, "no-doc", "N", false, "Don't print document separators (---)")
@@ -53,6 +61,8 @@ func New() *cobra.Command {
rootCmd.PersistentFlags().BoolVarP(&forceColor, "colors", "C", false, "force print with colors")
rootCmd.PersistentFlags().BoolVarP(&forceNoColor, "no-colors", "M", false, "force print with no colors")
rootCmd.PersistentFlags().StringVarP(&frontMatter, "front-matter", "f", "", "(extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact")
rootCmd.PersistentFlags().BoolVarP(&leadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression. This is a workaround for go-yaml to persist header content properly.")
rootCmd.AddCommand(
createEvaluateSequenceCommand(),
createEvaluateAllCommand(),
+2 -2
View File
@@ -11,7 +11,7 @@ var (
GitDescribe string
// Version is main version number that is being run at the moment.
Version = "4.9.3"
Version = "4.13.4"
// 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
@@ -25,7 +25,7 @@ const ProductName = "yq"
// GetVersionDisplay composes the parts of the version in a way that's suitable
// for displaying to humans.
func GetVersionDisplay() string {
return fmt.Sprintf("%s version %s\n", ProductName, getHumanVersion())
return fmt.Sprintf("yq (https://github.com/mikefarah/yq/) version %s\n", getHumanVersion())
}
func getHumanVersion() string {
+1 -1
View File
@@ -3,7 +3,7 @@ package cmd
import "testing"
func TestGetVersionDisplay(t *testing.T) {
var expectedVersion = ProductName + " version " + Version
var expectedVersion = ProductName + " (https://github.com/mikefarah/yq/) version " + Version
if VersionPrerelease != "" {
expectedVersion = expectedVersion + "-" + VersionPrerelease
}
+25
View File
@@ -1,3 +1,28 @@
yq (4.13.0) focal; urgency=medium
* New `with` operator for making multiple changes to a given path
* New `contains` operator, works like the `jq` equivalent
* Subtract operator now supports subtracting elements from arrays!
* Fixed Swapping values using variables #934
* Github Action now properly supports multiline output #936, thanks @pjxiao
* Fixed missing closing bracket validation #932
* Fix processing of hex numbers #929
* Fixed alternative and union operator issues #930
* Can now convert yaml to properties properties format (`-o=props`), See [docs](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties) for more info.
* Fixed document header/footer comment handling when merging (https://github.com/mikefarah/yq/issues/919)
* pretty print yaml 1.1 compatibility (https://github.com/mikefarah/yq/issues/914)
-- Roberto Mier Escandon <rmescandon@gmail.com> Thu, 16 Sep 2021 20:58:30 +0200
yq (4.9.6) focal; urgency=medium
* Added darwin/arm64 build, thanks @alecthomas
* Incremented docker alpine base version, thanks @da6d6i7-bronga
* Bug fix: multine expression
* Bug fix: special character
-- Roberto Mier Escandon <rmescandon@gmail.com> Tue, 29 Jun 2021 21:32:14 +0200
yq (3.3.2) focal; urgency=medium
* Bug fix: existStatus bug (#459)
+1 -2
View File
@@ -3,8 +3,7 @@ Section: devel
Priority: optional
Maintainer: Roberto Mier Escandón <rmescandon@gmail.com>
Build-Depends: debhelper (>=10),
dh-golang (>=1.34),
golang-1.13,
golang-1.15,
rsync
Standards-Version: 4.1.4
Homepage: https://github.com/mikefarah/yq.git
+4 -2
View File
@@ -17,7 +17,7 @@
PROJECT := yq
OWNER := mikefarah
REPO := github.com
GOVERSION := 1.13
GOVERSION := 1.15
export DH_OPTIONS
export DH_GOPKG := ${REPO}/${OWNER}/${PROJECT}
@@ -34,7 +34,7 @@ BINDIR := /usr/bin
ASSETSDIR := /usr/share/${PROJECT}
%:
dh $@ --builddirectory=${GOPATH} --buildsystem=golang
dh $@
override_dh_auto_build:
mkdir -p ${SRCDIR}
@@ -45,7 +45,9 @@ override_dh_auto_build:
(cd ${SRCDIR} && go install -buildmode=pie ./...)
override_dh_auto_test:
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
(cd ${SRCDIR} && go test -v ./...)
endif
override_dh_auto_install:
cp ${GOBIN}/yq ${DESTDIR}/${BINDIR}
+5 -6
View File
@@ -1,6 +1,5 @@
a:
key1: "value1"
key2: 2.6
ab:
key1: 6
key2: "h"
Resources:
S3Bucket:
Type: AWS::CloudFormation::Stack
Properties:
BucketName: !Ref MyBucketNameA
+3 -7
View File
@@ -1,7 +1,3 @@
a: other # better than the original
b: [3, 4]
c:
toast: leave
test: 1
tell: 1
tasty.taco: cool
#b1
b: 2
#b2
+1 -2
View File
@@ -1,2 +1 @@
# a: apple
# b: cat
# comment
+6
View File
@@ -0,0 +1,6 @@
---
a: apple
b: bannana
---
hello there
apples: great
+2
View File
@@ -0,0 +1,2 @@
---
a: test
+4 -1
View File
@@ -1,5 +1,8 @@
FROM mikefarah/yq:4.9.3
FROM mikefarah/yq:4.13.4
COPY entrypoint.sh /entrypoint.sh
# this seems to be the default user in github actions
USER 1001
ENTRYPOINT ["/entrypoint.sh"]
+8 -3
View File
@@ -1,4 +1,9 @@
#!/bin/sh -l
echo "$1"
eval $1
set -e
echo "::debug::\$cmd: $1"
RESULT=$(eval "$1")
RESULT="${RESULT//'%'/'%25'}"
RESULT="${RESULT//$'\n'/'%0A'}"
RESULT="${RESULT//$'\r'/'%0D'}"
echo "::debug::\$RESULT: $RESULT"
echo ::set-output name=result::"$RESULT"
+12 -3
View File
@@ -5,12 +5,21 @@ require (
github.com/fatih/color v1.10.0
github.com/goccy/go-yaml v1.8.9
github.com/jinzhu/copier v0.2.8
github.com/magiconair/properties v1.8.5
github.com/spf13/cobra v1.1.3
github.com/timtadh/data-structures v0.5.3 // indirect
github.com/timtadh/lexmachine v0.2.2
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
go 1.15
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/timtadh/data-structures v0.5.3 // indirect
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
go 1.17
+4 -2
View File
@@ -119,6 +119,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -256,8 +258,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+24 -5
View File
@@ -8,7 +8,7 @@ import (
// A yaml expression evaluator that runs the expression once against all files/nodes in memory.
type Evaluator interface {
EvaluateFiles(expression string, filenames []string, printer Printer) error
EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error
// EvaluateNodes takes an expression and one or more yaml nodes, returning a list of matching candidate nodes
EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error)
@@ -18,12 +18,12 @@ type Evaluator interface {
}
type allAtOnceEvaluator struct {
treeNavigator dataTreeNavigator
treeNavigator DataTreeNavigator
treeCreator ExpressionParser
}
func NewAllAtOnceEvaluator() Evaluator {
return &allAtOnceEvaluator{treeNavigator: newDataTreeNavigator(), treeCreator: NewExpressionParser()}
return &allAtOnceEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewExpressionParser()}
}
func (e *allAtOnceEvaluator) EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error) {
@@ -46,15 +46,21 @@ func (e *allAtOnceEvaluator) EvaluateCandidateNodes(expression string, inputCand
return context.MatchingNodes, nil
}
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool) error {
fileIndex := 0
firstFileLeadingContent := ""
var allDocuments *list.List = list.New()
for _, filename := range filenames {
reader, err := readStream(filename)
reader, leadingContent, err := readStream(filename, fileIndex == 0 && leadingContentPreProcessing)
if err != nil {
return err
}
if fileIndex == 0 {
firstFileLeadingContent = leadingContent
}
fileDocuments, err := readDocuments(reader, filename, fileIndex)
if err != nil {
return err
@@ -62,6 +68,19 @@ func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string
allDocuments.PushBackList(fileDocuments)
fileIndex = fileIndex + 1
}
if allDocuments.Len() == 0 {
candidateNode := &CandidateNode{
Document: 0,
Filename: "",
Node: &yaml.Node{Kind: yaml.DocumentNode, HeadComment: firstFileLeadingContent, Content: []*yaml.Node{{Tag: "!!null", Kind: yaml.ScalarNode}}},
FileIndex: 0,
}
allDocuments.PushBack(candidateNode)
} else {
allDocuments.Front().Value.(*CandidateNode).Node.HeadComment = firstFileLeadingContent
}
matches, err := e.EvaluateCandidateNodes(expression, allDocuments)
if err != nil {
return err
+42 -1
View File
@@ -2,8 +2,10 @@ package yqlib
import (
"container/list"
"fmt"
"github.com/jinzhu/copier"
logging "gopkg.in/op/go-logging.v1"
)
type Context struct {
@@ -41,19 +43,52 @@ func (n *Context) SetVariable(name string, value *list.List) {
}
func (n *Context) ChildContext(results *list.List) Context {
clone := Context{DontAutoCreate: n.DontAutoCreate}
clone.Variables = make(map[string]*list.List)
if len(n.Variables) > 0 {
err := copier.Copy(&clone.Variables, n.Variables)
if err != nil {
log.Error("Error cloning context :(")
panic(err)
}
}
clone.MatchingNodes = results
return clone
}
func (n *Context) ToString() string {
if !log.IsEnabledFor(logging.DEBUG) {
return ""
}
result := fmt.Sprintf("Context\nDontAutoCreate: %v\n", n.DontAutoCreate)
return result + NodesToString(n.MatchingNodes)
}
func (n *Context) DeepClone() Context {
clone := Context{}
err := copier.Copy(&clone, n)
// copier doesn't do lists properly for some reason
clone.MatchingNodes = list.New()
for el := n.MatchingNodes.Front(); el != nil; el = el.Next() {
clonedNode, err := el.Value.(*CandidateNode).Copy()
if err != nil {
log.Error("Error cloning context :(")
panic(err)
}
clone.MatchingNodes.PushBack(clonedNode)
}
if err != nil {
log.Error("Error cloning context :(")
panic(err)
}
clone.MatchingNodes = results
return clone
}
func (n *Context) Clone() Context {
clone := Context{}
err := copier.Copy(&clone, n)
if err != nil {
log.Error("Error cloning context :(")
panic(err)
@@ -66,3 +101,9 @@ func (n *Context) ReadOnlyClone() Context {
clone.DontAutoCreate = true
return clone
}
func (n *Context) WritableClone() Context {
clone := n.Clone()
clone.DontAutoCreate = false
return clone
}
+5 -5
View File
@@ -6,21 +6,21 @@ import (
logging "gopkg.in/op/go-logging.v1"
)
type dataTreeNavigator interface {
type DataTreeNavigator interface {
// given the context and a expressionNode,
// this will process the against the given expressionNode and return
// a new context of matching candidates
GetMatchingNodes(context Context, expressionNode *ExpressionNode) (Context, error)
}
type dataTreeNavigatorImpl struct {
type dataTreeNavigator struct {
}
func newDataTreeNavigator() dataTreeNavigator {
return &dataTreeNavigatorImpl{}
func NewDataTreeNavigator() DataTreeNavigator {
return &dataTreeNavigator{}
}
func (d *dataTreeNavigatorImpl) GetMatchingNodes(context Context, expressionNode *ExpressionNode) (Context, error) {
func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *ExpressionNode) (Context, error) {
if expressionNode == nil {
log.Debugf("getMatchingNodes - nothing to do")
return context, nil
+26
View File
@@ -127,6 +127,32 @@ b:
- 4
```
## Append another array using +=
Given a sample.yml file of:
```yaml
a:
- 1
- 2
b:
- 3
- 4
```
then
```bash
yq eval '.a += .b' sample.yml
```
will output
```yaml
a:
- 1
- 2
- 3
- 4
b:
- 3
- 4
```
## Relative append
Given a sample.yml file of:
```yaml
@@ -299,3 +299,33 @@ foobar:
thing: foobar_thing
```
## Dereference and update a field
`Use explode with multiply to dereference an object
Given a sample.yml file of:
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
!!merge <<: *item_value
thingTwo:
name: item_2
!!merge <<: *item_value
```
then
```bash
yq eval '.thingOne |= explode(.) * {"value": false}' sample.yml
```
will output
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
value: false
thingTwo:
name: item_2
!!merge <<: *item_value
```
+19 -1
View File
@@ -28,6 +28,24 @@ a:
g: foof
```
## Double elements in an array
Given a sample.yml file of:
```yaml
- 1
- 2
- 3
```
then
```bash
yq eval '.[] |= . * 2' sample.yml
```
will output
```yaml
- 2
- 4
- 6
```
## Update node from another file
Note this will also work when the second file is a scalar (string/number)
@@ -75,7 +93,7 @@ c: fieldC
```
then
```bash
yq eval '(.a, .c) |= "potatoe"' sample.yml
yq eval '(.a, .c) = "potatoe"' sample.yml
```
will output
```yaml
+85
View File
@@ -0,0 +1,85 @@
## Array contains array
Array is equal or subset of
Given a sample.yml file of:
```yaml
- foobar
- foobaz
- blarp
```
then
```bash
yq eval 'contains(["baz", "bar"])' sample.yml
```
will output
```yaml
true
```
## Object included in array
Given a sample.yml file of:
```yaml
"foo": 12
"bar":
- 1
- 2
- "barp": 12
"blip": 13
```
then
```bash
yq eval 'contains({"bar": [{"barp": 12}]})' sample.yml
```
will output
```yaml
true
```
## Object not included in array
Given a sample.yml file of:
```yaml
"foo": 12
"bar":
- 1
- 2
- "barp": 12
"blip": 13
```
then
```bash
yq eval 'contains({"foo": 12, "bar": [{"barp": 15}]})' sample.yml
```
will output
```yaml
false
```
## String contains substring
Given a sample.yml file of:
```yaml
foobar
```
then
```bash
yq eval 'contains("bar")' sample.yml
```
will output
```yaml
true
```
## String equals string
Given a sample.yml file of:
```yaml
meow
```
then
```bash
yq eval 'contains("meow")' sample.yml
```
will output
```yaml
true
```
+18
View File
@@ -98,3 +98,21 @@ KEY_a: 1
KEY_b: 2
```
## Use with_entries to filter the map
Given a sample.yml file of:
```yaml
a:
b: bird
c:
d: dog
```
then
```bash
yq eval 'with_entries(select(.value | has("b")))' sample.yml
```
will output
```yaml
a:
b: bird
```
+1 -1
View File
@@ -357,7 +357,7 @@ yq eval '.foobar * .foobarList' sample.yml
will output
```yaml
c: foobarList_c
<<:
!!merge <<:
- *foo
- *bar
thing: foobar_thing
+207
View File
@@ -1,5 +1,47 @@
# String Operators
## RegEx
This uses golangs native regex functions under the hood - See https://github.com/google/re2/wiki/Syntax for the supported syntax.
# String blocks, bash and newlines
Bash is notorious for chomping on precious trailing newline characters, making it tricky to set strings with newlines properly. In particular, the `$( exp )` _will trim trailing newlines_.
For instance to get this yaml:
```
a: |
cat
```
Using `$( exp )` wont work, as it will trim the trailing new line.
```
m=$(echo "cat\n") yq e -n '.a = strenv(m)'
a: cat
```
However, using printf works:
```
printf -v m "cat\n" ; m="$m" yq e -n '.a = strenv(m)'
a: |
cat
```
As well as having multiline expressions:
```
m="cat
" yq e -n '.a = strenv(m)'
a: |
cat
```
Similarly, if you're trying to set the content from a file, and want a trailing new line:
```
IFS= read -rd '' output < <(cat my_file)
output=$output ./yq e '.data.values = strenv(output)' first.yml
```
## Join strings
Given a sample.yml file of:
```yaml
@@ -18,6 +60,171 @@ will output
cat; meow; 1; ; true
```
## Match string
Given a sample.yml file of:
```yaml
foo bar foo
```
then
```bash
yq eval 'match("foo")' sample.yml
```
will output
```yaml
string: foo
offset: 0
length: 3
captures: []
```
## Match string, case insensitive
Given a sample.yml file of:
```yaml
foo bar FOO
```
then
```bash
yq eval '[match("(?i)foo"; "g")]' sample.yml
```
will output
```yaml
- string: foo
offset: 0
length: 3
captures: []
- string: FOO
offset: 8
length: 3
captures: []
```
## Match with capture groups
Given a sample.yml file of:
```yaml
abc abc
```
then
```bash
yq eval '[match("(abc)+"; "g")]' sample.yml
```
will output
```yaml
- string: abc
offset: 0
length: 3
captures:
- string: abc
offset: 0
length: 3
- string: abc
offset: 4
length: 3
captures:
- string: abc
offset: 4
length: 3
```
## Match with named capture groups
Given a sample.yml file of:
```yaml
foo bar foo foo foo
```
then
```bash
yq eval '[match("foo (?P<bar123>bar)? foo"; "g")]' sample.yml
```
will output
```yaml
- string: foo bar foo
offset: 0
length: 11
captures:
- string: bar
offset: 4
length: 3
name: bar123
- string: foo foo
offset: 12
length: 8
captures:
- string: null
offset: -1
length: 0
name: bar123
```
## Capture named groups into a map
Given a sample.yml file of:
```yaml
xyzzy-14
```
then
```bash
yq eval 'capture("(?P<a>[a-z]+)-(?P<n>[0-9]+)")' sample.yml
```
will output
```yaml
a: xyzzy
n: "14"
```
## Match without global flag
Given a sample.yml file of:
```yaml
cat cat
```
then
```bash
yq eval 'match("cat")' sample.yml
```
will output
```yaml
string: cat
offset: 0
length: 3
captures: []
```
## Match with global flag
Given a sample.yml file of:
```yaml
cat cat
```
then
```bash
yq eval '[match("cat"; "g")]' sample.yml
```
will output
```yaml
- string: cat
offset: 0
length: 3
captures: []
- string: cat
offset: 4
length: 3
captures: []
```
## Test using regex
Like jq'q equivalant, this works like match but only returns true/false instead of full match details
Given a sample.yml file of:
```yaml
- cat
- dog
```
then
```bash
yq eval '.[] | test("at")' sample.yml
```
will output
```yaml
true
false
```
## Substitute / Replace string
This uses golang regex, described [here](https://github.com/google/re2/wiki/Syntax)
Note the use of `|=` to run in context of the current string value.
+1 -3
View File
@@ -18,8 +18,6 @@ a:
```
## Update and set style of a particular node using path variables
You can use a variable to re-use a path
Given a sample.yml file of:
```yaml
a:
@@ -28,7 +26,7 @@ a:
```
then
```bash
yq eval '.a.b as $x | $x = "new" | $x style="double"' sample.yml
yq eval 'with(.a.b ; . = "new" | . style="double")' sample.yml
```
will output
```yaml
+39
View File
@@ -1,4 +1,43 @@
## Array subtraction
Running
```bash
yq eval --null-input '[1,2] - [2,3]'
```
will output
```yaml
- 1
```
## Array subtraction with nested array
Running
```bash
yq eval --null-input '[[1], 1, 2] - [[1], 3]'
```
will output
```yaml
- 1
- 2
```
## Array subtraction with nested object
Note that order of the keys does not matter
Given a sample.yml file of:
```yaml
- a: b
c: d
- a: b
```
then
```bash
yq eval '. - [{"c": "d", "a": "b"}]' sample.yml
```
will output
```yaml
- a: b
```
## Number subtraction - float
If the lhs or rhs are floats then the expression will be calculated with floats.
+31 -1
View File
@@ -1,4 +1,4 @@
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structurse.
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structures.
## Simple map navigation
Given a sample.yml file of:
```yaml
@@ -63,6 +63,22 @@ will output
frog
```
## Nested special characters
Given a sample.yml file of:
```yaml
a:
"key.withdots":
"another.key": apple
```
then
```bash
yq eval '.a["key.withdots"]["another.key"]' sample.yml
```
will output
```yaml
apple
```
## Keys with spaces
Use quotes with brackets around path elements with special characters
@@ -211,6 +227,20 @@ will output
1
```
## Traversing nested arrays by index
Given a sample.yml file of:
```yaml
[[], [cat]]
```
then
```bash
yq eval '.[1][0]' sample.yml
```
will output
```yaml
cat
```
## Maps with numeric keys
Given a sample.yml file of:
```yaml
+39 -1
View File
@@ -1,4 +1,6 @@
For more complex scenarios, variables can be used to hold values of expression to be used in other expressions.
Like the `jq` equivalents, variables are sometimes required for the more complex expressions (or swapping values between fields).
Note that there is also an additional `ref` operator that holds a reference (instead of a copy) of the path, allowing you to make multiple changes to the same path.
## Single value variable
Given a sample.yml file of:
@@ -56,3 +58,39 @@ title: A well-written article
author: Person McPherson
```
## Using variables to swap values
Given a sample.yml file of:
```yaml
a: a_value
b: b_value
```
then
```bash
yq eval '.a as $x | .b as $y | .b = $x | .a = $y' sample.yml
```
will output
```yaml
a: b_value
b: a_value
```
## Use ref to reference a path repeatedly
Note: You may find the `with` operator more useful.
Given a sample.yml file of:
```yaml
a:
b: thing
c: something
```
then
```bash
yq eval '.a.b ref $x | $x = "new" | $x style="double"' sample.yml
```
will output
```yaml
a:
b: "new"
c: something
```
+62
View File
@@ -0,0 +1,62 @@
Use the `with` operator to conveniently make multiple updates to a deeply nested path, or to update array elements relatively to each other. The first argument expression sets the root context, and the second expression runs against that root context.
## Update and style
Given a sample.yml file of:
```yaml
a:
deeply:
nested: value
```
then
```bash
yq eval 'with(.a.deeply.nested; . = "newValue" | . style="single")' sample.yml
```
will output
```yaml
a:
deeply:
nested: 'newValue'
```
## Update multiple deeply nested properties
Given a sample.yml file of:
```yaml
a:
deeply:
nested: value
other: thing
```
then
```bash
yq eval 'with(.a.deeply; .nested = "newValue" | .other= "newThing")' sample.yml
```
will output
```yaml
a:
deeply:
nested: newValue
other: newThing
```
## Update array elements relatively
The second expression runs with each element of the array as it's contextual root. This allows you to make updates relative to the element.
Given a sample.yml file of:
```yaml
myArray:
- a: apple
- a: banana
```
then
```bash
yq eval 'with(.myArray[]; .b = .a + " yum")' sample.yml
```
will output
```yaml
myArray:
- a: apple
b: apple yum
- a: banana
b: banana yum
```
+1 -3
View File
@@ -1,5 +1,3 @@
# Operators
In `yq` expressions are made up of operators and pipes. A context of nodes is passed through the expression and each operation takes the context as input and returns a new context as output. That output is piped in as input for the next operation in the expression. To begin with, the context is set to the first yaml document of the first yaml file (if processing in sequence using eval).
Lets look at a couple of examples.
@@ -71,7 +69,7 @@ dog
Both sides have now been evaluated, so now the operator copies across the value from the RHS to the value on the LHS, and it returns the now updated context:
```yaml
a: cat
a: dog
b: dog
```
+61
View File
@@ -0,0 +1,61 @@
# NAME
*yq* is a portable command-line YAML processor
# SYNOPSIS
yq [eval/eval-all] [expression] files..
eval/e - Apply the expression to each document in each yaml file in sequence
eval-all/ea - Loads all yaml documents of all yaml files and runs expression once
# DESCRIPTION
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.
This documentation is also available at https://mikefarah.gitbook.io/yq/
# QUICK GUIDE
## Read a value:
```bash
yq e '.a.b[0].c' file.yaml
```
## Pipe from STDIN:
```bash
cat file.yaml | yq e '.a.b[0].c' -
```
## Update a yaml file, inplace
```bash
yq e -i '.a.b[0].c = "cool"' file.yaml
```
## Update using environment variables
```bash
NAME=mike yq e -i '.a.b[0].c = strenv(NAME)' file.yaml
```
## Merge multiple files
```
yq ea '. as $item ireduce ({}; . * $item )' path/to/*.yml
```
## Multiple updates to a yaml file
```bash
yq e -i '
.a.b[0].c = "cool" |
.x.y.z = "foobar" |
.person.name = strenv(NAME)
' file.yaml
```
See the [documentation](https://mikefarah.gitbook.io/yq/) for more.
# 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)
- Powershell has its own...opinions: https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell
# BUGS / ISSUES / FEATURE REQUESTS
Please visit the GitHub page https://github.com/mikefarah/yq/.
+43
View File
@@ -1 +1,44 @@
# String Operators
## RegEx
This uses golangs native regex functions under the hood - See https://github.com/google/re2/wiki/Syntax for the supported syntax.
# String blocks, bash and newlines
Bash is notorious for chomping on precious trailing newline characters, making it tricky to set strings with newlines properly. In particular, the `$( exp )` _will trim trailing newlines_.
For instance to get this yaml:
```
a: |
cat
```
Using `$( exp )` wont work, as it will trim the trailing new line.
```
m=$(echo "cat\n") yq e -n '.a = strenv(m)'
a: cat
```
However, using printf works:
```
printf -v m "cat\n" ; m="$m" yq e -n '.a = strenv(m)'
a: |
cat
```
As well as having multiline expressions:
```
m="cat
" yq e -n '.a = strenv(m)'
a: |
cat
```
Similarly, if you're trying to set the content from a file, and want a trailing new line:
```
IFS= read -rd '' output < <(cat my_file)
output=$output ./yq e '.data.values = strenv(output)' first.yml
```
+1 -1
View File
@@ -1 +1 @@
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structurse.
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structures.
+3 -1
View File
@@ -1 +1,3 @@
For more complex scenarios, variables can be used to hold values of expression to be used in other expressions.
Like the `jq` equivalents, variables are sometimes required for the more complex expressions (or swapping values between fields).
Note that there is also an additional `ref` operator that holds a reference (instead of a copy) of the path, allowing you to make multiple changes to the same path.
+1
View File
@@ -0,0 +1 @@
Use the `with` operator to conveniently make multiple updates to a deeply nested path, or to update array elements relatively to each other. The first argument expression sets the root context, and the second expression runs against that root context.
+3 -3
View File
@@ -9,7 +9,7 @@ import (
yaml "gopkg.in/yaml.v3"
)
type encoder interface {
type Encoder interface {
Encode(node *yaml.Node) error
}
@@ -20,7 +20,7 @@ type yamlEncoder struct {
firstDoc bool
}
func newYamlEncoder(destination io.Writer, indent int, colorise bool) encoder {
func NewYamlEncoder(destination io.Writer, indent int, colorise bool) Encoder {
if indent < 0 {
indent = 0
}
@@ -74,7 +74,7 @@ func mapKeysToStrings(node *yaml.Node) {
}
}
func newJsonEncoder(destination io.Writer, indent int) encoder {
func NewJsonEncoder(destination io.Writer, indent int) Encoder {
var encoder = json.NewEncoder(destination)
encoder.SetEscapeHTML(false) // do not escape html chars e.g. &, <, >
+80
View File
@@ -0,0 +1,80 @@
package yqlib
import (
"fmt"
"io"
"strings"
"github.com/magiconair/properties"
yaml "gopkg.in/yaml.v3"
)
type propertiesEncoder struct {
destination io.Writer
}
func NewPropertiesEncoder(destination io.Writer) Encoder {
return &propertiesEncoder{destination}
}
func (pe *propertiesEncoder) Encode(node *yaml.Node) error {
mapKeysToStrings(node)
p := properties.NewProperties()
err := pe.doEncode(p, node, "")
if err != nil {
return err
}
_, err = p.WriteComment(pe.destination, "#", properties.UTF8)
return err
}
func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yaml.Node, path string) error {
p.SetComment(path,
strings.Replace(node.HeadComment, "#", "", 1)+
strings.Replace(node.LineComment, "#", "", 1))
switch node.Kind {
case yaml.ScalarNode:
_, _, err := p.Set(path, node.Value)
return err
case yaml.DocumentNode:
return pe.doEncode(p, node.Content[0], path)
case yaml.SequenceNode:
return pe.encodeArray(p, node.Content, path)
case yaml.MappingNode:
return pe.encodeMap(p, node.Content, path)
case yaml.AliasNode:
return pe.doEncode(p, node.Alias, path)
default:
return fmt.Errorf("Unsupported node %v", node.Tag)
}
}
func (pe *propertiesEncoder) appendPath(path string, key interface{}) string {
if path == "" {
return fmt.Sprintf("%v", key)
}
return fmt.Sprintf("%v.%v", path, key)
}
func (pe *propertiesEncoder) encodeArray(p *properties.Properties, kids []*yaml.Node, path string) error {
for index, child := range kids {
err := pe.doEncode(p, child, pe.appendPath(path, index))
if err != nil {
return err
}
}
return nil
}
func (pe *propertiesEncoder) encodeMap(p *properties.Properties, kids []*yaml.Node, path string) error {
for index := 0; index < len(kids); index = index + 2 {
key := kids[index]
value := kids[index+1]
err := pe.doEncode(p, value, pe.appendPath(path, key.Value))
if err != nil {
return err
}
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package yqlib
import (
"bufio"
"bytes"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func yamlToProps(sampleYaml string) string {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
var propsEncoder = NewPropertiesEncoder(writer)
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
if err != nil {
panic(err)
}
node := inputs.Front().Value.(*CandidateNode).Node
err = propsEncoder.Encode(node)
if err != nil {
panic(err)
}
writer.Flush()
return strings.TrimSuffix(output.String(), "\n")
}
func TestPropertiesEncoderSimple(t *testing.T) {
var sampleYaml = `a: 'bob cool'`
var expectedJson = `a = bob cool`
var actualProps = yamlToProps(sampleYaml)
test.AssertResult(t, expectedJson, actualProps)
}
func TestPropertiesEncoderSimpleWithComments(t *testing.T) {
var sampleYaml = `a: 'bob cool' # line`
var expectedJson = `# line
a = bob cool`
var actualProps = yamlToProps(sampleYaml)
test.AssertResult(t, expectedJson, actualProps)
}
func TestPropertiesEncoderDeep(t *testing.T) {
var sampleYaml = `a:
b: "bob cool"
`
var expectedJson = `a.b = bob cool`
var actualProps = yamlToProps(sampleYaml)
test.AssertResult(t, expectedJson, actualProps)
}
func TestPropertiesEncoderDeepWithComments(t *testing.T) {
var sampleYaml = `a: # a thing
b: "bob cool" # b thing
`
var expectedJson = `# b thing
a.b = bob cool`
var actualProps = yamlToProps(sampleYaml)
test.AssertResult(t, expectedJson, actualProps)
}
func TestPropertiesEncoderArray(t *testing.T) {
var sampleYaml = `a:
b: [{c: dog}, {c: cat}]
`
var expectedJson = `a.b.0.c = dog
a.b.1.c = cat`
var actualProps = yamlToProps(sampleYaml)
test.AssertResult(t, expectedJson, actualProps)
}
+36 -7
View File
@@ -6,37 +6,66 @@ import (
"github.com/mikefarah/yq/v4/test"
)
func TestPathTreeNoArgsForTwoArgOp(t *testing.T) {
func TestParserNoMatchingCloseCollect(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("[1,2")
test.AssertResultComplex(t, "Bad expression, could not find matching `]`", err.Error())
}
func TestParserNoMatchingCloseObjectInCollect(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(`[{"b": "c"]`)
test.AssertResultComplex(t, "Bad expression, could not find matching `}`", err.Error())
}
func TestParserNoMatchingCloseInCollect(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(`[(.a]`)
test.AssertResultComplex(t, "Bad expression, could not find matching `)`", err.Error())
}
func TestParserNoMatchingCloseCollectObject(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(`{"a": "b"`)
test.AssertResultComplex(t, "Bad expression, could not find matching `}`", err.Error())
}
func TestParserNoMatchingCloseCollectInCollectObject(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(`{"b": [1}`)
test.AssertResultComplex(t, "Bad expression, could not find matching `]`", err.Error())
}
func TestParserNoMatchingCloseBracketInCollectObject(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(`{"b": (1}`)
test.AssertResultComplex(t, "Bad expression, could not find matching `)`", err.Error())
}
func TestParserNoArgsForTwoArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("=")
test.AssertResultComplex(t, "'=' expects 2 args but there is 0", err.Error())
}
func TestPathTreeOneLhsArgsForTwoArgOp(t *testing.T) {
func TestParserOneLhsArgsForTwoArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(".a =")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
}
func TestPathTreeOneRhsArgsForTwoArgOp(t *testing.T) {
func TestParserOneRhsArgsForTwoArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("= .a")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
}
func TestPathTreeTwoArgsForTwoArgOp(t *testing.T) {
func TestParserTwoArgsForTwoArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression(".a = .b")
test.AssertResultComplex(t, nil, err)
}
func TestPathTreeNoArgsForOneArgOp(t *testing.T) {
func TestParserNoArgsForOneArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("explode")
test.AssertResultComplex(t, "'explode' expects 1 arg but received none", err.Error())
}
func TestPathTreeOneArgForOneArgOp(t *testing.T) {
func TestParserOneArgForOneArgOp(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("explode(.)")
test.AssertResultComplex(t, nil, err)
}
func TestPathTreeExtraArgs(t *testing.T) {
func TestParserExtraArgs(t *testing.T) {
_, err := NewExpressionParser().ParseExpression("sortKeys(.) explode(.)")
test.AssertResultComplex(t, "Bad expression, please check expression syntax", err.Error())
}
+23
View File
@@ -2,6 +2,7 @@ package yqlib
import (
"errors"
"fmt"
logging "gopkg.in/op/go-logging.v1"
)
@@ -24,6 +25,17 @@ func popOpToResult(opStack []*token, result []*Operation) ([]*token, []*Operatio
return opStack, append(result, newOp.Operation)
}
func validateNoOpenTokens(token *token) error {
if token.TokenType == openCollect {
return fmt.Errorf(("Bad expression, could not find matching `]`"))
} else if token.TokenType == openCollectObject {
return fmt.Errorf(("Bad expression, could not find matching `}`"))
} else if token.TokenType == openBracket {
return fmt.Errorf(("Bad expression, could not find matching `)`"))
}
return nil
}
func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Operation, error) {
var result []*Operation
// surround the whole thing with brackets
@@ -45,6 +57,10 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
}
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != opener {
missingClosingTokenErr := validateNoOpenTokens(opStack[len(opStack)-1])
if missingClosingTokenErr != nil {
return nil, missingClosingTokenErr
}
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
@@ -76,6 +92,11 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
case closeBracket:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket {
missingClosingTokenErr := validateNoOpenTokens(opStack[len(opStack)-1])
if missingClosingTokenErr != nil {
return nil, missingClosingTokenErr
}
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
@@ -98,6 +119,8 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
}
}
log.Debugf("opstackLen: %v", len(opStack))
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("PostFix Result:")
for _, currentToken := range result {
+44 -6
View File
@@ -7,11 +7,49 @@ import (
"github.com/mikefarah/yq/v4/test"
)
var variableWithNewLine = `"cat
"`
var pathTests = []struct {
path string
expectedTokens []interface{}
expectedPostFix []interface{}
}{
{
`.["cat"].["dog"]`,
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "cat (string)", "]", "SHORT_PIPE", "SELF", "TRAVERSE_ARRAY", "[", "dog (string)", "]"),
append(make([]interface{}, 0), "SELF", "cat (string)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SELF", "dog (string)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.["cat"]`,
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "cat (string)", "]"),
append(make([]interface{}, 0), "SELF", "cat (string)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
},
{
"with(.a;.=3)",
append(make([]interface{}, 0), "WITH", "(", "a", "BLOCK", "SELF", "ASSIGN", "3 (int64)", ")"),
append(make([]interface{}, 0), "a", "SELF", "3 (int64)", "ASSIGN", "BLOCK", "WITH"),
},
{
"0x12",
append(make([]interface{}, 0), "18 (int64)"),
append(make([]interface{}, 0), "18 (int64)"),
},
{
"0X12",
append(make([]interface{}, 0), "18 (int64)"),
append(make([]interface{}, 0), "18 (int64)"),
},
{
".a\n",
append(make([]interface{}, 0), "a"),
append(make([]interface{}, 0), "a"),
},
{
variableWithNewLine,
append(make([]interface{}, 0), "cat\n (string)"),
append(make([]interface{}, 0), "cat\n (string)"),
},
{
`.[0]`,
append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
@@ -99,8 +137,8 @@ var pathTests = []struct {
},
{
`.a.[]`,
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
append(make([]interface{}, 0), "a", "SHORT_PIPE", "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]"),
append(make([]interface{}, 0), "a", "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a[0]`,
@@ -114,8 +152,8 @@ var pathTests = []struct {
},
{
`.a.[0]`,
append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"),
append(make([]interface{}, 0), "a", "SHORT_PIPE", "SELF", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"),
append(make([]interface{}, 0), "a", "SELF", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE"),
},
{
`.a[].c`,
@@ -189,8 +227,8 @@ var pathTests = []struct {
},
{
`{.a: .c, .b.[]: .f.g[]}`,
append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "CREATE_MAP", "f", "SHORT_PIPE", "g", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "}"),
append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "f", "g", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"),
append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "SHORT_PIPE", "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "CREATE_MAP", "f", "SHORT_PIPE", "g", "TRAVERSE_ARRAY", "[", "EMPTY", "]", "}"),
append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "SELF", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "f", "g", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "SHORT_PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "SHORT_PIPE"),
},
{
`explode(.a.b)`,
+34 -17
View File
@@ -111,6 +111,7 @@ func multiplyWithPrefs() lex.Action {
if strings.Contains(options, "d") {
prefs.DeepMergeArrays = true
}
prefs.TraversePrefs.DontFollowAlias = true
op := &Operation{OperationType: multiplyOpType, Value: multiplyOpType.Type, StringValue: options, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op}, nil
}
@@ -166,6 +167,20 @@ func numberValue() lex.Action {
}
}
func hexValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var originalString = string(m.Bytes)
var numberString = originalString[2:]
log.Debugf("numberString: %v", numberString)
var number, errParsingInt = strconv.ParseInt(numberString, 16, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
return &token{TokenType: operationToken, Operation: createValueOperation(number, originalString)}, nil
}
}
func floatValue() lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
var numberString = string(m.Bytes)
@@ -276,11 +291,15 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`join`), opToken(joinStringOpType))
lexer.Add([]byte(`sub`), opToken(subStringOpType))
lexer.Add([]byte(`match`), opToken(matchOpType))
lexer.Add([]byte(`capture`), opToken(captureOpType))
lexer.Add([]byte(`test`), opToken(testOpType))
lexer.Add([]byte(`any`), opToken(anyOpType))
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
lexer.Add([]byte(`all`), opToken(allOpType))
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
lexer.Add([]byte(`contains`), opToken(containsOpType))
lexer.Add([]byte(`split`), opToken(splitStringOpType))
lexer.Add([]byte(`keys`), opToken(keysOpType))
@@ -298,6 +317,8 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`from_entries`), opToken(fromEntriesOpType))
lexer.Add([]byte(`with_entries`), opToken(withEntriesOpType))
lexer.Add([]byte(`with`), opToken(withOpType))
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{LineComment: true}))
lexer.Add([]byte(`headComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{HeadComment: true}))
@@ -320,11 +341,12 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte("( |\t|\n|\r)+"), skip)
lexer.Add([]byte(`\."[^ "]+"\??`), pathToken(true))
lexer.Add([]byte(`\.[^ \}\{\:\[\],\|\.\[\(\)=]+\??`), pathToken(false))
lexer.Add([]byte(`\.[^ ;\}\{\:\[\],\|\.\[\(\)=\n]+\??`), pathToken(false))
lexer.Add([]byte(`\.`), selfToken())
lexer.Add([]byte(`\|`), opToken(pipeOpType))
lexer.Add([]byte(`0[xX][0-9A-Fa-f]+`), hexValue())
lexer.Add([]byte(`-?\d+(\.\d+)`), floatValue())
lexer.Add([]byte(`-?[1-9](\.\d+)?[Ee][-+]?\d+`), floatValue())
lexer.Add([]byte(`-?\d+`), numberValue())
@@ -349,7 +371,8 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\-`), opToken(subtractOpType))
lexer.Add([]byte(`\-=`), opToken(subtractAssignOpType))
lexer.Add([]byte(`\$[a-zA-Z_-0-9]+`), getVariableOpToken())
lexer.Add([]byte(`as`), opToken(assignVariableOpType))
lexer.Add([]byte(`as`), opTokenWithPrefs(assignVariableOpType, nil, assignVarPreferences{}))
lexer.Add([]byte(`ref`), opTokenWithPrefs(assignVariableOpType, nil, assignVarPreferences{IsReference: true}))
err := lexer.CompileNFA()
if err != nil {
@@ -414,17 +437,16 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
log.Debug("processing %v", currentToken.toString(true))
if currentToken.TokenType == traverseArrayCollect {
// `.[exp]`` works by creating a traversal array of [self, exp] and piping that into the traverse array operator
//need to put a traverse array then a collect currentToken
// do this by adding traverse then converting currentToken to collect
if index == 0 || tokens[index-1].TokenType != operationToken ||
tokens[index-1].Operation.OperationType != traversePathOpType {
log.Debug(" adding self")
op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
log.Debug(" adding self")
op := &Operation{OperationType: selfReferenceOpType, StringValue: "SELF"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
log.Debug(" adding traverse array")
op := &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"}
op = &Operation{OperationType: traverseArrayOpType, StringValue: "TRAVERSE_ARRAY"}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
currentToken = &token{TokenType: openCollect}
@@ -452,20 +474,15 @@ func (p *expressionTokeniserImpl) handleToken(tokens []*token, index int, postPr
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == operationToken &&
tokens[index+1].Operation.OperationType == traversePathOpType {
((tokens[index+1].TokenType == operationToken && (tokens[index+1].Operation.OperationType == traversePathOpType)) ||
(tokens[index+1].TokenType == traverseArrayCollect)) {
log.Debug(" adding pipe because the next thing is traverse")
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE", StringValue: "."}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
}
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
tokens[index+1].TokenType == openCollect {
// if tokens[index].TokenType == closeCollect {
// log.Debug(" adding pipe because next is opencollect")
// op := &Operation{OperationType: shortPipeOpType, Value: "PIPE"}
// postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
// }
log.Debug(" adding traverArray because next is opencollect")
op := &Operation{OperationType: traverseArrayOpType}
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: op})
+39 -1
View File
@@ -2,6 +2,7 @@ package yqlib
import (
"io"
"io/ioutil"
"os"
)
@@ -23,9 +24,20 @@ func safelyRenameFile(from string, to string) {
}
}
func tryRemoveFile(filename string) {
log.Debug("Removing temp file: %v", filename)
removeErr := os.Remove(filename)
if removeErr != nil {
log.Errorf("Failed to remove temp file: %v", filename)
}
}
// thanks https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src) // nolint gosec
// ignore CWE-22 gosec issue - that's more targetted for http based apps that run in a public directory,
// and ensuring that it's not possible to give a path to a file outside thar directory.
in, err := os.Open(src) // #nosec
if err != nil {
return err
}
@@ -41,6 +53,13 @@ func copyFileContents(src, dst string) (err error) {
return out.Sync()
}
func SafelyCloseReader(reader io.Reader) {
switch reader := reader.(type) {
case *os.File:
safelyCloseFile(reader)
}
}
func safelyCloseFile(file *os.File) {
err := file.Close()
if err != nil {
@@ -48,3 +67,22 @@ func safelyCloseFile(file *os.File) {
log.Error(err.Error())
}
}
func createTempFile() (*os.File, error) {
_, err := os.Stat(os.TempDir())
if os.IsNotExist(err) {
err = os.Mkdir(os.TempDir(), 0700)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
file, err := ioutil.TempFile("", "temp")
if err != nil {
return nil, err
}
return file, err
}
+93
View File
@@ -0,0 +1,93 @@
package yqlib
import (
"bufio"
"io"
"os"
)
type frontMatterHandler interface {
Split() error
GetYamlFrontMatterFilename() string
GetContentReader() io.Reader
CleanUp()
}
type frontMatterHandlerImpl struct {
originalFilename string
yamlFrontMatterFilename string
contentReader io.Reader
}
func NewFrontMatterHandler(originalFilename string) frontMatterHandler {
return &frontMatterHandlerImpl{originalFilename, "", nil}
}
func (f *frontMatterHandlerImpl) GetYamlFrontMatterFilename() string {
return f.yamlFrontMatterFilename
}
func (f *frontMatterHandlerImpl) GetContentReader() io.Reader {
return f.contentReader
}
func (f *frontMatterHandlerImpl) CleanUp() {
tryRemoveFile(f.yamlFrontMatterFilename)
}
// Splits the given file by yaml front matter
// yaml content will be saved to first temporary file
// remaining content will be saved to second temporary file
func (f *frontMatterHandlerImpl) Split() error {
var reader *bufio.Reader
var err error
if f.originalFilename == "-" {
reader = bufio.NewReader(os.Stdin)
} else {
file, err := os.Open(f.originalFilename) // #nosec
if err != nil {
return err
}
reader = bufio.NewReader(file)
}
f.contentReader = reader
yamlTempFile, err := createTempFile()
if err != nil {
return err
}
f.yamlFrontMatterFilename = yamlTempFile.Name()
log.Debug("yamlTempFile: %v", yamlTempFile.Name())
lineCount := 0
for {
peekBytes, err := reader.Peek(3)
if err == io.EOF {
// we've finished reading the yaml content..I guess
break
} else if err != nil {
return err
}
if lineCount > 0 && string(peekBytes) == "---" {
// we've finished reading the yaml content..
break
}
line, errReading := reader.ReadString('\n')
lineCount = lineCount + 1
if errReading != nil && errReading != io.EOF {
return errReading
}
_, errWriting := yamlTempFile.Write([]byte(line))
if errWriting != nil {
return errWriting
}
}
safelyCloseFile(yamlTempFile)
return nil
}
+142
View File
@@ -0,0 +1,142 @@
package yqlib
import (
"io/ioutil"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func createTestFile(content string) string {
tempFile, err := createTempFile()
if err != nil {
panic(err)
}
_, err = tempFile.Write([]byte(content))
if err != nil {
panic(err)
}
safelyCloseFile(tempFile)
return tempFile.Name()
}
func readFile(filename string) string {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
return string(bytes)
}
func TestFrontMatterSplitWithLeadingSep(t *testing.T) {
file := createTestFile(`---
a: apple
b: banana
---
not a
yaml: doc
`)
expectedYamlFm := `---
a: apple
b: banana
`
expectedContent := `---
not a
yaml: doc
`
fmHandler := NewFrontMatterHandler(file)
err := fmHandler.Split()
if err != nil {
panic(err)
}
yamlFm := readFile(fmHandler.GetYamlFrontMatterFilename())
test.AssertResult(t, expectedYamlFm, yamlFm)
contentBytes, err := ioutil.ReadAll(fmHandler.GetContentReader())
if err != nil {
panic(err)
}
test.AssertResult(t, expectedContent, string(contentBytes))
tryRemoveFile(file)
fmHandler.CleanUp()
}
func TestFrontMatterSplitWithNoLeadingSep(t *testing.T) {
file := createTestFile(`a: apple
b: banana
---
not a
yaml: doc
`)
expectedYamlFm := `a: apple
b: banana
`
expectedContent := `---
not a
yaml: doc
`
fmHandler := NewFrontMatterHandler(file)
err := fmHandler.Split()
if err != nil {
panic(err)
}
yamlFm := readFile(fmHandler.GetYamlFrontMatterFilename())
test.AssertResult(t, expectedYamlFm, yamlFm)
contentBytes, err := ioutil.ReadAll(fmHandler.GetContentReader())
if err != nil {
panic(err)
}
test.AssertResult(t, expectedContent, string(contentBytes))
tryRemoveFile(file)
fmHandler.CleanUp()
}
func TestFrontMatterSplitWithArray(t *testing.T) {
file := createTestFile(`[1,2,3]
---
not a
yaml: doc
`)
expectedYamlFm := "[1,2,3]\n"
expectedContent := `---
not a
yaml: doc
`
fmHandler := NewFrontMatterHandler(file)
err := fmHandler.Split()
if err != nil {
panic(err)
}
yamlFm := readFile(fmHandler.GetYamlFrontMatterFilename())
test.AssertResult(t, expectedYamlFm, yamlFm)
contentBytes, err := ioutil.ReadAll(fmHandler.GetContentReader())
if err != nil {
panic(err)
}
test.AssertResult(t, expectedContent, string(contentBytes))
tryRemoveFile(file)
fmHandler.CleanUp()
}
+93 -5
View File
@@ -6,6 +6,8 @@ import (
"bytes"
"container/list"
"fmt"
"strconv"
"strings"
logging "gopkg.in/op/go-logging.v1"
yaml "gopkg.in/yaml.v3"
@@ -60,6 +62,7 @@ var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50,
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
var containsOpType = &operationType{Type: "CONTAINS", NumArgs: 1, Precedence: 50, Handler: containsOperator}
var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator}
var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, Precedence: 50, Handler: allOperator}
@@ -67,6 +70,8 @@ var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence:
var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator}
var withEntriesOpType = &operationType{Type: "WITH_ENTRIES", NumArgs: 1, Precedence: 50, Handler: withEntriesOperator}
var withOpType = &operationType{Type: "WITH", NumArgs: 1, Precedence: 50, Handler: withOperator}
var splitDocumentOpType = &operationType{Type: "SPLIT_DOC", NumArgs: 0, Precedence: 50, Handler: splitDocumentOperator}
var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator}
var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator}
@@ -83,6 +88,9 @@ var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50,
var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator}
var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator}
var subStringOpType = &operationType{Type: "SUBSTR", NumArgs: 1, Precedence: 50, Handler: substituteStringOperator}
var matchOpType = &operationType{Type: "MATCH", NumArgs: 1, Precedence: 50, Handler: matchOperator}
var captureOpType = &operationType{Type: "CAPTURE", NumArgs: 1, Precedence: 50, Handler: captureOperator}
var testOpType = &operationType{Type: "TEST", NumArgs: 1, Precedence: 50, Handler: testOperator}
var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator}
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: keysOperator}
@@ -114,8 +122,77 @@ type Operation struct {
UpdateAssign bool // used for assign ops, when true it means we evaluate the rhs given the lhs
}
func createValueOperation(value interface{}, stringValue string) *Operation {
var node yaml.Node = yaml.Node{Kind: yaml.ScalarNode}
func recurseNodeArrayEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
if len(lhs.Content) != len(rhs.Content) {
return false
}
for index := 0; index < len(lhs.Content); index = index + 1 {
if !recursiveNodeEqual(lhs.Content[index], rhs.Content[index]) {
return false
}
}
return true
}
func findInArray(array *yaml.Node, item *yaml.Node) int {
for index := 0; index < len(array.Content); index = index + 1 {
if recursiveNodeEqual(array.Content[index], item) {
return index
}
}
return -1
}
func recurseNodeObjectEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
if len(lhs.Content) != len(rhs.Content) {
return false
}
for index := 0; index < len(lhs.Content); index = index + 2 {
key := lhs.Content[index]
value := lhs.Content[index+1]
indexInRhs := findInArray(rhs, key)
if indexInRhs == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRhs+1]) {
return false
}
}
return true
}
func recursiveNodeEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
if lhs.Kind != rhs.Kind || lhs.Tag != rhs.Tag {
return false
} else if lhs.Tag == "!!null" {
return true
} else if lhs.Kind == yaml.ScalarNode {
return lhs.Value == rhs.Value
} else if lhs.Kind == yaml.SequenceNode {
return recurseNodeArrayEqual(lhs, rhs)
} else if lhs.Kind == yaml.MappingNode {
return recurseNodeObjectEqual(lhs, rhs)
}
return false
}
// yaml numbers can be hex encoded...
func parseInt(numberString string) (string, int64, error) {
if strings.HasPrefix(numberString, "0x") ||
strings.HasPrefix(numberString, "0X") {
num, err := strconv.ParseInt(numberString[2:], 16, 64) // nolint
return "0x%X", num, err
}
num, err := strconv.ParseInt(numberString, 10, 64) // nolint
return "%v", num, err
}
func createScalarNode(value interface{}, stringValue string) *yaml.Node {
var node = &yaml.Node{Kind: yaml.ScalarNode}
node.Value = stringValue
switch value.(type) {
@@ -130,17 +207,25 @@ func createValueOperation(value interface{}, stringValue string) *Operation {
case nil:
node.Tag = "!!null"
}
return node
}
func createValueOperation(value interface{}, stringValue string) *Operation {
var node *yaml.Node = createScalarNode(value, stringValue)
return &Operation{
OperationType: valueOpType,
Value: value,
StringValue: stringValue,
CandidateNode: &CandidateNode{Node: &node},
CandidateNode: &CandidateNode{Node: node},
}
}
// debugging purposes only
func (p *Operation) toString() string {
if p == nil {
return "OP IS NIL"
}
if p.OperationType == traversePathOpType {
return fmt.Sprintf("%v", p.Value)
} else if p.OperationType == selfReferenceOpType {
@@ -158,7 +243,7 @@ func NodesToString(collection *list.List) string {
return ""
}
result := ""
result := fmt.Sprintf("%v results\n", collection.Len())
for el := collection.Front(); el != nil; el = el.Next() {
result = result + "\n" + NodeToString(el.Value.(*CandidateNode))
}
@@ -179,7 +264,10 @@ func NodeToString(node *CandidateNode) string {
if errorEncoding != nil {
log.Error("Error debugging node, %v", errorEncoding.Error())
}
encoder.Close()
errorClosingEncoder := encoder.Close()
if errorClosingEncoder != nil {
log.Error("Error closing encoder: ", errorClosingEncoder.Error())
}
tag := value.Tag
if value.Kind == yaml.DocumentNode {
tag = "doc"
+41 -17
View File
@@ -9,26 +9,50 @@ func matchKey(name string, pattern string) (matched bool) {
log.Debug("wild!")
return true
}
return deepMatch([]rune(name), []rune(pattern))
return deepMatch(name, pattern)
}
func deepMatch(str, pattern []rune) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
// deepMatch reports whether the name matches the pattern in linear time.
// Source https://research.swtch.com/glob
func deepMatch(name, pattern string) bool {
px := 0
nx := 0
nextPx := 0
nextNx := 0
for px < len(pattern) || nx < len(name) {
if px < len(pattern) {
c := pattern[px]
switch c {
default: // ordinary character
if nx < len(name) && name[nx] == c {
px++
nx++
continue
}
case '?': // single-character wildcard
if nx < len(name) {
px++
nx++
continue
}
case '*': // zero-or-more-character wildcard
// Try to match at nx.
// If that doesn't work out,
// restart at nx+1 next.
nextPx = px
nextNx = nx + 1
px++
continue
}
case '?':
if len(str) == 0 {
return false
}
case '*':
return deepMatch(str, pattern[1:]) ||
(len(str) > 0 && deepMatch(str[1:], pattern))
}
str = str[1:]
pattern = pattern[1:]
// Mismatch. Maybe restart.
if 0 < nextNx && nextNx <= len(name) {
px = nextPx
nx = nextNx
continue
}
return false
}
return len(str) == 0 && len(pattern) == 0
// Matched all of pattern to all of name. Success.
return true
}
+40
View File
@@ -0,0 +1,40 @@
package yqlib
import (
"strings"
"testing"
)
func TestDeepMatch(t *testing.T) {
var tests = []struct {
name string
pattern string
ok bool
}{
{"", "", true},
{"", "x", false},
{"x", "", false},
{"abc", "abc", true},
{"abc", "*", true},
{"abc", "*c", true},
{"abc", "*b", false},
{"abc", "a*", true},
{"abc", "b*", false},
{"a", "a*", true},
{"a", "*a", true},
{"axbxcxdxe", "a*b*c*d*e*", true},
{"axbxcxdxexxx", "a*b*c*d*e*", true},
{"abxbbxdbxebxczzx", "a*b?c*x", true},
{"abxbbxdbxebxczzy", "a*b?c*x", false},
{strings.Repeat("a", 100), "a*a*a*a*b", false},
{"xxx", "*x", true},
}
for _, tt := range tests {
t.Run(tt.name+" "+tt.pattern, func(t *testing.T) {
if want, got := tt.ok, deepMatch(tt.name, tt.pattern); want != got {
t.Errorf("Expected %v got %v", want, got)
}
})
}
}
+4 -8
View File
@@ -15,11 +15,7 @@ func createAddOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
}
func addAssignOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
assignmentOp := &Operation{OperationType: assignOpType}
assignmentOp.UpdateAssign = true
selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}}
assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: expressionNode.Lhs, Rhs: createAddOp(selfExpression, expressionNode.Rhs)}
return d.GetMatchingNodes(context, assignmentOpNode)
return compoundAssignFunction(d, context, expressionNode, createAddOp)
}
func toNodes(candidate *CandidateNode) []*yaml.Node {
@@ -80,17 +76,17 @@ func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*Candida
target.Node.Tag = "!!str"
target.Node.Value = lhs.Value + rhs.Value
} else if lhs.Tag == "!!int" && rhs.Tag == "!!int" {
lhsNum, err := strconv.Atoi(lhs.Value)
format, lhsNum, err := parseInt(lhs.Value)
if err != nil {
return nil, err
}
rhsNum, err := strconv.Atoi(rhs.Value)
_, rhsNum, err := parseInt(rhs.Value)
if err != nil {
return nil, err
}
sum := lhsNum + rhsNum
target.Node.Tag = "!!int"
target.Node.Value = fmt.Sprintf("%v", sum)
target.Node.Value = fmt.Sprintf(format, sum)
} else if (lhs.Tag == "!!int" || lhs.Tag == "!!float") && (rhs.Tag == "!!int" || rhs.Tag == "!!float") {
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
if err != nil {
+8
View File
@@ -86,6 +86,14 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n",
},
},
{
description: "Append another array using +=",
document: `{a: [1,2], b: [3,4]}`,
expression: `.a += .b`,
expected: []string{
"D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n",
},
},
{
description: "Relative append",
document: `a: { a1: {b: [cat]}, a2: {b: [dog]}, a3: {} }`,
+4 -1
View File
@@ -2,13 +2,16 @@ package yqlib
func alternativeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- alternative")
return crossFunction(d, context.ReadOnlyClone(), expressionNode, alternativeFunc, true)
return crossFunction(d, context, expressionNode, alternativeFunc, true)
}
func alternativeFunc(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
if lhs == nil {
return rhs, nil
}
if rhs == nil {
return lhs, nil
}
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Alternative LHS: %v", lhs.Node.Tag)
+17
View File
@@ -5,6 +5,15 @@ import (
)
var alternativeOperatorScenarios = []expressionScenario{
{
// to match jq - we do not use a readonly clone context on the LHS.
skipDoc: true,
expression: `.b // .c`,
document: `a: bridge`,
expected: []string{
"D0, P[c], (!!null)::null\n",
},
},
{
skipDoc: true,
expression: `(.b // "hello") as $x`,
@@ -13,6 +22,14 @@ var alternativeOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::a: bridge\n",
},
},
{
skipDoc: true,
expression: `.a // .b`,
document: `a: 2`,
expected: []string{
"D0, P[a], (!!int)::2\n",
},
},
{
description: "LHS is defined",
expression: `.a // "hello"`,
+63 -29
View File
@@ -141,6 +141,48 @@ func explodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expr
return context, nil
}
func reconstructAliasedMap(node *yaml.Node, context Context) error {
var newContent = list.New()
// can I short cut here by prechecking if there's an anchor in the map?
// no it needs to recurse in overrideEntry.
for index := 0; index < len(node.Content); index = index + 2 {
keyNode := node.Content[index]
valueNode := node.Content[index+1]
log.Debugf("traversing %v", keyNode.Value)
if keyNode.Value != "<<" {
err := overrideEntry(node, keyNode, valueNode, index, context.ChildContext(newContent))
if err != nil {
return err
}
} else {
if valueNode.Kind == yaml.SequenceNode {
log.Debugf("an alias merge list!")
for index := len(valueNode.Content) - 1; index >= 0; index = index - 1 {
aliasNode := valueNode.Content[index]
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
}
} else {
log.Debugf("an alias merge!")
err := applyAlias(node, valueNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
}
}
}
node.Content = make([]*yaml.Node, newContent.Len())
index := 0
for newEl := newContent.Front(); newEl != nil; newEl = newEl.Next() {
node.Content[index] = newEl.Value.(*yaml.Node)
index++
}
return nil
}
func explodeNode(node *yaml.Node, context Context) error {
node.Anchor = ""
switch node.Kind {
@@ -165,43 +207,35 @@ func explodeNode(node *yaml.Node, context Context) error {
}
return nil
case yaml.MappingNode:
var newContent = list.New()
// //check the map has an alias in it
hasAlias := false
for index := 0; index < len(node.Content); index = index + 2 {
keyNode := node.Content[index]
valueNode := node.Content[index+1]
log.Debugf("traversing %v", keyNode.Value)
if keyNode.Value != "<<" {
err := overrideEntry(node, keyNode, valueNode, index, context.ChildContext(newContent))
if keyNode.Value == "<<" {
hasAlias = true
break
}
}
if hasAlias {
// this is a slow op, which is why we want to check before running it.
return reconstructAliasedMap(node, context)
} else {
// this map has no aliases, but it's kids might
for index := 0; index < len(node.Content); index = index + 2 {
keyNode := node.Content[index]
valueNode := node.Content[index+1]
err := explodeNode(keyNode, context)
if err != nil {
return err
}
} else {
if valueNode.Kind == yaml.SequenceNode {
log.Debugf("an alias merge list!")
for index := len(valueNode.Content) - 1; index >= 0; index = index - 1 {
aliasNode := valueNode.Content[index]
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
}
} else {
log.Debugf("an alias merge!")
err := applyAlias(node, valueNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
err = explodeNode(valueNode, context)
if err != nil {
return err
}
}
return nil
}
node.Content = make([]*yaml.Node, newContent.Len())
index := 0
for newEl := newContent.Front(); newEl != nil; newEl = newEl.Next() {
node.Content[index] = newEl.Value.(*yaml.Node)
index++
}
return nil
default:
return nil
}
@@ -12,6 +12,29 @@ var specDocument = `- &CENTER { x: 1, y: 2 }
var expectedSpecResult = "D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"
var simpleArrayRef = `
item_value: &item_value
value: true
thingOne:
name: item_1
<<: *item_value
thingTwo:
name: item_2
<<: *item_value
`
var expectedUpdatedArrayRef = `D0, P[], (doc)::item_value: &item_value
value: true
thingOne:
name: item_1
value: false
thingTwo:
name: item_2
!!merge <<: *item_value
`
var anchorOperatorScenarios = []expressionScenario{
{
description: "Merge one map",
@@ -197,6 +220,13 @@ foobar:
"D0, P[], (doc)::{f: {a: cat, b: {f: cat}, cat: {f: cat}}}\n",
},
},
{
description: "Dereference and update a field",
subdescription: "`Use explode with multiply to dereference an object",
document: simpleArrayRef,
expression: `.thingOne |= explode(.) * {"value": false}`,
expected: []string{expectedUpdatedArrayRef},
},
}
func TestAnchorAliasOperatorScenarios(t *testing.T) {
+11 -5
View File
@@ -1,21 +1,27 @@
package yqlib
func assignUpdateFunc(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
rhs.Node = unwrapDoc(rhs.Node)
lhs.UpdateFrom(rhs)
return lhs, nil
}
func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil {
return Context{}, err
}
var rhs Context
if !expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Rhs)
// this works because we already ran against LHS with an editable context.
_, err := crossFunction(d, context.ReadOnlyClone(), expressionNode, assignUpdateFunc, false)
return context, err
}
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
if expressionNode.Operation.UpdateAssign {
rhs, err = d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
}
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs)
if err != nil {
return Context{}, err
+9 -1
View File
@@ -36,6 +36,14 @@ var assignOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{a: {g: foof}}\n",
},
},
{
description: "Double elements in an array",
document: `[1,2,3]`,
expression: `.[] |= . * 2`,
expected: []string{
"D0, P[], (doc)::[2, 4, 6]\n",
},
},
{
description: "Update node from another file",
subdescription: "Note this will also work when the second file is a scalar (string/number)",
@@ -57,7 +65,7 @@ var assignOperatorScenarios = []expressionScenario{
{
description: "Updated multiple paths",
document: `{a: fieldA, b: fieldB, c: fieldC}`,
expression: `(.a, .c) |= "potatoe"`,
expression: `(.a, .c) = "potatoe"`,
expected: []string{
"D0, P[], (doc)::{a: potatoe, b: fieldB, c: potatoe}\n",
},
+16
View File
@@ -12,6 +12,22 @@ var booleanOperatorScenarios = []expressionScenario{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "b: hi",
expression: `.a or .c`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "b: false",
expression: `.b or .c`,
expected: []string{
"D0, P[b], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "b: hi",
+1 -2
View File
@@ -20,8 +20,7 @@ import (
func collectObjectOperator(d *dataTreeNavigator, originalContext Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- collectObjectOperation")
context := originalContext.Clone()
context.DontAutoCreate = false
context := originalContext.WritableClone()
if context.MatchingNodes.Len() == 0 {
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"}
+111
View File
@@ -0,0 +1,111 @@
package yqlib
import (
"fmt"
"strings"
yaml "gopkg.in/yaml.v3"
)
func containsOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
return crossFunction(d, context.ReadOnlyClone(), expressionNode, containsWithNodes, false)
}
func containsArrayElement(array *yaml.Node, item *yaml.Node) (bool, error) {
for index := 0; index < len(array.Content); index = index + 1 {
containedInArray, err := contains(array.Content[index], item)
if err != nil {
return false, err
}
if containedInArray {
return true, nil
}
}
return false, nil
}
func containsArray(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
if rhs.Kind != yaml.SequenceNode {
return containsArrayElement(lhs, rhs)
}
for index := 0; index < len(rhs.Content); index = index + 1 {
itemInArray, err := containsArrayElement(lhs, rhs.Content[index])
if err != nil {
return false, err
}
if !itemInArray {
return false, nil
}
}
return true, nil
}
func containsObject(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
if rhs.Kind != yaml.MappingNode {
return false, nil
}
for index := 0; index < len(rhs.Content); index = index + 2 {
rhsKey := rhs.Content[index]
rhsValue := rhs.Content[index+1]
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
lhsKeyIndex := findInArray(lhs, rhsKey)
log.Debugf("index is %v", lhsKeyIndex)
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 {
return false, nil
}
lhsValue := lhs.Content[lhsKeyIndex+1]
log.Debugf("lhsValue is %v", lhsValue.Value)
itemInArray, err := contains(lhsValue, rhsValue)
log.Debugf("rhsValue is %v", rhsValue.Value)
if err != nil {
return false, err
}
if !itemInArray {
return false, nil
}
}
return true, nil
}
func containsScalars(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
if lhs.Tag == "!!str" {
return strings.Contains(lhs.Value, rhs.Value), nil
}
return lhs.Value == rhs.Value, nil
}
func contains(lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
switch lhs.Kind {
case yaml.MappingNode:
return containsObject(lhs, rhs)
case yaml.SequenceNode:
return containsArray(lhs, rhs)
case yaml.ScalarNode:
if rhs.Kind != yaml.ScalarNode || lhs.Tag != rhs.Tag {
return false, nil
}
if lhs.Tag == "!!null" {
return rhs.Tag == "!!null", nil
}
return containsScalars(lhs, rhs)
}
return false, fmt.Errorf("%v not yet supported for contains", lhs.Tag)
}
func containsWithNodes(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
if lhs.Node.Kind != rhs.Node.Kind {
return nil, fmt.Errorf("%v cannot check contained in %v", rhs.Node.Tag, lhs.Node.Tag)
}
result, err := contains(lhs.Node, rhs.Node)
if err != nil {
return nil, err
}
return createBooleanCandidate(lhs, result), nil
}
+82
View File
@@ -0,0 +1,82 @@
package yqlib
import "testing"
var containsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: `null | contains(~)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
expression: `3 | contains(3)`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
expression: `3 | contains(32)`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Array contains array",
subdescription: "Array is equal or subset of",
document: `["foobar", "foobaz", "blarp"]`,
expression: `contains(["baz", "bar"])`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
expression: `["dog", "cat", "giraffe"] | contains(["camel"])`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Object included in array",
document: `{"foo": 12, "bar":[1,2,{"barp":12, "blip":13}]}`,
expression: `contains({"bar": [{"barp": 12}]})`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "Object not included in array",
document: `{"foo": 12, "bar":[1,2,{"barp":12, "blip":13}]}`,
expression: `contains({"foo": 12, "bar": [{"barp": 15}]})`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "String contains substring",
document: `"foobar"`,
expression: `contains("bar")`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "String equals string",
document: `"meow"`,
expression: `contains("meow")`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
}
func TestContainsOperatorScenarios(t *testing.T) {
for _, tt := range containsOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Contains", containsOperatorScenarios)
}
+8
View File
@@ -52,6 +52,14 @@ var entriesOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::KEY_a: 1\nKEY_b: 2\n",
},
},
{
description: "Use with_entries to filter the map",
document: `{a: { b: bird }, c: { d: dog }}`,
expression: `with_entries(select(.value | has("b")))`,
expected: []string{
"D0, P[], (!!map)::a: {b: bird}\n",
},
},
}
func TestEntriesOperatorScenarios(t *testing.T) {
+23
View File
@@ -5,6 +5,29 @@ import (
)
var equalsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: ".a == .b",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: ".a == .b",
expected: []string{
"D0, P[a], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: ".b == .a",
expected: []string{
"D0, P[a], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "cat",
+49 -11
View File
@@ -6,6 +6,7 @@ import (
"container/list"
"github.com/jinzhu/copier"
yaml "gopkg.in/yaml.v3"
)
@@ -20,8 +21,29 @@ func multiplyOperator(d *dataTreeNavigator, context Context, expressionNode *Exp
return crossFunction(d, context, expressionNode, multiply(expressionNode.Operation.Preferences.(multiplyPreferences)), false)
}
func getNewBlankNode(lhs *yaml.Node, rhs *yaml.Node) *yaml.Node {
blankNode := &yaml.Node{}
if lhs.HeadComment != "" {
blankNode.HeadComment = lhs.HeadComment
} else if rhs.HeadComment != "" {
blankNode.HeadComment = rhs.HeadComment
}
if lhs.FootComment != "" {
blankNode.FootComment = lhs.FootComment
} else if rhs.FootComment != "" {
blankNode.FootComment = rhs.FootComment
}
return blankNode
}
func multiply(preferences multiplyPreferences) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
// need to do this before unWrapping the potential document node
newBlankNode := getNewBlankNode(lhs.Node, rhs.Node)
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
log.Debugf("Multipling LHS: %v", lhs.Node.Tag)
@@ -29,14 +51,21 @@ func multiply(preferences multiplyPreferences) func(d *dataTreeNavigator, contex
if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode ||
(lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) {
var newBlank = lhs.CreateChild(nil, &yaml.Node{})
var newThing, err = mergeObjects(d, context, newBlank, lhs, multiplyPreferences{})
var newBlank = CandidateNode{}
err := copier.CopyWithOption(&newBlank, lhs, copier.Option{IgnoreEmpty: true, DeepCopy: true})
if err != nil {
return nil, err
}
return mergeObjects(d, context, newThing, rhs, preferences)
newBlank.Node.HeadComment = newBlankNode.HeadComment
newBlank.Node.FootComment = newBlankNode.FootComment
// var newBlank = lhs.CreateChild(nil, newBlankNode)
// log.Debugf("merge - merge lhs into blank")
// var newThing, err = mergeObjects(d, context.WritableClone(), newBlank, lhs, multiplyPreferences{})
// if err != nil {
// return nil, err
// }
return mergeObjects(d, context.WritableClone(), &newBlank, rhs, preferences)
} else if lhs.Node.Tag == "!!int" && rhs.Node.Tag == "!!int" {
return multiplyIntegers(lhs, rhs)
} else if (lhs.Node.Tag == "!!int" || lhs.Node.Tag == "!!float") && (rhs.Node.Tag == "!!int" || rhs.Node.Tag == "!!float") {
@@ -70,25 +99,26 @@ func multiplyIntegers(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, e
target.Node.Style = lhs.Node.Style
target.Node.Tag = "!!int"
lhsNum, err := strconv.Atoi(lhs.Node.Value)
format, lhsNum, err := parseInt(lhs.Node.Value)
if err != nil {
return nil, err
}
rhsNum, err := strconv.Atoi(rhs.Node.Value)
_, rhsNum, err := parseInt(rhs.Node.Value)
if err != nil {
return nil, err
}
target.Node.Value = fmt.Sprintf("%v", lhsNum*rhsNum)
target.Node.Value = fmt.Sprintf(format, lhsNum*rhsNum)
return target, nil
}
func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode, preferences multiplyPreferences) (*CandidateNode, error) {
shouldAppendArrays := preferences.AppendArrays
var results = list.New()
// shouldn't recurse arrays if appending
prefs := recursiveDescentPreferences{RecurseArray: !shouldAppendArrays,
// only need to recurse the array if we are doing a deep merge
prefs := recursiveDescentPreferences{RecurseArray: preferences.DeepMergeArrays,
TraversePreferences: traversePreferences{DontFollowAlias: true, IncludeMapKeys: true}}
log.Debugf("merge - preferences.DeepMergeArrays %v", preferences.DeepMergeArrays)
log.Debugf("merge - preferences.AppendArrays %v", preferences.AppendArrays)
err := recursiveDecent(d, results, context.SingleChildContext(rhs), prefs)
if err != nil {
return nil, err
@@ -118,14 +148,22 @@ func applyAssignment(d *dataTreeNavigator, context Context, pathIndexToStartFrom
log.Debugf("merge - applyAssignment lhs %v, rhs: %v", lhs.GetKey(), rhs.GetKey())
lhsPath := rhs.Path[pathIndexToStartFrom:]
log.Debugf("merge - lhsPath %v", lhsPath)
assignmentOp := &Operation{OperationType: assignAttributesOpType}
if shouldAppendArrays && rhs.Node.Kind == yaml.SequenceNode {
assignmentOp.OperationType = addAssignOpType
log.Debugf("merge - assignmentOp.OperationType = addAssignOpType")
} else if !preferences.DeepMergeArrays && rhs.Node.Kind == yaml.SequenceNode ||
(rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode) {
assignmentOp.OperationType = assignOpType
assignmentOp.UpdateAssign = false
log.Debugf("merge - rhs.Node.Kind == yaml.SequenceNode: %v", rhs.Node.Kind == yaml.SequenceNode)
log.Debugf("merge - rhs.Node.Kind == yaml.ScalarNode: %v", rhs.Node.Kind == yaml.ScalarNode)
log.Debugf("merge - rhs.Node.Kind == yaml.AliasNode: %v", rhs.Node.Kind == yaml.AliasNode)
log.Debugf("merge - assignmentOp.OperationType = assignOpType, no updateassign")
} else {
log.Debugf("merge - assignmentOp := &Operation{OperationType: assignAttributesOpType}")
}
rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhs}
+131 -1
View File
@@ -24,13 +24,121 @@ list2:
- "123"
`
var mergeArrayWithAnchors = `sample:
- &a
- <<: *a
`
var mergeArraysObjectKeysText = `It's a complex command, the trickyness comes from needing to have the right context in the expressions.
First we save the second array into a variable '$two' which lets us reference it later.
We then need to update the first array. We will use the relative update (|=) because we need to update relative to the current element of the array in the LHS in the RHS expression.
We set the current element of the first array as $cur. Now we multiply (merge) $cur with the matching entry in $two, by passing $two through a select filter.
`
var docWithHeader = `
# here
a: apple
`
var nodeWithHeader = `
# here
a: apple
`
var docNoComments = `
b: banana
`
var docWithFooter = `
a: apple
# footer
`
var nodeWithFooter = `
a: apple
# footer`
var document = `
a: &cat {name: cat}
b: {name: dog}
c:
<<: *cat
`
var multiplyOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: mergeArrayWithAnchors,
expression: `. * .`,
expected: []string{
"D0, P[], (!!map)::sample:\n - &a\n - !!merge <<: *a\n",
},
},
{
skipDoc: true,
document: docWithHeader,
document2: docNoComments,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::# here\na: apple\nb: banana\n",
},
},
{
skipDoc: true,
document: nodeWithHeader,
document2: docNoComments,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::# here\na: apple\nb: banana\n",
},
},
{
skipDoc: true,
document: docNoComments,
document2: docWithHeader,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::# here\nb: banana\na: apple\n",
},
},
{
skipDoc: true,
document: docNoComments,
document2: nodeWithHeader,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::b: banana\n# here\na: apple\n",
},
},
{
skipDoc: true,
document: docWithFooter,
document2: docNoComments,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::a: apple\nb: banana\n\n# footer\n",
},
},
{
skipDoc: true,
document: nodeWithFooter,
document2: docNoComments,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{ // not sure why there's an extra newline *shrug*
"D0, P[], (!!map)::a: apple\n# footer\n\nb: banana\n",
},
},
{
skipDoc: true,
document: docNoComments,
document2: docWithFooter,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::b: banana\na: apple\n\n# footer\n",
},
},
{
description: "Multiply integers",
expression: `3 * 4`,
@@ -47,6 +155,20 @@ var multiplyOperatorScenarios = []expressionScenario{
docExpected,
},
},
{
skipDoc: true,
expression: `.x = {"things": "whatever"} * {}`,
expected: []string{
"D0, P[], ()::x:\n things: whatever\n",
},
},
{
skipDoc: true,
expression: `.x = {} * {"things": "whatever"}`,
expected: []string{
"D0, P[], ()::x:\n things: whatever\n",
},
},
{
skipDoc: true,
expression: `3 * 4.5`,
@@ -290,7 +412,15 @@ var multiplyOperatorScenarios = []expressionScenario{
document: mergeDocSample,
expression: `.foobar * .foobarList`,
expected: []string{
"D0, P[foobar], (!!map)::c: foobarList_c\n<<: [*foo, *bar]\nthing: foobar_thing\nb: foobarList_b\n",
"D0, P[foobar], (!!map)::c: foobarList_c\n!!merge <<: [*foo, *bar]\nthing: foobar_thing\nb: foobarList_b\n",
},
},
{
skipDoc: true,
document: document,
expression: `.b * .c`,
expected: []string{
"D0, P[b], (!!map)::{name: dog, <<: *cat}\n",
},
},
}
+11 -8
View File
@@ -17,22 +17,25 @@ func selectOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
return Context{}, err
}
// grab the first value
first := rhs.MatchingNodes.Front()
// find any truthy node
var errDecoding error
includeResult := false
if first != nil {
result := first.Value.(*CandidateNode)
log.Debugf("result %v", NodeToString(result))
includeResult, errDecoding := isTruthy(result)
for resultEl := rhs.MatchingNodes.Front(); resultEl != nil; resultEl = resultEl.Next() {
result := resultEl.Value.(*CandidateNode)
includeResult, errDecoding = isTruthy(result)
log.Debugf("isTruthy %v", includeResult)
if errDecoding != nil {
return Context{}, errDecoding
}
if includeResult {
results.PushBack(candidate)
break
}
}
if includeResult {
results.PushBack(candidate)
}
}
return context.ChildContext(results), nil
}
+22
View File
@@ -5,6 +5,28 @@ import (
)
var selectOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `cat`,
expression: `select(false, true)`,
expected: []string{
"D0, P[], (doc)::cat\n",
},
},
{
skipDoc: true,
document: `cat`,
expression: `select(true, false)`,
expected: []string{
"D0, P[], (doc)::cat\n",
},
},
{
skipDoc: true,
document: `cat`,
expression: `select(false)`,
expected: []string{},
},
{
description: "Select elements from array",
document: `[cat,goat,dog]`,
+226
View File
@@ -74,6 +74,232 @@ func substituteStringOperator(d *dataTreeNavigator, context Context, expressionN
}
func addMatch(original []*yaml.Node, match string, offset int, name string) []*yaml.Node {
newContent := append(original,
createScalarNode("string", "string"))
if offset < 0 {
// offset of -1 means there was no match, force a null value like jq
newContent = append(newContent,
createScalarNode(nil, "null"),
)
} else {
newContent = append(newContent,
createScalarNode(match, match),
)
}
newContent = append(newContent,
createScalarNode("offset", "offset"),
createScalarNode(offset, fmt.Sprintf("%v", offset)),
createScalarNode("length", "length"),
createScalarNode(len(match), fmt.Sprintf("%v", len(match))))
if name != "" {
newContent = append(newContent,
createScalarNode("name", "name"),
createScalarNode(name, name),
)
}
return newContent
}
type matchPreferences struct {
Global bool
}
func getMatches(matchPrefs matchPreferences, regEx *regexp.Regexp, value string) ([][]string, [][]int) {
var allMatches [][]string
var allIndices [][]int
if matchPrefs.Global {
allMatches = regEx.FindAllStringSubmatch(value, -1)
allIndices = regEx.FindAllStringSubmatchIndex(value, -1)
} else {
allMatches = [][]string{regEx.FindStringSubmatch(value)}
allIndices = [][]int{regEx.FindStringSubmatchIndex(value)}
}
log.Debug("allMatches, %v", allMatches)
return allMatches, allIndices
}
func match(matchPrefs matchPreferences, regEx *regexp.Regexp, candidate *CandidateNode, value string, results *list.List) {
subNames := regEx.SubexpNames()
allMatches, allIndices := getMatches(matchPrefs, regEx, value)
// if all matches just has an empty array in it,
// then nothing matched
if len(allMatches) > 0 && len(allMatches[0]) == 0 {
return
}
for i, matches := range allMatches {
capturesNode := &yaml.Node{Kind: yaml.SequenceNode}
match, submatches := matches[0], matches[1:]
for j, submatch := range submatches {
captureNode := &yaml.Node{Kind: yaml.MappingNode}
captureNode.Content = addMatch(capturesNode.Content, submatch, allIndices[i][2+j*2], subNames[j+1])
capturesNode.Content = append(capturesNode.Content, captureNode)
}
node := &yaml.Node{Kind: yaml.MappingNode}
node.Content = addMatch(node.Content, match, allIndices[i][0], "")
node.Content = append(node.Content,
createScalarNode("captures", "captures"),
capturesNode,
)
results.PushBack(candidate.CreateChild(nil, node))
}
}
func capture(matchPrefs matchPreferences, regEx *regexp.Regexp, candidate *CandidateNode, value string, results *list.List) {
subNames := regEx.SubexpNames()
allMatches, allIndices := getMatches(matchPrefs, regEx, value)
// if all matches just has an empty array in it,
// then nothing matched
if len(allMatches) > 0 && len(allMatches[0]) == 0 {
return
}
for i, matches := range allMatches {
capturesNode := &yaml.Node{Kind: yaml.MappingNode}
_, submatches := matches[0], matches[1:]
for j, submatch := range submatches {
capturesNode.Content = append(capturesNode.Content,
createScalarNode(subNames[j+1], subNames[j+1]))
offset := allIndices[i][2+j*2]
// offset of -1 means there was no match, force a null value like jq
if offset < 0 {
capturesNode.Content = append(capturesNode.Content,
createScalarNode(nil, "null"),
)
} else {
capturesNode.Content = append(capturesNode.Content,
createScalarNode(submatch, submatch),
)
}
}
results.PushBack(candidate.CreateChild(nil, capturesNode))
}
}
func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (*regexp.Regexp, matchPreferences, error) {
regExExpNode := expressionNode.Rhs
matchPrefs := matchPreferences{}
// we got given parameters e.g. match(exp; params)
if expressionNode.Rhs.Operation.OperationType == blockOpType {
block := expressionNode.Rhs
regExExpNode = block.Lhs
replacementNodes, err := d.GetMatchingNodes(context, block.Rhs)
if err != nil {
return nil, matchPrefs, err
}
paramText := ""
if replacementNodes.MatchingNodes.Front() != nil {
paramText = replacementNodes.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
}
if strings.Contains(paramText, "g") {
paramText = strings.ReplaceAll(paramText, "g", "")
matchPrefs.Global = true
}
if strings.Contains(paramText, "i") {
return nil, matchPrefs, fmt.Errorf(`'i' is not a valid option for match. To ignore case, use an expression like match("(?i)cat")`)
}
if len(paramText) > 0 {
return nil, matchPrefs, fmt.Errorf(`Unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators`, paramText)
}
}
regExNodes, err := d.GetMatchingNodes(context.ReadOnlyClone(), regExExpNode)
if err != nil {
return nil, matchPrefs, err
}
log.Debug(NodesToString(regExNodes.MatchingNodes))
regExStr := ""
if regExNodes.MatchingNodes.Front() != nil {
regExStr = regExNodes.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
}
log.Debug("regEx %v", regExStr)
regEx, err := regexp.Compile(regExStr)
return regEx, matchPrefs, err
}
func matchOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
regEx, matchPrefs, err := extractMatchArguments(d, context, expressionNode)
if err != nil {
return Context{}, err
}
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
if node.Tag != "!!str" {
return Context{}, fmt.Errorf("cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag)
}
match(matchPrefs, regEx, candidate, node.Value, results)
}
return context.ChildContext(results), nil
}
func captureOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
regEx, matchPrefs, err := extractMatchArguments(d, context, expressionNode)
if err != nil {
return Context{}, err
}
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
if node.Tag != "!!str" {
return Context{}, fmt.Errorf("cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag)
}
capture(matchPrefs, regEx, candidate, node.Value, results)
}
return context.ChildContext(results), nil
}
func testOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
regEx, _, err := extractMatchArguments(d, context, expressionNode)
if err != nil {
return Context{}, err
}
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := unwrapDoc(candidate.Node)
if node.Tag != "!!str" {
return Context{}, fmt.Errorf("cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag)
}
matches := regEx.FindStringSubmatch(node.Value)
results.PushBack(createBooleanCandidate(candidate, len(matches) > 0))
}
return context.ChildContext(results), nil
}
func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- joinStringOperator")
joinStr := ""
+104
View File
@@ -13,6 +13,110 @@ var stringsOperatorScenarios = []expressionScenario{
"D0, P[], (!!str)::cat; meow; 1; ; true\n",
},
},
{
description: "Match string",
document: `foo bar foo`,
expression: `match("foo")`,
expected: []string{
"D0, P[], ()::string: foo\noffset: 0\nlength: 3\ncaptures: []\n",
},
},
{
description: "Match string, case insensitive",
document: `foo bar FOO`,
expression: `[match("(?i)foo"; "g")]`,
expected: []string{
"D0, P[], (!!seq)::- string: foo\n offset: 0\n length: 3\n captures: []\n- string: FOO\n offset: 8\n length: 3\n captures: []\n",
},
},
{
description: "Match with capture groups",
document: `abc abc`,
expression: `[match("(abc)+"; "g")]`,
expected: []string{
"D0, P[], (!!seq)::- string: abc\n offset: 0\n length: 3\n captures:\n - string: abc\n offset: 0\n length: 3\n- string: abc\n offset: 4\n length: 3\n captures:\n - string: abc\n offset: 4\n length: 3\n",
},
},
{
description: "Match with named capture groups",
document: `foo bar foo foo foo`,
expression: `[match("foo (?P<bar123>bar)? foo"; "g")]`,
expected: []string{
"D0, P[], (!!seq)::- string: foo bar foo\n offset: 0\n length: 11\n captures:\n - string: bar\n offset: 4\n length: 3\n name: bar123\n- string: foo foo\n offset: 12\n length: 8\n captures:\n - string: null\n offset: -1\n length: 0\n name: bar123\n",
},
},
{
description: "Capture named groups into a map",
document: `xyzzy-14`,
expression: `capture("(?P<a>[a-z]+)-(?P<n>[0-9]+)")`,
expected: []string{
"D0, P[], ()::a: xyzzy\nn: \"14\"\n",
},
},
{
skipDoc: true,
description: "Capture named groups into a map, with null",
document: `xyzzy-14`,
expression: `capture("(?P<a>[a-z]+)-(?P<n>[0-9]+)(?P<bar123>bar)?")`,
expected: []string{
"D0, P[], ()::a: xyzzy\nn: \"14\"\nbar123: null\n",
},
},
{
description: "Match without global flag",
document: `cat cat`,
expression: `match("cat")`,
expected: []string{
"D0, P[], ()::string: cat\noffset: 0\nlength: 3\ncaptures: []\n",
},
},
{
description: "Match with global flag",
document: `cat cat`,
expression: `[match("cat"; "g")]`,
expected: []string{
"D0, P[], (!!seq)::- string: cat\n offset: 0\n length: 3\n captures: []\n- string: cat\n offset: 4\n length: 3\n captures: []\n",
},
},
{
skipDoc: true,
description: "No match",
document: `dog`,
expression: `match("cat"; "g")`,
expected: []string{},
},
{
skipDoc: true,
description: "No match",
expression: `"dog" | match("cat", "g")`,
expected: []string{},
},
{
skipDoc: true,
description: "No match",
expression: `"dog" | match("cat")`,
expected: []string{},
},
{
description: "Test using regex",
subdescription: "Like jq'q equivalant, this works like match but only returns true/false instead of full match details",
document: `["cat", "dog"]`,
expression: `.[] | test("at")`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: `["cat*", "cat*", "cat"]`,
expression: `.[] | test("cat\*")`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::false\n",
},
},
{
description: "Substitute / Replace string",
subdescription: "This uses golang regex, described [here](https://github.com/google/re2/wiki/Syntax)\nNote the use of `|=` to run in context of the current string value.",
+3 -4
View File
@@ -14,10 +14,9 @@ var styleOperatorScenarios = []expressionScenario{
},
},
{
description: "Update and set style of a particular node using path variables",
subdescription: "You can use a variable to re-use a path",
document: `a: {b: thing, c: something}`,
expression: `.a.b as $x | $x = "new" | $x style="double"`,
description: "Update and set style of a particular node using path variables",
document: `a: {b: thing, c: something}`,
expression: `with(.a.b ; . = "new" | . style="double")`,
expected: []string{
"D0, P[], (doc)::a: {b: \"new\", c: something}\n",
},
+28 -15
View File
@@ -5,7 +5,7 @@ import (
"strconv"
yaml "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3"
)
func createSubtractOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
@@ -15,11 +15,7 @@ func createSubtractOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode
}
func subtractAssignOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
assignmentOp := &Operation{OperationType: assignOpType}
assignmentOp.UpdateAssign = true
selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}}
assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: expressionNode.Lhs, Rhs: createSubtractOp(selfExpression, expressionNode.Rhs)}
return d.GetMatchingNodes(context, assignmentOpNode)
return compoundAssignFunction(d, context, expressionNode, createSubtractOp)
}
func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
@@ -28,6 +24,24 @@ func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *Exp
return crossFunction(d, context.ReadOnlyClone(), expressionNode, subtract, false)
}
func subtractArray(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
newLhsArray := make([]*yaml.Node, 0)
for lindex := 0; lindex < len(lhs.Node.Content); lindex = lindex + 1 {
shouldInclude := true
for rindex := 0; rindex < len(rhs.Node.Content) && shouldInclude; rindex = rindex + 1 {
if recursiveNodeEqual(lhs.Node.Content[lindex], rhs.Node.Content[rindex]) {
shouldInclude = false
}
}
if shouldInclude {
newLhsArray = append(newLhsArray, lhs.Node.Content[lindex])
}
}
lhs.Node.Content = newLhsArray
return lhs, nil
}
func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = unwrapDoc(lhs.Node)
rhs.Node = unwrapDoc(rhs.Node)
@@ -44,14 +58,13 @@ func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *Ca
case yaml.MappingNode:
return nil, fmt.Errorf("Maps not yet supported for subtraction")
case yaml.SequenceNode:
return nil, fmt.Errorf("Sequences not yet supported for subtraction")
// target.Node.Kind = yaml.SequenceNode
// target.Node.Style = lhsNode.Style
// target.Node.Tag = "!!seq"
// target.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
if rhs.Node.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("%v (%v) cannot be subtracted from %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag)
}
return subtractArray(d, context, lhs, rhs)
case yaml.ScalarNode:
if rhs.Node.Kind != yaml.ScalarNode {
return nil, fmt.Errorf("%v (%v) cannot be added to a %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag)
return nil, fmt.Errorf("%v (%v) cannot be subtracted from %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag)
}
target.Node.Kind = yaml.ScalarNode
target.Node.Style = lhsNode.Style
@@ -66,17 +79,17 @@ func subtractScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*Ca
if lhs.Tag == "!!str" {
return nil, fmt.Errorf("strings cannot be subtracted")
} else if lhs.Tag == "!!int" && rhs.Tag == "!!int" {
lhsNum, err := strconv.Atoi(lhs.Value)
format, lhsNum, err := parseInt(lhs.Value)
if err != nil {
return nil, err
}
rhsNum, err := strconv.Atoi(rhs.Value)
_, rhsNum, err := parseInt(rhs.Value)
if err != nil {
return nil, err
}
result := lhsNum - rhsNum
target.Node.Tag = "!!int"
target.Node.Value = fmt.Sprintf("%v", result)
target.Node.Value = fmt.Sprintf(format, result)
} else if (lhs.Tag == "!!int" || lhs.Tag == "!!float") && (rhs.Tag == "!!int" || rhs.Tag == "!!float") {
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
if err != nil {
+45
View File
@@ -13,6 +13,51 @@ var subtractOperatorScenarios = []expressionScenario{
"D0, P[], (doc)::{}\n",
},
},
{
description: "Array subtraction",
expression: `[1,2] - [2,3]`,
expected: []string{
"D0, P[], (!!seq)::- 1\n",
},
},
{
skipDoc: true,
expression: `[2,1,2,2] - [2,3]`,
expected: []string{
"D0, P[], (!!seq)::- 1\n",
},
},
{
description: "Array subtraction with nested array",
expression: `[[1], 1, 2] - [[1], 3]`,
expected: []string{
"D0, P[], (!!seq)::- 1\n- 2\n",
},
},
{
skipDoc: true,
expression: `[[1], 1, [[[2]]]] - [[1], [[[3]]]]`,
expected: []string{
"D0, P[], (!!seq)::- 1\n- - - - 2\n",
},
},
{
description: "Array subtraction with nested object",
subdescription: `Note that order of the keys does not matter`,
document: `[{a: b, c: d}, {a: b}]`,
expression: `. - [{"c": "d", "a": "b"}]`,
expected: []string{
"D0, P[], (!!seq)::[{a: b}]\n",
},
},
{
skipDoc: true,
document: `[{a: [1], c: d}, {a: [2], c: d}, {a: b}]`,
expression: `. - [{"c": "d", "a": [1]}]`,
expected: []string{
"D0, P[], (!!seq)::[{a: [2], c: d}, {a: b}]\n",
},
},
{
description: "Number subtraction - float",
subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",
+9 -2
View File
@@ -127,9 +127,16 @@ func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesT
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
// auto vivification
node.Tag = ""
node.Kind = yaml.SequenceNode
//check that the indices are numeric, if not, then we should create an object
if len(indicesToTraverse) != 0 {
_, err := strconv.ParseInt(indicesToTraverse[0].Value, 10, 64)
if err != nil {
node.Kind = yaml.MappingNode
}
}
}
if node.Kind == yaml.AliasNode {
@@ -308,6 +315,6 @@ func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *C
func traverseArray(candidate *CandidateNode, operation *Operation, prefs traversePreferences) (*list.List, error) {
log.Debug("operation Value %v", operation.Value)
indices := []*yaml.Node{&yaml.Node{Value: operation.StringValue}}
indices := []*yaml.Node{{Value: operation.StringValue}}
return traverseArrayWithIndices(candidate, indices, prefs)
}
+32
View File
@@ -35,6 +35,14 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[0 0], (!!int)::1\n",
},
},
{
skipDoc: true,
document: `b: cat`,
expression: ".b\n",
expected: []string{
"D0, P[b], (!!str)::cat\n",
},
},
{
skipDoc: true,
document: `[[[1]]]`,
@@ -43,6 +51,13 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[0 0 0], (!!int)::1\n",
},
},
{
skipDoc: true,
expression: `.["cat"] = "thing"`,
expected: []string{
"D0, P[], ()::cat: thing\n",
},
},
{
description: "Simple map navigation",
document: `{a: {b: apple}}`,
@@ -77,6 +92,14 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[{}], (!!str)::frog\n",
},
},
{
description: "Nested special characters",
document: `a: {"key.withdots": {"another.key": apple}}`,
expression: `.a["key.withdots"]["another.key"]`,
expected: []string{
"D0, P[a key.withdots another.key], (!!str)::apple\n",
},
},
{
description: "Keys with spaces",
subdescription: "Use quotes with brackets around path elements with special characters",
@@ -244,6 +267,15 @@ var traversePathOperatorScenarios = []expressionScenario{
"D0, P[0], (!!int)::1\n",
},
},
{
description: "Traversing nested arrays by index",
dontFormatInputForDoc: true,
document: `[[], [cat]]`,
expression: `.[1][0]`,
expected: []string{
"D0, P[1 0], (!!str)::cat\n",
},
},
{
description: "Maps with numeric keys",
document: `{2: cat}`,
+22 -3
View File
@@ -1,17 +1,36 @@
package yqlib
func unionOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debug("unionOperator")
log.Debug("context: %v", NodesToString(context.MatchingNodes))
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil {
return Context{}, err
}
log.Debug("lhs: %v", NodesToString(lhs.MatchingNodes))
log.Debug("rhs input: %v", NodesToString(context.MatchingNodes))
log.Debug("rhs: %v", expressionNode.Rhs.Operation.toString())
rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs)
if err != nil {
return Context{}, err
}
for el := rhs.MatchingNodes.Front(); el != nil; el = el.Next() {
node := el.Value.(*CandidateNode)
lhs.MatchingNodes.PushBack(node)
log.Debug("lhs: %v", lhs.ToString())
log.Debug("rhs: %v", rhs.ToString())
// this can happen when both expressions modify the context
// instead of creating their own.
/// (.foo = "bar"), (.thing = "cat")
if rhs.MatchingNodes != lhs.MatchingNodes {
for el := rhs.MatchingNodes.Front(); el != nil; el = el.Next() {
node := el.Value.(*CandidateNode)
log.Debug("processing %v", NodeToString(node))
lhs.MatchingNodes.PushBack(node)
}
}
log.Debug("all together: %v", lhs.ToString())
return lhs, nil
}
+15 -14
View File
@@ -5,20 +5,21 @@ import (
)
var unionOperatorScenarios = []expressionScenario{
// {
// skipDoc: true,
// document: "{}",
// expression: `(.a, .b.c) as $x`,
// expected: []string{
// "D0, P[], (doc)::{}\n",
// },
// },
// {
// skipDoc: true,
// document: "{}",
// expression: `(.a, .b.c)`,
// expected: []string{},
// },
{
skipDoc: true,
document: "{}",
expression: `(.a, .b.c) as $x`,
expected: []string{
"D0, P[], (doc)::{}\n",
},
},
{
skipDoc: true,
expression: `(.foo = "bar"), (.toe = "jam")`,
expected: []string{
"D0, P[], ()::foo: bar\ntoe: jam\n",
},
},
{
description: "Combine scalars",
expression: `1, true, "cat"`,
+70
View File
@@ -12,6 +12,76 @@ var valueOperatorScenarios = []expressionScenario{
"D0, P[], (!!int)::1\n",
},
},
{
document: ``,
expression: `0x9f`,
expected: []string{
"D0, P[], (!!int)::0x9f\n",
},
},
{
document: ``,
expression: `0x1A`,
expected: []string{
"D0, P[], (!!int)::0x1A\n",
},
},
{
document: ``,
expression: `0x1A + 2`,
expected: []string{
"D0, P[], (!!int)::0x1C\n",
},
},
{
document: ``,
expression: `0x12 * 2`,
expected: []string{
"D0, P[], (!!int)::0x24\n",
},
},
{
document: ``,
expression: `0xF - 1`,
expected: []string{
"D0, P[], (!!int)::0xE\n",
},
},
{
document: ``,
expression: `12`,
expected: []string{
"D0, P[], (!!int)::12\n",
},
},
{
document: ``,
expression: `12 + 2`,
expected: []string{
"D0, P[], (!!int)::14\n",
},
},
{
document: ``,
expression: `12 * 2`,
expected: []string{
"D0, P[], (!!int)::24\n",
},
},
{
document: ``,
expression: `12 - 2`,
expected: []string{
"D0, P[], (!!int)::10\n",
},
},
{
document: ``,
expression: `0X12`,
expected: []string{
"D0, P[], (!!int)::0X12\n",
},
},
{
document: ``,
expression: `-1`,
+14 -1
View File
@@ -15,6 +15,10 @@ func getVariableOperator(d *dataTreeNavigator, context Context, expressionNode *
return context.ChildContext(result), nil
}
type assignVarPreferences struct {
IsReference bool
}
func assignVariableOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
lhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.Lhs)
if err != nil {
@@ -24,6 +28,15 @@ func assignVariableOperator(d *dataTreeNavigator, context Context, expressionNod
return Context{}, fmt.Errorf("RHS of 'as' operator must be a variable name e.g. $foo")
}
variableName := expressionNode.Rhs.Operation.StringValue
context.SetVariable(variableName, lhs.MatchingNodes)
prefs := expressionNode.Operation.Preferences.(assignVarPreferences)
var variableValue *list.List
if prefs.IsReference {
variableValue = lhs.MatchingNodes
} else {
variableValue = lhs.DeepClone().MatchingNodes
}
context.SetVariable(variableName, variableValue)
return context, nil
}
+20 -3
View File
@@ -34,15 +34,32 @@ var variableOperatorScenarios = []expressionScenario{
description: "Using variables as a lookup",
subdescription: "Example taken from [jq](https://stedolan.github.io/jq/manual/#Variable/SymbolicBindingOperator:...as$identifier|...)",
document: `{"posts": [{"title": "Frist psot", "author": "anon"},
{"title": "A well-written article", "author": "person1"}],
"realnames": {"anon": "Anonymous Coward",
"person1": "Person McPherson"}}`,
{"title": "A well-written article", "author": "person1"}],
"realnames": {"anon": "Anonymous Coward",
"person1": "Person McPherson"}}`,
expression: `.realnames as $names | .posts[] | {"title":.title, "author": $names[.author]}`,
expected: []string{
"D0, P[], (!!map)::title: \"Frist psot\"\nauthor: \"Anonymous Coward\"\n",
"D0, P[], (!!map)::title: \"A well-written article\"\nauthor: \"Person McPherson\"\n",
},
},
{
description: "Using variables to swap values",
document: "a: a_value\nb: b_value",
expression: `.a as $x | .b as $y | .b = $x | .a = $y`,
expected: []string{
"D0, P[], (doc)::a: b_value\nb: a_value\n",
},
},
{
description: "Use ref to reference a path repeatedly",
subdescription: "Note: You may find the `with` operator more useful.",
document: `a: {b: thing, c: something}`,
expression: `.a.b ref $x | $x = "new" | $x style="double"`,
expected: []string{
"D0, P[], (doc)::a: {b: \"new\", c: something}\n",
},
},
}
func TestVariableOperatorScenarios(t *testing.T) {
+30
View File
@@ -0,0 +1,30 @@
package yqlib
import "fmt"
func withOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- withOperator")
// with(path, exp)
if expressionNode.Rhs.Operation.OperationType != blockOpType {
return Context{}, fmt.Errorf("with must be given a block, got %v instead", expressionNode.Rhs.Operation.OperationType.Type)
}
pathExp := expressionNode.Rhs.Lhs
updateContext, err := d.GetMatchingNodes(context, pathExp)
if err != nil {
return Context{}, err
}
updateExp := expressionNode.Rhs.Rhs
_, err = d.GetMatchingNodes(updateContext, updateExp)
if err != nil {
return Context{}, err
}
return context, nil
}
+38
View File
@@ -0,0 +1,38 @@
package yqlib
import "testing"
var withOperatorScenarios = []expressionScenario{
{
description: "Update and style",
document: `a: {deeply: {nested: value}}`,
expression: `with(.a.deeply.nested; . = "newValue" | . style="single")`,
expected: []string{
"D0, P[], (doc)::a: {deeply: {nested: 'newValue'}}\n",
},
},
{
description: "Update multiple deeply nested properties",
document: `a: {deeply: {nested: value, other: thing}}`,
expression: `with(.a.deeply; .nested = "newValue" | .other= "newThing")`,
expected: []string{
"D0, P[], (doc)::a: {deeply: {nested: newValue, other: newThing}}\n",
},
},
{
description: "Update array elements relatively",
subdescription: "The second expression runs with each element of the array as it's contextual root. This allows you to make updates relative to the element.",
document: `myArray: [{a: apple},{a: banana}]`,
expression: `with(.myArray[]; .b = .a + " yum")`,
expected: []string{
"D0, P[], (doc)::myArray: [{a: apple, b: apple yum}, {a: banana, b: banana yum}]\n",
},
},
}
func TestWithOperatorScenarios(t *testing.T) {
for _, tt := range withOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "With", withOperatorScenarios)
}
+35 -2
View File
@@ -10,6 +10,32 @@ import (
type operatorHandler func(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error)
type compoundCalculation func(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode
func compoundAssignFunction(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, calculation compoundCalculation) (Context, error) {
lhs, err := d.GetMatchingNodes(context, expressionNode.Lhs)
if err != nil {
return Context{}, err
}
assignmentOp := &Operation{OperationType: assignOpType}
valueOp := &Operation{OperationType: valueOpType}
for el := lhs.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
valueOp.CandidateNode = candidate
valueExpression := &ExpressionNode{Operation: valueOp}
assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: valueExpression, Rhs: calculation(valueExpression, expressionNode.Rhs)}
_, err = d.GetMatchingNodes(context, assignmentOpNode)
if err != nil {
return Context{}, err
}
}
return context, nil
}
func unwrapDoc(node *yaml.Node) *yaml.Node {
if node.Kind == yaml.DocumentNode {
return node.Content[0]
@@ -31,7 +57,9 @@ func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *Candidat
if err != nil {
return err
}
results.PushBack(resultCandidate)
if resultCandidate != nil {
results.PushBack(resultCandidate)
}
return nil
}
@@ -42,7 +70,9 @@ func resultsForRhs(d *dataTreeNavigator, context Context, lhsCandidate *Candidat
if err != nil {
return err
}
results.PushBack(resultCandidate)
if resultCandidate != nil {
results.PushBack(resultCandidate)
}
}
return nil
}
@@ -91,9 +121,12 @@ func crossFunction(d *dataTreeNavigator, context Context, expressionNode *Expres
}
}
if evaluateAllTogether {
log.Debug("crossFunction evaluateAllTogether!")
return doCrossFunc(d, context, expressionNode, calculation, calcWhenEmpty)
}
log.Debug("crossFunction evaluate apart!")
for matchEl := context.MatchingNodes.Front(); matchEl != nil; matchEl = matchEl.Next() {
innerResults, err := doCrossFunc(d, context.SingleChildContext(matchEl.Value.(*CandidateNode)), expressionNode, calculation, calcWhenEmpty)
if err != nil {
+5 -5
View File
@@ -65,7 +65,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
os.Setenv("myenv", s.environmentVariable)
}
context, err := newDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
if err != nil {
t.Error(fmt.Errorf("%v: %v", err, s.expression))
@@ -107,14 +107,14 @@ func copyFromHeader(title string, out *os.File) error {
func formatYaml(yaml string, filename string) string {
var output bytes.Buffer
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
printer := NewPrinter(bufio.NewWriter(&output), YamlOutputFormat, true, false, 2, true)
node, err := NewExpressionParser().ParseExpression(".. style= \"\"")
if err != nil {
panic(err)
}
streamEvaluator := NewStreamEvaluator()
err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer)
_, err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer, "")
if err != nil {
panic(err)
}
@@ -216,7 +216,7 @@ func documentInput(w *bufio.Writer, s expressionScenario) (string, string) {
func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formattedDoc string, formattedDoc2 string) {
var output bytes.Buffer
var err error
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
printer := NewPrinter(bufio.NewWriter(&output), YamlOutputFormat, true, false, 2, true)
node, err := NewExpressionParser().ParseExpression(s.expression)
if err != nil {
@@ -251,7 +251,7 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
}
context, err := newDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, node)
if err != nil {
t.Error(err, s.expression)
}

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