mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-06 04:45:39 +00:00
Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2f1d5ccf7 | ||
|
|
16f149b351 | ||
|
|
5da9215306 | ||
|
|
e95bb7e472 | ||
|
|
2074319595 | ||
|
|
be992d8add | ||
|
|
637bb1fecd | ||
|
|
bc23b42789 | ||
|
|
8e2c9b612d | ||
|
|
0970cd4b05 | ||
|
|
bf3591a234 | ||
|
|
09f1565d51 | ||
|
|
13d340ff51 | ||
|
|
5cf0adcc5b | ||
|
|
30e16a33c3 | ||
|
|
25dfcf280f | ||
|
|
91a166e8d8 | ||
|
|
f9b0d7e45d | ||
|
|
48a851bf57 | ||
|
|
131aa0b7cc | ||
|
|
ef3c14f806 | ||
|
|
26434e221e | ||
|
|
0eebc242fb | ||
|
|
87a62da881 | ||
|
|
ef507264e1 | ||
|
|
2a40eb3d04 | ||
|
|
e3cb1dc7c6 | ||
|
|
1b9b4ac518 | ||
|
|
9b67d655f1 | ||
|
|
f8850c043c | ||
|
|
c5a342359d | ||
|
|
196a99e912 | ||
|
|
fa99bf12c3 | ||
|
|
71117613d6 | ||
|
|
0cb1bbe698 | ||
|
|
8fc8eedd3b | ||
|
|
7620a727b5 | ||
|
|
6f9201a0ca | ||
|
|
5b19ddbcd0 | ||
|
|
1ac2a3d96a | ||
|
|
4a12c3f908 | ||
|
|
41377138ef | ||
|
|
ae5cf9ff04 | ||
|
|
fe449b956a | ||
|
|
8f3291d316 | ||
|
|
2861815f71 | ||
|
|
fcb79822dd | ||
|
|
e9acb9b734 | ||
|
|
83b282c413 | ||
|
|
54fa4324ea | ||
|
|
ee6c30dac2 | ||
|
|
722c9aa16c | ||
|
|
702dd16048 | ||
|
|
d1dff4661b | ||
|
|
cb97935554 | ||
|
|
cfe2eee7e6 | ||
|
|
1a433d1035 | ||
|
|
1c0d8b9da9 | ||
|
|
0110a3cea8 | ||
|
|
54482d44b3 | ||
|
|
33f3351c01 | ||
|
|
6cb656ced0 | ||
|
|
ecc43d7c9e | ||
|
|
1deec5e450 | ||
|
|
ff45fad14c | ||
|
|
6679d3c02b | ||
|
|
54a7fc8f0c | ||
|
|
0d3ab07928 | ||
|
|
d93987a93a | ||
|
|
751d8ad57b | ||
|
|
6dd681a7c0 | ||
|
|
fc7c337d8f | ||
|
|
e969dd789f | ||
|
|
dc4b4ea1df | ||
|
|
602586d8fd | ||
|
|
9a0335abb2 | ||
|
|
838c51691c | ||
|
|
c8f6c1a042 | ||
|
|
0e803833fb | ||
|
|
30ca9ffde7 | ||
|
|
2927a28283 | ||
|
|
c47fe40a30 | ||
|
|
8c018da9c9 | ||
|
|
44c55c8a54 | ||
|
|
22e609b2d9 | ||
|
|
3b2423e871 | ||
|
|
68f0322ba3 | ||
|
|
d69c7d1a36 | ||
|
|
b0ba9589d7 | ||
|
|
80139ae1cc | ||
|
|
0374ad6b4b | ||
|
|
2ef934281e | ||
|
|
17f66dc6c6 | ||
|
|
dcb9c2a543 | ||
|
|
8f5d876bf0 | ||
|
|
7d8d3ab902 | ||
|
|
11f4dc1a03 | ||
|
|
0f4fb8d35e | ||
|
|
80c319aa0c | ||
|
|
b25ae78545 | ||
|
|
b151522485 | ||
|
|
c5cbf9760b | ||
|
|
b5cb9a2f20 | ||
|
|
133ba767a6 | ||
|
|
5db3dcf394 | ||
|
|
4c148178e2 | ||
|
|
4df6e46f95 | ||
|
|
6a965bc39a | ||
|
|
34d3a29308 | ||
|
|
16e4df2304 | ||
|
|
79a92d0478 | ||
|
|
88a31ae8c6 | ||
|
|
5a7e72a743 | ||
|
|
562531d936 | ||
|
|
2c471b6498 | ||
|
|
f4ef6ef3cf | ||
|
|
f49f2bd2d8 | ||
|
|
6ccc7b7797 | ||
|
|
b3e1fbb7d1 | ||
|
|
288ca2d114 | ||
|
|
eb04fa87af | ||
|
|
2be0094729 | ||
|
|
3c18d5b035 | ||
|
|
2dcc2293da | ||
|
|
eb4fde4ef8 | ||
|
|
06ea4cf62e | ||
|
|
37089d24af | ||
|
|
7cf88a0291 | ||
|
|
41adc1ad18 | ||
|
|
b4b96f2a68 | ||
|
|
2824d66a65 | ||
|
|
4bbffa9022 | ||
|
|
bdeedbd275 | ||
|
|
3d918acc2a | ||
|
|
01005cc8fd | ||
|
|
c4468165f2 | ||
|
|
e35d32a0b6 | ||
|
|
78192a915b | ||
|
|
c4f4e6d416 | ||
|
|
5f90039bdc | ||
|
|
c6fa371d8d | ||
|
|
3a27e39778 | ||
|
|
414a085563 | ||
|
|
542801926f | ||
|
|
1bcc44ff9b | ||
|
|
a6f1b02340 | ||
|
|
f98028c925 | ||
|
|
c6029376a5 | ||
|
|
23abf50fef | ||
|
|
64ec1f4aa7 | ||
|
|
4973c355e6 | ||
|
|
ecbdcada9f | ||
|
|
029ba68014 | ||
|
|
4a06cce376 | ||
|
|
37e48cea44 | ||
|
|
207bec6b29 | ||
|
|
7198d16575 | ||
|
|
5d6c2047cf | ||
|
|
7f60daad20 | ||
|
|
b7cbe59fd7 | ||
|
|
9fa353b123 | ||
|
|
c6ecad1546 | ||
|
|
56eb3655b8 | ||
|
|
1de4ec59f2 | ||
|
|
c132c32731 | ||
|
|
0914121d29 | ||
|
|
aa5134e645 | ||
|
|
4d620bfa26 | ||
|
|
b8d90fd574 | ||
|
|
c1b81f1a03 | ||
|
|
ea40e14fb1 | ||
|
|
b974d973ee | ||
|
|
66ec487792 | ||
|
|
161be10791 | ||
|
|
aa858520a8 | ||
|
|
ac2889c296 | ||
|
|
626624af7b | ||
|
|
b0d2522f80 | ||
|
|
2ee38e15b6 | ||
|
|
4e9d5e8e48 | ||
|
|
1338b521ff | ||
|
|
3a5323824f | ||
|
|
8780172b33 | ||
|
|
5f9bf8d241 | ||
|
|
065b200af9 | ||
|
|
745a7ffb3c | ||
|
|
a305d706d4 | ||
|
|
0671ccd2cc | ||
|
|
4d8cd450bd | ||
|
|
d2d657eacc | ||
|
|
f4fd8c585a | ||
|
|
e4bf8a1e0a | ||
|
|
fd405749f9 | ||
|
|
51ddf8d357 | ||
|
|
77eccfd3db | ||
|
|
554bf5a2f2 | ||
|
|
8162f3a100 | ||
|
|
48707369a0 | ||
|
|
4f72c37de7 | ||
|
|
795f9c954c | ||
|
|
3d35386ad9 | ||
|
|
154a4ace01 | ||
|
|
effdfe1221 | ||
|
|
8af768a015 | ||
|
|
5f3dcb1ccf | ||
|
|
6270c29f54 | ||
|
|
df3101ce53 | ||
|
|
65e79845d4 | ||
|
|
b4d8131197 | ||
|
|
c75a2fad86 | ||
|
|
8d430cf41c | ||
|
|
2e96a28270 | ||
|
|
656f07d0c2 | ||
|
|
1852073f29 | ||
|
|
7d2c774e8f | ||
|
|
69076dfe81 | ||
|
|
9e17cd683f | ||
|
|
eb3d0e63e3 | ||
|
|
2072808def | ||
|
|
7d47b36b69 | ||
|
|
53f10ae360 | ||
|
|
22510ab8d5 | ||
|
|
588d0bb3dd | ||
|
|
7ccaf8e700 | ||
|
|
a1a27b8536 | ||
|
|
1b91fc63ea | ||
|
|
9e0c5fd3c9 | ||
|
|
5d0481c0d2 | ||
|
|
f91176a204 | ||
|
|
8e86bdb876 | ||
|
|
fc164ca9c3 | ||
|
|
810e9d921e | ||
|
|
45be35c063 | ||
|
|
39fbf01fa8 | ||
|
|
306dc931a5 | ||
|
|
f00852bc6c | ||
|
|
c716d157f2 | ||
|
|
e49e588ab5 | ||
|
|
389486829d | ||
|
|
d32e71f25b | ||
|
|
796317b885 | ||
|
|
258b84a05e | ||
|
|
e056b91a00 | ||
|
|
85b0985a60 | ||
|
|
874cbc4d3c | ||
|
|
f6c780e793 | ||
|
|
8c25f33df4 | ||
|
|
2869919cb4 | ||
|
|
458d02f3ab | ||
|
|
877a47cb19 | ||
|
|
3050ca5303 | ||
|
|
49b6477c49 | ||
|
|
78bc9baffd | ||
|
|
1f2b0fe76b | ||
|
|
1228bcfa75 | ||
|
|
7f72595a12 | ||
|
|
ff2c1c930c | ||
|
|
36d410b348 | ||
|
|
6dfe002058 | ||
|
|
ed4f468c97 | ||
|
|
8b2ba41c6c | ||
|
|
02b28073bf | ||
|
|
6957399dc0 |
4
.github/ISSUE_TEMPLATE/bug_report_v4.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report_v4.md
vendored
@ -34,13 +34,13 @@ The command you ran:
|
||||
yq eval-all 'select(fileIndex==0) | .a.b.c' data1.yml data2.yml
|
||||
```
|
||||
|
||||
**Actual behavior**
|
||||
**Actual behaviour**
|
||||
|
||||
```yaml
|
||||
cat: meow
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
**Expected behaviour**
|
||||
|
||||
```yaml
|
||||
this: should really work
|
||||
|
||||
1
.github/instructions/instructions.md
vendored
Normal file
1
.github/instructions/instructions.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
When you find a bug - make sure to include a new test that exposes the bug, as well as the fix for the bug itself.
|
||||
10
.github/workflows/codeql.yml
vendored
10
.github/workflows/codeql.yml
vendored
@ -20,6 +20,8 @@ on:
|
||||
schedule:
|
||||
- cron: '24 3 * * 1'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@ -38,11 +40,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -53,7 +55,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@ -67,4 +69,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
107
.github/workflows/docker-githubaction.yml
vendored
Normal file
107
.github/workflows/docker-githubaction.yml
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
name: Release Docker GitHub Action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
publishGithubActionDocker:
|
||||
environment: dockerhub
|
||||
env:
|
||||
IMAGE_NAME: mikefarah/yq
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Verify Dockerfile base image digest matches yq:4
|
||||
run: |
|
||||
PINNED_DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' github-action/Dockerfile | head -1)
|
||||
if [ -z "${PINNED_DIGEST}" ]; then
|
||||
echo "::error::Could not find a sha256 digest in github-action/Dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:4" --format '{{printf "%s" .Manifest.Digest}}')
|
||||
|
||||
echo "Dockerfile pins: ${PINNED_DIGEST}"
|
||||
echo "mikefarah/yq:4 is: ${LATEST_DIGEST}"
|
||||
|
||||
if [ "${PINNED_DIGEST}" != "${LATEST_DIGEST}" ]; then
|
||||
echo "::error::github-action/Dockerfile digest does not match the current mikefarah/yq:4 image"
|
||||
echo "Update the FROM line in github-action/Dockerfile to:"
|
||||
echo " FROM mikefarah/yq:4@${LATEST_DIGEST}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve version from yq:4
|
||||
run: |
|
||||
IMAGE_VERSION=$(docker run --rm "${IMAGE_NAME}:4" --version | awk '{print $NF}' | sed 's/^v//')
|
||||
if [ -z "${IMAGE_VERSION}" ]; then
|
||||
echo "::error::Could not determine yq version from ${IMAGE_NAME}:4"
|
||||
exit 1
|
||||
fi
|
||||
echo "Resolved yq version: ${IMAGE_VERSION}"
|
||||
echo "IMAGE_VERSION=${IMAGE_VERSION}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push github-action image
|
||||
working-directory: github-action
|
||||
run: |
|
||||
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64,linux/arm/v7,linux/s390x"
|
||||
|
||||
echo "Building and pushing github-action image for version ${IMAGE_VERSION}"
|
||||
docker buildx build \
|
||||
--label "org.opencontainers.image.authors=https://github.com/mikefarah/yq/graphs/contributors" \
|
||||
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
|
||||
--label "org.opencontainers.image.description=yq is a portable command-line data file processor" \
|
||||
--label "org.opencontainers.image.documentation=https://mikefarah.gitbook.io/yq/" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
|
||||
--label "org.opencontainers.image.source=https://github.com/mikefarah/yq" \
|
||||
--label "org.opencontainers.image.title=yq" \
|
||||
--label "org.opencontainers.image.url=https://mikefarah.gitbook.io/yq/" \
|
||||
--label "org.opencontainers.image.version=${IMAGE_VERSION}" \
|
||||
--platform "${PLATFORMS}" \
|
||||
--pull \
|
||||
--push \
|
||||
-t "${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
|
||||
-t "${IMAGE_NAME}:4-githubaction" \
|
||||
-t "${IMAGE_NAME}:latest-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
|
||||
.
|
||||
|
||||
- name: Report action.yml digest to pin
|
||||
run: |
|
||||
GITHUBACTION_DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:4-githubaction" --format '{{printf "%s" .Manifest.Digest}}')
|
||||
echo "Published ${IMAGE_NAME}:4-githubaction at ${GITHUBACTION_DIGEST}"
|
||||
echo "Update action.yml image to:"
|
||||
echo " docker://${IMAGE_NAME}:4-githubaction@${GITHUBACTION_DIGEST}"
|
||||
38
.github/workflows/docker-release.yml
vendored
38
.github/workflows/docker-release.yml
vendored
@ -7,23 +7,28 @@ on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
publishDocker:
|
||||
environment: dockerhub
|
||||
env:
|
||||
IMAGE_NAME: mikefarah/yq
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
version: latest
|
||||
|
||||
@ -31,13 +36,13 @@ jobs:
|
||||
run: echo ${{ steps.buildx.outputs.platforms }} && docker version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@ -75,26 +80,3 @@ jobs:
|
||||
-t "ghcr.io/${IMAGE_NAME}:4" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:latest" \
|
||||
.
|
||||
|
||||
cd github-action
|
||||
docker buildx build \
|
||||
--label "org.opencontainers.image.authors=https://github.com/mikefarah/yq/graphs/contributors" \
|
||||
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
|
||||
--label "org.opencontainers.image.description=yq is a portable command-line data file processor" \
|
||||
--label "org.opencontainers.image.documentation=https://mikefarah.gitbook.io/yq/" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
|
||||
--label "org.opencontainers.image.source=https://github.com/mikefarah/yq" \
|
||||
--label "org.opencontainers.image.title=yq" \
|
||||
--label "org.opencontainers.image.url=https://mikefarah.gitbook.io/yq/" \
|
||||
--label "org.opencontainers.image.version=${IMAGE_VERSION}" \
|
||||
--platform "${PLATFORMS}" \
|
||||
--pull \
|
||||
--push \
|
||||
-t "${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
|
||||
-t "${IMAGE_NAME}:4-githubaction" \
|
||||
-t "${IMAGE_NAME}:latest-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
|
||||
.
|
||||
|
||||
32
.github/workflows/go.yml
vendored
32
.github/workflows/go.yml
vendored
@ -5,25 +5,51 @@ permissions:
|
||||
|
||||
jobs:
|
||||
|
||||
verify-action-digest:
|
||||
name: Verify action.yml image digest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Verify action.yml digest matches published image
|
||||
run: |
|
||||
PINNED_DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' action.yml | head -1)
|
||||
if [ -z "${PINNED_DIGEST}" ]; then
|
||||
echo "::error::action.yml does not pin the runtime image by digest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_DIGEST=$(docker buildx imagetools inspect docker.io/mikefarah/yq:4-githubaction --format '{{printf "%s" .Manifest.Digest}}')
|
||||
|
||||
echo "action.yml pins: ${PINNED_DIGEST}"
|
||||
echo "mikefarah/yq:4-githubaction: ${LATEST_DIGEST}"
|
||||
|
||||
if [ "${PINNED_DIGEST}" != "${LATEST_DIGEST}" ]; then
|
||||
echo "::error::action.yml digest does not match the current mikefarah/yq:4-githubaction image"
|
||||
echo "Update the image line in action.yml to:"
|
||||
echo " docker://mikefarah/yq:4-githubaction@${LATEST_DIGEST}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version: '^1.20'
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
curl -sSfL https://raw.githubusercontent.com/golang/dep/1f7c19e5f52f49ffb9f956f64c010be14683468b/install.sh | env DEP_RELEASE_TAG=v0.5.4 sh
|
||||
dep ensure
|
||||
fi
|
||||
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@ -5,12 +5,17 @@ on:
|
||||
- 'v4.*'
|
||||
- 'draft-*'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
publishGitRelease:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version: '^1.20'
|
||||
check-latest: true
|
||||
@ -24,7 +29,7 @@ jobs:
|
||||
run: echo "VERSION=${GITHUB_REF##*/}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Generate man page
|
||||
uses: docker://pandoc/core:2.14.2
|
||||
uses: docker://pandoc/core:2.14.2@sha256:04e127c6642a2b9d447c26fe0ac6a5932efa8f508eda9f07da51b6e621dd7c19
|
||||
id: gen-man-page
|
||||
with:
|
||||
args: >-
|
||||
@ -37,14 +42,22 @@ jobs:
|
||||
--output=yq.1
|
||||
man.md
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- name: Cross compile
|
||||
run: |
|
||||
sudo apt-get install rhash -y
|
||||
go install github.com/goreleaser/goreleaser/v2@latest
|
||||
go install github.com/goreleaser/goreleaser/v2@v2.16.0
|
||||
./scripts/xcompile.sh
|
||||
|
||||
- name: Sign checksums
|
||||
run: |
|
||||
cosign sign-blob --yes --bundle build/checksums.bundle build/checksums
|
||||
cosign sign-blob --yes --bundle build/checksums-bsd.bundle build/checksums-bsd
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
files: build/*
|
||||
draft: true
|
||||
|
||||
78
.github/workflows/scorecard.yml
vendored
Normal file
78
.github/workflows/scorecard.yml
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
branch_protection_rule:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '39 7 * * 2'
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
|
||||
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
# Uncomment the permissions below if installing in a private repository.
|
||||
# contents: read
|
||||
# actions: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||
# - you are installing Scorecard on a *private* repository
|
||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
|
||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
|
||||
# Public repositories:
|
||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||
# - Allows the repository to include the Scorecard badge.
|
||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories:
|
||||
# - `publish_results` will always be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
|
||||
# file_mode: git
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
14
.github/workflows/snap-release.yml
vendored
14
.github/workflows/snap-release.yml
vendored
@ -7,15 +7,23 @@ on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
buildSnap:
|
||||
environment: snap
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: snapcore/action-build@v1
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
|
||||
id: build
|
||||
- uses: snapcore/action-publish@v1
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
||||
with:
|
||||
snapcraft-args: "remote-build --launchpad-accept-public-upload"
|
||||
- uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/test-yq.yml
vendored
2
.github/workflows/test-yq.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Get test
|
||||
id: get_value
|
||||
uses: mikefarah/yq@master
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -25,6 +25,7 @@ _testmain.go
|
||||
cover.out
|
||||
coverage.out
|
||||
coverage.html
|
||||
coverage_sorted.txt
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
@ -42,9 +43,11 @@ yq*.snap
|
||||
|
||||
test.yml
|
||||
test*.yml
|
||||
test*.tf
|
||||
test*.xml
|
||||
test*.toml
|
||||
test*.yaml
|
||||
*.kyaml
|
||||
test_dir1/
|
||||
test_dir2/
|
||||
0.yml
|
||||
@ -67,3 +70,7 @@ debian/files
|
||||
.vscode
|
||||
|
||||
yq3
|
||||
|
||||
# Golang
|
||||
.gomodcache/
|
||||
.gocache/
|
||||
|
||||
@ -14,6 +14,11 @@ linters:
|
||||
- unconvert
|
||||
- unparam
|
||||
settings:
|
||||
misspell:
|
||||
locale: UK
|
||||
ignore-rules:
|
||||
- color
|
||||
- colors
|
||||
depguard:
|
||||
rules:
|
||||
prevent_unmaintained_packages:
|
||||
|
||||
@ -23,6 +23,7 @@ builds:
|
||||
- linux_amd64
|
||||
- linux_arm
|
||||
- linux_arm64
|
||||
- linux_loong64
|
||||
- linux_mips
|
||||
- linux_mips64
|
||||
- linux_mips64le
|
||||
@ -38,7 +39,6 @@ builds:
|
||||
- openbsd_amd64
|
||||
- windows_386
|
||||
- windows_amd64
|
||||
- windows_arm
|
||||
- windows_arm64
|
||||
|
||||
no_unique_dist_dir: true
|
||||
|
||||
500
AGENTS.md
Normal file
500
AGENTS.md
Normal file
@ -0,0 +1,500 @@
|
||||
# yq — agent instructions
|
||||
|
||||
## ⚠️ MANDATORY: GitHub agent disclosure
|
||||
|
||||
**Always required. No exceptions.**
|
||||
|
||||
Whenever you perform **any** GitHub action on behalf of the user, you **must** disclose that an AI agent (Cursor) wrote the content and is acting on the user's behalf — **not the user personally**. Do this **before** submitting; never post first and add the disclosure later.
|
||||
|
||||
Applies to **all** GitHub interactions, including:
|
||||
|
||||
- Pull requests (titles, descriptions, and reviews)
|
||||
- PR comments and inline review comments
|
||||
- Issues (new issues, comments, and updates)
|
||||
- Any other post or reply on GitHub
|
||||
|
||||
**How to disclose:** Put it prominently at the **top** of every PR description, review body, comment, or issue. Use wording like:
|
||||
|
||||
|
||||
Inline review comments must include a short disclosure too (e.g. `> Generated by Cursor acting on the user's behalf, not the user personally.`).
|
||||
|
||||
**Never** submit a GitHub action without this disclosure.
|
||||
|
||||
---
|
||||
|
||||
Always run the spellcheck before raising a PR:
|
||||
|
||||
```bash
|
||||
bash scripts/spelling.sh
|
||||
```
|
||||
|
||||
This is also included in the full CI pipeline via `make local test`.
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Overview
|
||||
|
||||
**yq** is a Go CLI for querying and transforming YAML, JSON, XML, INI, and other structured formats. There are no long-running services — development is build-and-test against a local `./yq` binary.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Go ≥ 1.25** (see `go.mod`)
|
||||
- **Bash** (acceptance tests)
|
||||
- **Docker/Podman** is optional; use `make local <target>` to run natively when containers are unavailable
|
||||
|
||||
### PATH
|
||||
|
||||
After `scripts/devtools.sh`, add Go tool binaries to PATH:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/go/bin:$PATH"
|
||||
```
|
||||
|
||||
`golangci-lint` and `typos` install to `$HOME/go/bin`; `gosec` installs to `./bin/gosec` in the repo root.
|
||||
|
||||
### Common commands (local, no Docker)
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Install dev tools | `bash scripts/devtools.sh` |
|
||||
| Vendor dependencies | `make local vendor` |
|
||||
| Build binary | `go build -o yq .` or `make local build` |
|
||||
| Format | `make local format` |
|
||||
| Lint | `make local check` |
|
||||
| Unit tests | `make local test` or `bash scripts/test.sh` |
|
||||
| Acceptance (E2E) | `bash scripts/acceptance.sh` (requires `./yq` built first) |
|
||||
|
||||
`make local build` runs the full CI chain (format → spelling → gosec → lint → unit tests → build → acceptance). For a faster loop, build with `go build -o yq .` and run `bash scripts/acceptance.sh`.
|
||||
|
||||
### Caveats
|
||||
|
||||
- **`make` without `local`** tries Docker/Podman (`Dockerfile.dev`). In Cloud Agent VMs without Docker, always prefix with `make local`.
|
||||
- **Spelling step** uses `typos` (installed by `scripts/devtools.sh`).
|
||||
- **`make local test` / `scripts/check.sh`** require `golangci-lint` on PATH (`devtools.sh`).
|
||||
|
||||
---
|
||||
|
||||
# General rules
|
||||
✅ **DO:**
|
||||
- You can use ./yq with the `--debug-node-info` flag to get a deeper understanding of the ast.
|
||||
- run ./scripts/format.sh to format the code; then ./scripts/check.sh lint and finally ./scripts/spelling.sh to check spelling.
|
||||
- Add comprehensive tests to cover the changes
|
||||
- Run test suite to ensure there is no regression
|
||||
- Use UK english spelling
|
||||
- **Follow the mandatory GitHub agent disclosure rule above** on every GitHub action — no exceptions
|
||||
|
||||
❌ **DON'T:**
|
||||
- Git add or commit
|
||||
- Add comments to functions that are self-explanatory
|
||||
- **Post to GitHub without the mandatory agent disclosure** (PRs, reviews, comments, issues, or any other GitHub interaction)
|
||||
|
||||
|
||||
|
||||
# Adding a New Encoder/Decoder
|
||||
|
||||
This guide explains how to add support for a new format (encoder/decoder) to yq without modifying `candidate_node.go`.
|
||||
|
||||
## Overview
|
||||
|
||||
The encoder/decoder architecture in yq is based on two main interfaces:
|
||||
|
||||
- **Encoder**: Converts a `CandidateNode` to output in a specific format
|
||||
- **Decoder**: Reads input in a specific format and creates a `CandidateNode`
|
||||
|
||||
Each format is registered in `pkg/yqlib/format.go` and made available through factory functions.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Files
|
||||
|
||||
- `pkg/yqlib/encoder.go` - Defines the `Encoder` interface
|
||||
- `pkg/yqlib/decoder.go` - Defines the `Decoder` interface
|
||||
- `pkg/yqlib/format.go` - Format registry and factory functions
|
||||
- `pkg/yqlib/operator_encoder_decoder.go` - Encode/decode operators
|
||||
- `pkg/yqlib/encoder_*.go` - Encoder implementations
|
||||
- `pkg/yqlib/decoder_*.go` - Decoder implementations
|
||||
|
||||
### Interfaces
|
||||
|
||||
**Encoder Interface:**
|
||||
```go
|
||||
type Encoder interface {
|
||||
Encode(writer io.Writer, node *CandidateNode) error
|
||||
PrintDocumentSeparator(writer io.Writer) error
|
||||
PrintLeadingContent(writer io.Writer, content string) error
|
||||
CanHandleAliases() bool
|
||||
}
|
||||
```
|
||||
|
||||
**Decoder Interface:**
|
||||
```go
|
||||
type Decoder interface {
|
||||
Init(reader io.Reader) error
|
||||
Decode() (*CandidateNode, error)
|
||||
}
|
||||
```
|
||||
|
||||
## Step-by-Step: Adding a New Encoder/Decoder
|
||||
|
||||
### Step 1: Create the Encoder File
|
||||
|
||||
Create `pkg/yqlib/encoder_<format>.go` implementing the `Encoder` interface:
|
||||
- `Encode()` - Convert a `CandidateNode` to your format and write to the output writer
|
||||
- `PrintDocumentSeparator()` - Handle document separators if your format requires them
|
||||
- `PrintLeadingContent()` - Handle leading content/comments if supported
|
||||
- `CanHandleAliases()` - Return whether your format supports YAML aliases
|
||||
|
||||
See `encoder_json.go` or `encoder_base64.go` for examples.
|
||||
|
||||
### Step 2: Create the Decoder File
|
||||
|
||||
Create `pkg/yqlib/decoder_<format>.go` implementing the `Decoder` interface:
|
||||
- `Init()` - Initialize the decoder with the input reader and set up any needed state
|
||||
- `Decode()` - Decode one document from the input and return a `CandidateNode`, or `io.EOF` when finished
|
||||
|
||||
See `decoder_json.go` or `decoder_base64.go` for examples.
|
||||
|
||||
### Step 3: Create Tests (Mandatory)
|
||||
|
||||
Create a test file `pkg/yqlib/<format>_test.go` using the `formatScenario` pattern:
|
||||
- Define test scenarios as `formatScenario` structs with fields: `description`, `input`, `expected`, `scenarioType`
|
||||
- `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation)
|
||||
- Create a helper function `test<Format>Scenario()` that switches on `scenarioType`
|
||||
- Create main test function `Test<Format>FormatScenarios()` that iterates over scenarios
|
||||
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
|
||||
|
||||
Test coverage must include:
|
||||
- Basic data types (scalars, arrays, objects/maps)
|
||||
- Nested structures
|
||||
- Edge cases (empty inputs, special characters, escape sequences)
|
||||
- Format-specific features or syntax
|
||||
- Round-trip tests: decode → encode → decode should preserve data
|
||||
|
||||
See `hcl_test.go` for a complete example.
|
||||
|
||||
### Step 4: Register the Format in format.go
|
||||
|
||||
Edit `pkg/yqlib/format.go`:
|
||||
|
||||
1. Add a new format variable:
|
||||
- `"<format>"` is the formal name (e.g., "json", "yaml")
|
||||
- `[]string{...}` contains short aliases (can be empty)
|
||||
- The first function creates an encoder (can be nil for encode-only formats)
|
||||
- The second function creates a decoder (can be nil for decode-only formats)
|
||||
|
||||
2. Add the format to the `Formats` slice in the same file
|
||||
|
||||
See existing formats in `format.go` for the exact structure.
|
||||
|
||||
### Step 5: Handle Encoder Configuration (if needed)
|
||||
|
||||
If your format has preferences/configuration options:
|
||||
|
||||
1. Create a preferences struct with your configuration fields
|
||||
2. Update the encoder to accept preferences in its factory function
|
||||
3. Update `format.go` to pass the configured preferences
|
||||
4. Update `operator_encoder_decoder.go` if special indent handling is needed (see existing formats like JSON and YAML for the pattern)
|
||||
|
||||
This pattern is optional and only needed if your format has user-configurable options.
|
||||
|
||||
## Build Tags
|
||||
|
||||
Use build tags to allow optional compilation of formats:
|
||||
- Add `//go:build !yq_no<format>` at the top of your encoder and decoder files
|
||||
- Create a no-build version in `pkg/yqlib/no_<format>.go` that returns nil for encoder/decoder factories
|
||||
|
||||
This allows users to compile yq without certain formats using: `go build -tags yq_no<format>`
|
||||
|
||||
## Working with CandidateNode
|
||||
|
||||
The `CandidateNode` struct represents a YAML node with:
|
||||
- `Kind`: The node type (ScalarNode, SequenceNode, MappingNode)
|
||||
- `Tag`: The YAML tag (e.g., "!!str", "!!int", "!!map")
|
||||
- `Value`: The scalar value (for ScalarNode only)
|
||||
- `Content`: Child nodes (for SequenceNode and MappingNode)
|
||||
|
||||
Key methods:
|
||||
- `node.guessTagFromCustomType()` - Infer the tag from Go type
|
||||
- `node.AsList()` - Convert to a list for processing
|
||||
- `node.CreateReplacement()` - Create a new replacement node
|
||||
- `NewCandidate()` - Create a new CandidateNode
|
||||
|
||||
## Key Points
|
||||
|
||||
✅ **DO:**
|
||||
- Implement only the `Encoder` and `Decoder` interfaces
|
||||
- Register your format in `format.go` only
|
||||
- Keep format-specific logic in your encoder/decoder files
|
||||
- Use the candidate_node style attribute to store style information for round-trip. Ask if this needs to be updated with new styles.
|
||||
- Use build tags for optional compilation
|
||||
- Add comprehensive tests
|
||||
- Run the specific encoder/decoder test (e.g. <format>_test.go) whenever you make ay changes to the encoder_<format> or decoder_<format>
|
||||
- Handle errors gracefully
|
||||
- Add the no build directive, like the xml encoder and decoder, that enables a minimal yq builds. e.g. `//go:build !yq_<format>`. Be sure to also update the build_small-yq.sh and build-tinygo-yq.sh to not include the new format.
|
||||
|
||||
❌ **DON'T:**
|
||||
- Modify `candidate_node.go` to add format-specific logic
|
||||
- Add format-specific fields to `CandidateNode`
|
||||
- Create special cases in core navigation or evaluation logic
|
||||
- Bypass the encoder/decoder interfaces
|
||||
- Use candidate_node tag attribute for anything other than indicate the data type
|
||||
|
||||
## Examples
|
||||
|
||||
Refer to existing format implementations for patterns:
|
||||
|
||||
- **Simple encoder/decoder**: `encoder_json.go`, `decoder_json.go`
|
||||
- **Complex with preferences**: `encoder_yaml.go`, `decoder_yaml.go`
|
||||
- **Encoder-only**: `encoder_sh.go` (ShFormat has nil decoder)
|
||||
- **String-only operations**: `encoder_base64.go`, `decoder_base64.go`
|
||||
|
||||
## Testing Your Implementation (Mandatory)
|
||||
|
||||
Tests must be implemented in `<format>_test.go` following the `formatScenario` pattern:
|
||||
|
||||
1. **Create test scenarios** using the `formatScenario` struct with fields:
|
||||
- `description`: Brief description of what's being tested
|
||||
- `input`: Sample input in your format
|
||||
- `expected`: Expected output (typically in YAML for decode tests)
|
||||
- `scenarioType`: Either `"decode"` or `"roundtrip"`
|
||||
|
||||
2. **Test coverage must include:**
|
||||
- Basic data types (scalars, arrays, objects/maps)
|
||||
- Nested structures
|
||||
- Edge cases (empty inputs, special characters, escape sequences)
|
||||
- Format-specific features or syntax
|
||||
- Round-trip tests: decode → encode → decode should preserve data
|
||||
|
||||
3. **Test function pattern:**
|
||||
- `test<Format>Scenario()`: Helper function that switches on `scenarioType`
|
||||
- `Test<Format>FormatScenarios()`: Main test function that iterates over scenarios
|
||||
|
||||
4. **Example from existing formats:**
|
||||
- See `hcl_test.go` for a complete example
|
||||
- See `yaml_test.go` for YAML-specific patterns
|
||||
- See `json_test.go` for more complex scenarios
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Format with Indentation
|
||||
Use preferences to control output formatting:
|
||||
```go
|
||||
type <format>Preferences struct {
|
||||
Indent int
|
||||
}
|
||||
|
||||
func (prefs *<format>Preferences) Copy() <format>Preferences {
|
||||
return *prefs
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Documents
|
||||
Decoders should support reading multiple documents:
|
||||
```go
|
||||
func (dec *<format>Decoder) Decode() (*CandidateNode, error) {
|
||||
if dec.finished {
|
||||
return nil, io.EOF
|
||||
}
|
||||
// ... decode next document ...
|
||||
if noMoreDocuments {
|
||||
dec.finished = true
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Adding a New Operator
|
||||
|
||||
This guide explains how to add a new operator to yq. Operators are the core of yq's expression language and process `CandidateNode` objects without requiring modifications to `candidate_node.go` itself.
|
||||
|
||||
## Overview
|
||||
|
||||
Operators transform data by implementing a handler function that processes a `Context` containing `CandidateNode` objects. Each operator is:
|
||||
|
||||
1. Defined as an `operationType` in `operation.go`
|
||||
2. Registered in the lexer in `lexer_participle.go`
|
||||
3. Implemented in its own `operator_<type>.go` file
|
||||
4. Tested in `operator_<type>_test.go`
|
||||
5. Documented in `pkg/yqlib/doc/operators/headers/<type>.md`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Files
|
||||
|
||||
- `pkg/yqlib/operation.go` - Defines `operationType` and operator registry
|
||||
- `pkg/yqlib/lexer_participle.go` - Registers operators with their syntax patterns
|
||||
- `pkg/yqlib/operator_<type>.go` - Operator implementation
|
||||
- `pkg/yqlib/operator_<type>_test.go` - Operator tests using `expressionScenario`
|
||||
- `pkg/yqlib/doc/operators/headers/<type>.md` - Documentation header
|
||||
|
||||
### Core Types
|
||||
|
||||
**operationType:**
|
||||
```go
|
||||
type operationType struct {
|
||||
Type string // Unique operator name (e.g., "REVERSE")
|
||||
NumArgs uint // Number of arguments (0 for no args)
|
||||
Precedence uint // Operator precedence (higher = higher precedence)
|
||||
Handler operatorHandler // The function that executes the operator
|
||||
CheckForPostTraverse bool // Whether to apply post-traversal logic
|
||||
ToString func(*Operation) string // Custom string representation
|
||||
}
|
||||
```
|
||||
|
||||
**operatorHandler signature:**
|
||||
```go
|
||||
type operatorHandler func(*dataTreeNavigator, Context, *ExpressionNode) (Context, error)
|
||||
```
|
||||
|
||||
**expressionScenario for tests:**
|
||||
```go
|
||||
type expressionScenario struct {
|
||||
description string
|
||||
subdescription string
|
||||
document string
|
||||
expression string
|
||||
expected []string
|
||||
skipDoc bool
|
||||
expectedError string
|
||||
}
|
||||
```
|
||||
|
||||
## Step-by-Step: Adding a New Operator
|
||||
|
||||
### Step 1: Create the Operator Implementation File
|
||||
|
||||
Create `pkg/yqlib/operator_<type>.go` implementing the operator handler function:
|
||||
- Implement the `operatorHandler` function signature
|
||||
- Process nodes from `context.MatchingNodes`
|
||||
- Return a new `Context` with results using `context.ChildContext()`
|
||||
- Use `candidate.CreateReplacement()` or `candidate.CreateReplacementWithComments()` to create new nodes
|
||||
- Handle errors gracefully with meaningful error messages
|
||||
|
||||
See `operator_reverse.go` or `operator_keys.go` for examples.
|
||||
|
||||
### Step 2: Register the Operator in operation.go
|
||||
|
||||
Add the operator type definition to `pkg/yqlib/operation.go`:
|
||||
|
||||
```go
|
||||
var <type>OpType = &operationType{
|
||||
Type: "<TYPE>", // All caps, matches pattern in lexer
|
||||
NumArgs: 0, // 0 for no args, 1+ for args
|
||||
Precedence: 50, // Typical range: 40-55
|
||||
Handler: <type>Operator, // Reference to handler function
|
||||
}
|
||||
```
|
||||
|
||||
**Precedence guidelines:**
|
||||
- 10-20: Logical operators (OR, AND, UNION)
|
||||
- 30: Pipe operator
|
||||
- 40: Assignment and comparison operators
|
||||
- 42: Arithmetic operators (ADD, SUBTRACT, MULTIPLY, DIVIDE)
|
||||
- 50-52: Most other operators
|
||||
- 55: High precedence (e.g., GET_VARIABLE)
|
||||
|
||||
**Optional fields:**
|
||||
- `CheckForPostTraverse: true` - If your operator can have another directly after it without the pipe character. Most of the time this is false.
|
||||
- `ToString: customToString` - Custom string representation (rarely needed)
|
||||
|
||||
### Step 3: Register the Operator in lexer_participle.go
|
||||
|
||||
Edit `pkg/yqlib/lexer_participle.go` to add the operator to the lexer rules:
|
||||
- Use `simpleOp()` for simple keyword patterns
|
||||
- Use object syntax for regex patterns or complex syntax
|
||||
- Support optional characters with `_?` and aliases with `|`
|
||||
|
||||
See existing operators in `lexer_participle.go` for pattern examples.
|
||||
|
||||
### Step 4: Create Tests (Mandatory)
|
||||
|
||||
Create `pkg/yqlib/operator_<type>_test.go` using the `expressionScenario` pattern:
|
||||
- Define test scenarios with `description`, `document`, `expression`, and `expected` fields
|
||||
- `expected` is a slice of strings showing output format: `"D<doc>, P[<path>], (<tag>)::<value>\n"`
|
||||
- Set `skipDoc: true` for edge cases you don't want in generated documentation
|
||||
- Include `subdescription` for longer test names
|
||||
- Set `expectedError` if testing error cases
|
||||
- Create main test function that iterates over scenarios
|
||||
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
|
||||
|
||||
Test coverage must include:
|
||||
- Basic data types and nested structures
|
||||
- Edge cases (empty inputs, special characters, type errors)
|
||||
- Multiple outputs if applicable
|
||||
- Format-specific features
|
||||
|
||||
See `operator_reverse_test.go` for a simple example and `operator_keys_test.go` for complex cases.
|
||||
|
||||
### Step 5: Create Documentation Header
|
||||
|
||||
Create `pkg/yqlib/doc/operators/headers/<type>.md`:
|
||||
- Use the exact operator name as the title
|
||||
- Include a concise 1-2 sentence summary
|
||||
- Add additional context or examples if the operator is complex
|
||||
|
||||
See existing headers in `doc/operators/headers/` for examples.
|
||||
|
||||
## Working with Context and CandidateNode
|
||||
|
||||
### Context Management
|
||||
- `context.ChildContext(results)` - Create child context with results
|
||||
- `context.GetVariable("varName")` - Get variables stored in context
|
||||
- `context.SetVariable("varName", value)` - Set variables in context
|
||||
|
||||
### CandidateNode Operations
|
||||
- `candidate.CreateReplacement(ScalarNode, "!!str", stringValue)` - Create a replacement node
|
||||
- `candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style)` - With style preserved
|
||||
- `candidate.Kind` - The node type (ScalarNode, SequenceNode, MappingNode)
|
||||
- `candidate.Tag` - The YAML tag (!!str, !!int, etc.)
|
||||
- `candidate.Value` - The scalar value (for ScalarNode only)
|
||||
- `candidate.Content` - Child nodes (for SequenceNode and MappingNode)
|
||||
- `candidate.guessTagFromCustomType()` - Infer the tag from Go type
|
||||
- `candidate.AsList()` - Convert to a list representation
|
||||
|
||||
## Key Points
|
||||
|
||||
✅ **DO:**
|
||||
- Implement the operator handler with the correct signature
|
||||
- Register in `operation.go` with appropriate precedence
|
||||
- Add the lexer pattern in `lexer_participle.go`
|
||||
- Write comprehensive tests covering normal and edge cases
|
||||
- Create a documentation header in `doc/operators/headers/`
|
||||
- Use `Context.ChildContext()` for proper context threading
|
||||
- Handle all node types gracefully
|
||||
- Return meaningful error messages
|
||||
|
||||
❌ **DON'T:**
|
||||
- Modify `candidate_node.go` (operators shouldn't need this)
|
||||
- Modify core navigation or evaluation logic
|
||||
- Bypass the handler function pattern
|
||||
- Add format-specific or operator-specific fields to `CandidateNode`
|
||||
- Skip tests or documentation
|
||||
|
||||
## Examples
|
||||
|
||||
Refer to existing operator implementations for patterns:
|
||||
|
||||
- **No-argument operator**: `operator_reverse.go` - Processes arrays/sequences
|
||||
- **Single-argument operator**: `operator_map.go` - Takes an expression argument
|
||||
- **Complex multi-output**: `operator_keys.go` - Produces multiple results
|
||||
- **With preferences**: `operator_to_number.go` - Configuration options
|
||||
- **Error handling**: `operator_error.go` - Control flow with errors
|
||||
- **String operations**: `operator_strings.go` - Multiple related operators
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
Refer to existing test files for specific patterns:
|
||||
- Basic expression tests in `operator_reverse_test.go`
|
||||
- Multi-output tests in `operator_keys_test.go`
|
||||
- Error handling tests in `operator_error_test.go`
|
||||
- Tests with `skipDoc` flag to exclude from generated documentation
|
||||
|
||||
## Common Patterns
|
||||
|
||||
Refer to existing operator implementations for these patterns:
|
||||
- Simple transformation: see `operator_reverse.go`
|
||||
- Type checking: see `operator_error.go`
|
||||
- Working with arguments: see `operator_map.go`
|
||||
- Post-traversal operators: see `operator_with.go`
|
||||
@ -11,7 +11,7 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
Examples of behaviour that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
@ -20,7 +20,7 @@ include:
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behaviour by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
@ -34,13 +34,13 @@ Examples of unacceptable behavior by participants include:
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
behaviour and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behaviour.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
permanently any contributor for other behaviours that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
@ -54,7 +54,7 @@ further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
|
||||
reported by contacting the project team at mikefarah@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
|
||||
240
CONTRIBUTING.md
240
CONTRIBUTING.md
@ -3,55 +3,227 @@ Not all new PRs will be merged in
|
||||
|
||||
It's recommended to check with the owner first (e.g. raise an issue) to discuss a new feature before developing, to ensure your hard efforts don't go to waste.
|
||||
|
||||
PRs to fix bugs and issues are almost always be welcome, just make sure you write tests as well.
|
||||
PRs to fix bugs and issues are almost always welcome :pray: please ensure you write tests as well.
|
||||
|
||||
** PRs that significantly refactor code and release pipelines PRs will generally _NOT_ be accepted **
|
||||
|
||||
Significant refactors take a lot of time to understand and can have all sorts of unintended side effects.
|
||||
|
||||
Release pipeline PRs are a security risk - it's too easy for a serious vulnerability to sneak in (either intended or not). If there is a new cool way of releasing things, raise an issue for discussion first - it will need to be gone over with a fine tooth comb.
|
||||
|
||||
At this stage, yq is not going to maintain any other release platforms other than GitHub and Docker - that said, I'm more than happy to put in other community maintained methods in the README for visibility :heart:
|
||||
The following types of PRs will _not_ be accepted:
|
||||
- **Significant refactors** take a lot of time to understand and can have all sorts of unintended side effects. If you think there's a better way to do things (that requires significant changes) raise an issue for discussion first :)
|
||||
- **Release pipeline PRs** are a security risk - it's too easy for a serious vulnerability to sneak in (either intended or not). If there is a new cool way of releasing things, raise an issue for discussion first - it will need to be gone over with a fine tooth comb.
|
||||
- **Version bumps** are handled by dependabot, the bot will auto-raise PRs and they will be regularly merged in.
|
||||
- **New release platforms** At this stage, yq is not going to maintain any other release platforms other than GitHub and Docker - that said, I'm more than happy to put in other community maintained methods in the README for visibility :heart:
|
||||
|
||||
|
||||
# Development
|
||||
|
||||
1. Install (Golang)[https://golang.org/]
|
||||
1. Run `scripts/devtools.sh` to install the required devtools
|
||||
2. Run `make [local] vendor` to install the vendor dependencies
|
||||
2. Run `make [local] test` to ensure you can run the existing tests
|
||||
3. Write unit tests - (see existing examples). Changes will not be accepted without corresponding unit tests.
|
||||
4. Make the code changes.
|
||||
5. `make [local] test` to lint code and run tests
|
||||
6. Profit! ok no profit, but raise a PR and get kudos :)
|
||||
## Initial Setup
|
||||
|
||||
1. Install [Golang](https://golang.org/) (version 1.24.0 or later)
|
||||
2. Run `scripts/devtools.sh` to install required development tools:
|
||||
- golangci-lint for code linting
|
||||
- gosec for security analysis
|
||||
3. Run `make [local] vendor` to install vendor dependencies
|
||||
4. Run `make [local] test` to ensure you can run the existing tests
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Write unit tests first** - Changes will not be accepted without corresponding unit tests (see Testing section below)
|
||||
2. **Make your code changes**
|
||||
3. **Run tests and linting**: `make [local] test` (this runs formatting, linting, security checks, and tests)
|
||||
4. **Create your PR** and get kudos! :)
|
||||
|
||||
## Make Commands
|
||||
|
||||
- Use `make [local] <command>` for local development (runs in Docker container)
|
||||
- Use `make <command>` for CI/CD environments
|
||||
- Common commands:
|
||||
- `make [local] vendor` - Install dependencies
|
||||
- `make [local] test` - Run all checks and tests
|
||||
- `make [local] build` - Build the yq binary
|
||||
- `make [local] format` - Format code
|
||||
- `make [local] check` - Run linting and security checks
|
||||
|
||||
# Code Quality
|
||||
|
||||
## Linting and Formatting
|
||||
|
||||
The project uses strict linting rules defined in `.golangci.yml`. All code must pass:
|
||||
|
||||
- **Code formatting**: gofmt, goimports, gci
|
||||
- **Linting**: revive, errorlint, gosec, misspell, and others
|
||||
- **Security checks**: gosec security analysis
|
||||
- **Spelling checks**: misspell detection
|
||||
|
||||
Run `make [local] check` to verify your code meets all quality standards.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- Follow standard Go conventions
|
||||
- Use meaningful variable names
|
||||
- Add comments for public functions and complex logic
|
||||
- Keep functions focused and reasonably sized
|
||||
- Use the project's existing patterns and conventions
|
||||
|
||||
# Testing
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests in yq use the `expressionScenario` pattern. Each test scenario includes:
|
||||
- `expression`: The yq expression to test
|
||||
- `document`: Input YAML/JSON (optional)
|
||||
- `expected`: Expected output
|
||||
- `skipDoc`: Whether to skip documentation generation
|
||||
|
||||
## Writing Tests
|
||||
|
||||
1. **Find the appropriate test file** (e.g., `operator_add_test.go` for addition operations)
|
||||
2. **Add your test scenario** to the `*OperatorScenarios` slice
|
||||
3. **Run the specific test**: `go test -run TestAddOperatorScenarios` (replace with appropriate test name)
|
||||
4. **Verify documentation generation** (see Documentation section)
|
||||
|
||||
## Test Examples
|
||||
|
||||
```go
|
||||
var addOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
expression: `"foo" + "bar"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::foobar\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
document: "apples: 3",
|
||||
expression: `.apples + 3`,
|
||||
expected: []string{
|
||||
"D0, P[apples], (!!int)::6\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
- **All tests**: `make [local] test`
|
||||
- **Specific test**: `go test -run TestName`
|
||||
- **With coverage**: `make [local] cover`
|
||||
|
||||
# Documentation
|
||||
|
||||
The various operator documentation (e.g. 'strings') are generated from the 'master' branch, and have a statically defined header (e.g. `pkg/yqlib/doc/operators/headers/add.md`) and the bulk of the docs are generated from the unit tests e.g. `pkg/yqlib/operator_add_test.go`.
|
||||
## Documentation Generation
|
||||
|
||||
The pipeline will run the tests and automatically concatenate the files together, and put them under
|
||||
`pkg/qylib/doc/add.md`. These files are checked in the master branch (and are copied to the gitbook branch as part of the release process).
|
||||
The project uses a documentation system that combines static headers with dynamically generated content from tests.
|
||||
|
||||
### How It Works
|
||||
|
||||
Remaining static documentation is in the 'githook' branch (where the generated docs are copied across into)
|
||||
1. **Static headers** are defined in `pkg/yqlib/doc/operators/headers/*.md`
|
||||
2. **Dynamic content** is generated from test scenarios in `*_test.go` files
|
||||
3. **Generated docs** are created in `pkg/yqlib/doc/*.md` by concatenating headers with test-generated content
|
||||
4. **Documentation is synced** to the gitbook branch for the website
|
||||
|
||||
## How to contribute
|
||||
### Updating Operator Documentation
|
||||
|
||||
The first step is to find if what you want is automatically generated or not - start by looking in the master branch.
|
||||
#### For Test-Generated Documentation
|
||||
|
||||
Note that PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
|
||||
Most operator documentation is generated from tests. To update:
|
||||
|
||||
### Updating dynamic documentation from master
|
||||
- Search for the documentation you want to update. If you find matches in a `*_test.go` file - update that, as that will automatically update the matching `*.md` file
|
||||
- Assuming you are updating a `*_test.go` file, once updated, run the test to regenerated the docs. E.g. for the 'Add' test generated docs, from the pkg/yqlib folder run:
|
||||
`go test -run TestAddOperatorScenarios` which will run that test defined in the `operator_add_test.go` file.
|
||||
- Ensure the tests still pass, and check the generated documentation have your update.
|
||||
- Note: If the documentation is only in a `headers/*.md` file, then just update that directly
|
||||
- Raise a PR to merge the changes into master!
|
||||
1. **Find the test file** (e.g., `operator_add_test.go`)
|
||||
2. **Update test scenarios** - each `expressionScenario` with `skipDoc: false` becomes documentation
|
||||
3. **Run the test** to regenerate docs:
|
||||
```bash
|
||||
cd pkg/yqlib
|
||||
go test -run TestAddOperatorScenarios
|
||||
```
|
||||
4. **Verify the generated documentation** in `pkg/yqlib/doc/add.md`
|
||||
5. **Create a PR** with your changes
|
||||
|
||||
### Updating static documentation from the gitbook branch
|
||||
If you haven't found what you want to update in the master branch, then check the gitbook branch directly as there are a few pages in there that are not in master.
|
||||
#### For Header-Only Documentation
|
||||
|
||||
- Update the `*.md` files
|
||||
- Raise a PR to merge the changes into gitbook.
|
||||
If documentation exists only in `headers/*.md` files:
|
||||
1. **Update the header file directly** (e.g., `pkg/yqlib/doc/operators/headers/add.md`)
|
||||
2. **Create a PR** with your changes
|
||||
|
||||
### Updating Static Documentation
|
||||
|
||||
For documentation not in the master branch:
|
||||
|
||||
1. **Check the gitbook branch** for additional pages
|
||||
2. **Update the `*.md` files** directly
|
||||
3. **Create a PR** to the gitbook branch
|
||||
|
||||
### Documentation Best Practices
|
||||
|
||||
- **Write clear, concise examples** in test scenarios
|
||||
- **Use meaningful variable names** in examples
|
||||
- **Include edge cases** and error conditions
|
||||
- **Test your documentation changes** by running the specific test
|
||||
- **Verify generated output** matches expectations
|
||||
|
||||
Note: PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Common Setup Issues
|
||||
|
||||
### Docker/Podman Issues
|
||||
- **Problem**: `make` commands fail with Docker errors
|
||||
- **Solution**: Ensure Docker or Podman is running and accessible
|
||||
- **Alternative**: Use `make local <command>` to run in containers
|
||||
|
||||
### Go Version Issues
|
||||
- **Problem**: Build fails with Go version errors
|
||||
- **Solution**: Ensure you have Go 1.24.0 or later installed
|
||||
- **Check**: Run `go version` to verify
|
||||
|
||||
### Vendor Dependencies
|
||||
- **Problem**: `make vendor` fails or dependencies are outdated
|
||||
- **Solution**:
|
||||
```bash
|
||||
go mod tidy
|
||||
make [local] vendor
|
||||
```
|
||||
|
||||
### Linting Failures
|
||||
- **Problem**: `make check` fails with linting errors
|
||||
- **Solution**:
|
||||
```bash
|
||||
make [local] format # Auto-fix formatting
|
||||
# Manually fix remaining linting issues
|
||||
make [local] check # Verify fixes
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
- **Problem**: Tests fail locally but pass in CI
|
||||
- **Solution**:
|
||||
```bash
|
||||
make [local] test # Run in Docker container
|
||||
```
|
||||
|
||||
- **Problem**: Tests fail with a VCS error:
|
||||
```bash
|
||||
error obtaining VCS status: exit status 128
|
||||
Use -buildvcs=false to disable VCS stamping.
|
||||
```
|
||||
- **Solution**:
|
||||
Git security mechanisms prevent Golang from detecting the Git details inside
|
||||
the container; either build with the `local` option, or pass GOFLAGS to
|
||||
disable Golang buildvcs behaviour.
|
||||
```bash
|
||||
make local test
|
||||
# OR
|
||||
make test GOFLAGS='-buildvcs=true'
|
||||
```
|
||||
|
||||
### Documentation Generation Issues
|
||||
- **Problem**: Generated docs don't update after test changes
|
||||
- **Solution**:
|
||||
```bash
|
||||
cd pkg/yqlib
|
||||
go test -run TestSpecificOperatorScenarios
|
||||
# Check if generated file updated in pkg/yqlib/doc/
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Check existing issues**: Search GitHub issues for similar problems
|
||||
- **Create an issue**: If you can't find a solution, create a detailed issue
|
||||
- **Ask questions**: Use GitHub Discussions for general questions
|
||||
- **Join the community**: Check the project's community channels
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.2 AS builder
|
||||
FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3 AS builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
@ -10,7 +10,7 @@ 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 AS production
|
||||
FROM alpine:3@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS production
|
||||
LABEL maintainer="Mike Farah <mikefarah@users.noreply.github.com>"
|
||||
|
||||
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
FROM golang:1.25.2
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y npm && \
|
||||
npm install -g npx cspell@latest
|
||||
FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3
|
||||
|
||||
COPY scripts/devtools.sh /opt/devtools.sh
|
||||
|
||||
|
||||
3
Makefile
3
Makefile
@ -35,13 +35,14 @@ clean:
|
||||
## prefix before other make targets to run in your local dev environment
|
||||
local: | quiet
|
||||
@$(eval ENGINERUN= )
|
||||
@$(eval GOFLAGS="$(GOFLAGS)" )
|
||||
@mkdir -p tmp
|
||||
@touch tmp/dev_image_id
|
||||
quiet: # this is silly but shuts up 'Nothing to be done for `local`'
|
||||
@:
|
||||
|
||||
prepare: tmp/dev_image_id
|
||||
tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh
|
||||
tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh _typos.toml
|
||||
@mkdir -p tmp
|
||||
@${ENGINE} rmi -f ${DEV_IMAGE} > /dev/null 2>&1 || true
|
||||
@${ENGINE} build -t ${DEV_IMAGE} -f Dockerfile.dev .
|
||||
|
||||
@ -4,6 +4,7 @@ IMPORT_PATH := github.com/mikefarah/${PROJECT}
|
||||
export GIT_COMMIT = $(shell git rev-parse --short HEAD)
|
||||
export GIT_DIRTY = $(shell test -n "$$(git status --porcelain)" && echo "+CHANGES" || true)
|
||||
export GIT_DESCRIBE = $(shell git describe --tags --always)
|
||||
GOFLAGS :=
|
||||
LDFLAGS :=
|
||||
LDFLAGS += -X main.GitCommit=${GIT_COMMIT}${GIT_DIRTY}
|
||||
LDFLAGS += -X main.GitDescribe=${GIT_DESCRIBE}
|
||||
@ -26,13 +27,15 @@ ifeq ($(CYG_CHECK),1)
|
||||
else
|
||||
# all non-windows environments
|
||||
ROOT := $(shell pwd)
|
||||
SELINUX := $(shell which getenforce 2>&1 >/dev/null && echo :z)
|
||||
# Deliberately use `command -v` instead of `which` to be POSIX compliant
|
||||
SELINUX := $(shell command -v getenforce >/dev/null 2>&1 && echo :z)
|
||||
endif
|
||||
|
||||
DEV_IMAGE := ${PROJECT}_dev
|
||||
|
||||
ENGINERUN := ${ENGINE} run --rm \
|
||||
-e LDFLAGS="${LDFLAGS}" \
|
||||
-e GOFLAGS="${GOFLAGS}" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
-v ${ROOT}/vendor:/go/src${SELINUX} \
|
||||
-v ${ROOT}:/${PROJECT}/src/${IMPORT_PATH}${SELINUX} \
|
||||
|
||||
211
README.md
211
README.md
@ -3,44 +3,46 @@
|
||||
    
|
||||
|
||||
|
||||
a lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
|
||||
A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, kyaml, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
|
||||
|
||||
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 and Podman, all listed below.
|
||||
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 and Podman, all listed below.
|
||||
|
||||
## Quick Usage Guide
|
||||
|
||||
Read a value:
|
||||
### Basic Operations
|
||||
|
||||
**Read a value:**
|
||||
```bash
|
||||
yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
Pipe from STDIN:
|
||||
**Pipe from STDIN:**
|
||||
```bash
|
||||
yq '.a.b[0].c' < file.yaml
|
||||
```
|
||||
|
||||
Update a yaml file, in place
|
||||
**Update a yaml file in place:**
|
||||
```bash
|
||||
yq -i '.a.b[0].c = "cool"' file.yaml
|
||||
```
|
||||
|
||||
Update using environment variables
|
||||
**Update using environment variables:**
|
||||
```bash
|
||||
NAME=mike yq -i '.a.b[0].c = strenv(NAME)' file.yaml
|
||||
```
|
||||
|
||||
Merge multiple files
|
||||
### Advanced Operations
|
||||
|
||||
**Merge multiple files:**
|
||||
```bash
|
||||
# merge two files
|
||||
yq -n 'load("file1.yaml") * load("file2.yaml")'
|
||||
|
||||
# merge using globs:
|
||||
# note the use of `ea` to evaluate all the files at once
|
||||
# instead of in sequence
|
||||
# merge using globs (note: `ea` evaluates all files at once instead of in sequence)
|
||||
yq ea '. as $item ireduce ({}; . * $item )' path/to/*.yml
|
||||
```
|
||||
|
||||
Multiple updates to a yaml file
|
||||
**Multiple updates to a yaml file:**
|
||||
```bash
|
||||
yq -i '
|
||||
.a.b[0].c = "cool" |
|
||||
@ -49,14 +51,22 @@ yq -i '
|
||||
' file.yaml
|
||||
```
|
||||
|
||||
Find and update an item in an array:
|
||||
**Find and update an item in an array:**
|
||||
```bash
|
||||
yq '(.[] | select(.name == "foo") | .address) = "12 cat st"'
|
||||
# Note: requires input file - add your file at the end
|
||||
yq -i '(.[] | select(.name == "foo") | .address) = "12 cat st"' data.yaml
|
||||
```
|
||||
|
||||
Convert JSON to YAML
|
||||
**Convert between formats:**
|
||||
```bash
|
||||
# Convert JSON to YAML (pretty print)
|
||||
yq -Poy sample.json
|
||||
|
||||
# Convert YAML to JSON
|
||||
yq -o json file.yaml
|
||||
|
||||
# Convert XML to YAML
|
||||
yq -o yaml file.xml
|
||||
```
|
||||
|
||||
See [recipes](https://mikefarah.gitbook.io/yq/recipes) for more examples and the [documentation](https://mikefarah.gitbook.io/yq/) for more information.
|
||||
@ -68,31 +78,31 @@ Take a look at the discussions for [common questions](https://github.com/mikefar
|
||||
### [Download the latest binary](https://github.com/mikefarah/yq/releases/latest)
|
||||
|
||||
### wget
|
||||
Use wget to download, gzipped pre-compiled binaries:
|
||||
Use wget to download pre-compiled binaries. Choose your platform and architecture:
|
||||
|
||||
|
||||
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
|
||||
|
||||
#### Compressed via tar.gz
|
||||
**For Linux (example):**
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
|
||||
tar xz && mv ${BINARY} /usr/local/bin/yq
|
||||
```
|
||||
# Set your platform variables (adjust as needed)
|
||||
VERSION=v4.2.0
|
||||
PLATFORM=linux_amd64
|
||||
|
||||
#### Plain binary
|
||||
# Download compressed binary
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM}.tar.gz -O - |\
|
||||
tar xz && sudo mv yq_${PLATFORM} /usr/local/bin/yq
|
||||
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/local/bin/yq &&\
|
||||
# Or download plain binary
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM} -O /usr/local/bin/yq &&\
|
||||
chmod +x /usr/local/bin/yq
|
||||
```
|
||||
|
||||
#### Latest version
|
||||
|
||||
**Latest version (Linux AMD64):**
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq &&\
|
||||
chmod +x /usr/local/bin/yq
|
||||
```
|
||||
|
||||
**Available platforms:** `linux_amd64`, `linux_arm64`, `linux_arm`, `linux_386`, `darwin_amd64`, `darwin_arm64`, `windows_amd64`, `windows_386`, etc.
|
||||
|
||||
### MacOS / Linux via Homebrew:
|
||||
Using [Homebrew](https://brew.sh/)
|
||||
```
|
||||
@ -123,28 +133,31 @@ rm /etc/myfile.tmp
|
||||
```
|
||||
|
||||
### Run with Docker or Podman
|
||||
#### Oneshot use:
|
||||
|
||||
#### One-time use:
|
||||
```bash
|
||||
docker run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
|
||||
# Docker - process files in current directory
|
||||
docker run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
|
||||
# Podman - same usage as Docker
|
||||
podman run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
Note that you can run `yq` in docker without network access and other privileges if you desire,
|
||||
namely `--security-opt=no-new-privileges --cap-drop all --network none`.
|
||||
|
||||
**Security note:** You can run `yq` in Docker with restricted privileges:
|
||||
```bash
|
||||
podman run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
|
||||
docker run --rm --security-opt=no-new-privileges --cap-drop all --network none \
|
||||
-v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
#### Pipe in via STDIN:
|
||||
#### Pipe data via STDIN:
|
||||
|
||||
You'll need to pass the `-i\--interactive` flag to docker:
|
||||
You'll need to pass the `-i --interactive` flag to Docker/Podman:
|
||||
|
||||
```bash
|
||||
# Process piped data
|
||||
docker run -i --rm mikefarah/yq '.this.thing' < myfile.yml
|
||||
```
|
||||
|
||||
```bash
|
||||
# Same with Podman
|
||||
podman run -i --rm mikefarah/yq '.this.thing' < myfile.yml
|
||||
```
|
||||
|
||||
@ -340,7 +353,7 @@ gah install yq
|
||||
- Supports yaml [front matter](https://mikefarah.gitbook.io/yq/usage/front-matter) blocks (e.g. jekyll/assemble)
|
||||
- Colorized yaml output
|
||||
- [Date/Time manipulation and formatting with TZ](https://mikefarah.gitbook.io/yq/operators/datetime)
|
||||
- [Deeply data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
|
||||
- [Deep data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
|
||||
- [Sort keys](https://mikefarah.gitbook.io/yq/operators/sort-keys)
|
||||
- Manipulate yaml [comments](https://mikefarah.gitbook.io/yq/operators/comment-operators), [styling](https://mikefarah.gitbook.io/yq/operators/style), [tags](https://mikefarah.gitbook.io/yq/operators/tag) and [anchors and aliases](https://mikefarah.gitbook.io/yq/operators/anchor-and-alias-operators).
|
||||
- [Update in place](https://mikefarah.gitbook.io/yq/v/v4.x/commands/evaluate#flags)
|
||||
@ -350,6 +363,8 @@ gah install yq
|
||||
- [Load content from other files](https://mikefarah.gitbook.io/yq/operators/load)
|
||||
- [Convert to/from json/ndjson](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
|
||||
- [Convert to/from xml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/xml)
|
||||
- [Convert to/from hcl (terraform)](https://mikefarah.gitbook.io/yq/v/v4.x/usage/hcl)
|
||||
- [Convert to/from toml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/toml)
|
||||
- [Convert to/from properties](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties)
|
||||
- [Convert to/from csv/tsv](https://mikefarah.gitbook.io/yq/usage/csv-tsv)
|
||||
- [General shell completion scripts (bash/zsh/fish/powershell)](https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion)
|
||||
@ -367,10 +382,18 @@ Usage:
|
||||
|
||||
Examples:
|
||||
|
||||
# yq defaults to 'eval' command if no command is specified. See "yq eval --help" for more examples.
|
||||
yq '.stuff' < myfile.yml # outputs the data at the "stuff" node from "myfile.yml"
|
||||
# yq tries to auto-detect the file format based off the extension, and defaults to YAML if it's unknown (or piping through STDIN)
|
||||
# Use the '-p/--input-format' flag to specify a format type.
|
||||
cat file.xml | yq -p xml
|
||||
|
||||
yq -i '.stuff = "foo"' myfile.yml # update myfile.yml in place
|
||||
# read the "stuff" node from "myfile.yml"
|
||||
yq '.stuff' < myfile.yml
|
||||
|
||||
# update myfile.yml in place
|
||||
yq -i '.stuff = "foo"' myfile.yml
|
||||
|
||||
# print contents of sample.json as idiomatic YAML
|
||||
yq -P -oy sample.json
|
||||
|
||||
|
||||
Available Commands:
|
||||
@ -380,49 +403,75 @@ Available Commands:
|
||||
help Help about any command
|
||||
|
||||
Flags:
|
||||
-C, --colors force print with colors
|
||||
--csv-auto-parse parse CSV YAML/JSON values (default true)
|
||||
--csv-separator char CSV Separator character (default ,)
|
||||
-e, --exit-status set exit status if there are no matches or null or false is returned
|
||||
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
|
||||
--from-file string Load expression from specified file.
|
||||
-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 separators before processing expression. (default true)
|
||||
-h, --help help for yq
|
||||
-I, --indent int sets indent level for output (default 2)
|
||||
-i, --inplace update the file in place of first file given.
|
||||
-p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l|ini|i] parse format for input. (default "auto")
|
||||
--lua-globals output keys as top-level global variables
|
||||
--lua-prefix string prefix (default "return ")
|
||||
--lua-suffix string suffix (default ";\n")
|
||||
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
|
||||
-M, --no-colors force print with no colors
|
||||
-N, --no-doc Don't print document separators (---)
|
||||
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
|
||||
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
|
||||
-o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l|ini|i] output format type. (default "auto")
|
||||
-P, --prettyPrint pretty print, shorthand for '... style = ""'
|
||||
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
|
||||
--properties-separator string separator to use between keys and values (default " = ")
|
||||
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
|
||||
--split-exp-file string Use a file to specify the split-exp expression.
|
||||
--string-interpolation Toggles strings interpolation of \(exp) (default true)
|
||||
--tsv-auto-parse parse TSV YAML/JSON values (default true)
|
||||
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
|
||||
-v, --verbose verbose mode
|
||||
-V, --version Print version information and quit
|
||||
--xml-attribute-prefix string prefix for xml attributes (default "+@")
|
||||
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
|
||||
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
|
||||
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
|
||||
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
|
||||
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
|
||||
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
|
||||
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
|
||||
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
|
||||
-C, --colors force print with colors
|
||||
--csv-auto-parse parse CSV YAML/JSON values (default true)
|
||||
--csv-separator char CSV Separator character (default ,)
|
||||
--debug-node-info debug node info
|
||||
-e, --exit-status set exit status if there are no matches or null or false is returned
|
||||
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
|
||||
--from-file string Load expression from specified file.
|
||||
-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 separators before processing expression. (default true)
|
||||
-h, --help help for yq
|
||||
-I, --indent int sets indent level for output (default 2)
|
||||
-i, --inplace update the file in place of first file given.
|
||||
-p, --input-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|lua|l|ini|i] parse format for input. (default "auto")
|
||||
--lua-globals output keys as top-level global variables
|
||||
--lua-prefix string prefix (default "return ")
|
||||
--lua-suffix string suffix (default ";\n")
|
||||
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
|
||||
-M, --no-colors force print with no colors
|
||||
-N, --no-doc Don't print document separators (---)
|
||||
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
|
||||
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
|
||||
-o, --output-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|shell|s|lua|l|ini|i] output format type. (default "auto")
|
||||
-P, --prettyPrint pretty print, shorthand for '... style = ""'
|
||||
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
|
||||
--properties-separator string separator to use between keys and values (default " = ")
|
||||
--security-disable-env-ops Disable env related operations.
|
||||
--security-disable-file-ops Disable file related operations (e.g. load)
|
||||
--shell-key-separator string separator for shell variable key paths (default "_")
|
||||
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
|
||||
--split-exp-file string Use a file to specify the split-exp expression.
|
||||
--string-interpolation Toggles strings interpolation of \(exp) (default true)
|
||||
--tsv-auto-parse parse TSV YAML/JSON values (default true)
|
||||
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
|
||||
-v, --verbose verbose mode
|
||||
-V, --version Print version information and quit
|
||||
--xml-attribute-prefix string prefix for xml attributes (default "+@")
|
||||
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
|
||||
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
|
||||
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
|
||||
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
|
||||
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
|
||||
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
|
||||
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
|
||||
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
|
||||
--yaml-fix-merge-anchor-to-spec Fix merge anchor to match YAML spec. Will default to true in late 2025
|
||||
|
||||
Use "yq [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**PowerShell quoting issues:**
|
||||
```powershell
|
||||
# Use single quotes for expressions
|
||||
yq '.a.b[0].c' file.yaml
|
||||
|
||||
# Or escape double quotes
|
||||
yq ".a.b[0].c = \"value\"" file.yaml
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Check existing issues**: [GitHub Issues](https://github.com/mikefarah/yq/issues)
|
||||
- **Ask questions**: [GitHub Discussions](https://github.com/mikefarah/yq/discussions)
|
||||
- **Documentation**: [Complete documentation](https://mikefarah.gitbook.io/yq/)
|
||||
- **Examples**: [Recipes and examples](https://mikefarah.gitbook.io/yq/recipes)
|
||||
|
||||
## 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 on quoting yq](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell)
|
||||
|
||||
26
SECURITY.md
Normal file
26
SECURITY.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **do not** report security vulnerabilities through public GitHub issues.
|
||||
|
||||
Instead, use GitHub's private vulnerability reporting feature:
|
||||
👉 https://github.com/mikefarah/yq/security
|
||||
|
||||
This allows vulnerabilities to be triaged and addressed confidentially before any public disclosure.
|
||||
|
||||
## Scope
|
||||
|
||||
### HTTP / TLS / Network vulnerabilities
|
||||
|
||||
yq is a command-line YAML/JSON/TOML processor that reads from files or standard input and writes to standard output. **yq does not include any HTTP or network libraries** and makes no network connections at runtime. CVEs related to HTTP, TLS, or networking are therefore **not applicable** to yq.
|
||||
|
||||
### Dependency version bumps
|
||||
|
||||
yq uses [Dependabot](https://docs.github.com/en/code-security/dependabot) to automatically raise pull requests for:
|
||||
|
||||
- Go module dependencies
|
||||
- Go toolchain version
|
||||
- Docker base images
|
||||
|
||||
Please **do not** raise pull requests or issues solely to bump dependency or Go versions — Dependabot handles this automatically and the maintainers merge those PRs regularly.
|
||||
20
_typos.toml
Normal file
20
_typos.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[files]
|
||||
extend-exclude = ["vendor", "bin"]
|
||||
|
||||
[default]
|
||||
locale = "en"
|
||||
extend-ignore-identifiers-re = [
|
||||
"NdJson",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
AttributeIDSupressMenu = "AttributeIDSupressMenu"
|
||||
|
||||
[default.extend-words]
|
||||
Teh = "Teh"
|
||||
teh = "teh"
|
||||
Supress = "Supress"
|
||||
HashiCorp = "HashiCorp"
|
||||
Hashi = "Hashi"
|
||||
fot = "fot"
|
||||
nd = "nd"
|
||||
@ -6,6 +6,7 @@ setUp() {
|
||||
rm test*.csv 2>/dev/null || true
|
||||
rm test*.tsv 2>/dev/null || true
|
||||
rm test*.xml 2>/dev/null || true
|
||||
rm test*.tf 2>/dev/null || true
|
||||
}
|
||||
|
||||
testInputProperties() {
|
||||
@ -153,6 +154,37 @@ EOM
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testInputKYaml() {
|
||||
cat >test.kyaml <<'EOL'
|
||||
# leading
|
||||
{
|
||||
a: 1, # a line
|
||||
# head b
|
||||
b: 2,
|
||||
c: [
|
||||
# head d
|
||||
"d", # d line
|
||||
],
|
||||
}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected <<'EOM'
|
||||
# leading
|
||||
a: 1 # a line
|
||||
# head b
|
||||
b: 2
|
||||
c:
|
||||
# head d
|
||||
- d # d line
|
||||
EOM
|
||||
|
||||
X=$(./yq e -p=kyaml -P test.kyaml)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(./yq ea -p=kyaml -P test.kyaml)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -255,4 +287,61 @@ EOM
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testInputTerraform() {
|
||||
cat >test.tf <<EOL
|
||||
resource "aws_s3_bucket" "example" {
|
||||
bucket = "my-bucket"
|
||||
tags = {
|
||||
Environment = "Dev"
|
||||
Project = "Test"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
resource "aws_s3_bucket" "example" {
|
||||
bucket = "my-bucket"
|
||||
tags = {
|
||||
Environment = "Dev"
|
||||
Project = "Test"
|
||||
}
|
||||
}
|
||||
EOM
|
||||
|
||||
X=$(./yq test.tf)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(./yq ea test.tf)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testInputTerraformGithubAction() {
|
||||
cat >test.tf <<EOL
|
||||
resource "aws_s3_bucket" "example" {
|
||||
bucket = "my-bucket"
|
||||
|
||||
tags = {
|
||||
Environment = "Dev"
|
||||
Project = "Test"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
resource "aws_s3_bucket" "example" {
|
||||
bucket = "my-bucket"
|
||||
tags = {
|
||||
Environment = "Dev"
|
||||
Project = "Test"
|
||||
}
|
||||
}
|
||||
EOM
|
||||
|
||||
X=$(cat /dev/null | ./yq test.tf)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(cat /dev/null | ./yq ea test.tf)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
source ./scripts/shunit2
|
||||
@ -280,6 +280,55 @@ EOM
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testOutputKYaml() {
|
||||
cat >test.yml <<'EOL'
|
||||
# leading
|
||||
a: 1 # a line
|
||||
# head b
|
||||
b: 2
|
||||
c:
|
||||
# head d
|
||||
- d # d line
|
||||
EOL
|
||||
|
||||
read -r -d '' expected <<'EOM'
|
||||
# leading
|
||||
{
|
||||
a: 1, # a line
|
||||
# head b
|
||||
b: 2,
|
||||
c: [
|
||||
# head d
|
||||
"d", # d line
|
||||
],
|
||||
}
|
||||
EOM
|
||||
|
||||
X=$(./yq e --output-format=kyaml test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(./yq ea --output-format=kyaml test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testOutputKYamlShort() {
|
||||
cat >test.yml <<EOL
|
||||
a: b
|
||||
EOL
|
||||
|
||||
read -r -d '' expected <<'EOM'
|
||||
{
|
||||
a: "b",
|
||||
}
|
||||
EOM
|
||||
|
||||
X=$(./yq e -o=ky test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(./yq ea -o=ky test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testOutputXmComplex() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: {c: ["cat", "dog"], +@f: meow}}
|
||||
|
||||
@ -12,6 +12,6 @@ outputs:
|
||||
description: "The complete result from the yq command being run"
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'docker://mikefarah/yq:4-githubaction'
|
||||
image: 'docker://mikefarah/yq:4-githubaction@sha256:e1b8c865f299ea6b02910a7ddf147d5d431244d4cc116f89c2148c9f53822906'
|
||||
args:
|
||||
- ${{ inputs.cmd }}
|
||||
|
||||
@ -60,7 +60,7 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
out := cmd.OutOrStdout()
|
||||
|
||||
if writeInplace {
|
||||
// only use colors if its forced
|
||||
// only use colours if its forced
|
||||
colorsEnabled = forceColor
|
||||
writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0])
|
||||
out, err = writeInPlaceHandler.CreateTempFile()
|
||||
@ -101,12 +101,15 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
}
|
||||
|
||||
if frontMatter != "" {
|
||||
originalFilename := args[0]
|
||||
frontMatterHandler := yqlib.NewFrontMatterHandler(args[0])
|
||||
err = frontMatterHandler.Split()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args[0] = frontMatterHandler.GetYamlFrontMatterFilename()
|
||||
yqlib.SetFilenameAlias(args[0], originalFilename)
|
||||
defer yqlib.ClearFilenameAliases()
|
||||
|
||||
if frontMatter == "process" {
|
||||
reader := frontMatterHandler.GetContentReader()
|
||||
|
||||
329
cmd/evaluate_all_command_test.go
Normal file
329
cmd/evaluate_all_command_test.go
Normal file
@ -0,0 +1,329 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateEvaluateAllCommand(t *testing.T) {
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("createEvaluateAllCommand returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if cmd.Use != "eval-all [expression] [yaml_file1]..." {
|
||||
t.Errorf("Expected Use to be 'eval-all [expression] [yaml_file1]...', got %q", cmd.Use)
|
||||
}
|
||||
|
||||
if cmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if cmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test aliases
|
||||
expectedAliases := []string{"ea"}
|
||||
if len(cmd.Aliases) != len(expectedAliases) {
|
||||
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAliases {
|
||||
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
|
||||
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NoArgs(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with no arguments and no null input
|
||||
nullInput = false
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateAll(cmd, []string{})
|
||||
|
||||
// Should not error, but should print usage
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with no args should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have printed usage information
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected usage information to be printed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NullInput(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with null input
|
||||
nullInput = true
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateAll(cmd, []string{})
|
||||
|
||||
// Should not error when using null input
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with null input should not error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithSingleFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with a single file
|
||||
err = evaluateAll(cmd, []string{yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with single file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have some output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with single file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithMultipleFiles(t *testing.T) {
|
||||
// Create temporary YAML files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
yamlFile1 := filepath.Join(tempDir, "test1.yaml")
|
||||
yamlContent1 := []byte("name: test1\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile1, yamlContent1, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file 1: %v", err)
|
||||
}
|
||||
|
||||
yamlFile2 := filepath.Join(tempDir, "test2.yaml")
|
||||
yamlContent2 := []byte("name: test2\nage: 30\n")
|
||||
err = os.WriteFile(yamlFile2, yamlContent2, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file 2: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with multiple files
|
||||
err = evaluateAll(cmd, []string{yamlFile1, yamlFile2})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with multiple files should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with multiple files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithExpression(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with expression
|
||||
err = evaluateAll(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with expression should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with expression")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WriteInPlace(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable write in place
|
||||
originalWriteInplace := writeInplace
|
||||
writeInplace = true
|
||||
defer func() { writeInplace = originalWriteInplace }()
|
||||
|
||||
// Test with write in place
|
||||
err = evaluateAll(cmd, []string{".name = \"updated\"", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with write in place should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated
|
||||
updatedContent, err := os.ReadFile(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
|
||||
// Should contain the updated content
|
||||
if !strings.Contains(string(updatedContent), "updated") {
|
||||
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_ExitStatus(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable exit status
|
||||
originalExitStatus := exitStatus
|
||||
exitStatus = true
|
||||
defer func() { exitStatus = originalExitStatus }()
|
||||
|
||||
// Test with expression that should find no matches
|
||||
err = evaluateAll(cmd, []string{".nonexistent", yamlFile})
|
||||
|
||||
// Should error when no matches found and exit status is enabled
|
||||
if err == nil {
|
||||
t.Error("Expected error when no matches found and exit status is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithMultipleDocuments(t *testing.T) {
|
||||
// Create a temporary YAML file with multiple documents
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("---\nname: doc1\nage: 25\n---\nname: doc2\nage: 30\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with multiple documents
|
||||
err = evaluateAll(cmd, []string{".", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with multiple documents should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with multiple documents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NulSepOutput(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable nul separator output
|
||||
originalNulSepOutput := nulSepOutput
|
||||
nulSepOutput = true
|
||||
defer func() { nulSepOutput = originalNulSepOutput }()
|
||||
|
||||
// Test with nul separator output
|
||||
err = evaluateAll(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with nul separator output should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with nul separator output")
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
}
|
||||
|
||||
if writeInplace {
|
||||
// only use colors if its forced
|
||||
// only use colours if its forced
|
||||
colorsEnabled = forceColor
|
||||
writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0])
|
||||
out, err = writeInPlaceHandler.CreateTempFile()
|
||||
@ -122,12 +122,15 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
|
||||
if frontMatter != "" {
|
||||
yqlib.GetLogger().Debug("using front matter handler")
|
||||
originalFilename := args[0]
|
||||
frontMatterHandler := yqlib.NewFrontMatterHandler(args[0])
|
||||
err = frontMatterHandler.Split()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args[0] = frontMatterHandler.GetYamlFrontMatterFilename()
|
||||
yqlib.SetFilenameAlias(args[0], originalFilename)
|
||||
defer yqlib.ClearFilenameAliases()
|
||||
|
||||
if frontMatter == "process" {
|
||||
reader := frontMatterHandler.GetContentReader()
|
||||
|
||||
277
cmd/evaluate_sequence_command_test.go
Normal file
277
cmd/evaluate_sequence_command_test.go
Normal file
@ -0,0 +1,277 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateEvaluateSequenceCommand(t *testing.T) {
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("createEvaluateSequenceCommand returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if cmd.Use != "eval [expression] [yaml_file1]..." {
|
||||
t.Errorf("Expected Use to be 'eval [expression] [yaml_file1]...', got %q", cmd.Use)
|
||||
}
|
||||
|
||||
if cmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if cmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test aliases
|
||||
expectedAliases := []string{"e"}
|
||||
if len(cmd.Aliases) != len(expectedAliases) {
|
||||
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAliases {
|
||||
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
|
||||
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessExpression(t *testing.T) {
|
||||
// Reset global variables
|
||||
originalPrettyPrint := prettyPrint
|
||||
defer func() { prettyPrint = originalPrettyPrint }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prettyPrint bool
|
||||
expression string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty expression without pretty print",
|
||||
prettyPrint: false,
|
||||
expression: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: "",
|
||||
expected: `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
{
|
||||
name: "simple expression without pretty print",
|
||||
prettyPrint: false,
|
||||
expression: ".a.b",
|
||||
expected: ".a.b",
|
||||
},
|
||||
{
|
||||
name: "simple expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: ".a.b",
|
||||
expected: `.a.b | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
{
|
||||
name: "complex expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: ".items[] | select(.active == true)",
|
||||
expected: `.items[] | select(.active == true) | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prettyPrint = tt.prettyPrint
|
||||
result := processExpression(tt.expression)
|
||||
if result != tt.expected {
|
||||
t.Errorf("processExpression(%q) = %q, want %q", tt.expression, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_NoArgs(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with no arguments and no null input
|
||||
nullInput = false
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateSequence(cmd, []string{})
|
||||
|
||||
// Should not error, but should print usage
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with no args should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have printed usage information
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected usage information to be printed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_NullInput(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with null input
|
||||
nullInput = true
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateSequence(cmd, []string{})
|
||||
|
||||
// Should not error when using null input
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with null input should not error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WithFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with a file
|
||||
err = evaluateSequence(cmd, []string{yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have some output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateSequence with file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WithExpressionAndFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with expression and file
|
||||
err = evaluateSequence(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with expression and file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateSequence with expression and file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WriteInPlace(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable write in place
|
||||
originalWriteInplace := writeInplace
|
||||
writeInplace = true
|
||||
defer func() { writeInplace = originalWriteInplace }()
|
||||
|
||||
// Test with write in place
|
||||
err = evaluateSequence(cmd, []string{".name = \"updated\"", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with write in place should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated
|
||||
updatedContent, err := os.ReadFile(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
|
||||
// Should contain the updated content
|
||||
if !strings.Contains(string(updatedContent), "updated") {
|
||||
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_ExitStatus(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable exit status
|
||||
originalExitStatus := exitStatus
|
||||
exitStatus = true
|
||||
defer func() { exitStatus = originalExitStatus }()
|
||||
|
||||
// Test with expression that should find no matches
|
||||
err = evaluateSequence(cmd, []string{".nonexistent", yamlFile})
|
||||
|
||||
// Should error when no matches found and exit status is enabled
|
||||
if err == nil {
|
||||
t.Error("Expected error when no matches found and exit status is enabled")
|
||||
}
|
||||
}
|
||||
39
cmd/root.go
39
cmd/root.go
@ -2,12 +2,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"github.com/spf13/cobra"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type runeValue rune
|
||||
@ -68,30 +68,21 @@ yq -P -oy sample.json
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
cmd.SetOut(cmd.OutOrStdout())
|
||||
level := logging.WARNING
|
||||
stringFormat := `[%{level}] %{color}%{time:15:04:05}%{color:reset} %{message}`
|
||||
|
||||
// when NO_COLOR environment variable presents and not an empty string the coloured output should be disabled;
|
||||
// refer to no-color.org
|
||||
forceNoColor = forceNoColor || os.Getenv("NO_COLOR") != ""
|
||||
|
||||
if verbose && forceNoColor {
|
||||
level = logging.DEBUG
|
||||
stringFormat = `[%{level:5.5s}] %{time:15:04:05} %{shortfile:-33s} %{shortfunc:-25s} %{message}`
|
||||
} else if verbose {
|
||||
level = logging.DEBUG
|
||||
stringFormat = `[%{level:5.5s}] %{color}%{time:15:04:05}%{color:bold} %{shortfile:-33s} %{shortfunc:-25s}%{color:reset} %{message}`
|
||||
} else if forceNoColor {
|
||||
stringFormat = `[%{level}] %{time:15:04:05} %{message}`
|
||||
level := slog.LevelWarn
|
||||
if verbose {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
var format = logging.MustStringFormatter(stringFormat)
|
||||
var backend = logging.AddModuleLevel(
|
||||
logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format))
|
||||
yqlib.GetLogger().SetLevel(level)
|
||||
opts := &slog.HandlerOptions{Level: level, AddSource: verbose}
|
||||
handler := slog.NewTextHandler(os.Stderr, opts)
|
||||
yqlib.GetLogger().SetSlogger(slog.New(handler))
|
||||
|
||||
backend.SetLevel(level, "")
|
||||
|
||||
logging.SetBackend(backend)
|
||||
yqlib.InitExpressionParser()
|
||||
|
||||
return nil
|
||||
@ -165,9 +156,16 @@ yq -P -oy sample.json
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables")
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "ini-preserve-quotes", yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "preserve surrounding quotes on INI values during round-trip")
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)")
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "shell-key-separator", yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "separator for shell variable key paths")
|
||||
if err = rootCmd.RegisterFlagCompletionFunc("shell-key-separator", cobra.NoFileCompletions); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.StringInterpolationEnabled, "string-interpolation", yqlib.StringInterpolationEnabled, "Toggles strings interpolation of \\(exp)")
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.")
|
||||
@ -179,7 +177,7 @@ yq -P -oy sample.json
|
||||
}
|
||||
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
|
||||
rootCmd.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the file in place of first file given.")
|
||||
rootCmd.PersistentFlags().VarP(unwrapScalarFlag, "unwrapScalar", "r", "unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml")
|
||||
rootCmd.PersistentFlags().VarP(unwrapScalarFlag, "unwrapScalar", "r", "unwrap scalar, print the value with no quotes, colours or comments. Defaults to true for yaml")
|
||||
rootCmd.PersistentFlags().Lookup("unwrapScalar").NoOptDefVal = "true"
|
||||
rootCmd.PersistentFlags().BoolVarP(&nulSepOutput, "nul-output", "0", false, "Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.")
|
||||
|
||||
@ -198,6 +196,7 @@ yq -P -oy sample.json
|
||||
}
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.CompactSequenceIndent, "yaml-compact-seq-indent", "c", false, "Use compact sequence indentation where '- ' is considered part of the indentation.")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.")
|
||||
if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil {
|
||||
@ -213,6 +212,10 @@ yq -P -oy sample.json
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableFileOps, "security-disable-file-ops", "", false, "Disable file related operations (e.g. load)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.EnableSystemOps, "security-enable-system-operator", "", false, "Enable system operator to allow execution of external commands.")
|
||||
|
||||
rootCmd.AddCommand(
|
||||
createEvaluateSequenceCommand(),
|
||||
createEvaluateAllCommand(),
|
||||
|
||||
265
cmd/root_test.go
Normal file
265
cmd/root_test.go
Normal file
@ -0,0 +1,265 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRuneVar(t *testing.T) {
|
||||
var r rune
|
||||
runeVar := newRuneVar(&r)
|
||||
|
||||
if runeVar == nil {
|
||||
t.Fatal("newRuneVar returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
runeVal rune
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple character",
|
||||
runeVal: 'a',
|
||||
expected: "a",
|
||||
},
|
||||
{
|
||||
name: "special character",
|
||||
runeVal: '\n',
|
||||
expected: "\n",
|
||||
},
|
||||
{
|
||||
name: "unicode character",
|
||||
runeVal: 'ñ',
|
||||
expected: "ñ",
|
||||
},
|
||||
{
|
||||
name: "zero rune",
|
||||
runeVal: 0,
|
||||
expected: string(rune(0)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
runeVal := runeValue(tt.runeVal)
|
||||
result := runeVal.String()
|
||||
if result != tt.expected {
|
||||
t.Errorf("runeValue.String() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Set(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected rune
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple character",
|
||||
input: "a",
|
||||
expected: 'a',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "newline escape",
|
||||
input: "\\n",
|
||||
expected: '\n',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "tab escape",
|
||||
input: "\\t",
|
||||
expected: '\t',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "carriage return escape",
|
||||
input: "\\r",
|
||||
expected: '\r',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "form feed escape",
|
||||
input: "\\f",
|
||||
expected: '\f',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "vertical tab escape",
|
||||
input: "\\v",
|
||||
expected: '\v',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "multiple characters",
|
||||
input: "ab",
|
||||
expected: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "special character",
|
||||
input: "ñ",
|
||||
expected: 'ñ',
|
||||
expectError: true, // This will fail because the Set function checks len(val) != 1
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
err := runeVal.Set(tt.input)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input %q, but got none", tt.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for input %q: %v", tt.input, err)
|
||||
}
|
||||
if r != tt.expected {
|
||||
t.Errorf("Expected rune %q (%d), got %q (%d)",
|
||||
string(tt.expected), tt.expected, string(r), r)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Set_ErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "empty string error",
|
||||
input: "",
|
||||
expectedError: "[] is not a valid character. Must be length 1 was 0",
|
||||
},
|
||||
{
|
||||
name: "multiple characters error",
|
||||
input: "abc",
|
||||
expectedError: "[abc] is not a valid character. Must be length 1 was 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
err := runeVal.Set(tt.input)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input %q, but got none", tt.input)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.expectedError) {
|
||||
t.Errorf("Expected error message to contain %q, got %q",
|
||||
tt.expectedError, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Type(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
result := runeVal.Type()
|
||||
expected := "char"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("runeValue.Type() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
rootCmd := New()
|
||||
|
||||
if rootCmd == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if rootCmd.Use != "yq" {
|
||||
t.Errorf("Expected Use to be 'yq', got %q", rootCmd.Use)
|
||||
}
|
||||
|
||||
if rootCmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if rootCmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test that the command has the expected subcommands
|
||||
expectedCommands := []string{"eval", "eval-all", "completion"}
|
||||
actualCommands := make([]string, 0, len(rootCmd.Commands()))
|
||||
|
||||
for _, cmd := range rootCmd.Commands() {
|
||||
actualCommands = append(actualCommands, cmd.Name())
|
||||
}
|
||||
|
||||
for _, expected := range expectedCommands {
|
||||
found := false
|
||||
for _, actual := range actualCommands {
|
||||
if actual == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected command %q not found in actual commands: %v",
|
||||
expected, actualCommands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_FlagCompletions(t *testing.T) {
|
||||
rootCmd := New()
|
||||
|
||||
// Test that flag completion functions are registered
|
||||
// This is a basic smoke test - we can't easily test the actual completion logic
|
||||
// without more complex setup
|
||||
flags := []string{
|
||||
"output-format",
|
||||
"input-format",
|
||||
"xml-attribute-prefix",
|
||||
"xml-content-name",
|
||||
"xml-proc-inst-prefix",
|
||||
"xml-directive-name",
|
||||
"lua-prefix",
|
||||
"lua-suffix",
|
||||
"properties-separator",
|
||||
"indent",
|
||||
"front-matter",
|
||||
"expression",
|
||||
"split-exp",
|
||||
}
|
||||
|
||||
for _, flagName := range flags {
|
||||
flag := rootCmd.PersistentFlags().Lookup(flagName)
|
||||
if flag == nil {
|
||||
t.Errorf("Expected flag %q to exist", flagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
cmd/utils.go
26
cmd/utils.go
@ -3,12 +3,12 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
func isAutomaticOutputFormat() bool {
|
||||
@ -104,8 +104,8 @@ func configureFormats(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
yqlib.GetLogger().Debug("Using input format %v", inputFormat)
|
||||
yqlib.GetLogger().Debug("Using output format %v", outputFormat)
|
||||
yqlib.GetLogger().Debugf("Using input format %v", inputFormat)
|
||||
yqlib.GetLogger().Debugf("Using output format %v", outputFormat)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -117,7 +117,7 @@ func configureInputFormat(inputFilename string) error {
|
||||
_, err := yqlib.FormatFromString(inputFormat)
|
||||
if err != nil {
|
||||
// unknown file type, default to yaml
|
||||
yqlib.GetLogger().Debug("Unknown file format extension '%v', defaulting to yaml", inputFormat)
|
||||
yqlib.GetLogger().Debugf("Unknown file format extension '%v', defaulting to yaml", inputFormat)
|
||||
inputFormat = "yaml"
|
||||
if isAutomaticOutputFormat() {
|
||||
outputFormat = "yaml"
|
||||
@ -132,7 +132,7 @@ func configureInputFormat(inputFilename string) error {
|
||||
//
|
||||
outputFormat = yqlib.FormatStringFromFilename(inputFilename)
|
||||
if inputFilename != "-" {
|
||||
yqlib.GetLogger().Warning("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat)
|
||||
yqlib.GetLogger().Warningf("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat)
|
||||
}
|
||||
outputFormat = "yaml"
|
||||
}
|
||||
@ -166,6 +166,9 @@ func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {
|
||||
}
|
||||
yqlib.ConfiguredYamlPreferences.EvaluateTogether = evaluateTogether
|
||||
|
||||
if format.DecoderFactory == nil {
|
||||
return nil, fmt.Errorf("no support for %s input format", inputFormat)
|
||||
}
|
||||
yqlibDecoder := format.DecoderFactory()
|
||||
if yqlibDecoder == nil {
|
||||
return nil, fmt.Errorf("no support for %s input format", inputFormat)
|
||||
@ -197,16 +200,23 @@ func configureEncoder() (yqlib.Encoder, error) {
|
||||
}
|
||||
yqlib.ConfiguredXMLPreferences.Indent = indent
|
||||
yqlib.ConfiguredYamlPreferences.Indent = indent
|
||||
yqlib.ConfiguredKYamlPreferences.Indent = indent
|
||||
yqlib.ConfiguredJSONPreferences.Indent = indent
|
||||
|
||||
yqlib.ConfiguredYamlPreferences.UnwrapScalar = unwrapScalar
|
||||
yqlib.ConfiguredKYamlPreferences.UnwrapScalar = unwrapScalar
|
||||
yqlib.ConfiguredPropertiesPreferences.UnwrapScalar = unwrapScalar
|
||||
yqlib.ConfiguredJSONPreferences.UnwrapScalar = unwrapScalar
|
||||
yqlib.ConfiguredShellVariablesPreferences.UnwrapScalar = unwrapScalar
|
||||
|
||||
yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled
|
||||
yqlib.ConfiguredKYamlPreferences.ColorsEnabled = colorsEnabled
|
||||
yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled
|
||||
yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled
|
||||
yqlib.ConfiguredTomlPreferences.ColorsEnabled = colorsEnabled
|
||||
|
||||
yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators
|
||||
yqlib.ConfiguredKYamlPreferences.PrintDocSeparators = !noDocSeparators
|
||||
|
||||
encoder := yqlibOutputFormat.EncoderFactory()
|
||||
|
||||
@ -225,7 +235,7 @@ func maybeFile(str string) bool {
|
||||
yqlib.GetLogger().Debugf("checking '%v' is a file", str)
|
||||
stat, err := os.Stat(str) // #nosec
|
||||
result := err == nil && !stat.IsDir()
|
||||
if yqlib.GetLogger().IsEnabledFor(logging.DEBUG) {
|
||||
if yqlib.GetLogger().IsEnabledFor(slog.LevelDebug) {
|
||||
if err != nil {
|
||||
yqlib.GetLogger().Debugf("error: %v", err)
|
||||
} else {
|
||||
@ -270,7 +280,7 @@ func processArgs(originalArgs []string) (string, []string, error) {
|
||||
|
||||
if expressionFile == "" && maybeFirstArgIsAFile && strings.HasSuffix(args[0], ".yq") {
|
||||
// lets check if an expression file was given
|
||||
yqlib.GetLogger().Debug("Assuming arg %v is an expression file", args[0])
|
||||
yqlib.GetLogger().Debugf("Assuming arg %v is an expression file", args[0])
|
||||
expressionFile = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
@ -286,7 +296,7 @@ func processArgs(originalArgs []string) (string, []string, error) {
|
||||
|
||||
yqlib.GetLogger().Debugf("processed args: %v", args)
|
||||
if expression == "" && len(args) > 0 && args[0] != "-" && !maybeFile(args[0]) {
|
||||
yqlib.GetLogger().Debug("assuming expression is '%v'", args[0])
|
||||
yqlib.GetLogger().Debugf("assuming expression is '%v'", args[0])
|
||||
expression = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
@ -911,7 +911,7 @@ func stringsEqual(a, b []string) bool {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
if a[i] != b[i] { //nolint:gosec // G602 false positive: b length equality is checked above
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -926,13 +926,13 @@ func TestSetupColors(t *testing.T) {
|
||||
expectColors bool
|
||||
}{
|
||||
{
|
||||
name: "force color enabled",
|
||||
name: "force colour enabled",
|
||||
forceColor: true,
|
||||
forceNoColor: false,
|
||||
expectColors: true,
|
||||
},
|
||||
{
|
||||
name: "force no color enabled",
|
||||
name: "force no colour enabled",
|
||||
forceColor: false,
|
||||
forceNoColor: true,
|
||||
expectColors: false,
|
||||
|
||||
@ -11,7 +11,7 @@ var (
|
||||
GitDescribe string
|
||||
|
||||
// Version is main version number that is being run at the moment.
|
||||
Version = "v4.48.1"
|
||||
Version = "v4.53.3"
|
||||
|
||||
// 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
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetVersionDisplay(t *testing.T) {
|
||||
var expectedVersion = ProductName + " (https://github.com/mikefarah/yq/) version " + Version
|
||||
@ -25,6 +28,18 @@ func TestGetVersionDisplay(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_getHumanVersion(t *testing.T) {
|
||||
// Save original values
|
||||
origGitDescribe := GitDescribe
|
||||
origGitCommit := GitCommit
|
||||
origVersionPrerelease := VersionPrerelease
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
GitDescribe = origGitDescribe
|
||||
GitCommit = origGitCommit
|
||||
VersionPrerelease = origVersionPrerelease
|
||||
}()
|
||||
|
||||
GitDescribe = "e42813d"
|
||||
GitCommit = "e42813d+CHANGES"
|
||||
var wanted string
|
||||
@ -49,3 +64,118 @@ func Test_getHumanVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getHumanVersion_NoGitDescribe(t *testing.T) {
|
||||
// Save original values
|
||||
origGitDescribe := GitDescribe
|
||||
origGitCommit := GitCommit
|
||||
origVersionPrerelease := VersionPrerelease
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
GitDescribe = origGitDescribe
|
||||
GitCommit = origGitCommit
|
||||
VersionPrerelease = origVersionPrerelease
|
||||
}()
|
||||
|
||||
GitDescribe = ""
|
||||
GitCommit = ""
|
||||
VersionPrerelease = ""
|
||||
|
||||
got := getHumanVersion()
|
||||
if got != Version {
|
||||
t.Errorf("getHumanVersion() = %v, want %v", got, Version)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getHumanVersion_WithPrerelease(t *testing.T) {
|
||||
// Save original values
|
||||
origGitDescribe := GitDescribe
|
||||
origGitCommit := GitCommit
|
||||
origVersionPrerelease := VersionPrerelease
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
GitDescribe = origGitDescribe
|
||||
GitCommit = origGitCommit
|
||||
VersionPrerelease = origVersionPrerelease
|
||||
}()
|
||||
|
||||
GitDescribe = ""
|
||||
GitCommit = "abc123"
|
||||
VersionPrerelease = "beta"
|
||||
|
||||
got := getHumanVersion()
|
||||
expected := Version + "-beta (abc123)"
|
||||
if got != expected {
|
||||
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getHumanVersion_PrereleaseInVersion(t *testing.T) {
|
||||
// Save original values
|
||||
origGitDescribe := GitDescribe
|
||||
origGitCommit := GitCommit
|
||||
origVersionPrerelease := VersionPrerelease
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
GitDescribe = origGitDescribe
|
||||
GitCommit = origGitCommit
|
||||
VersionPrerelease = origVersionPrerelease
|
||||
}()
|
||||
|
||||
GitDescribe = "v1.2.3-rc1"
|
||||
GitCommit = "xyz789"
|
||||
VersionPrerelease = "rc1"
|
||||
|
||||
got := getHumanVersion()
|
||||
// Should not duplicate "rc1" since it's already in GitDescribe
|
||||
expected := "v1.2.3-rc1 (xyz789)"
|
||||
if got != expected {
|
||||
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getHumanVersion_StripSingleQuotes(t *testing.T) {
|
||||
// Save original values
|
||||
origGitDescribe := GitDescribe
|
||||
origGitCommit := GitCommit
|
||||
origVersionPrerelease := VersionPrerelease
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
GitDescribe = origGitDescribe
|
||||
GitCommit = origGitCommit
|
||||
VersionPrerelease = origVersionPrerelease
|
||||
}()
|
||||
|
||||
GitDescribe = "'v1.2.3'"
|
||||
GitCommit = "'commit123'"
|
||||
VersionPrerelease = ""
|
||||
|
||||
got := getHumanVersion()
|
||||
// Should strip single quotes
|
||||
if strings.Contains(got, "'") {
|
||||
t.Errorf("getHumanVersion() = %v, should not contain single quotes", got)
|
||||
}
|
||||
expected := "v1.2.3"
|
||||
if got != expected {
|
||||
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductName(t *testing.T) {
|
||||
if ProductName != "yq" {
|
||||
t.Errorf("ProductName = %v, want yq", ProductName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionIsSet(t *testing.T) {
|
||||
if Version == "" {
|
||||
t.Error("Version should not be empty")
|
||||
}
|
||||
if !strings.HasPrefix(Version, "v") {
|
||||
t.Errorf("Version %v should start with 'v'", Version)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
---
|
||||
$schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json
|
||||
version: '0.2'
|
||||
language: en-GB
|
||||
dictionaryDefinitions:
|
||||
- name: project-words
|
||||
path: './project-words.txt'
|
||||
addWords: true
|
||||
dictionaries:
|
||||
- project-words
|
||||
ignorePaths:
|
||||
- 'vendor'
|
||||
- 'bin'
|
||||
- '/project-words.txt'
|
||||
@ -1,8 +1 @@
|
||||
# 001
|
||||
---
|
||||
abc: # 001
|
||||
- 1 # one
|
||||
- 2 # two
|
||||
|
||||
---
|
||||
def # 002
|
||||
a: apple
|
||||
10
examples/kyaml.kyaml
Normal file
10
examples/kyaml.kyaml
Normal file
@ -0,0 +1,10 @@
|
||||
# leading
|
||||
{
|
||||
a: 1, # a line
|
||||
# head b
|
||||
b: 2,
|
||||
c: [
|
||||
# head d
|
||||
"d", # d line
|
||||
],
|
||||
}
|
||||
7
examples/kyaml.yml
Normal file
7
examples/kyaml.yml
Normal file
@ -0,0 +1,7 @@
|
||||
# leading
|
||||
a: 1 # a line
|
||||
# head b
|
||||
b: 2
|
||||
c:
|
||||
# head d
|
||||
- d # d line
|
||||
8
examples/sample.hcl
Normal file
8
examples/sample.hcl
Normal file
@ -0,0 +1,8 @@
|
||||
# Arithmetic with literals and application-provided variables
|
||||
sum = 1 + addend
|
||||
|
||||
# String interpolation and templates
|
||||
message = "Hello, ${name}!"
|
||||
|
||||
# Application-provided functions
|
||||
shouty_message = upper(message)
|
||||
27
examples/sample.tf
Normal file
27
examples/sample.tf
Normal file
@ -0,0 +1,27 @@
|
||||
# main.tf
|
||||
|
||||
# Define required providers and minimum Terraform version
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.2"
|
||||
}
|
||||
|
||||
# Configure the AWS provider
|
||||
provider "aws" {
|
||||
region = var.aws_region
|
||||
}
|
||||
|
||||
# Define an S3 bucket resource
|
||||
resource "aws_s3_bucket" "example_bucket" {
|
||||
bucket = var.bucket_name
|
||||
|
||||
tags = {
|
||||
Environment = "Development"
|
||||
Project = "TerraformExample"
|
||||
}
|
||||
}
|
||||
8
examples/sample2.hcl
Normal file
8
examples/sample2.hcl
Normal file
@ -0,0 +1,8 @@
|
||||
# Arithmetic with literals and application-provided variables
|
||||
sum = 1 + addend
|
||||
|
||||
# String interpolation and templates
|
||||
message = "Hello, ${name}!"
|
||||
|
||||
# Application-provided functions
|
||||
shouty_message = upper(message)
|
||||
@ -1,4 +1,4 @@
|
||||
FROM mikefarah/yq:4
|
||||
FROM mikefarah/yq:4@sha256:11a1f0b604b13dbbdc662260d8db6f644b22d8553122a25c1b5b2e8713ca6977
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
34
go.mod
34
go.mod
@ -6,30 +6,36 @@ require (
|
||||
github.com/alecthomas/repr v0.5.2
|
||||
github.com/dimchansky/utfbom v1.1.1
|
||||
github.com/elliotchance/orderedmap v1.8.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/go-ini/ini v1.67.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pelletier/go-toml/v2 v2.4.2
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/text v0.30.0
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
github.com/zclconf/go-cty v1.18.1
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/text v0.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
)
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
go 1.25.0
|
||||
|
||||
70
go.sum
70
go.sum
@ -1,26 +1,37 @@
|
||||
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
|
||||
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
|
||||
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=
|
||||
github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@ -33,36 +44,45 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
|
||||
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
|
||||
github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
30
go_install_test.go
Normal file
30
go_install_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
//go:build goinstall
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/zip"
|
||||
)
|
||||
|
||||
// TestGoInstallCompatibility ensures the module can be zipped for go install.
|
||||
// This is an integration test that uses the same zip.CreateFromDir function
|
||||
// that go install uses internally. If this test fails, go install will fail.
|
||||
//
|
||||
// Built with the goinstall tag and run after the main test suite (see scripts/test.sh)
|
||||
// so it does not race with pkg/yqlib tests that rewrite doc/*.md during execution.
|
||||
//
|
||||
// See: https://github.com/mikefarah/yq/issues/2587
|
||||
func TestGoInstallCompatibility(t *testing.T) {
|
||||
mod := module.Version{
|
||||
Path: "github.com/mikefarah/yq/v4",
|
||||
Version: "v4.0.0", // the actual version doesn't matter for validation
|
||||
}
|
||||
|
||||
if err := zip.CreateFromDir(io.Discard, mod, "."); err != nil {
|
||||
t.Fatalf("Module cannot be zipped for go install: %v", err)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,63 @@
|
||||
# How it works
|
||||
# Expression Syntax: A Visual Guide
|
||||
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.
|
||||
|
||||
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).
|
||||
Let's break down the process step by step using a diagram. We'll start with a single YAML document, apply an expression, and observe how the context changes at each step.
|
||||
|
||||
Lets look at a couple of examples.
|
||||
Given a document like:
|
||||
|
||||
```yaml
|
||||
root:
|
||||
items:
|
||||
- name: apple
|
||||
type: fruit
|
||||
- name: carrot
|
||||
type: vegetable
|
||||
- name: banana
|
||||
type: fruit
|
||||
```
|
||||
|
||||
You can use dot notation to access nested structures. For example, to access the `name` of the first item, you would use the expression `.root.items[0].name`, which would return `apple`.
|
||||
|
||||
But lets see how we could find all the fruit under `items`
|
||||
|
||||
## Step 1: Initial Context
|
||||
The context starts at the root of the YAML document. In this case, the entire document is the initial context.
|
||||
|
||||
```
|
||||
root
|
||||
└── items
|
||||
├── name: apple
|
||||
│ type: fruit
|
||||
├── name: carrot
|
||||
│ type: vegetable
|
||||
└── name: banana
|
||||
type: fruit
|
||||
```
|
||||
|
||||
## Step 2: Splatting the Array
|
||||
Using the expression `.root.items[]`, we "splat" the items array. This means each element of the array becomes its own node in the context:
|
||||
|
||||
```
|
||||
Node 1: { name: apple, type: fruit }
|
||||
Node 2: { name: carrot, type: vegetable }
|
||||
Node 3: { name: banana, type: fruit }
|
||||
```
|
||||
|
||||
## Step 3: Filtering the Nodes
|
||||
Next, we apply a filter to select only the nodes where type is fruit. The expression `.root.items[] | select(.type == "fruit")` filters the nodes:
|
||||
|
||||
```
|
||||
Filtered Node 1: { name: apple, type: fruit }
|
||||
Filtered Node 2: { name: banana, type: fruit }
|
||||
```
|
||||
|
||||
## Step 4: Extracting a Field
|
||||
Finally, we extract the name field from the filtered nodes using `.root.items[] | select(.type == "fruit") | .name` This results in:
|
||||
|
||||
```
|
||||
apple
|
||||
banana
|
||||
```
|
||||
|
||||
## Simple assignment example
|
||||
|
||||
@ -44,7 +99,6 @@ a: dog
|
||||
b: dog
|
||||
```
|
||||
|
||||
|
||||
## Complex assignment, operator precedence rules
|
||||
|
||||
Just like math expressions - `yq` expressions have an order of precedence. The pipe `|` operator has a low order of precedence, so operators with higher precedence will get evaluated first.
|
||||
@ -73,7 +127,7 @@ name: sally
|
||||
fruit: mango
|
||||
```
|
||||
|
||||
To properly update this yaml, you will need to use brackets (think BODMAS from maths) and wrap the entire LHS:
|
||||
**Important**: To properly update this YAML, you must wrap the entire LHS in parentheses. Think of it like using brackets in math to ensure the correct order of operations.
|
||||
`(.[] | select(.name == "sally") | .fruit) = "mango"`
|
||||
|
||||
|
||||
|
||||
@ -54,3 +54,25 @@ func TestAllAtOnceEvaluateNodes(t *testing.T) {
|
||||
test.AssertResultComplex(t, tt.expected, resultsToString(t, list))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTomlDecoderCanBeReinitializedAcrossDocuments(t *testing.T) {
|
||||
decoder := NewTomlDecoder()
|
||||
|
||||
firstDocuments, err := ReadDocuments(strings.NewReader("id = \"Foobar\"\n"), decoder)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read first TOML document: %v", err)
|
||||
}
|
||||
if firstDocuments.Len() != 1 {
|
||||
t.Fatalf("expected first document count to be 1, got %d", firstDocuments.Len())
|
||||
}
|
||||
test.AssertResult(t, "Foobar", firstDocuments.Front().Value.(*CandidateNode).Content[1].Value)
|
||||
|
||||
secondDocuments, err := ReadDocuments(strings.NewReader("id = \"Banana\"\n"), decoder)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read second TOML document: %v", err)
|
||||
}
|
||||
if secondDocuments.Len() != 1 {
|
||||
t.Fatalf("expected second document count to be 1, got %d", secondDocuments.Len())
|
||||
}
|
||||
test.AssertResult(t, "Banana", secondDocuments.Front().Value.(*CandidateNode).Content[1].Value)
|
||||
}
|
||||
|
||||
269
pkg/yqlib/base64_test.go
Normal file
269
pkg/yqlib/base64_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
//go:build !yq_nobase64
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
)
|
||||
|
||||
const base64EncodedSimple = "YSBzcGVjaWFsIHN0cmluZw=="
|
||||
const base64DecodedSimpleExtraSpaces = "\n " + base64EncodedSimple + " \n"
|
||||
const base64DecodedSimple = "a special string"
|
||||
|
||||
const base64EncodedUTF8 = "V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig=="
|
||||
const base64DecodedUTF8 = "Works with UTF-16 😊"
|
||||
|
||||
const base64EncodedYaml = "YTogYXBwbGUK"
|
||||
const base64DecodedYaml = "a: apple\n"
|
||||
|
||||
const base64EncodedEmpty = ""
|
||||
const base64DecodedEmpty = ""
|
||||
|
||||
const base64MissingPadding = "Y2F0cw"
|
||||
const base64DecodedMissingPadding = "cats"
|
||||
|
||||
const base64EncodedCats = "Y2F0cw=="
|
||||
const base64DecodedCats = "cats"
|
||||
|
||||
var base64Scenarios = []formatScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "empty decode",
|
||||
input: base64EncodedEmpty,
|
||||
expected: base64DecodedEmpty + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "simple decode",
|
||||
input: base64EncodedSimple,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Decode base64: simple",
|
||||
subdescription: "Decoded data is assumed to be a string.",
|
||||
input: base64EncodedSimple,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Decode base64: UTF-8",
|
||||
subdescription: "Base64 decoding supports UTF-8 encoded strings.",
|
||||
input: base64EncodedUTF8,
|
||||
expected: base64DecodedUTF8 + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode missing padding",
|
||||
input: base64MissingPadding,
|
||||
expected: base64DecodedMissingPadding + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
|
||||
description: "Decode with extra spaces",
|
||||
subdescription: "Extra leading/trailing whitespace is stripped",
|
||||
input: base64DecodedSimpleExtraSpaces,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode with padding",
|
||||
input: base64EncodedCats,
|
||||
expected: base64DecodedCats + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode yaml document",
|
||||
input: base64EncodedYaml,
|
||||
expected: base64DecodedYaml + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Encode base64: string",
|
||||
input: "\"" + base64DecodedSimple + "\"",
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "Encode base64: string from document",
|
||||
subdescription: "Extract a string field and encode it to base64.",
|
||||
input: "coolData: \"" + base64DecodedSimple + "\"",
|
||||
expression: ".coolData",
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode empty string",
|
||||
input: "\"\"",
|
||||
expected: "",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode UTF-8 string",
|
||||
input: "\"" + base64DecodedUTF8 + "\"",
|
||||
expected: base64EncodedUTF8,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode cats",
|
||||
input: "\"" + base64DecodedCats + "\"",
|
||||
expected: base64EncodedCats,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: simple",
|
||||
skipDoc: true,
|
||||
input: base64EncodedSimple,
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: UTF-8",
|
||||
skipDoc: true,
|
||||
input: base64EncodedUTF8,
|
||||
expected: base64EncodedUTF8,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: missing padding",
|
||||
skipDoc: true,
|
||||
input: base64MissingPadding,
|
||||
expected: base64EncodedCats,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: empty",
|
||||
skipDoc: true,
|
||||
input: base64EncodedEmpty,
|
||||
expected: base64EncodedEmpty,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Encode error: non-string",
|
||||
skipDoc: true,
|
||||
input: "123",
|
||||
expectedError: "cannot encode !!int as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "Encode error: array",
|
||||
skipDoc: true,
|
||||
input: "[1, 2, 3]",
|
||||
expectedError: "cannot encode !!seq as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "Encode error: map",
|
||||
skipDoc: true,
|
||||
input: "{b: c}",
|
||||
expectedError: "cannot encode !!map as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
}
|
||||
|
||||
func testBase64Scenario(t *testing.T, s formatScenario) {
|
||||
switch s.scenarioType {
|
||||
case "", "decode":
|
||||
yamlPrefs := ConfiguredYamlPreferences.Copy()
|
||||
yamlPrefs.Indent = 4
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(yamlPrefs)), s.description)
|
||||
case "encode":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder()), s.description)
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewBase64Encoder()), s.description)
|
||||
case "encode-error":
|
||||
result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
|
||||
} else {
|
||||
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
}
|
||||
|
||||
func documentBase64Scenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
||||
s := i.(formatScenario)
|
||||
|
||||
if s.skipDoc {
|
||||
return
|
||||
}
|
||||
switch s.scenarioType {
|
||||
case "", "decode":
|
||||
documentBase64DecodeScenario(w, s)
|
||||
case "encode":
|
||||
documentBase64EncodeScenario(w, s)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
}
|
||||
|
||||
func documentBase64DecodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||
|
||||
if s.subdescription != "" {
|
||||
writeOrPanic(w, s.subdescription)
|
||||
writeOrPanic(w, "\n\n")
|
||||
}
|
||||
|
||||
writeOrPanic(w, "Given a sample.txt file of:\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
expression := s.expression
|
||||
if expression == "" {
|
||||
expression = "."
|
||||
}
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=base64 -oy '%v' sample.txt\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func documentBase64EncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||
|
||||
if s.subdescription != "" {
|
||||
writeOrPanic(w, s.subdescription)
|
||||
writeOrPanic(w, "\n\n")
|
||||
}
|
||||
|
||||
writeOrPanic(w, "Given a sample.yml file of:\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
expression := s.expression
|
||||
if expression == "" {
|
||||
expression = "."
|
||||
}
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=base64 '%v' sample.yml\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())))
|
||||
}
|
||||
|
||||
func TestBase64Scenarios(t *testing.T) {
|
||||
for _, tt := range base64Scenarios {
|
||||
testBase64Scenario(t, tt)
|
||||
}
|
||||
genericScenarios := make([]interface{}, len(base64Scenarios))
|
||||
for i, s := range base64Scenarios {
|
||||
genericScenarios[i] = s
|
||||
}
|
||||
documentScenarios(t, "usage", "base64", genericScenarios, documentBase64Scenario)
|
||||
}
|
||||
@ -27,10 +27,30 @@ const (
|
||||
FlowStyle
|
||||
)
|
||||
|
||||
// EncodeHint controls how a mapping node is serialised by format-specific encoders
|
||||
// that distinguish between inline and block/section representations (e.g. TOML, HCL).
|
||||
type EncodeHint int
|
||||
|
||||
const (
|
||||
// EncodeHintDefault lets the encoder choose the representation (e.g. TOML block
|
||||
// mappings default to [section] headers).
|
||||
EncodeHintDefault EncodeHint = iota
|
||||
// EncodeHintSeparateBlock forces the node to be emitted as a separate block or
|
||||
// table-section header (used by TOML [section] and HCL block decoders).
|
||||
EncodeHintSeparateBlock
|
||||
// EncodeHintInline forces the node to be emitted as an inline / flow table
|
||||
// (used by TOML inline-table decoder and TOML encoder).
|
||||
EncodeHintInline
|
||||
)
|
||||
|
||||
func createStringScalarNode(stringValue string) *CandidateNode {
|
||||
var node = &CandidateNode{Kind: ScalarNode}
|
||||
node.Value = stringValue
|
||||
node.Tag = "!!str"
|
||||
if stringValue == "<<" {
|
||||
node.Tag = "!!merge"
|
||||
} else {
|
||||
node.Tag = "!!str"
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@ -97,6 +117,9 @@ type CandidateNode struct {
|
||||
// (e.g. top level cross document merge). This property does not propagate to child nodes.
|
||||
EvaluateTogether bool
|
||||
IsMapKey bool
|
||||
// EncodeHint controls how a mapping node is serialised by format-specific encoders
|
||||
// (e.g. TOML, HCL) that support both inline and block/section representations.
|
||||
EncodeHint EncodeHint
|
||||
}
|
||||
|
||||
func (n *CandidateNode) CreateChild() *CandidateNode {
|
||||
@ -277,7 +300,7 @@ func (n *CandidateNode) AddChild(rawChild *CandidateNode) {
|
||||
|
||||
func (n *CandidateNode) AddChildren(children []*CandidateNode) {
|
||||
if n.Kind == MappingNode {
|
||||
for i := 0; i < len(children); i += 2 {
|
||||
for i := 0; i < len(children)-1; i += 2 {
|
||||
key := children[i]
|
||||
value := children[i+1]
|
||||
n.AddKeyValueChild(key, value)
|
||||
@ -320,11 +343,11 @@ func (n *CandidateNode) guessTagFromCustomType() string {
|
||||
dataBucket, errorReading := parseSnippet(n.Value)
|
||||
|
||||
if errorReading != nil {
|
||||
log.Debug("guessTagFromCustomType: could not guess underlying tag type %v", errorReading)
|
||||
log.Debugf("guessTagFromCustomType: could not guess underlying tag type %v", errorReading)
|
||||
return n.Tag
|
||||
}
|
||||
guessedTag := dataBucket.Tag
|
||||
log.Info("im guessing the tag %v is a %v", n.Tag, guessedTag)
|
||||
log.Infof("im guessing the tag %v is a %v", n.Tag, guessedTag)
|
||||
return guessedTag
|
||||
}
|
||||
|
||||
@ -407,6 +430,8 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
|
||||
|
||||
EvaluateTogether: n.EvaluateTogether,
|
||||
IsMapKey: n.IsMapKey,
|
||||
|
||||
EncodeHint: n.EncodeHint,
|
||||
}
|
||||
|
||||
if cloneContent {
|
||||
@ -440,7 +465,7 @@ func (n *CandidateNode) UpdateFrom(other *CandidateNode, prefs assignPreferences
|
||||
}
|
||||
|
||||
func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignPreferences) {
|
||||
log.Debug("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other))
|
||||
log.Debugf("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other))
|
||||
if n.Kind != other.Kind {
|
||||
// clear out the contents when switching to a different type
|
||||
// e.g. map to array
|
||||
@ -460,6 +485,9 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
|
||||
n.Anchor = other.Anchor
|
||||
}
|
||||
|
||||
// Preserve EncodeHint for format-specific encoding hints
|
||||
n.EncodeHint = other.EncodeHint
|
||||
|
||||
// merge will pickup the style of the new thing
|
||||
// when autocreating nodes
|
||||
|
||||
|
||||
@ -36,13 +36,13 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
|
||||
switch commentMapComment.Position {
|
||||
case yaml.CommentHeadPosition:
|
||||
o.HeadComment = comment.String()
|
||||
log.Debug("its a head comment %v", comment.String())
|
||||
log.Debugf("its a head comment %v", comment.String())
|
||||
case yaml.CommentLinePosition:
|
||||
o.LineComment = comment.String()
|
||||
log.Debug("its a line comment %v", comment.String())
|
||||
log.Debugf("its a line comment %v", comment.String())
|
||||
case yaml.CommentFootPosition:
|
||||
o.FootComment = comment.String()
|
||||
log.Debug("its a foot comment %v", comment.String())
|
||||
log.Debugf("its a foot comment %v", comment.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,8 +93,8 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
|
||||
log.Debugf("folded Type %v", astLiteral.Start.Type)
|
||||
o.Style = FoldedStyle
|
||||
}
|
||||
log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Value)
|
||||
log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Type)
|
||||
log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.Value)
|
||||
log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.Type)
|
||||
// TODO: here I could put the original value with line breaks
|
||||
// to solve the multiline > problem
|
||||
o.Value = astLiteral.Value.Value
|
||||
@ -187,7 +187,7 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
|
||||
}
|
||||
|
||||
func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error {
|
||||
log.Debug("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key)
|
||||
log.Debugf("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key)
|
||||
|
||||
// AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling
|
||||
// particularly for the anchorMap
|
||||
@ -197,7 +197,7 @@ func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingVa
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value)
|
||||
log.Debugf("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value)
|
||||
if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
@ -120,7 +123,7 @@ func (o *CandidateNode) UnmarshalJSON(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("UnmarshalJSON - scalar is %v", scalar)
|
||||
log.Debugf("UnmarshalJSON - scalar is %v", scalar)
|
||||
|
||||
return o.setScalarFromJson(scalar)
|
||||
|
||||
@ -140,6 +143,12 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
|
||||
return buf.Bytes(), err
|
||||
case ScalarNode:
|
||||
log.Debugf("MarshalJSON ScalarNode")
|
||||
if o.guessTagFromCustomType() == "!!float" {
|
||||
if raw, ok := jsonFloatLiteral(o.Value); ok {
|
||||
buf.WriteString(raw)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
}
|
||||
value, err := o.GetValueRep()
|
||||
if err != nil {
|
||||
return buf.Bytes(), err
|
||||
@ -177,3 +186,85 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
}
|
||||
|
||||
// jsonFloatLiteral returns a JSON-shaped representation of a YAML !!float scalar
|
||||
// value, preserving the original textual form (e.g. "50.0" stays "50.0") whenever
|
||||
// possible. The second return value is false when the value cannot be safely
|
||||
// rendered as a JSON number (e.g. ".inf", ".nan", or anything that parses to a
|
||||
// non-finite float); callers should fall back to the normal encoding path in
|
||||
// that case, which preserves the existing behaviour for those inputs.
|
||||
func jsonFloatLiteral(raw string) (string, bool) {
|
||||
if raw == "" {
|
||||
return "", false
|
||||
}
|
||||
f, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if math.IsInf(f, 0) || math.IsNaN(f) {
|
||||
return "", false
|
||||
}
|
||||
if isJSONNumberLiteral(raw) {
|
||||
return raw, true
|
||||
}
|
||||
formatted := strconv.FormatFloat(f, 'f', -1, 64)
|
||||
if !strings.ContainsAny(formatted, ".eE") {
|
||||
formatted += ".0"
|
||||
}
|
||||
return formatted, true
|
||||
}
|
||||
|
||||
// isJSONNumberLiteral reports whether s is already a valid JSON number literal
|
||||
// representing a fractional value (i.e. contains a "." or an exponent), so it
|
||||
// can be emitted verbatim without round-tripping through a float64.
|
||||
func isJSONNumberLiteral(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
i := 0
|
||||
if s[i] == '-' {
|
||||
i++
|
||||
if i == len(s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// integer part: 0 or [1-9][0-9]*
|
||||
if s[i] == '0' {
|
||||
i++
|
||||
} else if s[i] >= '1' && s[i] <= '9' {
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
hasFraction := false
|
||||
if i < len(s) && s[i] == '.' {
|
||||
hasFraction = true
|
||||
i++
|
||||
if i == len(s) || s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
}
|
||||
hasExponent := false
|
||||
if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
|
||||
hasExponent = true
|
||||
i++
|
||||
if i < len(s) && (s[i] == '+' || s[i] == '-') {
|
||||
i++
|
||||
}
|
||||
if i == len(s) || s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
}
|
||||
if i != len(s) {
|
||||
return false
|
||||
}
|
||||
return hasFraction || hasExponent
|
||||
}
|
||||
@ -106,6 +106,31 @@ func TestCreateScalarNodeScenarios(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type createStringScalarNodeScenario struct {
|
||||
stringValue string
|
||||
expectedTag string
|
||||
}
|
||||
|
||||
var createStringScalarNodeScenarios = []createStringScalarNodeScenario{
|
||||
{
|
||||
stringValue: "yourKey",
|
||||
expectedTag: "!!str",
|
||||
},
|
||||
{
|
||||
stringValue: "<<",
|
||||
expectedTag: "!!merge",
|
||||
},
|
||||
}
|
||||
|
||||
func TestCreateStringScalarNodeScenarios(t *testing.T) {
|
||||
for _, tt := range createStringScalarNodeScenarios {
|
||||
actual := createStringScalarNode(tt.stringValue)
|
||||
test.AssertResultWithContext(t, tt.stringValue, actual.Value, fmt.Sprintf("Value for: %v", tt.stringValue))
|
||||
test.AssertResultWithContext(t, tt.expectedTag, actual.Tag, fmt.Sprintf("Tag for: %v", tt.stringValue))
|
||||
test.AssertResultWithContext(t, ScalarNode, actual.Kind, fmt.Sprintf("Kind for: %v", tt.stringValue))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetKeyForMapValue(t *testing.T) {
|
||||
key := createStringScalarNode("yourKey")
|
||||
n := CandidateNode{Key: key, Value: "meow", document: 3}
|
||||
@ -152,8 +177,6 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) {
|
||||
key := CandidateNode{Value: "cool", IsMapKey: true}
|
||||
node := CandidateNode{}
|
||||
|
||||
// if we use a key in a new node as a value, it should no longer be marked as a key
|
||||
|
||||
_, keyIsValueNow := node.AddKeyValueChild(&CandidateNode{Value: "newKey"}, &key)
|
||||
|
||||
test.AssertResult(t, keyIsValueNow.IsMapKey, false)
|
||||
@ -204,3 +227,193 @@ func TestConvertToNodeInfo(t *testing.T) {
|
||||
test.AssertResult(t, 2, childInfo.Line)
|
||||
test.AssertResult(t, 3, childInfo.Column)
|
||||
}
|
||||
|
||||
func TestCandidateNodeGetPath(t *testing.T) {
|
||||
// Test root node with no parent
|
||||
root := CandidateNode{Value: "root"}
|
||||
path := root.GetPath()
|
||||
test.AssertResult(t, 0, len(path))
|
||||
|
||||
// Test node with key
|
||||
key := createStringScalarNode("myKey")
|
||||
node := CandidateNode{Key: key, Value: "myValue"}
|
||||
path = node.GetPath()
|
||||
test.AssertResult(t, 1, len(path))
|
||||
test.AssertResult(t, "myKey", path[0])
|
||||
|
||||
// Test nested path
|
||||
parent := CandidateNode{}
|
||||
parentKey := createStringScalarNode("parent")
|
||||
parent.Key = parentKey
|
||||
node.Parent = &parent
|
||||
path = node.GetPath()
|
||||
test.AssertResult(t, 2, len(path))
|
||||
test.AssertResult(t, "parent", path[0])
|
||||
test.AssertResult(t, "myKey", path[1])
|
||||
}
|
||||
|
||||
func TestCandidateNodeGetNicePath(t *testing.T) {
|
||||
// Test simple key
|
||||
key := createStringScalarNode("simple")
|
||||
node := CandidateNode{Key: key}
|
||||
nicePath := node.GetNicePath()
|
||||
test.AssertResult(t, "simple", nicePath)
|
||||
|
||||
// Test array index
|
||||
arrayKey := createScalarNode(0, "0")
|
||||
arrayNode := CandidateNode{Key: arrayKey}
|
||||
nicePath = arrayNode.GetNicePath()
|
||||
test.AssertResult(t, "[0]", nicePath)
|
||||
|
||||
dotKey := createStringScalarNode("key.with.dots")
|
||||
dotNode := CandidateNode{Key: dotKey}
|
||||
nicePath = dotNode.GetNicePath()
|
||||
test.AssertResult(t, "key.with.dots", nicePath)
|
||||
|
||||
// Test nested path
|
||||
parentKey := createStringScalarNode("parent")
|
||||
parent := CandidateNode{Key: parentKey}
|
||||
childKey := createStringScalarNode("child")
|
||||
child := CandidateNode{Key: childKey, Parent: &parent}
|
||||
nicePath = child.GetNicePath()
|
||||
test.AssertResult(t, "parent.child", nicePath)
|
||||
}
|
||||
|
||||
func TestCandidateNodeFilterMapContentByKey(t *testing.T) {
|
||||
// Create a map with multiple key-value pairs
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
key3 := createStringScalarNode("key3")
|
||||
value3 := createStringScalarNode("value3")
|
||||
|
||||
mapNode := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2, key3, value3},
|
||||
}
|
||||
|
||||
// Filter by key predicate that matches key1 and key3
|
||||
filtered := mapNode.FilterMapContentByKey(func(key *CandidateNode) bool {
|
||||
return key.Value == "key1" || key.Value == "key3"
|
||||
})
|
||||
|
||||
// Should return key1, value1, key3, value3
|
||||
test.AssertResult(t, 4, len(filtered))
|
||||
test.AssertResult(t, "key1", filtered[0].Value)
|
||||
test.AssertResult(t, "value1", filtered[1].Value)
|
||||
test.AssertResult(t, "key3", filtered[2].Value)
|
||||
test.AssertResult(t, "value3", filtered[3].Value)
|
||||
}
|
||||
|
||||
func TestCandidateNodeVisitValues(t *testing.T) {
|
||||
// Test mapping node
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
mapNode := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2},
|
||||
}
|
||||
|
||||
var visited []string
|
||||
err := mapNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 2, len(visited))
|
||||
test.AssertResult(t, "value1", visited[0])
|
||||
test.AssertResult(t, "value2", visited[1])
|
||||
|
||||
// Test sequence node
|
||||
item1 := createStringScalarNode("item1")
|
||||
item2 := createStringScalarNode("item2")
|
||||
|
||||
seqNode := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{item1, item2},
|
||||
}
|
||||
|
||||
visited = []string{}
|
||||
err = seqNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 2, len(visited))
|
||||
test.AssertResult(t, "item1", visited[0])
|
||||
test.AssertResult(t, "item2", visited[1])
|
||||
|
||||
// Test scalar node (should not visit anything)
|
||||
scalarNode := &CandidateNode{
|
||||
Kind: ScalarNode,
|
||||
Value: "scalar",
|
||||
}
|
||||
|
||||
visited = []string{}
|
||||
err = scalarNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 0, len(visited))
|
||||
}
|
||||
|
||||
func TestCandidateNodeCanVisitValues(t *testing.T) {
|
||||
mapNode := &CandidateNode{Kind: MappingNode}
|
||||
seqNode := &CandidateNode{Kind: SequenceNode}
|
||||
scalarNode := &CandidateNode{Kind: ScalarNode}
|
||||
|
||||
test.AssertResult(t, true, mapNode.CanVisitValues())
|
||||
test.AssertResult(t, true, seqNode.CanVisitValues())
|
||||
test.AssertResult(t, false, scalarNode.CanVisitValues())
|
||||
}
|
||||
|
||||
func TestCandidateNodeAddChild(t *testing.T) {
|
||||
parent := &CandidateNode{Kind: SequenceNode}
|
||||
child := createStringScalarNode("child")
|
||||
|
||||
parent.AddChild(child)
|
||||
|
||||
test.AssertResult(t, 1, len(parent.Content))
|
||||
test.AssertResult(t, false, parent.Content[0].IsMapKey)
|
||||
test.AssertResult(t, "0", parent.Content[0].Key.Value)
|
||||
// Check that parent is set correctly
|
||||
if parent.Content[0].Parent != parent {
|
||||
t.Errorf("Expected parent to be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateNodeAddChildren(t *testing.T) {
|
||||
// Test sequence node
|
||||
parent := &CandidateNode{Kind: SequenceNode}
|
||||
child1 := createStringScalarNode("child1")
|
||||
child2 := createStringScalarNode("child2")
|
||||
|
||||
parent.AddChildren([]*CandidateNode{child1, child2})
|
||||
|
||||
test.AssertResult(t, 2, len(parent.Content))
|
||||
test.AssertResult(t, "child1", parent.Content[0].Value)
|
||||
test.AssertResult(t, "child2", parent.Content[1].Value)
|
||||
|
||||
// Test mapping node
|
||||
mapParent := &CandidateNode{Kind: MappingNode}
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
mapParent.AddChildren([]*CandidateNode{key1, value1, key2, value2})
|
||||
|
||||
test.AssertResult(t, 4, len(mapParent.Content))
|
||||
test.AssertResult(t, true, mapParent.Content[0].IsMapKey) // key1
|
||||
test.AssertResult(t, false, mapParent.Content[1].IsMapKey) // value1
|
||||
test.AssertResult(t, true, mapParent.Content[2].IsMapKey) // key2
|
||||
test.AssertResult(t, false, mapParent.Content[3].IsMapKey) // value2
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package yqlib
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func MapYamlStyle(original yaml.Style) Style {
|
||||
@ -55,13 +55,13 @@ func (o *CandidateNode) copyFromYamlNode(node *yaml.Node, anchorMap map[string]*
|
||||
|
||||
if o.Anchor != "" {
|
||||
anchorMap[o.Anchor] = o
|
||||
log.Debug("set anchor %v to %v", o.Anchor, NodeToString(o))
|
||||
log.Debugf("set anchor %v to %v", o.Anchor, NodeToString(o))
|
||||
}
|
||||
|
||||
// its a single alias
|
||||
if node.Alias != nil && node.Alias.Anchor != "" {
|
||||
o.Alias = anchorMap[node.Alias.Anchor]
|
||||
log.Debug("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor]))
|
||||
log.Debugf("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor]))
|
||||
}
|
||||
o.HeadComment = node.HeadComment
|
||||
o.LineComment = node.LineComment
|
||||
@ -106,7 +106,7 @@ func (o *CandidateNode) UnmarshalYAML(node *yaml.Node, anchorMap map[string]*Can
|
||||
log.Debugf("UnmarshalYAML %v", node.Tag)
|
||||
switch node.Kind {
|
||||
case yaml.AliasNode:
|
||||
log.Debug("UnmarshalYAML - alias from yaml: %v", o.Tag)
|
||||
log.Debugf("UnmarshalYAML - alias from yaml: %v", o.Tag)
|
||||
o.Kind = AliasNode
|
||||
o.copyFromYamlNode(node, anchorMap)
|
||||
return nil
|
||||
@ -176,15 +176,15 @@ func (o *CandidateNode) UnmarshalYAML(node *yaml.Node, anchorMap map[string]*Can
|
||||
}
|
||||
|
||||
func (o *CandidateNode) MarshalYAML() (*yaml.Node, error) {
|
||||
log.Debug("MarshalYAML to yaml: %v", o.Tag)
|
||||
log.Debugf("MarshalYAML to yaml: %v", o.Tag)
|
||||
switch o.Kind {
|
||||
case AliasNode:
|
||||
log.Debug("MarshalYAML - alias to yaml: %v", o.Tag)
|
||||
log.Debugf("MarshalYAML - alias to yaml: %v", o.Tag)
|
||||
target := &yaml.Node{Kind: yaml.AliasNode}
|
||||
o.copyToYamlNode(target)
|
||||
return target, nil
|
||||
case ScalarNode:
|
||||
log.Debug("MarshalYAML - scalar: %v", o.Value)
|
||||
log.Debugf("MarshalYAML - scalar: %v", o.Value)
|
||||
target := &yaml.Node{Kind: yaml.ScalarNode}
|
||||
o.copyToYamlNode(target)
|
||||
return target, nil
|
||||
|
||||
@ -18,7 +18,7 @@ func changeOwner(info fs.FileInfo, file *os.File) error {
|
||||
// this happens with snap confinement
|
||||
// not really a big issue as users can chown
|
||||
// the file themselves if required.
|
||||
log.Info("Skipping chown: %v", err)
|
||||
log.Infof("Skipping chown: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
139
pkg/yqlib/chown_linux_test.go
Normal file
139
pkg/yqlib/chown_linux_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
//go:build linux
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestChangeOwner(t *testing.T) {
|
||||
// Create a temporary file for testing
|
||||
tempDir := t.TempDir()
|
||||
testFile := filepath.Join(tempDir, "testfile.txt")
|
||||
|
||||
// Create a test file
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat test file: %v", err)
|
||||
}
|
||||
|
||||
// Create another temporary file to change ownership of
|
||||
tempFile, err := os.CreateTemp(tempDir, "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Test changeOwner function
|
||||
err = changeOwner(info, tempFile)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify that the function doesn't panic with valid input
|
||||
tempFile2, err := os.CreateTemp(tempDir, "chown_test2_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create second temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile2.Name())
|
||||
tempFile2.Close()
|
||||
|
||||
// Test with the second file
|
||||
err = changeOwner(info, tempFile2)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed on second file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeOwnerWithInvalidFileInfo(t *testing.T) {
|
||||
// Create a mock file info that doesn't have syscall.Stat_t
|
||||
mockInfo := &mockFileInfo{
|
||||
name: "mock",
|
||||
size: 0,
|
||||
mode: 0600,
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Test changeOwner with mock file info (should not panic)
|
||||
err = changeOwner(mockInfo, tempFile)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed with mock file info: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeOwnerWithNonExistentFile(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat temp file: %v", err)
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
os.Remove(tempFile.Name())
|
||||
|
||||
err = changeOwner(info, tempFile)
|
||||
// The function should not panic even if the file doesn't exist
|
||||
if err != nil {
|
||||
t.Logf("Expected error when changing owner of non-existent file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// mockFileInfo implements fs.FileInfo but doesn't have syscall.Stat_t
|
||||
type mockFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
func (m *mockFileInfo) Name() string { return m.name }
|
||||
func (m *mockFileInfo) Size() int64 { return m.size }
|
||||
func (m *mockFileInfo) Mode() os.FileMode { return m.mode }
|
||||
func (m *mockFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (m *mockFileInfo) IsDir() bool { return false }
|
||||
func (m *mockFileInfo) Sys() interface{} { return nil } // This will cause the type assertion to fail
|
||||
|
||||
func TestChangeOwnerWithSyscallStatT(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat temp file: %v", err)
|
||||
}
|
||||
|
||||
err = changeOwner(info, tempFile)
|
||||
if err != nil {
|
||||
t.Logf("changeOwner returned error (this might be expected in some environments): %v", err)
|
||||
}
|
||||
}
|
||||
153
pkg/yqlib/color_print_test.go
Normal file
153
pkg/yqlib/color_print_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
attr color.Attribute
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "reset color",
|
||||
attr: color.Reset,
|
||||
expected: "\x1b[0m",
|
||||
},
|
||||
{
|
||||
name: "red color",
|
||||
attr: color.FgRed,
|
||||
expected: "\x1b[31m",
|
||||
},
|
||||
{
|
||||
name: "green color",
|
||||
attr: color.FgGreen,
|
||||
expected: "\x1b[32m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := format(tt.attr)
|
||||
if result != tt.expected {
|
||||
t.Errorf("format(%d) = %q, want %q", tt.attr, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorizeAndPrint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlBytes []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple yaml",
|
||||
yamlBytes: []byte("name: test\nage: 25\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with strings",
|
||||
yamlBytes: []byte("name: \"hello world\"\nactive: true\ncount: 42\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with anchors and aliases",
|
||||
yamlBytes: []byte("default: &default\n name: test\nuser: *default\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with comments",
|
||||
yamlBytes: []byte("# This is a comment\nname: test\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty yaml",
|
||||
yamlBytes: []byte(""),
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := colorizeAndPrint(tt.yamlBytes, &buf)
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if !tt.expectErr && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check that output contains escape sequences (color codes)
|
||||
if !tt.expectErr && len(tt.yamlBytes) > 0 {
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "\x1b[") {
|
||||
t.Error("Expected output to contain color escape sequences")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorizeAndPrintWithDifferentYamlTypes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
yaml string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "boolean values",
|
||||
yaml: "active: true\ninactive: false\n",
|
||||
},
|
||||
{
|
||||
name: "numeric values",
|
||||
yaml: "integer: 42\nfloat: 3.14\nnegative: -10\n",
|
||||
},
|
||||
{
|
||||
name: "map keys",
|
||||
yaml: "user:\n name: john\n age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "string values",
|
||||
yaml: "message: \"hello world\"\ndescription: 'single quotes'\n",
|
||||
},
|
||||
{
|
||||
name: "mixed types",
|
||||
yaml: "config:\n debug: true\n port: 8080\n host: \"localhost\"\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := colorizeAndPrint([]byte(tc.yaml), &buf)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify output contains color codes
|
||||
if !tc.expectErr {
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "\x1b[") {
|
||||
t.Error("Expected output to contain color escape sequences")
|
||||
}
|
||||
// Should end with newline
|
||||
if !strings.HasSuffix(output, "\n") {
|
||||
t.Error("Expected output to end with newline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,9 +3,8 @@ package yqlib
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
@ -75,7 +74,7 @@ func (n *Context) ChildContext(results *list.List) Context {
|
||||
}
|
||||
|
||||
func (n *Context) ToString() string {
|
||||
if !log.IsEnabledFor(logging.DEBUG) {
|
||||
if !log.IsEnabledFor(slog.LevelDebug) {
|
||||
return ""
|
||||
}
|
||||
result := fmt.Sprintf("Context\nDontAutoCreate: %v\n", n.DontAutoCreate)
|
||||
|
||||
@ -2,11 +2,11 @@ package yqlib
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
func TestChildContext(t *testing.T) {
|
||||
@ -155,8 +155,8 @@ func TestToString(t *testing.T) {
|
||||
test.AssertResultComplex(t, "", result)
|
||||
|
||||
// Test with debug logging enabled
|
||||
logging.SetLevel(logging.DEBUG, "")
|
||||
defer logging.SetLevel(logging.INFO, "") // Reset to default
|
||||
GetLogger().SetLevel(slog.LevelDebug)
|
||||
defer GetLogger().SetLevel(slog.LevelWarn) // Reset to default
|
||||
|
||||
result2 := context.ToString()
|
||||
test.AssertResultComplex(t, true, len(result2) > 0)
|
||||
|
||||
@ -2,8 +2,7 @@ package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type DataTreeNavigator interface {
|
||||
@ -55,7 +54,7 @@ func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *Ex
|
||||
return context, nil
|
||||
}
|
||||
log.Debugf("Processing Op: %v", expressionNode.Operation.toString())
|
||||
if log.IsEnabledFor(logging.DEBUG) {
|
||||
if log.IsEnabledFor(slog.LevelDebug) {
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
log.Debug(NodeToString(el.Value.(*CandidateNode)))
|
||||
}
|
||||
|
||||
@ -312,7 +312,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
|
||||
Value: "value",
|
||||
}
|
||||
|
||||
// Try to assign to a path on a scalar (should fail)
|
||||
path := []interface{}{"key"}
|
||||
err := navigator.DeeplyAssign(context, path, assignNode)
|
||||
|
||||
@ -321,7 +320,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
|
||||
t.Logf("Actual error: %v", err)
|
||||
}
|
||||
|
||||
// This should fail because we can't assign to a scalar
|
||||
test.AssertResult(t, nil, err)
|
||||
}
|
||||
|
||||
|
||||
@ -9,28 +9,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type base64Padder struct {
|
||||
count int
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (c *base64Padder) pad(buf []byte) (int, error) {
|
||||
pad := strings.Repeat("=", (4 - c.count%4))
|
||||
n, err := strings.NewReader(pad).Read(buf)
|
||||
c.count += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *base64Padder) Read(buf []byte) (int, error) {
|
||||
n, err := c.Reader.Read(buf)
|
||||
c.count += n
|
||||
|
||||
if err == io.EOF && c.count%4 != 0 {
|
||||
return c.pad(buf)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type base64Decoder struct {
|
||||
reader io.Reader
|
||||
finished bool
|
||||
@ -43,7 +21,25 @@ func NewBase64Decoder() Decoder {
|
||||
}
|
||||
|
||||
func (dec *base64Decoder) Init(reader io.Reader) error {
|
||||
dec.reader = &base64Padder{Reader: reader}
|
||||
// Read all data from the reader and strip leading/trailing whitespace
|
||||
// This is necessary because base64 decoding needs to see the complete input
|
||||
// to handle padding correctly, and we need to strip whitespace before decoding.
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(reader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strip leading and trailing whitespace
|
||||
stripped := strings.TrimSpace(buf.String())
|
||||
|
||||
// Add padding if needed (base64 strings should be a multiple of 4 characters)
|
||||
padLen := len(stripped) % 4
|
||||
if padLen > 0 {
|
||||
stripped += strings.Repeat("=", 4-padLen)
|
||||
}
|
||||
|
||||
// Create a new reader from the stripped and padded data
|
||||
dec.reader = strings.NewReader(stripped)
|
||||
dec.readAnything = false
|
||||
dec.finished = false
|
||||
return nil
|
||||
|
||||
471
pkg/yqlib/decoder_hcl.go
Normal file
471
pkg/yqlib/decoder_hcl.go
Normal file
@ -0,0 +1,471 @@
|
||||
//go:build !yq_nohcl
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type hclDecoder struct {
|
||||
file *hcl.File
|
||||
fileBytes []byte
|
||||
readAnything bool
|
||||
documentIndex uint
|
||||
}
|
||||
|
||||
func NewHclDecoder() Decoder {
|
||||
return &hclDecoder{}
|
||||
}
|
||||
|
||||
// sortedAttributes returns attributes in declaration order by source position
|
||||
func sortedAttributes(attrs hclsyntax.Attributes) []*attributeWithName {
|
||||
var sorted []*attributeWithName
|
||||
for name, attr := range attrs {
|
||||
sorted = append(sorted, &attributeWithName{Name: name, Attr: attr})
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Attr.Range().Start.Byte < sorted[j].Attr.Range().Start.Byte
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
type attributeWithName struct {
|
||||
Name string
|
||||
Attr *hclsyntax.Attribute
|
||||
}
|
||||
|
||||
// extractLineComment extracts any inline comment after the given position
|
||||
func extractLineComment(src []byte, endPos int) string {
|
||||
// Look for # comment after the token
|
||||
for i := endPos; i < len(src); i++ {
|
||||
if src[i] == '#' {
|
||||
// Found comment, extract until end of line
|
||||
start := i
|
||||
for i < len(src) && src[i] != '\n' {
|
||||
i++
|
||||
}
|
||||
return strings.TrimSpace(string(src[start:i]))
|
||||
}
|
||||
if src[i] == '\n' {
|
||||
// Hit newline before comment
|
||||
break
|
||||
}
|
||||
// Skip whitespace and other characters
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractHeadComment extracts comments before a given start position
|
||||
func extractHeadComment(src []byte, startPos int) string {
|
||||
var comments []string
|
||||
|
||||
// Start just before the token and skip trailing whitespace
|
||||
i := startPos - 1
|
||||
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||
i--
|
||||
}
|
||||
|
||||
for i >= 0 {
|
||||
// Find line boundaries
|
||||
lineEnd := i
|
||||
for i >= 0 && src[i] != '\n' {
|
||||
i--
|
||||
}
|
||||
lineStart := i + 1
|
||||
|
||||
line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if trimmed == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
break
|
||||
}
|
||||
|
||||
comments = append([]string{trimmed}, comments...)
|
||||
|
||||
// Move to previous line (skip any whitespace/newlines)
|
||||
i = lineStart - 1
|
||||
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
if len(comments) > 0 {
|
||||
return strings.Join(comments, "\n")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dec *hclDecoder) Init(reader io.Reader) error {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, diags := hclsyntax.ParseConfig(data, "input.hcl", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags != nil && diags.HasErrors() {
|
||||
return fmt.Errorf("hcl parse error: %w", diags)
|
||||
}
|
||||
dec.file = file
|
||||
dec.fileBytes = data
|
||||
dec.readAnything = false
|
||||
dec.documentIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *hclDecoder) Decode() (*CandidateNode, error) {
|
||||
if dec.readAnything {
|
||||
return nil, io.EOF
|
||||
}
|
||||
dec.readAnything = true
|
||||
|
||||
if dec.file == nil {
|
||||
return nil, fmt.Errorf("no hcl file parsed")
|
||||
}
|
||||
|
||||
root := &CandidateNode{Kind: MappingNode}
|
||||
|
||||
// process attributes in declaration order
|
||||
body := dec.file.Body.(*hclsyntax.Body)
|
||||
firstAttr := true
|
||||
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
||||
keyNode := createStringScalarNode(attrWithName.Name)
|
||||
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
|
||||
|
||||
// Attach comments if any
|
||||
attrRange := attrWithName.Attr.Range()
|
||||
headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte)
|
||||
if firstAttr && headComment != "" {
|
||||
// For the first attribute, apply its head comment to the root
|
||||
root.HeadComment = headComment
|
||||
firstAttr = false
|
||||
} else if headComment != "" {
|
||||
keyNode.HeadComment = headComment
|
||||
}
|
||||
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {
|
||||
valNode.LineComment = lineComment
|
||||
}
|
||||
|
||||
root.AddKeyValueChild(keyNode, valNode)
|
||||
}
|
||||
|
||||
// process blocks
|
||||
// Count blocks by type at THIS level to detect multiple separate blocks
|
||||
blocksByType := make(map[string]int)
|
||||
for _, block := range body.Blocks {
|
||||
blocksByType[block.Type]++
|
||||
}
|
||||
|
||||
for _, block := range body.Blocks {
|
||||
addBlockToMapping(root, block, dec.fileBytes, blocksByType[block.Type] > 1)
|
||||
}
|
||||
|
||||
dec.documentIndex++
|
||||
root.document = dec.documentIndex - 1
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode {
|
||||
node := &CandidateNode{Kind: MappingNode}
|
||||
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
||||
key := createStringScalarNode(attrWithName.Name)
|
||||
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
|
||||
|
||||
// Attach comments if any
|
||||
attrRange := attrWithName.Attr.Range()
|
||||
if headComment := extractHeadComment(src, attrRange.Start.Byte); headComment != "" {
|
||||
key.HeadComment = headComment
|
||||
}
|
||||
if lineComment := extractLineComment(src, attrRange.End.Byte); lineComment != "" {
|
||||
val.LineComment = lineComment
|
||||
}
|
||||
|
||||
node.AddKeyValueChild(key, val)
|
||||
}
|
||||
|
||||
// Process nested blocks, counting blocks by type at THIS level
|
||||
// to detect which block types appear multiple times
|
||||
blocksByType := make(map[string]int)
|
||||
for _, block := range body.Blocks {
|
||||
blocksByType[block.Type]++
|
||||
}
|
||||
|
||||
for _, block := range body.Blocks {
|
||||
addBlockToMapping(node, block, src, blocksByType[block.Type] > 1)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// addBlockToMapping nests block type and labels into the parent mapping, merging children.
|
||||
// isMultipleBlocksOfType indicates if there are multiple blocks of this type at THIS level
|
||||
func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte, isMultipleBlocksOfType bool) {
|
||||
bodyNode := hclBodyToNode(block.Body, src)
|
||||
current := parent
|
||||
|
||||
// ensure block type mapping exists
|
||||
var typeNode *CandidateNode
|
||||
for i := 0; i < len(current.Content); i += 2 {
|
||||
if current.Content[i].Value == block.Type {
|
||||
typeNode = current.Content[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if typeNode == nil {
|
||||
_, typeNode = current.AddKeyValueChild(createStringScalarNode(block.Type), &CandidateNode{Kind: MappingNode})
|
||||
// Mark the type node if there are multiple blocks of this type at this level
|
||||
// This tells the encoder to emit them as separate blocks rather than consolidating them
|
||||
if isMultipleBlocksOfType {
|
||||
typeNode.EncodeHint = EncodeHintSeparateBlock
|
||||
}
|
||||
}
|
||||
current = typeNode
|
||||
|
||||
// walk labels, creating/merging mappings
|
||||
for _, label := range block.Labels {
|
||||
var next *CandidateNode
|
||||
for i := 0; i < len(current.Content); i += 2 {
|
||||
if current.Content[i].Value == label {
|
||||
next = current.Content[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if next == nil {
|
||||
_, next = current.AddKeyValueChild(createStringScalarNode(label), &CandidateNode{Kind: MappingNode})
|
||||
}
|
||||
current = next
|
||||
}
|
||||
|
||||
// merge body attributes/blocks into the final mapping
|
||||
for i := 0; i < len(bodyNode.Content); i += 2 {
|
||||
current.AddKeyValueChild(bodyNode.Content[i], bodyNode.Content[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode {
|
||||
// handle literal values directly
|
||||
switch e := expr.(type) {
|
||||
case *hclsyntax.LiteralValueExpr:
|
||||
v := e.Val
|
||||
if v.IsNull() {
|
||||
return createScalarNode(nil, "")
|
||||
}
|
||||
switch {
|
||||
case v.Type().Equals(cty.String):
|
||||
// prefer to extract exact source (to avoid extra quoting) when available
|
||||
// Prefer the actual cty string value
|
||||
s := v.AsString()
|
||||
node := createScalarNode(s, s)
|
||||
// Don't set style for regular quoted strings - let YAML handle naturally
|
||||
return node
|
||||
case v.Type().Equals(cty.Bool):
|
||||
b := v.True()
|
||||
return createScalarNode(b, strconv.FormatBool(b))
|
||||
case v.Type() == cty.Number:
|
||||
// prefer integers when the numeric value is integral
|
||||
bf := v.AsBigFloat()
|
||||
if bf == nil {
|
||||
// fallback to string
|
||||
return createStringScalarNode(v.GoString())
|
||||
}
|
||||
// check if bf represents an exact integer
|
||||
if intVal, acc := bf.Int(nil); acc == big.Exact {
|
||||
s := intVal.String()
|
||||
return createScalarNode(intVal.Int64(), s)
|
||||
}
|
||||
s := bf.Text('g', -1)
|
||||
return createScalarNode(0.0, s)
|
||||
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
|
||||
seq := &CandidateNode{Kind: SequenceNode}
|
||||
it := v.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
// convert cty.Value to a node by wrapping in literal expr via string representation
|
||||
child := convertCtyValueToNode(val)
|
||||
seq.AddChild(child)
|
||||
}
|
||||
return seq
|
||||
case v.Type().IsMapType() || v.Type().IsObjectType():
|
||||
m := &CandidateNode{Kind: MappingNode}
|
||||
it := v.ElementIterator()
|
||||
for it.Next() {
|
||||
key, val := it.Element()
|
||||
keyStr := key.AsString()
|
||||
keyNode := createStringScalarNode(keyStr)
|
||||
valNode := convertCtyValueToNode(val)
|
||||
m.AddKeyValueChild(keyNode, valNode)
|
||||
}
|
||||
return m
|
||||
default:
|
||||
// fallback to string
|
||||
s := v.GoString()
|
||||
return createStringScalarNode(s)
|
||||
}
|
||||
case *hclsyntax.TupleConsExpr:
|
||||
// parse tuple/list into YAML sequence
|
||||
seq := &CandidateNode{Kind: SequenceNode}
|
||||
for _, exprVal := range e.Exprs {
|
||||
child := convertHclExprToNode(exprVal, src)
|
||||
seq.AddChild(child)
|
||||
}
|
||||
return seq
|
||||
case *hclsyntax.ObjectConsExpr:
|
||||
// parse object into YAML mapping
|
||||
m := &CandidateNode{Kind: MappingNode}
|
||||
m.Style = FlowStyle // Mark as inline object (flow style) for encoder
|
||||
for _, item := range e.Items {
|
||||
// evaluate key expression to get the key string
|
||||
keyVal, keyDiags := item.KeyExpr.Value(nil)
|
||||
if keyDiags != nil && keyDiags.HasErrors() {
|
||||
// fallback: try to extract key from source
|
||||
r := item.KeyExpr.Range()
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start >= 0 && end >= start && end <= len(src) {
|
||||
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start:end])))
|
||||
valNode := convertHclExprToNode(item.ValueExpr, src)
|
||||
m.AddKeyValueChild(keyNode, valNode)
|
||||
}
|
||||
continue
|
||||
}
|
||||
keyStr := keyVal.AsString()
|
||||
keyNode := createStringScalarNode(keyStr)
|
||||
valNode := convertHclExprToNode(item.ValueExpr, src)
|
||||
m.AddKeyValueChild(keyNode, valNode)
|
||||
}
|
||||
return m
|
||||
case *hclsyntax.TemplateExpr:
|
||||
// Reconstruct template string, preserving ${} syntax for interpolations
|
||||
var parts []string
|
||||
for _, p := range e.Parts {
|
||||
switch lp := p.(type) {
|
||||
case *hclsyntax.LiteralValueExpr:
|
||||
if lp.Val.Type().Equals(cty.String) {
|
||||
parts = append(parts, lp.Val.AsString())
|
||||
} else {
|
||||
parts = append(parts, lp.Val.GoString())
|
||||
}
|
||||
default:
|
||||
// Non-literal expression - reconstruct with ${} wrapper
|
||||
r := p.Range()
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start >= 0 && end >= start && end <= len(src) {
|
||||
exprText := string(src[start:end])
|
||||
parts = append(parts, "${"+exprText+"}")
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("${%v}", p))
|
||||
}
|
||||
}
|
||||
}
|
||||
combined := strings.Join(parts, "")
|
||||
node := createScalarNode(combined, combined)
|
||||
// Set DoubleQuotedStyle for all templates (which includes all quoted strings in HCL)
|
||||
// This ensures HCL roundtrips preserve quotes, and YAML properly quotes strings with ${}
|
||||
node.Style = DoubleQuotedStyle
|
||||
return node
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
// Simple identifier/traversal (e.g. unquoted string literal in HCL)
|
||||
r := e.Range()
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start >= 0 && end >= start && end <= len(src) {
|
||||
text := strings.TrimSpace(string(src[start:end]))
|
||||
return createStringScalarNode(text)
|
||||
}
|
||||
// Fallback to root name if source unavailable
|
||||
if len(e.Traversal) > 0 {
|
||||
if root, ok := e.Traversal[0].(hcl.TraverseRoot); ok {
|
||||
return createStringScalarNode(root.Name)
|
||||
}
|
||||
}
|
||||
return createStringScalarNode("")
|
||||
case *hclsyntax.FunctionCallExpr:
|
||||
// Preserve function calls as raw expressions for roundtrip
|
||||
r := e.Range()
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start >= 0 && end >= start && end <= len(src) {
|
||||
text := strings.TrimSpace(string(src[start:end]))
|
||||
node := createStringScalarNode(text)
|
||||
node.Style = 0
|
||||
return node
|
||||
}
|
||||
node := createStringScalarNode(e.Name)
|
||||
node.Style = 0
|
||||
return node
|
||||
default:
|
||||
// try to evaluate the expression (handles unary, binary ops, etc.)
|
||||
val, diags := expr.Value(nil)
|
||||
if diags == nil || !diags.HasErrors() {
|
||||
// successfully evaluated, convert cty.Value to node
|
||||
return convertCtyValueToNode(val)
|
||||
}
|
||||
// fallback: extract source text for the expression
|
||||
r := expr.Range()
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start >= 0 && end >= start && end <= len(src) {
|
||||
text := string(src[start:end])
|
||||
// Mark as unquoted expression so encoder emits without quoting
|
||||
node := createStringScalarNode(text)
|
||||
node.Style = 0
|
||||
return node
|
||||
}
|
||||
return createStringScalarNode(fmt.Sprintf("%v", expr))
|
||||
}
|
||||
}
|
||||
|
||||
func convertCtyValueToNode(v cty.Value) *CandidateNode {
|
||||
if v.IsNull() {
|
||||
return createScalarNode(nil, "")
|
||||
}
|
||||
switch {
|
||||
case v.Type().Equals(cty.String):
|
||||
return createScalarNode("", v.AsString())
|
||||
case v.Type().Equals(cty.Bool):
|
||||
b := v.True()
|
||||
return createScalarNode(b, strconv.FormatBool(b))
|
||||
case v.Type() == cty.Number:
|
||||
bf := v.AsBigFloat()
|
||||
if bf == nil {
|
||||
return createStringScalarNode(v.GoString())
|
||||
}
|
||||
if intVal, acc := bf.Int(nil); acc == big.Exact {
|
||||
s := intVal.String()
|
||||
return createScalarNode(intVal.Int64(), s)
|
||||
}
|
||||
s := bf.Text('g', -1)
|
||||
return createScalarNode(0.0, s)
|
||||
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
|
||||
seq := &CandidateNode{Kind: SequenceNode}
|
||||
it := v.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
seq.AddChild(convertCtyValueToNode(val))
|
||||
}
|
||||
return seq
|
||||
case v.Type().IsMapType() || v.Type().IsObjectType():
|
||||
m := &CandidateNode{Kind: MappingNode}
|
||||
it := v.ElementIterator()
|
||||
for it.Next() {
|
||||
key, val := it.Element()
|
||||
keyNode := createStringScalarNode(key.AsString())
|
||||
valNode := convertCtyValueToNode(val)
|
||||
m.AddKeyValueChild(keyNode, valNode)
|
||||
}
|
||||
return m
|
||||
default:
|
||||
return createStringScalarNode(v.GoString())
|
||||
}
|
||||
}
|
||||
@ -12,17 +12,20 @@ import (
|
||||
type iniDecoder struct {
|
||||
reader io.Reader
|
||||
finished bool // Flag to signal completion of processing
|
||||
prefs INIPreferences
|
||||
}
|
||||
|
||||
func NewINIDecoder() Decoder {
|
||||
func NewINIDecoder(prefs INIPreferences) Decoder {
|
||||
return &iniDecoder{
|
||||
finished: false, // Initialize the flag as false
|
||||
finished: false, // Initialise the flag as false
|
||||
prefs: prefs,
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *iniDecoder) Init(reader io.Reader) error {
|
||||
// Store the reader for use in Decode
|
||||
dec.reader = reader
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -39,7 +42,10 @@ func (dec *iniDecoder) Decode() (*CandidateNode, error) {
|
||||
}
|
||||
|
||||
// Parse the INI content
|
||||
cfg, err := ini.Load(content)
|
||||
loadOpts := ini.LoadOptions{
|
||||
PreserveSurroundedQuote: dec.prefs.PreserveSurroundedQuote,
|
||||
}
|
||||
cfg, err := ini.LoadSources(loadOpts, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse INI content: %w", err)
|
||||
}
|
||||
|
||||
@ -16,10 +16,11 @@ type propertiesDecoder struct {
|
||||
reader io.Reader
|
||||
finished bool
|
||||
d DataTreeNavigator
|
||||
prefs PropertiesPreferences
|
||||
}
|
||||
|
||||
func NewPropertiesDecoder() Decoder {
|
||||
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
|
||||
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false, prefs: ConfiguredPropertiesPreferences.Copy()}
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) Init(reader io.Reader) error {
|
||||
@ -28,20 +29,56 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePropKey(key string) []interface{} {
|
||||
func parsePropKey(key string, prefs PropertiesPreferences) []interface{} {
|
||||
pathStrArray := strings.Split(key, ".")
|
||||
path := make([]interface{}, len(pathStrArray))
|
||||
for i, pathStr := range pathStrArray {
|
||||
num, err := strconv.ParseInt(pathStr, 10, 32)
|
||||
if err == nil {
|
||||
path[i] = num
|
||||
} else {
|
||||
path[i] = pathStr
|
||||
}
|
||||
path := make([]interface{}, 0, len(pathStrArray))
|
||||
for _, pathStr := range pathStrArray {
|
||||
path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func appendPropKeySegment(path []interface{}, segment string, useArrayBrackets bool) []interface{} {
|
||||
if useArrayBrackets && strings.Contains(segment, "[") {
|
||||
bracketPath, ok := parsePropKeyArrayBracketSegment(segment)
|
||||
if ok {
|
||||
return append(path, bracketPath...)
|
||||
}
|
||||
}
|
||||
|
||||
num, err := strconv.ParseInt(segment, 10, 32)
|
||||
if err == nil {
|
||||
return append(path, num)
|
||||
}
|
||||
return append(path, segment)
|
||||
}
|
||||
|
||||
func parsePropKeyArrayBracketSegment(segment string) ([]interface{}, bool) {
|
||||
path := []interface{}{}
|
||||
bracketIndex := strings.Index(segment, "[")
|
||||
if bracketIndex > 0 {
|
||||
path = append(path, segment[:bracketIndex])
|
||||
}
|
||||
|
||||
remaining := segment[bracketIndex:]
|
||||
for remaining != "" {
|
||||
if !strings.HasPrefix(remaining, "[") {
|
||||
return nil, false
|
||||
}
|
||||
closingBracket := strings.Index(remaining, "]")
|
||||
if closingBracket < 0 {
|
||||
return nil, false
|
||||
}
|
||||
arrayIndex, err := strconv.ParseInt(remaining[1:closingBracket], 10, 32)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
path = append(path, arrayIndex)
|
||||
remaining = remaining[closingBracket+1:]
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) processComment(c string) string {
|
||||
if c == "" {
|
||||
return ""
|
||||
@ -75,7 +112,7 @@ func (dec *propertiesDecoder) applyPropertyComments(context Context, path []inte
|
||||
|
||||
func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error {
|
||||
value, _ := properties.Get(key)
|
||||
path := parsePropKey(key)
|
||||
path := parsePropKey(key, dec.prefs)
|
||||
|
||||
propertyComments := properties.GetComments(key)
|
||||
if len(propertyComments) > 0 {
|
||||
|
||||
@ -68,7 +68,7 @@ func mustProcessFormatScenario(s formatScenario, decoder Decoder, encoder Encode
|
||||
|
||||
result, err := processFormatScenario(s, decoder, encoder)
|
||||
if err != nil {
|
||||
log.Error("Bad scenario %v: %w", s.description, err)
|
||||
log.Errorf("Bad scenario %v: %v", s.description, err)
|
||||
return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error())
|
||||
}
|
||||
return result
|
||||
|
||||
@ -8,16 +8,19 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
type tomlDecoder struct {
|
||||
parser toml.Parser
|
||||
finished bool
|
||||
d DataTreeNavigator
|
||||
rootMap *CandidateNode
|
||||
parser toml.Parser
|
||||
finished bool
|
||||
d DataTreeNavigator
|
||||
rootMap *CandidateNode
|
||||
pendingComments []string // Head comments collected from Comment nodes
|
||||
firstContentSeen bool // Track if we've processed the first non-comment node
|
||||
}
|
||||
|
||||
func NewTomlDecoder() Decoder {
|
||||
@ -28,7 +31,7 @@ func NewTomlDecoder() Decoder {
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) Init(reader io.Reader) error {
|
||||
dec.parser = toml.Parser{}
|
||||
dec.parser = toml.Parser{KeepComments: true}
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := buf.ReadFrom(reader)
|
||||
if err != nil {
|
||||
@ -39,9 +42,24 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
}
|
||||
dec.pendingComments = make([]string, 0)
|
||||
dec.firstContentSeen = false
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) attachOrphanedCommentsToNode(tableNodeValue *CandidateNode) {
|
||||
if len(dec.pendingComments) > 0 {
|
||||
comments := strings.Join(dec.pendingComments, "\n")
|
||||
if tableNodeValue.HeadComment == "" {
|
||||
tableNodeValue.HeadComment = comments
|
||||
} else {
|
||||
tableNodeValue.HeadComment = tableNodeValue.HeadComment + "\n" + comments
|
||||
}
|
||||
dec.pendingComments = make([]string, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
|
||||
path := make([]interface{}, 0)
|
||||
for {
|
||||
@ -56,13 +74,24 @@ func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
|
||||
func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error {
|
||||
value := tomlNode.Value()
|
||||
path := dec.getFullPath(value.Next())
|
||||
log.Debug("processKeyValueIntoMap: %v", path)
|
||||
|
||||
valueNode, err := dec.decodeNode(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attach pending head comments
|
||||
if len(dec.pendingComments) > 0 {
|
||||
valueNode.HeadComment = strings.Join(dec.pendingComments, "\n")
|
||||
dec.pendingComments = make([]string, 0)
|
||||
}
|
||||
|
||||
// Check for inline comment chained to the KeyValue node
|
||||
nextNode := tomlNode.Next()
|
||||
if nextNode != nil && nextNode.Kind == toml.Comment {
|
||||
valueNode.LineComment = string(nextNode.Data)
|
||||
}
|
||||
|
||||
context := Context{}
|
||||
context = context.SingleChildContext(rootMap)
|
||||
|
||||
@ -77,15 +106,19 @@ func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode
|
||||
|
||||
for dec.parser.NextExpression() {
|
||||
nextItem := dec.parser.Expression()
|
||||
log.Debug("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind)
|
||||
log.Debugf("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind)
|
||||
|
||||
if nextItem.Kind == toml.KeyValue {
|
||||
switch nextItem.Kind {
|
||||
case toml.KeyValue:
|
||||
if err := dec.processKeyValueIntoMap(rootMap, nextItem); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
case toml.Comment:
|
||||
// Standalone comment - add to pending for next element
|
||||
dec.pendingComments = append(dec.pendingComments, string(nextItem.Data))
|
||||
default:
|
||||
// run out of key values
|
||||
log.Debug("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind)
|
||||
log.Debugf("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
@ -117,21 +150,39 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
|
||||
}
|
||||
|
||||
return &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
Content: content,
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
EncodeHint: EncodeHintInline,
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) {
|
||||
content := make([]*CandidateNode, 0)
|
||||
var pendingArrayComments []string
|
||||
|
||||
iterator := tomlNode.Children()
|
||||
for iterator.Next() {
|
||||
child := iterator.Node()
|
||||
|
||||
// Handle comments within arrays
|
||||
if child.Kind == toml.Comment {
|
||||
// Collect comments to attach to the next array element
|
||||
pendingArrayComments = append(pendingArrayComments, string(child.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
yamlNode, err := dec.decodeNode(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attach any pending comments to this array element
|
||||
if len(pendingArrayComments) > 0 {
|
||||
yamlNode.HeadComment = strings.Join(pendingArrayComments, "\n")
|
||||
pendingArrayComments = make([]string, 0)
|
||||
}
|
||||
|
||||
content = append(content, yamlNode)
|
||||
}
|
||||
|
||||
@ -221,7 +272,7 @@ func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
|
||||
|
||||
currentNode := dec.parser.Expression()
|
||||
|
||||
log.Debug("currentNode: %v ", currentNode.Kind)
|
||||
log.Debugf("currentNode: %v ", currentNode.Kind)
|
||||
runAgainstCurrentExp, err = dec.processTopLevelNode(currentNode)
|
||||
if err != nil {
|
||||
return dec.rootMap, err
|
||||
@ -248,52 +299,102 @@ func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
|
||||
func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error) {
|
||||
var runAgainstCurrentExp bool
|
||||
var err error
|
||||
log.Debug("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
|
||||
log.Debugf("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
|
||||
switch currentNode.Kind {
|
||||
case toml.Comment:
|
||||
// Collect comment to attach to next element
|
||||
commentText := string(currentNode.Data)
|
||||
// If we haven't seen any content yet, accumulate comments for root
|
||||
if !dec.firstContentSeen {
|
||||
if dec.rootMap.HeadComment == "" {
|
||||
dec.rootMap.HeadComment = commentText
|
||||
} else {
|
||||
dec.rootMap.HeadComment = dec.rootMap.HeadComment + "\n" + commentText
|
||||
}
|
||||
} else {
|
||||
// We've seen content, so these comments are for the next element
|
||||
dec.pendingComments = append(dec.pendingComments, commentText)
|
||||
}
|
||||
return false, nil
|
||||
case toml.Table:
|
||||
dec.firstContentSeen = true
|
||||
runAgainstCurrentExp, err = dec.processTable(currentNode)
|
||||
case toml.ArrayTable:
|
||||
dec.firstContentSeen = true
|
||||
runAgainstCurrentExp, err = dec.processArrayTable(currentNode)
|
||||
default:
|
||||
dec.firstContentSeen = true
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode)
|
||||
}
|
||||
|
||||
log.Debug("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap))
|
||||
log.Debugf("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap))
|
||||
return runAgainstCurrentExp, err
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
||||
log.Debug("Enter processTable")
|
||||
fullPath := dec.getFullPath(currentNode.Child())
|
||||
log.Debug("fullpath: %v", fullPath)
|
||||
child := currentNode.Child()
|
||||
fullPath := dec.getFullPath(child)
|
||||
log.Debugf("fullpath: %v", fullPath)
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
|
||||
fullPath, err := getPathToUse(fullPath, dec, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
tableNodeValue := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
Content: make([]*CandidateNode, 0),
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
Content: make([]*CandidateNode, 0),
|
||||
EncodeHint: EncodeHintSeparateBlock,
|
||||
}
|
||||
|
||||
// Attach pending head comments to the table
|
||||
if len(dec.pendingComments) > 0 {
|
||||
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
|
||||
dec.pendingComments = make([]string, 0)
|
||||
}
|
||||
|
||||
var tableValue *toml.Node
|
||||
runAgainstCurrentExp := false
|
||||
var err error
|
||||
hasValue := dec.parser.NextExpression()
|
||||
// check to see if there is any table data
|
||||
if hasValue {
|
||||
sawKeyValue := false
|
||||
for dec.parser.NextExpression() {
|
||||
tableValue = dec.parser.Expression()
|
||||
// next expression is not table data, so we are done
|
||||
if tableValue.Kind != toml.KeyValue {
|
||||
log.Debug("got an empty table")
|
||||
runAgainstCurrentExp = true
|
||||
} else {
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
}
|
||||
// Allow standalone comments inside the table before the first key-value.
|
||||
// These should be associated with the next element in the table (usually the first key-value),
|
||||
// not treated as "end of table" (which would cause subsequent key-values to be parsed at root).
|
||||
if tableValue.Kind == toml.Comment {
|
||||
dec.pendingComments = append(dec.pendingComments, string(tableValue.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
// next expression is not table data, so we are done (but we need to re-process it at top-level)
|
||||
if tableValue.Kind != toml.KeyValue {
|
||||
log.Debug("got an empty table (or reached next section)")
|
||||
// If the table had only comments, attach them to the table itself so they don't leak to the next node.
|
||||
if !sawKeyValue {
|
||||
dec.attachOrphanedCommentsToNode(tableNodeValue)
|
||||
}
|
||||
runAgainstCurrentExp = true
|
||||
break
|
||||
}
|
||||
|
||||
sawKeyValue = true
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
}
|
||||
break
|
||||
}
|
||||
// If we hit EOF after only seeing comments inside this table, attach them to the table itself
|
||||
// so they don't leak to whatever comes next.
|
||||
if !sawKeyValue {
|
||||
dec.attachOrphanedCommentsToNode(tableNodeValue)
|
||||
}
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -302,7 +403,7 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode *CandidateNode) error {
|
||||
log.Debug("arrayAppend to path: %v,%v", path, NodeToString(rhsNode))
|
||||
log.Debugf("arrayAppend to path: %v,%v", path, NodeToString(rhsNode))
|
||||
rhsCandidateNode := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Tag: "!!seq",
|
||||
@ -324,35 +425,130 @@ func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) {
|
||||
log.Debug("Entering processArrayTable")
|
||||
fullPath := dec.getFullPath(currentNode.Child())
|
||||
log.Debug("Fullpath: %v", fullPath)
|
||||
log.Debug("Enter processArrayTable")
|
||||
child := currentNode.Child()
|
||||
fullPath := dec.getFullPath(child)
|
||||
log.Debugf("Fullpath: %v", fullPath)
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
|
||||
fullPath, err := getPathToUse(fullPath, dec, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// need to use the array append exp to add another entry to
|
||||
// this array: fullpath += [ thing ]
|
||||
|
||||
hasValue := dec.parser.NextExpression()
|
||||
if !hasValue {
|
||||
return false, fmt.Errorf("error retrieving table %v value: %w", fullPath, dec.parser.Error())
|
||||
}
|
||||
|
||||
tableNodeValue := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
EncodeHint: EncodeHintSeparateBlock,
|
||||
}
|
||||
|
||||
tableValue := dec.parser.Expression()
|
||||
runAgainstCurrentExp, err := dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
log.Debugf("table node err: %w", err)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
// Attach pending head comments to the array table
|
||||
if len(dec.pendingComments) > 0 {
|
||||
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
|
||||
dec.pendingComments = make([]string, 0)
|
||||
}
|
||||
c := Context{}
|
||||
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
runAgainstCurrentExp := false
|
||||
sawKeyValue := false
|
||||
if hasValue {
|
||||
for {
|
||||
exp := dec.parser.Expression()
|
||||
// Allow standalone comments inside array tables before the first key-value.
|
||||
if exp.Kind == toml.Comment {
|
||||
dec.pendingComments = append(dec.pendingComments, string(exp.Data))
|
||||
hasValue = dec.parser.NextExpression()
|
||||
if !hasValue {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// if the next value is a ArrayTable or Table, then its not part of this declaration (not a key value pair)
|
||||
// so lets leave that expression for the next round of parsing
|
||||
if exp.Kind == toml.ArrayTable || exp.Kind == toml.Table {
|
||||
// If this array-table entry had only comments, attach them to the entry so they don't leak.
|
||||
if !sawKeyValue {
|
||||
dec.attachOrphanedCommentsToNode(tableNodeValue)
|
||||
}
|
||||
runAgainstCurrentExp = true
|
||||
break
|
||||
}
|
||||
|
||||
sawKeyValue = true
|
||||
// otherwise, if there is a value, it must be some key value pairs of the
|
||||
// first object in the array!
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, exp)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// If we hit EOF after only seeing comments inside this array-table entry, attach them to the entry
|
||||
// so they don't leak to whatever comes next.
|
||||
if !sawKeyValue && len(dec.pendingComments) > 0 {
|
||||
comments := strings.Join(dec.pendingComments, "\n")
|
||||
if tableNodeValue.HeadComment == "" {
|
||||
tableNodeValue.HeadComment = comments
|
||||
} else {
|
||||
tableNodeValue.HeadComment = tableNodeValue.HeadComment + "\n" + comments
|
||||
}
|
||||
dec.pendingComments = make([]string, 0)
|
||||
}
|
||||
|
||||
// += function
|
||||
err = dec.arrayAppend(c, fullPath, tableNodeValue)
|
||||
|
||||
return runAgainstCurrentExp, err
|
||||
}
|
||||
|
||||
// if fullPath points to an array of maps rather than a map
|
||||
// then it should set this element into the _last_ element of that array.
|
||||
// Because TOML. So we'll inject the last index into the path.
|
||||
|
||||
func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) {
|
||||
// We need to check the entire path (except the last element), not just the immediate parent,
|
||||
// because we may have nested array tables like [[array.subarray.subsubarray]]
|
||||
// where both 'array' and 'subarray' are arrays that already exist.
|
||||
|
||||
if len(fullPath) == 0 {
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
resultPath := make([]interface{}, 0, len(fullPath)*2) // preallocate with extra space for indices
|
||||
|
||||
// Process all segments except the last one
|
||||
for i := 0; i < len(fullPath)-1; i++ {
|
||||
resultPath = append(resultPath, fullPath[i])
|
||||
|
||||
// Check if the current path segment points to an array
|
||||
readOp := createTraversalTree(resultPath, traversePreferences{DontAutoCreate: true}, false)
|
||||
resultContext, err := dec.d.GetMatchingNodes(c, readOp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resultContext.MatchingNodes.Len() >= 1 {
|
||||
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
|
||||
// If this segment points to an array, we need to add the last index
|
||||
// before continuing with the rest of the path
|
||||
if match.Kind == SequenceNode && len(match.Content) > 0 {
|
||||
lastIndex := len(match.Content) - 1
|
||||
resultPath = append(resultPath, lastIndex)
|
||||
log.Debugf("Path segment %v is an array, injecting index %d", resultPath[:len(resultPath)-1], lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last segment
|
||||
resultPath = append(resultPath, fullPath[len(fullPath)-1])
|
||||
|
||||
log.Debugf("getPathToUse: original path %v -> result path %v", fullPath, resultPath)
|
||||
return resultPath, nil
|
||||
}
|
||||
|
||||
160
pkg/yqlib/decoder_uri_test.go
Normal file
160
pkg/yqlib/decoder_uri_test.go
Normal file
@ -0,0 +1,160 @@
|
||||
//go:build !yq_nouri
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
)
|
||||
|
||||
func TestUriDecoder_Init(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("test")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeSimpleString(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("hello%20world")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "!!str", node.Tag)
|
||||
test.AssertResult(t, "hello world", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeSpecialCharacters(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("hello%21%40%23%24%25")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "hello!@#$%", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeUTF8(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("%E2%9C%93%20check")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "✓ check", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodePlusSign(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("a+b")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
// Note: url.QueryUnescape does NOT convert + to space
|
||||
// That's only for form encoding (url.ParseQuery)
|
||||
test.AssertResult(t, "a b", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeEmptyString(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "", node.Value)
|
||||
|
||||
// Second decode should return EOF
|
||||
node, err = decoder.Decode()
|
||||
test.AssertResult(t, io.EOF, err)
|
||||
test.AssertResult(t, (*CandidateNode)(nil), node)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeMultipleCalls(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("test")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
// First decode
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "test", node.Value)
|
||||
|
||||
// Second decode should return EOF since we've consumed all input
|
||||
node, err = decoder.Decode()
|
||||
test.AssertResult(t, io.EOF, err)
|
||||
test.AssertResult(t, (*CandidateNode)(nil), node)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeInvalidEscape(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("test%ZZ")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
_, err = decoder.Decode()
|
||||
// Should return an error for invalid escape sequence
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid escape sequence, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeSlashAndQuery(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("path%2Fto%2Ffile%3Fquery%3Dvalue")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "path/to/file?query=value", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodePercent(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("100%25")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "100%", node.Value)
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeNoEscaping(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
reader := strings.NewReader("simple_text-123")
|
||||
err := decoder.Init(reader)
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
node, err := decoder.Decode()
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, "simple_text-123", node.Value)
|
||||
}
|
||||
|
||||
// Mock reader that returns an error
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(_ []byte) (n int, err error) {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
func TestUriDecoder_DecodeReadError(t *testing.T) {
|
||||
decoder := NewUriDecoder()
|
||||
err := decoder.Init(&errorReader{})
|
||||
test.AssertResult(t, nil, err)
|
||||
|
||||
_, err = decoder.Decode()
|
||||
test.AssertResult(t, io.ErrUnexpectedEOF, err)
|
||||
}
|
||||
@ -64,7 +64,7 @@ func (dec *xmlDecoder) processComment(c string) string {
|
||||
}
|
||||
|
||||
func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
|
||||
log.Debug("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment)
|
||||
log.Debugf("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment)
|
||||
yamlNode := &CandidateNode{Kind: MappingNode, Tag: "!!map"}
|
||||
|
||||
if len(n.Data) > 0 {
|
||||
@ -92,7 +92,7 @@ func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
|
||||
log.Debugf("label=%v, i=%v, keyValuePair.FootComment: %v", label, i, keyValuePair.FootComment)
|
||||
labelNode.FootComment = dec.processComment(keyValuePair.FootComment)
|
||||
|
||||
log.Debug("len of children in %v is %v", label, len(children))
|
||||
log.Debugf("len of children in %v is %v", label, len(children))
|
||||
if len(children) > 1 {
|
||||
valueNode, err = dec.createSequence(children)
|
||||
if err != nil {
|
||||
@ -105,7 +105,7 @@ func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
|
||||
if len(children[0].Children) == 0 && children[0].HeadComment != "" {
|
||||
if len(children[0].Data) > 0 {
|
||||
|
||||
log.Debug("scalar comment hack, currentlabel [%v]", labelNode.HeadComment)
|
||||
log.Debugf("scalar comment hack, currentlabel [%v]", labelNode.HeadComment)
|
||||
labelNode.HeadComment = joinComments([]string{labelNode.HeadComment, strings.TrimSpace(children[0].HeadComment)}, "\n")
|
||||
children[0].HeadComment = ""
|
||||
} else {
|
||||
@ -151,7 +151,7 @@ func (dec *xmlDecoder) convertToYamlNode(n *xmlNode) (*CandidateNode, error) {
|
||||
|
||||
scalar := dec.createValueNodeFromData(n.Data)
|
||||
|
||||
log.Debug("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment)
|
||||
log.Debugf("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment)
|
||||
scalar.HeadComment = dec.processComment(n.HeadComment)
|
||||
scalar.LineComment = dec.processComment(n.LineComment)
|
||||
if scalar.Tag == "!!seq" {
|
||||
@ -211,17 +211,17 @@ func (n *xmlNode) AddChild(s string, c *xmlNode) {
|
||||
if n.Children == nil {
|
||||
n.Children = make([]*xmlChildrenKv, 0)
|
||||
}
|
||||
log.Debug("looking for %s", s)
|
||||
log.Debugf("looking for %s", s)
|
||||
// see if we can find an existing entry to add to
|
||||
for _, childEntry := range n.Children {
|
||||
if childEntry.K == s {
|
||||
log.Debug("found it, appending an entry%s", s)
|
||||
log.Debugf("found it, appending an entry%s", s)
|
||||
childEntry.V = append(childEntry.V, c)
|
||||
log.Debug("yay len of children in %v is %v", s, len(childEntry.V))
|
||||
log.Debugf("yay len of children in %v is %v", s, len(childEntry.V))
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debug("not there, making a new one %s", s)
|
||||
log.Debugf("not there, making a new one %s", s)
|
||||
n.Children = append(n.Children, &xmlChildrenKv{K: s, V: []*xmlNode{c}})
|
||||
}
|
||||
|
||||
@ -267,7 +267,7 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
|
||||
|
||||
switch se := t.(type) {
|
||||
case xml.StartElement:
|
||||
log.Debug("start element %v", se.Name.Local)
|
||||
log.Debugf("start element %v", se.Name.Local)
|
||||
elem.state = "started"
|
||||
// Build new a new current element and link it to its parent
|
||||
var label = se.Name.Local
|
||||
@ -302,14 +302,14 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
|
||||
if len(newBit) > 0 {
|
||||
elem.n.Data = append(elem.n.Data, newBit)
|
||||
elem.state = "chardata"
|
||||
log.Debug("chardata [%v] for %v", elem.n.Data, elem.label)
|
||||
log.Debugf("chardata [%v] for %v", elem.n.Data, elem.label)
|
||||
}
|
||||
case xml.EndElement:
|
||||
if elem == nil {
|
||||
log.Debug("no element, probably bad xml")
|
||||
continue
|
||||
}
|
||||
log.Debug("end element %v", elem.label)
|
||||
log.Debugf("end element %v", elem.label)
|
||||
elem.state = "finished"
|
||||
// And add it to its parent list
|
||||
if elem.parent != nil {
|
||||
@ -326,10 +326,10 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
|
||||
applyFootComment(elem, commentStr)
|
||||
|
||||
case "chardata":
|
||||
log.Debug("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
|
||||
log.Debugf("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
|
||||
elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ")
|
||||
default:
|
||||
log.Debug("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
|
||||
log.Debugf("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
|
||||
elem.n.HeadComment = joinComments([]string{elem.n.HeadComment, commentStr}, " ")
|
||||
}
|
||||
|
||||
@ -354,7 +354,7 @@ func applyFootComment(elem *element, commentStr string) {
|
||||
if len(elem.n.Children) > 0 {
|
||||
lastChildIndex := len(elem.n.Children) - 1
|
||||
childKv := elem.n.Children[lastChildIndex]
|
||||
log.Debug("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr)
|
||||
log.Debugf("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr)
|
||||
// if it's an array of scalars, put the foot comment on the scalar itself
|
||||
if len(childKv.V) > 0 && len(childKv.V[0].Children) == 0 {
|
||||
nodeToUpdate := childKv.V[len(childKv.V)-1]
|
||||
@ -363,7 +363,7 @@ func applyFootComment(elem *element, commentStr string) {
|
||||
childKv.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ")
|
||||
}
|
||||
} else {
|
||||
log.Debug("got a foot comment for %v: [%v]", elem.label, commentStr)
|
||||
log.Debugf("got a foot comment for %v: [%v]", elem.label, commentStr)
|
||||
elem.n.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ")
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,14 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
commentLineRe = regexp.MustCompile(`^\s*#`)
|
||||
yamlDirectiveLineRe = regexp.MustCompile(`^\s*%YAML`)
|
||||
separatorLineRe = regexp.MustCompile(`^\s*---\s*$`)
|
||||
separatorPrefixRe = regexp.MustCompile(`^\s*---\s+`)
|
||||
)
|
||||
|
||||
type yamlDecoder struct {
|
||||
@ -33,51 +40,72 @@ func NewYamlDecoder(prefs YamlPreferences) Decoder {
|
||||
}
|
||||
|
||||
func (dec *yamlDecoder) processReadStream(reader *bufio.Reader) (io.Reader, string, error) {
|
||||
var commentLineRegEx = regexp.MustCompile(`^\s*#`)
|
||||
var yamlDirectiveLineRegEx = regexp.MustCompile(`^\s*%YA`)
|
||||
var sb strings.Builder
|
||||
|
||||
for {
|
||||
peekBytes, err := reader.Peek(4)
|
||||
if errors.Is(err, io.EOF) {
|
||||
// EOF are handled else where..
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
} else if string(peekBytes[0]) == "\n" {
|
||||
_, err := reader.ReadString('\n')
|
||||
sb.WriteString("\n")
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else if string(peekBytes) == "--- " {
|
||||
_, err := reader.ReadString(' ')
|
||||
sb.WriteString("$yqDocSeparator$\n")
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else if string(peekBytes) == "---\n" {
|
||||
_, err := reader.ReadString('\n')
|
||||
sb.WriteString("$yqDocSeparator$\n")
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else if commentLineRegEx.MatchString(string(peekBytes)) || yamlDirectiveLineRegEx.MatchString(string(peekBytes)) {
|
||||
line, err := reader.ReadString('\n')
|
||||
sb.WriteString(line)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else {
|
||||
line, err := reader.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) && line == "" {
|
||||
// no more data
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
|
||||
// Determine newline style and strip it for inspection
|
||||
newline := ""
|
||||
if strings.HasSuffix(line, "\r\n") {
|
||||
newline = "\r\n"
|
||||
line = strings.TrimSuffix(line, "\r\n")
|
||||
} else if strings.HasSuffix(line, "\n") {
|
||||
newline = "\n"
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Document separator: exact line '---' or a '--- ' prefix followed by content
|
||||
if separatorLineRe.MatchString(trimmed) {
|
||||
sb.WriteString("$yqDocSeparator$")
|
||||
sb.WriteString(newline)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle lines that start with '--- ' followed by more content (e.g. '--- cat')
|
||||
if separatorPrefixRe.MatchString(line) {
|
||||
match := separatorPrefixRe.FindString(line)
|
||||
remainder := line[len(match):]
|
||||
// normalise separator newline: if original had none, default to LF
|
||||
sepNewline := newline
|
||||
if sepNewline == "" {
|
||||
sepNewline = "\n"
|
||||
}
|
||||
sb.WriteString("$yqDocSeparator$")
|
||||
sb.WriteString(sepNewline)
|
||||
// push the remainder back onto the reader and continue processing
|
||||
reader = bufio.NewReader(io.MultiReader(strings.NewReader(remainder), reader))
|
||||
if errors.Is(err, io.EOF) && remainder == "" {
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Comments, YAML directives, and blank lines are leading content
|
||||
if commentLineRe.MatchString(line) || yamlDirectiveLineRe.MatchString(line) || trimmed == "" {
|
||||
sb.WriteString(line)
|
||||
sb.WriteString(newline)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// First non-leading line: push it back onto a reader and return
|
||||
originalLine := line + newline
|
||||
return io.MultiReader(strings.NewReader(originalLine), reader), sb.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names).
|
||||
|
||||
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
|
||||
`yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
@ -22,7 +22,7 @@ see https://yaml.org/type/merge.html
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
- &CENTRE
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
@ -32,7 +32,7 @@ Given a sample.yml file of:
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<: *CENTER
|
||||
- <<: *CENTRE
|
||||
r: 10
|
||||
```
|
||||
then
|
||||
@ -191,7 +191,7 @@ Given a sample.yml file of:
|
||||
```yaml
|
||||
f:
|
||||
a: &a cat
|
||||
*a: b
|
||||
*a : b
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@ -213,10 +213,10 @@ item_value: &item_value
|
||||
value: true
|
||||
thingOne:
|
||||
name: item_1
|
||||
!!merge <<: *item_value
|
||||
<<: *item_value
|
||||
thingTwo:
|
||||
name: item_2
|
||||
!!merge <<: *item_value
|
||||
<<: *item_value
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@ -231,7 +231,7 @@ thingOne:
|
||||
value: false
|
||||
thingTwo:
|
||||
name: item_2
|
||||
!!merge <<: *item_value
|
||||
<<: *item_value
|
||||
```
|
||||
|
||||
## LEGACY: Explode with merge anchors
|
||||
@ -249,13 +249,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -288,7 +288,7 @@ see https://yaml.org/type/merge.html. This has the correct data, but the wrong k
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
- &CENTRE
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
@ -298,8 +298,8 @@ Given a sample.yml file of:
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *CENTER
|
||||
- <<:
|
||||
- *CENTRE
|
||||
- *BIG
|
||||
```
|
||||
then
|
||||
@ -318,7 +318,7 @@ see https://yaml.org/type/merge.html. This has the correct data, but the wrong k
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
- &CENTRE
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
@ -328,7 +328,7 @@ Given a sample.yml file of:
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- <<:
|
||||
- *BIG
|
||||
- *LEFT
|
||||
- *SMALL
|
||||
@ -361,13 +361,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -401,7 +401,7 @@ Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
- &CENTRE
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
@ -411,8 +411,8 @@ Given a sample.yml file of:
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *CENTER
|
||||
- <<:
|
||||
- *CENTRE
|
||||
- *BIG
|
||||
```
|
||||
then
|
||||
@ -432,7 +432,7 @@ Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
- &CENTRE
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
@ -442,7 +442,7 @@ Given a sample.yml file of:
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- <<:
|
||||
- *BIG
|
||||
- *LEFT
|
||||
- *SMALL
|
||||
@ -467,7 +467,7 @@ Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b: &b 42
|
||||
!!merge <<:
|
||||
<<:
|
||||
c: *b
|
||||
```
|
||||
then
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Various operators for parsing and manipulating dates.
|
||||
|
||||
## Date time formattings
|
||||
## Date time formatting
|
||||
This uses Golang's built in time library for parsing and formatting date times.
|
||||
|
||||
When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing.
|
||||
|
||||
@ -55,7 +55,7 @@ yq '.a = .a / 0 | .b = .b / 0' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float +Inf
|
||||
b: !!float -Inf
|
||||
a: +Inf
|
||||
b: -Inf
|
||||
```
|
||||
|
||||
|
||||
@ -29,6 +29,9 @@ as follows:
|
||||
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
|
||||
```
|
||||
|
||||
## Disabling env operators
|
||||
If required, you can use the `--security-disable-env-ops` to disable env operations.
|
||||
|
||||
|
||||
## Read string environment variable
|
||||
Running
|
||||
@ -254,3 +257,39 @@ will output
|
||||
Error: variable ${notThere} not set
|
||||
```
|
||||
|
||||
## env() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'env("MYENV")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
## strenv() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'strenv("MYENV")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
## envsubst() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input '"value: ${MYENV}" | envsubst'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
# First
|
||||
|
||||
Returns the first matching element in an array, or first matching value in a map.
|
||||
|
||||
Can be given an expression to match with, otherwise will just return the first.
|
||||
|
||||
## First matching element from array
|
||||
Given a sample.yml file of:
|
||||
@ -20,8 +25,10 @@ Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
b: firstCat
|
||||
- a: apple
|
||||
- a: cat
|
||||
b: secondCat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@ -30,6 +37,7 @@ yq 'first(.a == "cat")' sample.yml
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
b: firstCat
|
||||
```
|
||||
|
||||
## First matching element from array with numeric condition
|
||||
@ -38,6 +46,7 @@ Given a sample.yml file of:
|
||||
- a: 10
|
||||
- a: 100
|
||||
- a: 1
|
||||
- a: 101
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@ -53,7 +62,10 @@ Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: false
|
||||
- a: true
|
||||
b: firstTrue
|
||||
- a: false
|
||||
- a: true
|
||||
b: secondTrue
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@ -62,6 +74,7 @@ yq 'first(.a == true)' sample.yml
|
||||
will output
|
||||
```yaml
|
||||
a: true
|
||||
b: firstTrue
|
||||
```
|
||||
|
||||
## First matching element from array with null values
|
||||
@ -84,19 +97,19 @@ a: cat
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: dog
|
||||
b: 5
|
||||
b: 7
|
||||
- a: cat
|
||||
b: 3
|
||||
- a: apple
|
||||
b: 7
|
||||
b: 5
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.b > 4)' sample.yml
|
||||
yq 'first(.b > 4 and .b < 6)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: dog
|
||||
a: apple
|
||||
b: 5
|
||||
```
|
||||
|
||||
@ -127,7 +140,7 @@ x:
|
||||
y:
|
||||
a: 100
|
||||
z:
|
||||
a: 1
|
||||
a: 101
|
||||
```
|
||||
then
|
||||
```bash
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names).
|
||||
|
||||
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
|
||||
`yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Various operators for parsing and manipulating dates.
|
||||
|
||||
## Date time formattings
|
||||
## Date time formatting
|
||||
This uses Golang's built in time library for parsing and formatting date times.
|
||||
|
||||
When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing.
|
||||
|
||||
@ -29,3 +29,6 @@ as follows:
|
||||
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
|
||||
```
|
||||
|
||||
## Disabling env operators
|
||||
If required, you can use the `--security-disable-env-ops` to disable env operations.
|
||||
|
||||
|
||||
5
pkg/yqlib/doc/operators/headers/first.md
Normal file
5
pkg/yqlib/doc/operators/headers/first.md
Normal file
@ -0,0 +1,5 @@
|
||||
# First
|
||||
|
||||
Returns the first matching element in an array, or first matching value in a map.
|
||||
|
||||
Can be given an expression to match with, otherwise will just return the first.
|
||||
@ -46,3 +46,7 @@ this.is = a properties file
|
||||
```
|
||||
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
|
||||
```
|
||||
|
||||
## Disabling file operators
|
||||
If required, you can use the `--security-disable-file-ops` to disable file operations.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Slice/Splice Array
|
||||
# Slice Array or String
|
||||
|
||||
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
|
||||
The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.
|
||||
|
||||
You may leave out the first or second number, which will refer to the start or end of the array respectively.
|
||||
You may leave out the first or second number, which will refer to the start or end of the array or string respectively.
|
||||
|
||||
27
pkg/yqlib/doc/operators/headers/system-operators.md
Normal file
27
pkg/yqlib/doc/operators/headers/system-operators.md
Normal file
@ -0,0 +1,27 @@
|
||||
# System Operators
|
||||
|
||||
The `system` operator allows you to run an external command and use its output as a value in your expression.
|
||||
|
||||
**Security warning**: The system operator is disabled by default. You must explicitly pass `--security-enable-system-operator` to use it.
|
||||
|
||||
**Note:** When enabled, the system operator can replicate the functionality of `env` and `load`
|
||||
operators via external commands. Enabling it effectively overrides `--security-disable-env-ops`
|
||||
and `--security-disable-file-ops`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
yq --security-enable-system-operator --null-input '.field = system("command"; "arg1")'
|
||||
```
|
||||
|
||||
The operator takes:
|
||||
- A command string (required)
|
||||
- An argument (or an array of arguments), separated from the command by `;` (optional)
|
||||
|
||||
The current matched node's value is serialised and piped to the command via stdin. The command's stdout (with trailing newline stripped) is returned as a string.
|
||||
|
||||
## Disabling the system operator
|
||||
|
||||
The system operator is disabled by default. When disabled, an error is returned instead of running the command, consistent with `--security-disable-env-ops` and `--security-disable-file-ops`.
|
||||
|
||||
Use `--security-enable-system-operator` flag to enable it.
|
||||
@ -47,6 +47,10 @@ this.is = a properties file
|
||||
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
|
||||
```
|
||||
|
||||
## Disabling file operators
|
||||
If required, you can use the `--security-disable-file-ops` to disable file operations.
|
||||
|
||||
|
||||
## Simple example
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@ -194,3 +198,63 @@ cool: things
|
||||
more_stuff: my secret chilli recipe is....
|
||||
```
|
||||
|
||||
## load() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load("../../examples/thing.yml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_str() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_str("../../examples/thing.yml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_xml() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_xml("../../examples/small.xml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_props() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_props("../../examples/small.properties")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_base64() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_base64("../../examples/base64.txt")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float 2
|
||||
a: 2
|
||||
b: 2.5
|
||||
```
|
||||
|
||||
@ -69,7 +69,7 @@ yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float NaN
|
||||
a: NaN
|
||||
b: 0
|
||||
```
|
||||
|
||||
|
||||
@ -471,13 +471,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -487,7 +487,7 @@ yq '.foobar * .foobarList' sample.yml
|
||||
will output
|
||||
```yaml
|
||||
c: foobarList_c
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
thing: foobar_thing
|
||||
|
||||
@ -79,6 +79,46 @@ will output
|
||||
c: cat
|
||||
```
|
||||
|
||||
## Get the top (root) parent
|
||||
Use negative numbers to get the top parents. You can think of this as indexing into the 'parents' array above
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a.b.c | parent(-1)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
|
||||
## Root
|
||||
Alias for parent(-1), returns the top level parent. This is usually the document node.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a.b.c | root' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
|
||||
## N-th parent
|
||||
You can optionally supply the number of levels to go up for the parent, the default being 1.
|
||||
|
||||
@ -116,6 +156,25 @@ a:
|
||||
c: cat
|
||||
```
|
||||
|
||||
## N-th negative
|
||||
Similarly, use negative numbers to index backwards from the parents array
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a.b.c | parent(-2)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
|
||||
## No parent
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@ -131,13 +131,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -147,7 +147,7 @@ yq '.foobar | [..]' sample.yml
|
||||
will output
|
||||
```yaml
|
||||
- c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
- foobar_c
|
||||
- *foo
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# Slice/Splice Array
|
||||
# Slice Array or String
|
||||
|
||||
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
|
||||
The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.
|
||||
|
||||
You may leave out the first or second number, which will refer to the start or end of the array respectively.
|
||||
You may leave out the first or second number, which will refer to the start or end of the array or string respectively.
|
||||
|
||||
## Slicing arrays
|
||||
Given a sample.yml file of:
|
||||
@ -103,3 +103,81 @@ will output
|
||||
- cow
|
||||
```
|
||||
|
||||
## Slicing strings
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.country[0:5]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Austr
|
||||
```
|
||||
|
||||
## Slicing strings - without the second number
|
||||
Finishes at the end of the string
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.country[5:]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
alia
|
||||
```
|
||||
|
||||
## Slicing strings - without the first number
|
||||
Starts from the start of the string
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.country[:5]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Austr
|
||||
```
|
||||
|
||||
## Slicing strings - use negative numbers to count backwards from the end
|
||||
Negative indices count from the end of the string
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.country[-5:]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
ralia
|
||||
```
|
||||
|
||||
## Slicing strings - Unicode
|
||||
Indices are rune-based, so multi-byte characters are handled correctly
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
greeting: héllo
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.greeting[1:3]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
él
|
||||
```
|
||||
|
||||
|
||||
76
pkg/yqlib/doc/operators/system-operators.md
Normal file
76
pkg/yqlib/doc/operators/system-operators.md
Normal file
@ -0,0 +1,76 @@
|
||||
# System Operators
|
||||
|
||||
The `system` operator allows you to run an external command and use its output as a value in your expression.
|
||||
|
||||
**Security warning**: The system operator is disabled by default. You must explicitly pass `--security-enable-system-operator` to use it.
|
||||
|
||||
**Note:** When enabled, the system operator can replicate the functionality of `env` and `load`
|
||||
operators via external commands. Enabling it effectively overrides `--security-disable-env-ops`
|
||||
and `--security-disable-file-ops`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
yq --security-enable-system-operator --null-input '.field = system("command"; "arg1")'
|
||||
```
|
||||
|
||||
The operator takes:
|
||||
- A command string (required)
|
||||
- An argument (or an array of arguments), separated from the command by `;` (optional)
|
||||
|
||||
The current matched node's value is serialised and piped to the command via stdin. The command's stdout (with trailing newline stripped) is returned as a string.
|
||||
|
||||
## Disabling the system operator
|
||||
|
||||
The system operator is disabled by default. When disabled, an error is returned instead of running the command, consistent with `--security-disable-env-ops` and `--security-disable-file-ops`.
|
||||
|
||||
Use `--security-enable-system-operator` flag to enable it.
|
||||
|
||||
## system operator returns error when disabled
|
||||
Use `--security-enable-system-operator` to enable the system operator.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.country = system("/usr/bin/echo"; "test")' sample.yml
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: system operations are disabled, use --security-enable-system-operator to enable
|
||||
```
|
||||
|
||||
## Run a command with an argument
|
||||
Use `--security-enable-system-operator` to enable the system operator.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
country: Australia
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq --security-enable-system-operator '.country = system("/bin/echo"; "test")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
country: test
|
||||
```
|
||||
|
||||
## Run a command without arguments
|
||||
Omit the semicolon and args to run the command with no extra arguments.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: hello
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq --security-enable-system-operator '.a = system("/bin/echo")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: ""
|
||||
```
|
||||
|
||||
@ -294,13 +294,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -325,13 +325,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -376,13 +376,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -409,13 +409,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -442,13 +442,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -477,13 +477,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -513,13 +513,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -546,13 +546,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -579,13 +579,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
@ -614,13 +614,13 @@ bar: &bar
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
<<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
<<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
|
||||
88
pkg/yqlib/doc/usage/base64.md
Normal file
88
pkg/yqlib/doc/usage/base64.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Base64
|
||||
|
||||
Encode and decode to and from Base64.
|
||||
|
||||
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
|
||||
|
||||
|
||||
See below for examples
|
||||
|
||||
|
||||
## Decode base64: simple
|
||||
Decoded data is assumed to be a string.
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a special string
|
||||
```
|
||||
|
||||
## Decode base64: UTF-8
|
||||
Base64 decoding supports UTF-8 encoded strings.
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig==
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Works with UTF-16 😊
|
||||
```
|
||||
|
||||
## Decode with extra spaces
|
||||
Extra leading/trailing whitespace is stripped
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
|
||||
YSBzcGVjaWFsIHN0cmluZw==
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a special string
|
||||
```
|
||||
|
||||
## Encode base64: string
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
"a special string"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=base64 '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==```
|
||||
|
||||
## Encode base64: string from document
|
||||
Extract a string field and encode it to base64.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
coolData: "a special string"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=base64 '.coolData' sample.yml
|
||||
```
|
||||
will output
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==```
|
||||
|
||||
@ -125,6 +125,22 @@ will output
|
||||
{"whatever":"cat"}
|
||||
```
|
||||
|
||||
## Encode json: preserve floats with trailing zero
|
||||
Whole-number floats keep their decimal point so downstream consumers see a JSON number with a fractional part (matches Go's encoding/json, Python's json, and jq).
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
percentiles: [50.0, 95.0, 99.0, 99.9]
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=json -I=0 '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```json
|
||||
{"percentiles":[50.0,95.0,99.0,99.9]}
|
||||
```
|
||||
|
||||
## Roundtrip JSON Lines / NDJSON
|
||||
Given a sample.json file of:
|
||||
```json
|
||||
|
||||
201
pkg/yqlib/doc/usage/hcl.md
Normal file
201
pkg/yqlib/doc/usage/hcl.md
Normal file
@ -0,0 +1,201 @@
|
||||
# HCL
|
||||
|
||||
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
|
||||
|
||||
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
|
||||
- Blocks and attributes
|
||||
- String interpolation and expressions (preserved without quotes)
|
||||
- Comments (leading, head, and line comments)
|
||||
- Nested structures (maps and lists)
|
||||
- Syntax colorisation when enabled
|
||||
|
||||
|
||||
## Parse HCL
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
io_mode = "async"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -oy sample.hcl
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
io_mode: "async"
|
||||
```
|
||||
|
||||
## Roundtrip: Sample Doc
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
service "cat" {
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "management" {
|
||||
command = ["/usr/local/bin/awesome-app", "management"]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq sample.hcl
|
||||
```
|
||||
will output
|
||||
```hcl
|
||||
service "cat" {
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
process "management" {
|
||||
command = ["/usr/local/bin/awesome-app", "management"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Roundtrip: With an update
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
service "cat" {
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "management" {
|
||||
command = ["/usr/local/bin/awesome-app", "management"]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.service.cat.process.main.command += "meow"' sample.hcl
|
||||
```
|
||||
will output
|
||||
```hcl
|
||||
service "cat" {
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server", "meow"]
|
||||
}
|
||||
process "management" {
|
||||
command = ["/usr/local/bin/awesome-app", "management"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parse HCL: Sample Doc
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
service "cat" {
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "management" {
|
||||
command = ["/usr/local/bin/awesome-app", "management"]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -oy sample.hcl
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
service:
|
||||
cat:
|
||||
process:
|
||||
main:
|
||||
command:
|
||||
- "/usr/local/bin/awesome-app"
|
||||
- "server"
|
||||
management:
|
||||
command:
|
||||
- "/usr/local/bin/awesome-app"
|
||||
- "management"
|
||||
```
|
||||
|
||||
## Parse HCL: with comments
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
# Configuration
|
||||
port = 8080 # server port
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -oy sample.hcl
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
# Configuration
|
||||
port: 8080 # server port
|
||||
```
|
||||
|
||||
## Roundtrip: with comments
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
# Configuration
|
||||
port = 8080
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq sample.hcl
|
||||
```
|
||||
will output
|
||||
```hcl
|
||||
# Configuration
|
||||
port = 8080
|
||||
```
|
||||
|
||||
## Roundtrip: With templates, functions and arithmetic
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
# Arithmetic with literals and application-provided variables
|
||||
sum = 1 + addend
|
||||
|
||||
# String interpolation and templates
|
||||
message = "Hello, ${name}!"
|
||||
|
||||
# Application-provided functions
|
||||
shouty_message = upper(message)
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq sample.hcl
|
||||
```
|
||||
will output
|
||||
```hcl
|
||||
# Arithmetic with literals and application-provided variables
|
||||
sum = 1 + addend
|
||||
# String interpolation and templates
|
||||
message = "Hello, ${name}!"
|
||||
# Application-provided functions
|
||||
shouty_message = upper(message)
|
||||
```
|
||||
|
||||
## Roundtrip: Separate blocks with same name.
|
||||
Given a sample.hcl file of:
|
||||
```hcl
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-12345"
|
||||
}
|
||||
resource "aws_instance" "db" {
|
||||
ami = "ami-67890"
|
||||
}
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq sample.hcl
|
||||
```
|
||||
will output
|
||||
```hcl
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-12345"
|
||||
}
|
||||
resource "aws_instance" "db" {
|
||||
ami = "ami-67890"
|
||||
}
|
||||
```
|
||||
|
||||
9
pkg/yqlib/doc/usage/headers/base64.md
Normal file
9
pkg/yqlib/doc/usage/headers/base64.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Base64
|
||||
|
||||
Encode and decode to and from Base64.
|
||||
|
||||
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
|
||||
|
||||
|
||||
See below for examples
|
||||
|
||||
11
pkg/yqlib/doc/usage/headers/hcl.md
Normal file
11
pkg/yqlib/doc/usage/headers/hcl.md
Normal file
@ -0,0 +1,11 @@
|
||||
# HCL
|
||||
|
||||
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
|
||||
|
||||
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
|
||||
- Blocks and attributes
|
||||
- String interpolation and expressions (preserved without quotes)
|
||||
- Comments (leading, head, and line comments)
|
||||
- Nested structures (maps and lists)
|
||||
- Syntax colorisation when enabled
|
||||
|
||||
9
pkg/yqlib/doc/usage/headers/kyaml.md
Normal file
9
pkg/yqlib/doc/usage/headers/kyaml.md
Normal file
@ -0,0 +1,9 @@
|
||||
# KYaml
|
||||
|
||||
Encode and decode to and from KYaml (a restricted subset of YAML that uses flow-style collections).
|
||||
|
||||
KYaml is useful when you want YAML data rendered in a compact, JSON-like form while still supporting YAML features like comments.
|
||||
|
||||
Notes:
|
||||
- Strings are always double-quoted in KYaml output.
|
||||
- Anchors and aliases are expanded (KYaml output does not emit them).
|
||||
@ -1,4 +1,4 @@
|
||||
# TOML
|
||||
|
||||
Decode from TOML. Note that `yq` does not yet support outputting in TOML format (and therefore it cannot roundtrip)
|
||||
Encode and decode to and from TOML.
|
||||
|
||||
|
||||
253
pkg/yqlib/doc/usage/kyaml.md
Normal file
253
pkg/yqlib/doc/usage/kyaml.md
Normal file
@ -0,0 +1,253 @@
|
||||
# KYaml
|
||||
|
||||
Encode and decode to and from KYaml (a restricted subset of YAML that uses flow-style collections).
|
||||
|
||||
KYaml is useful when you want YAML data rendered in a compact, JSON-like form while still supporting YAML features like comments.
|
||||
|
||||
Notes:
|
||||
- Strings are always double-quoted in KYaml output.
|
||||
- Anchors and aliases are expanded (KYaml output does not emit them).
|
||||
|
||||
## Encode kyaml: plain string scalar
|
||||
Strings are always double-quoted in KYaml output.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
cat
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
"cat"
|
||||
```
|
||||
|
||||
## encode flow mapping and sequence
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: b
|
||||
c:
|
||||
- d
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
a: "b",
|
||||
c: [
|
||||
"d",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## encode non-string scalars
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 12
|
||||
b: true
|
||||
c: null
|
||||
d: "true"
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
a: 12,
|
||||
b: true,
|
||||
c: null,
|
||||
d: "true",
|
||||
}
|
||||
```
|
||||
|
||||
## quote non-identifier keys
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
"1a": b
|
||||
"has space": c
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
"1a": "b",
|
||||
"has space": "c",
|
||||
}
|
||||
```
|
||||
|
||||
## escape quoted strings
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: "line1\nline2\t\"q\""
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
a: "line1\nline2\t\"q\"",
|
||||
}
|
||||
```
|
||||
|
||||
## preserve comments when encoding
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# leading
|
||||
a: 1 # a line
|
||||
# head b
|
||||
b: 2
|
||||
c:
|
||||
# head d
|
||||
- d # d line
|
||||
- e
|
||||
# trailing
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
# leading
|
||||
{
|
||||
a: 1, # a line
|
||||
# head b
|
||||
b: 2,
|
||||
c: [
|
||||
# head d
|
||||
"d", # d line
|
||||
"e",
|
||||
],
|
||||
# trailing
|
||||
}
|
||||
```
|
||||
|
||||
## Encode kyaml: anchors and aliases
|
||||
KYaml output does not support anchors/aliases; they are expanded to concrete values.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
base: &base
|
||||
a: b
|
||||
copy: *base
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
base: {
|
||||
a: "b",
|
||||
},
|
||||
copy: {
|
||||
a: "b",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Encode kyaml: yaml to kyaml shows formatting differences
|
||||
KYaml uses flow-style collections (braces/brackets) and explicit commas.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
person:
|
||||
name: John
|
||||
pets:
|
||||
- cat
|
||||
- dog
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{
|
||||
person: {
|
||||
name: "John",
|
||||
pets: [
|
||||
"cat",
|
||||
"dog",
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Encode kyaml: nested lists of objects
|
||||
Lists and objects can be nested arbitrarily; KYaml always uses flow-style collections.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- name: a
|
||||
items:
|
||||
- id: 1
|
||||
tags:
|
||||
- k: x
|
||||
v: y
|
||||
- k: x2
|
||||
v: y2
|
||||
- id: 2
|
||||
tags:
|
||||
- k: z
|
||||
v: w
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=kyaml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
[
|
||||
{
|
||||
name: "a",
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
tags: [
|
||||
{
|
||||
k: "x",
|
||||
v: "y",
|
||||
},
|
||||
{
|
||||
k: "x2",
|
||||
v: "y2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tags: [
|
||||
{
|
||||
k: "z",
|
||||
v: "w",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
@ -84,3 +84,23 @@ will output
|
||||
name='Miles O'"'"'Brien'
|
||||
```
|
||||
|
||||
## Encode shell variables: custom separator
|
||||
Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
my_app:
|
||||
db_config:
|
||||
host: localhost
|
||||
port: 5432
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=shell --shell-key-separator="__" sample.yml
|
||||
```
|
||||
will output
|
||||
```sh
|
||||
my_app__db_config__host=localhost
|
||||
my_app__db_config__port=5432
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user