mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-07 14:25:38 +00:00
Compare commits
53 Commits
92bdd0681d
...
4edf958bcf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4edf958bcf | ||
|
|
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 | ||
|
|
9ce5c8afee | ||
|
|
f20f287d5b | ||
|
|
c8efd595fc | ||
|
|
fc8a3fc3ce |
10
.github/workflows/codeql.yml
vendored
10
.github/workflows/codeql.yml
vendored
@ -20,6 +20,8 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '24 3 * * 1'
|
- cron: '24 3 * * 1'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
@ -38,11 +40,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v4
|
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ 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
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@ -67,4 +69,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
|
|||||||
15
.github/workflows/docker-release.yml
vendored
15
.github/workflows/docker-release.yml
vendored
@ -7,23 +7,28 @@ on:
|
|||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publishDocker:
|
publishDocker:
|
||||||
environment: dockerhub
|
environment: dockerhub
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: mikefarah/yq
|
IMAGE_NAME: mikefarah/yq
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
with:
|
with:
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
@ -31,13 +36,13 @@ jobs:
|
|||||||
run: echo ${{ steps.buildx.outputs.platforms }} && docker version
|
run: echo ${{ steps.buildx.outputs.platforms }} && docker version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@ -11,13 +11,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: '^1.20'
|
go-version: '^1.20'
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@ -5,12 +5,17 @@ on:
|
|||||||
- 'v4.*'
|
- 'v4.*'
|
||||||
- 'draft-*'
|
- 'draft-*'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publishGitRelease:
|
publishGitRelease:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: '^1.20'
|
go-version: '^1.20'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -37,14 +42,22 @@ jobs:
|
|||||||
--output=yq.1
|
--output=yq.1
|
||||||
man.md
|
man.md
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
|
||||||
- name: Cross compile
|
- name: Cross compile
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install rhash -y
|
sudo apt-get install rhash -y
|
||||||
go install github.com/goreleaser/goreleaser/v2@latest
|
go install github.com/goreleaser/goreleaser/v2@latest
|
||||||
./scripts/xcompile.sh
|
./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
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
with:
|
with:
|
||||||
files: build/*
|
files: build/*
|
||||||
draft: true
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
10
.github/workflows/snap-release.yml
vendored
10
.github/workflows/snap-release.yml
vendored
@ -7,19 +7,23 @@ on:
|
|||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
buildSnap:
|
buildSnap:
|
||||||
environment: snap
|
environment: snap
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: snapcore/action-build@v1
|
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
|
||||||
id: build
|
id: build
|
||||||
env:
|
env:
|
||||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
||||||
with:
|
with:
|
||||||
snapcraft-args: "remote-build --launchpad-accept-public-upload"
|
snapcraft-args: "remote-build --launchpad-accept-public-upload"
|
||||||
- uses: snapcore/action-publish@v1
|
- uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
|
||||||
env:
|
env:
|
||||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/test-yq.yml
vendored
2
.github/workflows/test-yq.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Get test
|
- name: Get test
|
||||||
id: get_value
|
id: get_value
|
||||||
uses: mikefarah/yq@master
|
uses: mikefarah/yq@master
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.1 AS builder
|
FROM golang:1.26.3@sha256:313faae491b410a35402c05d35e7518ae99103d957308e940e1ae2cfa0aac29b AS builder
|
||||||
|
|
||||||
WORKDIR /go/src/mikefarah/yq
|
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
|
# Choose alpine as a base image to make this useful for CI, as many
|
||||||
# CI tools expect an interactive shell inside the container
|
# CI tools expect an interactive shell inside the container
|
||||||
FROM alpine:3 AS production
|
FROM alpine:3@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 AS production
|
||||||
LABEL maintainer="Mike Farah <mikefarah@users.noreply.github.com>"
|
LABEL maintainer="Mike Farah <mikefarah@users.noreply.github.com>"
|
||||||
|
|
||||||
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
|
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.1
|
FROM golang:1.26.3@sha256:313faae491b410a35402c05d35e7518ae99103d957308e940e1ae2cfa0aac29b
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y npm && \
|
apt-get install -y npm && \
|
||||||
|
|||||||
@ -478,3 +478,4 @@ yq ".a.b[0].c = \"value\"" file.yaml
|
|||||||
- "yes", "no" were dropped as boolean values in the yaml 1.2 standard - which is the standard yq assumes.
|
- "yes", "no" were dropped as boolean values in the yaml 1.2 standard - which is the standard yq assumes.
|
||||||
|
|
||||||
See [tips and tricks](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks) for more common problems and solutions.
|
See [tips and tricks](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks) for more common problems and solutions.
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
104
action.yml
104
action.yml
@ -1,17 +1,105 @@
|
|||||||
name: 'yq - portable yaml processor'
|
name: "yq - portable yaml processor"
|
||||||
description: 'create, read, update, delete, merge, validate and do more with yaml'
|
description: "create, read, update, delete, merge, validate and do more with yaml"
|
||||||
branding:
|
branding:
|
||||||
icon: command
|
icon: command
|
||||||
color: gray-dark
|
color: gray-dark
|
||||||
inputs:
|
inputs:
|
||||||
|
image:
|
||||||
|
description: 'Container image to run. Example: "mikefarah/yq:4-githubaction" or fully qualified "artifacts.example.com/repo/mikefarah/yq:4-githubaction".'
|
||||||
|
required: false
|
||||||
|
default: "mikefarah/yq:4-githubaction"
|
||||||
|
registry:
|
||||||
|
description: "Optional artifact repository hostname to prefix the `image`. Leave empty if your `image` already includes a registry."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
registry_username:
|
||||||
|
description: "Optional registry username for `docker login` (use with `registry_password`)."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
registry_password:
|
||||||
|
description: "Optional registry password for `docker login` (use with `registry_username`). Pass secrets via workflow `with:` from secrets."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
cmd:
|
cmd:
|
||||||
description: 'The Command which should be run'
|
description: "The Command which should be run"
|
||||||
required: true
|
required: true
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- id: pull-with-credentials
|
||||||
|
name: Pull image using provided credentials
|
||||||
|
if: ${{ inputs.registry_username && inputs.registry_password && inputs.registry }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IMAGE_INPUT: ${{ inputs.image }}
|
||||||
|
REGISTRY: ${{ inputs.registry }}
|
||||||
|
REG_USER: ${{ inputs.registry_username }}
|
||||||
|
REG_PASS: ${{ inputs.registry_password }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
IMAGE="$IMAGE_INPUT"
|
||||||
|
if [ -n "$REGISTRY" ]; then
|
||||||
|
REG="${REGISTRY%/}"
|
||||||
|
IMAGE="$REG/$IMAGE"
|
||||||
|
fi
|
||||||
|
echo "Using image: $IMAGE"
|
||||||
|
echo "Credentials provided; attempting docker login to $REGISTRY"
|
||||||
|
if [ -n "$REG_PASS" ]; then
|
||||||
|
echo "::add-mask::$REG_PASS"
|
||||||
|
fi
|
||||||
|
echo "$REG_PASS" | docker login "$REGISTRY" --username "$REG_USER" --password-stdin
|
||||||
|
if docker pull "$IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Image pulled successfully after login."
|
||||||
|
else
|
||||||
|
echo "Failed to pull image after login; proceeding to run (docker run may fail)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- id: pull-anonymous
|
||||||
|
name: Pull image anonymously
|
||||||
|
if: ${{ !(inputs.registry_username && inputs.registry_password && inputs.registry) }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IMAGE_INPUT: ${{ inputs.image }}
|
||||||
|
REGISTRY: ${{ inputs.registry }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
IMAGE="$IMAGE_INPUT"
|
||||||
|
if [ -n "$REGISTRY" ]; then
|
||||||
|
REG="${REGISTRY%/}"
|
||||||
|
IMAGE="$REG/$IMAGE"
|
||||||
|
fi
|
||||||
|
echo "Using image: $IMAGE"
|
||||||
|
echo "No credentials provided (or registry not set); attempting anonymous pull"
|
||||||
|
if docker pull "$IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Anonymous pull succeeded."
|
||||||
|
else
|
||||||
|
echo "Anonymous pull failed; proceeding to run (docker run may fail if auth required)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- id: run
|
||||||
|
name: Run yq container
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IMAGE_INPUT: ${{ inputs.image }}
|
||||||
|
REGISTRY: ${{ inputs.registry }}
|
||||||
|
CMD_INPUT: ${{ inputs.cmd }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
IMAGE="$IMAGE_INPUT"
|
||||||
|
if [ -n "$REGISTRY" ]; then
|
||||||
|
REG="${REGISTRY%/}"
|
||||||
|
IMAGE="$REG/$IMAGE"
|
||||||
|
fi
|
||||||
|
echo "Using image: $IMAGE"
|
||||||
|
RC=0
|
||||||
|
OUTPUT=$(docker run --rm -v "$GITHUB_WORKSPACE":/work -w /work "$IMAGE" sh -lc "$CMD_INPUT" 2>&1) || RC=$?
|
||||||
|
echo "result<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$OUTPUT" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
if [ "$RC" -ne 0 ]; then
|
||||||
|
exit "$RC"
|
||||||
|
fi
|
||||||
outputs:
|
outputs:
|
||||||
result:
|
result:
|
||||||
description: "The complete result from the yq command being run"
|
description: "The complete result from the yq command being run"
|
||||||
runs:
|
value: ${{ steps.run.outputs.result }}
|
||||||
using: 'docker'
|
|
||||||
image: 'docker://mikefarah/yq:4-githubaction'
|
|
||||||
args:
|
|
||||||
- ${{ inputs.cmd }}
|
|
||||||
|
|||||||
@ -212,6 +212,7 @@ yq -P -oy sample.json
|
|||||||
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.")
|
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.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(
|
rootCmd.AddCommand(
|
||||||
createEvaluateSequenceCommand(),
|
createEvaluateSequenceCommand(),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ var (
|
|||||||
GitDescribe string
|
GitDescribe string
|
||||||
|
|
||||||
// Version is main version number that is being run at the moment.
|
// Version is main version number that is being run at the moment.
|
||||||
Version = "v4.52.5"
|
Version = "v4.53.2"
|
||||||
|
|
||||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
||||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM mikefarah/yq:4
|
FROM mikefarah/yq:4@sha256:603ebff15eb308a05f1c5b8b7613179cad859aed3ec9fdd04f2ef5d32345950e
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
18
go.mod
18
go.mod
@ -13,16 +13,16 @@ require (
|
|||||||
github.com/hashicorp/hcl/v2 v2.24.0
|
github.com/hashicorp/hcl/v2 v2.24.0
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/magiconair/properties v1.8.10
|
github.com/magiconair/properties v1.8.10
|
||||||
github.com/pelletier/go-toml/v2 v2.3.0
|
github.com/pelletier/go-toml/v2 v2.3.1
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/yuin/gopher-lua v1.1.1
|
github.com/yuin/gopher-lua v1.1.2
|
||||||
github.com/zclconf/go-cty v1.18.0
|
github.com/zclconf/go-cty v1.18.1
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||||
golang.org/x/mod v0.34.0
|
golang.org/x/mod v0.36.0
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.54.0
|
||||||
golang.org/x/text v0.35.0
|
golang.org/x/text v0.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -34,8 +34,8 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|||||||
36
go.sum
36
go.sum
@ -46,8 +46,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
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/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.1/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 h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@ -61,28 +61,28 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||||
github.com/zclconf/go-cty v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA=
|
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
|
||||||
github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
|
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 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
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=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -27,6 +27,22 @@ const (
|
|||||||
FlowStyle
|
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 {
|
func createStringScalarNode(stringValue string) *CandidateNode {
|
||||||
var node = &CandidateNode{Kind: ScalarNode}
|
var node = &CandidateNode{Kind: ScalarNode}
|
||||||
node.Value = stringValue
|
node.Value = stringValue
|
||||||
@ -97,9 +113,9 @@ type CandidateNode struct {
|
|||||||
// (e.g. top level cross document merge). This property does not propagate to child nodes.
|
// (e.g. top level cross document merge). This property does not propagate to child nodes.
|
||||||
EvaluateTogether bool
|
EvaluateTogether bool
|
||||||
IsMapKey bool
|
IsMapKey bool
|
||||||
// For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables
|
// EncodeHint controls how a mapping node is serialised by format-specific encoders
|
||||||
// rather than consolidated into nested mappings (default behaviour)
|
// (e.g. TOML, HCL) that support both inline and block/section representations.
|
||||||
EncodeSeparate bool
|
EncodeHint EncodeHint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *CandidateNode) CreateChild() *CandidateNode {
|
func (n *CandidateNode) CreateChild() *CandidateNode {
|
||||||
@ -411,7 +427,7 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
|
|||||||
EvaluateTogether: n.EvaluateTogether,
|
EvaluateTogether: n.EvaluateTogether,
|
||||||
IsMapKey: n.IsMapKey,
|
IsMapKey: n.IsMapKey,
|
||||||
|
|
||||||
EncodeSeparate: n.EncodeSeparate,
|
EncodeHint: n.EncodeHint,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cloneContent {
|
if cloneContent {
|
||||||
@ -465,8 +481,8 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
|
|||||||
n.Anchor = other.Anchor
|
n.Anchor = other.Anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve EncodeSeparate flag for format-specific encoding hints
|
// Preserve EncodeHint for format-specific encoding hints
|
||||||
n.EncodeSeparate = other.EncodeSeparate
|
n.EncodeHint = other.EncodeHint
|
||||||
|
|
||||||
// merge will pickup the style of the new thing
|
// merge will pickup the style of the new thing
|
||||||
// when autocreating nodes
|
// when autocreating nodes
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
)
|
)
|
||||||
@ -140,6 +143,12 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
|
|||||||
return buf.Bytes(), err
|
return buf.Bytes(), err
|
||||||
case ScalarNode:
|
case ScalarNode:
|
||||||
log.Debugf("MarshalJSON 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()
|
value, err := o.GetValueRep()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buf.Bytes(), err
|
return buf.Bytes(), err
|
||||||
@ -177,3 +186,85 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
|
|||||||
return buf.Bytes(), err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -226,7 +226,7 @@ func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte
|
|||||||
// Mark the type node if there are multiple blocks of this type at this level
|
// 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
|
// This tells the encoder to emit them as separate blocks rather than consolidating them
|
||||||
if isMultipleBlocksOfType {
|
if isMultipleBlocksOfType {
|
||||||
typeNode.EncodeSeparate = true
|
typeNode.EncodeHint = EncodeHintSeparateBlock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current = typeNode
|
current = typeNode
|
||||||
|
|||||||
@ -16,10 +16,11 @@ type propertiesDecoder struct {
|
|||||||
reader io.Reader
|
reader io.Reader
|
||||||
finished bool
|
finished bool
|
||||||
d DataTreeNavigator
|
d DataTreeNavigator
|
||||||
|
prefs PropertiesPreferences
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPropertiesDecoder() Decoder {
|
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 {
|
func (dec *propertiesDecoder) Init(reader io.Reader) error {
|
||||||
@ -28,20 +29,56 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePropKey(key string) []interface{} {
|
func parsePropKey(key string, prefs PropertiesPreferences) []interface{} {
|
||||||
pathStrArray := strings.Split(key, ".")
|
pathStrArray := strings.Split(key, ".")
|
||||||
path := make([]interface{}, len(pathStrArray))
|
path := make([]interface{}, 0, len(pathStrArray))
|
||||||
for i, pathStr := range pathStrArray {
|
for _, pathStr := range pathStrArray {
|
||||||
num, err := strconv.ParseInt(pathStr, 10, 32)
|
path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets)
|
||||||
if err == nil {
|
|
||||||
path[i] = num
|
|
||||||
} else {
|
|
||||||
path[i] = pathStr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return path
|
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 {
|
func (dec *propertiesDecoder) processComment(c string) string {
|
||||||
if c == "" {
|
if c == "" {
|
||||||
return ""
|
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 {
|
func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error {
|
||||||
value, _ := properties.Get(key)
|
value, _ := properties.Get(key)
|
||||||
path := parsePropKey(key)
|
path := parsePropKey(key, dec.prefs)
|
||||||
|
|
||||||
propertyComments := properties.GetComments(key)
|
propertyComments := properties.GetComments(key)
|
||||||
if len(propertyComments) > 0 {
|
if len(propertyComments) > 0 {
|
||||||
|
|||||||
@ -150,9 +150,10 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CandidateNode{
|
return &CandidateNode{
|
||||||
Kind: MappingNode,
|
Kind: MappingNode,
|
||||||
Tag: "!!map",
|
Tag: "!!map",
|
||||||
Content: content,
|
EncodeHint: EncodeHintInline,
|
||||||
|
Content: content,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,10 +346,10 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableNodeValue := &CandidateNode{
|
tableNodeValue := &CandidateNode{
|
||||||
Kind: MappingNode,
|
Kind: MappingNode,
|
||||||
Tag: "!!map",
|
Tag: "!!map",
|
||||||
Content: make([]*CandidateNode, 0),
|
Content: make([]*CandidateNode, 0),
|
||||||
EncodeSeparate: true,
|
EncodeHint: EncodeHintSeparateBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach pending head comments to the table
|
// Attach pending head comments to the table
|
||||||
@ -442,9 +443,9 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
|
|||||||
hasValue := dec.parser.NextExpression()
|
hasValue := dec.parser.NextExpression()
|
||||||
|
|
||||||
tableNodeValue := &CandidateNode{
|
tableNodeValue := &CandidateNode{
|
||||||
Kind: MappingNode,
|
Kind: MappingNode,
|
||||||
Tag: "!!map",
|
Tag: "!!map",
|
||||||
EncodeSeparate: true,
|
EncodeHint: EncodeHintSeparateBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach pending head comments to the array table
|
// Attach pending head comments to the array table
|
||||||
|
|||||||
@ -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.
|
||||||
@ -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
|
## Slicing arrays
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
@ -103,3 +103,81 @@ will output
|
|||||||
- cow
|
- 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("/usr/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("/usr/bin/echo")' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
a: ""
|
||||||
|
```
|
||||||
|
|
||||||
@ -125,6 +125,22 @@ will output
|
|||||||
{"whatever":"cat"}
|
{"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
|
## Roundtrip JSON Lines / NDJSON
|
||||||
Given a sample.json file of:
|
Given a sample.json file of:
|
||||||
```json
|
```json
|
||||||
|
|||||||
@ -384,3 +384,20 @@ ip = "10.0.0.2"
|
|||||||
role = "backend"
|
role = "backend"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Encode: Simple mapping produces table section
|
||||||
|
Given a sample.yml file of:
|
||||||
|
```yaml
|
||||||
|
arg:
|
||||||
|
hello: foo
|
||||||
|
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq -o toml '.' sample.yml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```toml
|
||||||
|
[arg]
|
||||||
|
hello = "foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -449,8 +449,8 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If EncodeSeparate is set, emit children as separate blocks regardless of label extraction
|
// If EncodeHintSeparateBlock is set, emit children as separate blocks regardless of label extraction
|
||||||
if valueNode.EncodeSeparate {
|
if valueNode.EncodeHint == EncodeHintSeparateBlock {
|
||||||
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
|
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -537,9 +537,9 @@ func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockTy
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only emit as separate blocks if EncodeSeparate is true
|
// Only emit as separate blocks if EncodeHintSeparateBlock is set
|
||||||
// This allows the encoder to respect the original block structure preserved by the decoder
|
// This allows the encoder to respect the original block structure preserved by the decoder
|
||||||
if !valueNode.EncodeSeparate {
|
if valueNode.EncodeHint != EncodeHintSeparateBlock {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,27 @@ func (te *tomlEncoder) CanHandleAliases() bool {
|
|||||||
|
|
||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
|
// tomlKey returns the key quoted if it contains characters that are not valid
|
||||||
|
// in a TOML bare key. TOML bare keys may only contain ASCII letters, ASCII
|
||||||
|
// digits, underscores, and dashes.
|
||||||
|
func tomlKey(key string) string {
|
||||||
|
for _, r := range key {
|
||||||
|
if (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '_' && r != '-' {
|
||||||
|
return fmt.Sprintf("%q", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// tomlDottedKey joins path components, quoting any that require it.
|
||||||
|
func tomlDottedKey(path []string) string {
|
||||||
|
parts := make([]string, len(path))
|
||||||
|
for i, p := range path {
|
||||||
|
parts[i] = tomlKey(p)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
func (te *tomlEncoder) writeComment(w io.Writer, comment string) error {
|
func (te *tomlEncoder) writeComment(w io.Writer, comment string) error {
|
||||||
if comment == "" {
|
if comment == "" {
|
||||||
return nil
|
return nil
|
||||||
@ -111,12 +132,23 @@ func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve existing order by iterating Content
|
|
||||||
for i := 0; i < len(node.Content); i += 2 {
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
keyNode := node.Content[i]
|
keyNode := node.Content[i]
|
||||||
valNode := node.Content[i+1]
|
valNode := node.Content[i+1]
|
||||||
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
|
if isTomlAttribute(valNode) {
|
||||||
return err
|
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
|
keyNode := node.Content[i]
|
||||||
|
valNode := node.Content[i+1]
|
||||||
|
if !isTomlAttribute(valNode) {
|
||||||
|
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -148,9 +180,15 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
|
|||||||
}
|
}
|
||||||
if allMaps {
|
if allMaps {
|
||||||
key := path[len(path)-1]
|
key := path[len(path)-1]
|
||||||
|
quotedKey := tomlKey(key)
|
||||||
|
if te.wroteRootAttr {
|
||||||
|
if _, err := w.Write([]byte("\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
te.wroteRootAttr = false
|
||||||
|
}
|
||||||
for _, it := range node.Content {
|
for _, it := range node.Content {
|
||||||
// [[key]] then body
|
if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
|
||||||
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
|
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
|
||||||
@ -162,12 +200,12 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
|
|||||||
// Regular array attribute
|
// Regular array attribute
|
||||||
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
|
// Use inline table syntax only for nodes explicitly marked as TOML inline tables.
|
||||||
if !node.EncodeSeparate {
|
// YAML flow-style mappings are not treated as inline tables; the FlowStyle attribute
|
||||||
// If children contain mappings or arrays of mappings, prefer separate sections
|
// is a YAML-specific rendering hint and should not affect TOML output. This ensures
|
||||||
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
|
// that auto-detected JSON input (parsed as YAML flow style) produces readable table
|
||||||
return te.encodeSeparateMapping(w, path, node)
|
// sections, consistent with explicitly parsed JSON input.
|
||||||
}
|
if node.EncodeHint == EncodeHintInline {
|
||||||
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
||||||
}
|
}
|
||||||
return te.encodeSeparateMapping(w, path, node)
|
return te.encodeSeparateMapping(w, path, node)
|
||||||
@ -176,7 +214,30 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTomlArrayOfTables(seq *CandidateNode) bool {
|
||||||
|
if len(seq.Content) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, it := range seq.Content {
|
||||||
|
if it.Kind != MappingNode || it.EncodeHint == EncodeHintInline {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTomlAttribute(node *CandidateNode) bool {
|
||||||
|
if node.Kind == ScalarNode {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return node.Kind == SequenceNode && !isTomlArrayOfTables(node)
|
||||||
|
}
|
||||||
|
|
||||||
func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error {
|
func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error {
|
||||||
|
if value.Tag == "!!null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
te.wroteRootAttr = true // Mark that we wrote a root attribute
|
te.wroteRootAttr = true // Mark that we wrote a root attribute
|
||||||
|
|
||||||
// Write head comment before the attribute
|
// Write head comment before the attribute
|
||||||
@ -185,7 +246,7 @@ func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateN
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the attribute
|
// Write the attribute
|
||||||
line := key + " = " + te.formatScalar(value)
|
line := tomlKey(key) + " = " + te.formatScalar(value)
|
||||||
|
|
||||||
// Add line comment if present
|
// Add line comment if present
|
||||||
if value.LineComment != "" {
|
if value.LineComment != "" {
|
||||||
@ -210,7 +271,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
|
|||||||
|
|
||||||
// Handle empty arrays
|
// Handle empty arrays
|
||||||
if len(seq.Content) == 0 {
|
if len(seq.Content) == 0 {
|
||||||
line := key + " = []"
|
line := tomlKey(key) + " = []"
|
||||||
if seq.LineComment != "" {
|
if seq.LineComment != "" {
|
||||||
lineComment := strings.TrimSpace(seq.LineComment)
|
lineComment := strings.TrimSpace(seq.LineComment)
|
||||||
if !strings.HasPrefix(lineComment, "#") {
|
if !strings.HasPrefix(lineComment, "#") {
|
||||||
@ -233,7 +294,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
|
|||||||
|
|
||||||
if hasElementComments {
|
if hasElementComments {
|
||||||
// Write multiline array format with comments
|
// Write multiline array format with comments
|
||||||
if _, err := w.Write([]byte(key + " = [\n")); err != nil {
|
if _, err := w.Write([]byte(tomlKey(key) + " = [\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +385,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
line := key + " = [" + strings.Join(items, ", ") + "]"
|
line := tomlKey(key) + " = [" + strings.Join(items, ", ") + "]"
|
||||||
|
|
||||||
// Add line comment if present
|
// Add line comment if present
|
||||||
if seq.LineComment != "" {
|
if seq.LineComment != "" {
|
||||||
@ -372,21 +433,24 @@ func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
|
|||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
switch v.Kind {
|
switch v.Kind {
|
||||||
case ScalarNode:
|
case ScalarNode:
|
||||||
parts = append(parts, fmt.Sprintf("%s = %s", k, te.formatScalar(v)))
|
if v.Tag == "!!null" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), te.formatScalar(v)))
|
||||||
case SequenceNode:
|
case SequenceNode:
|
||||||
// inline array in inline table
|
// inline array in inline table
|
||||||
arr, err := te.sequenceToInlineArray(v)
|
arr, err := te.sequenceToInlineArray(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf("%s = %s", k, arr))
|
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), arr))
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
// nested inline table
|
// nested inline table
|
||||||
inline, err := te.mappingToInlineTable(v)
|
inline, err := te.mappingToInlineTable(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf("%s = %s", k, inline))
|
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), inline))
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
|
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
|
||||||
}
|
}
|
||||||
@ -399,7 +463,7 @@ func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *Can
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write([]byte(key + " = " + inline + "\n"))
|
_, err = w.Write([]byte(tomlKey(key) + " = " + inline + "\n"))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,7 +485,7 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write table header [a.b.c]
|
// Write table header [a.b.c]
|
||||||
header := "[" + strings.Join(path, ".") + "]\n"
|
header := "[" + tomlDottedKey(path) + "]\n"
|
||||||
_, err := w.Write([]byte(header))
|
_, err := w.Write([]byte(header))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -429,24 +493,21 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
|
|||||||
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
|
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
|
||||||
// It emits the table header for this mapping if it has any content, then processes children.
|
// It emits the table header for this mapping if it has any content, then processes children.
|
||||||
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
|
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
|
||||||
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes)
|
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
|
||||||
|
// Inline mapping children also count as attributes since they render as key = { ... }.
|
||||||
hasAttrs := false
|
hasAttrs := false
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
if v.Kind == ScalarNode {
|
if v.Kind == ScalarNode && v.Tag != "!!null" {
|
||||||
|
hasAttrs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if v.Kind == MappingNode && v.EncodeHint == EncodeHintInline {
|
||||||
hasAttrs = true
|
hasAttrs = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if v.Kind == SequenceNode {
|
if v.Kind == SequenceNode {
|
||||||
// Check if it's NOT an array of tables
|
if !isTomlArrayOfTables(v) {
|
||||||
allMaps := true
|
|
||||||
for _, it := range v.Content {
|
|
||||||
if it.Kind != MappingNode {
|
|
||||||
allMaps = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allMaps {
|
|
||||||
hasAttrs = true
|
hasAttrs = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -464,31 +525,26 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No attributes, just nested structures - process children
|
// No attributes, just nested table structures - process children recursively
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
switch v.Kind {
|
switch v.Kind {
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
// Emit [path.k]
|
|
||||||
newPath := append(append([]string{}, path...), k)
|
newPath := append(append([]string{}, path...), k)
|
||||||
if err := te.writeTableHeader(w, newPath, v); err != nil {
|
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case SequenceNode:
|
case SequenceNode:
|
||||||
// If sequence of maps, emit [[path.k]] per element
|
// If sequence of maps, emit [[path.k]] per element
|
||||||
allMaps := true
|
if isTomlArrayOfTables(v) {
|
||||||
for _, it := range v.Content {
|
key := tomlDottedKey(append(append([]string{}, path...), k))
|
||||||
if it.Kind != MappingNode {
|
if te.wroteRootAttr {
|
||||||
allMaps = false
|
if _, err := w.Write([]byte("\n")); err != nil {
|
||||||
break
|
return err
|
||||||
|
}
|
||||||
|
te.wroteRootAttr = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if allMaps {
|
|
||||||
key := strings.Join(append(append([]string{}, path...), k), ".")
|
|
||||||
for _, it := range v.Content {
|
for _, it := range v.Content {
|
||||||
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
|
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -513,42 +569,9 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *tomlEncoder) hasEncodeSeparateChild(m *CandidateNode) bool {
|
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
|
||||||
v := m.Content[i+1]
|
|
||||||
if v.Kind == MappingNode && v.EncodeSeparate {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (te *tomlEncoder) hasStructuralChildren(m *CandidateNode) bool {
|
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
|
||||||
v := m.Content[i+1]
|
|
||||||
// Only consider it structural if mapping has EncodeSeparate or is non-empty
|
|
||||||
if v.Kind == MappingNode && v.EncodeSeparate {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if v.Kind == SequenceNode {
|
|
||||||
allMaps := true
|
|
||||||
for _, it := range v.Content {
|
|
||||||
if it.Kind != MappingNode {
|
|
||||||
allMaps = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allMaps {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context
|
// encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context
|
||||||
func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error {
|
func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error {
|
||||||
// First, attributes (scalars and non-map arrays)
|
// First, attributes (scalars, inline mappings, and non-map arrays)
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
@ -557,15 +580,14 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
|
|||||||
if err := te.writeAttribute(w, k, v); err != nil {
|
if err := te.writeAttribute(w, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case SequenceNode:
|
case MappingNode:
|
||||||
allMaps := true
|
if v.EncodeHint == EncodeHintInline {
|
||||||
for _, it := range v.Content {
|
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
||||||
if it.Kind != MappingNode {
|
return err
|
||||||
allMaps = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !allMaps {
|
case SequenceNode:
|
||||||
|
if !isTomlArrayOfTables(v) {
|
||||||
if err := te.writeArrayAttribute(w, k, v); err != nil {
|
if err := te.writeArrayAttribute(w, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -578,15 +600,8 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
|
|||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
if v.Kind == SequenceNode {
|
if v.Kind == SequenceNode {
|
||||||
allMaps := true
|
if isTomlArrayOfTables(v) {
|
||||||
for _, it := range v.Content {
|
dotted := tomlDottedKey(append(append([]string{}, path...), k))
|
||||||
if it.Kind != MappingNode {
|
|
||||||
allMaps = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allMaps {
|
|
||||||
dotted := strings.Join(append(append([]string{}, path...), k), ".")
|
|
||||||
for _, it := range v.Content {
|
for _, it := range v.Content {
|
||||||
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
|
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -599,12 +614,14 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes
|
// Finally, child mappings: inline-hint ones were emitted above as attributes,
|
||||||
|
// while all others are emitted as separate sub-table sections.
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
if v.Kind == MappingNode && !v.EncodeSeparate {
|
if v.Kind == MappingNode && v.EncodeHint != EncodeHintInline {
|
||||||
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
subPath := append(append([]string{}, path...), k)
|
||||||
|
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,6 +220,54 @@ var jsonScenarios = []formatScenario{
|
|||||||
expected: "{\"stuff\":\"cool\"}\n{\"whatever\":\"cat\"}\n",
|
expected: "{\"stuff\":\"cool\"}\n{\"whatever\":\"cat\"}\n",
|
||||||
scenarioType: "encode",
|
scenarioType: "encode",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: preserve floats with trailing zero",
|
||||||
|
subdescription: "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).",
|
||||||
|
input: `percentiles: [50.0, 95.0, 99.0, 99.9]`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"percentiles\":[50.0,95.0,99.0,99.9]}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: ints stay ints",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `a: 50`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"a\":50}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: !!float tagged whole number gets .0",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `a: !!float 5`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"a\":5.0}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: scientific notation float preserved",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `a: 1.5e-3`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"a\":1.5e-3}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: negative float preserved",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `a: -7.0`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"a\":-7.0}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Encode json: mixed int and float array",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `a: [1, 2.0, 3, 4.5]`,
|
||||||
|
indent: 0,
|
||||||
|
expected: "{\"a\":[1,2.0,3,4.5]}\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Roundtrip JSON Lines / NDJSON",
|
description: "Roundtrip JSON Lines / NDJSON",
|
||||||
input: sampleNdJson,
|
input: sampleNdJson,
|
||||||
|
|||||||
@ -131,6 +131,11 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
|
|||||||
log.Debugf("previous token is : traverseArrayOpType")
|
log.Debugf("previous token is : traverseArrayOpType")
|
||||||
// need to put the number 0 before this token, as that is implied
|
// need to put the number 0 before this token, as that is implied
|
||||||
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
|
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
|
||||||
|
} else if index >= 2 && tokens[index-1].TokenType == openCollect &&
|
||||||
|
(tokens[index-2].TokenType == operationToken || tokens[index-2].TokenType == closeCollect || tokens[index-2].TokenType == closeCollectObject) {
|
||||||
|
log.Debugf("previous token is : openCollect following a traversal, implying 0 start")
|
||||||
|
// need to put the number 0 before this token, as that is implied
|
||||||
|
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -96,6 +96,8 @@ var participleYqRules = []*participleYqRule{
|
|||||||
simpleOp("load_?str|str_?load", loadStringOpType),
|
simpleOp("load_?str|str_?load", loadStringOpType),
|
||||||
{"LoadYaml", `load`, loadOp(NewYamlDecoder(LoadYamlPreferences)), 0},
|
{"LoadYaml", `load`, loadOp(NewYamlDecoder(LoadYamlPreferences)), 0},
|
||||||
|
|
||||||
|
simpleOp("system", systemOpType),
|
||||||
|
|
||||||
{"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0},
|
{"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0},
|
||||||
|
|
||||||
simpleOp("select", selectOpType),
|
simpleOp("select", selectOpType),
|
||||||
|
|||||||
@ -80,7 +80,7 @@ func recurseNodeObjectEqual(lhs *CandidateNode, rhs *CandidateNode) bool {
|
|||||||
key := lhs.Content[index]
|
key := lhs.Content[index]
|
||||||
value := lhs.Content[index+1]
|
value := lhs.Content[index+1]
|
||||||
|
|
||||||
indexInRHS := findInArray(rhs, key)
|
indexInRHS := findKeyInMap(rhs, key)
|
||||||
|
|
||||||
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
|
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -24,7 +24,7 @@ type parseSnippetScenario struct {
|
|||||||
var parseSnippetScenarios = []parseSnippetScenario{
|
var parseSnippetScenarios = []parseSnippetScenario{
|
||||||
{
|
{
|
||||||
snippet: ":",
|
snippet: ":",
|
||||||
expectedError: "yaml: did not find expected key",
|
expectedError: "yaml: while parsing a block mapping at <unknown position>: did not find expected key",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
snippet: "",
|
snippet: "",
|
||||||
@ -300,6 +300,24 @@ func TestRecurseNodeObjectEqual(t *testing.T) {
|
|||||||
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
|
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
|
||||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
|
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
|
||||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
|
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
|
||||||
|
|
||||||
|
// A null key must not match a null value in the other map.
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383860504
|
||||||
|
nullKey := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
nullVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
intKey := createScalarNode(2, "2")
|
||||||
|
intKey.Tag = "!!int"
|
||||||
|
intVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
|
||||||
|
mapWithNullKey := &CandidateNode{
|
||||||
|
Kind: MappingNode,
|
||||||
|
Content: []*CandidateNode{nullKey, nullVal},
|
||||||
|
}
|
||||||
|
mapWithIntKey := &CandidateNode{
|
||||||
|
Kind: MappingNode,
|
||||||
|
Content: []*CandidateNode{intKey, intVal},
|
||||||
|
}
|
||||||
|
test.AssertResult(t, false, recurseNodeObjectEqual(mapWithNullKey, mapWithIntKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInt(t *testing.T) {
|
func TestParseInt(t *testing.T) {
|
||||||
|
|||||||
@ -164,6 +164,8 @@ var stringInterpolationOpType = &operationType{Type: "STRING_INT", NumArgs: 0, P
|
|||||||
var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 52, Handler: loadOperator, CheckForPostTraverse: true}
|
var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 52, Handler: loadOperator, CheckForPostTraverse: true}
|
||||||
var loadStringOpType = &operationType{Type: "LOAD_STRING", NumArgs: 1, Precedence: 52, Handler: loadStringOperator}
|
var loadStringOpType = &operationType{Type: "LOAD_STRING", NumArgs: 1, Precedence: 52, Handler: loadStringOperator}
|
||||||
|
|
||||||
|
var systemOpType = &operationType{Type: "SYSTEM", NumArgs: 1, Precedence: 50, Handler: systemOperator}
|
||||||
|
|
||||||
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 52, Handler: keysOperator, CheckForPostTraverse: true}
|
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 52, Handler: keysOperator, CheckForPostTraverse: true}
|
||||||
|
|
||||||
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}
|
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}
|
||||||
|
|||||||
@ -527,6 +527,18 @@ var addOperatorScenarios = []expressionScenario{
|
|||||||
expression: `.a += [2]`,
|
expression: `.a += [2]`,
|
||||||
expectedError: "!!seq () cannot be added to a !!str (a)",
|
expectedError: "!!seq () cannot be added to a !!str (a)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383860504
|
||||||
|
// Adding a map to itself must not panic when sequence keys contain
|
||||||
|
// single-entry mappings with a null key in one and a non-null key
|
||||||
|
// in the other.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2",
|
||||||
|
expression: `. += .`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddOperatorScenarios(t *testing.T) {
|
func TestAddOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
@ -170,6 +170,10 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
|
|||||||
if mergeNodeSeq.Kind == AliasNode {
|
if mergeNodeSeq.Kind == AliasNode {
|
||||||
mergeNodeSeq = mergeNodeSeq.Alias
|
mergeNodeSeq = mergeNodeSeq.Alias
|
||||||
}
|
}
|
||||||
|
mergeNodeSeq = mergeNodeSeq.Copy()
|
||||||
|
if err := explodeNode(mergeNodeSeq, Context{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if mergeNodeSeq.Kind != MappingNode {
|
if mergeNodeSeq.Kind != MappingNode {
|
||||||
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag)
|
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag)
|
||||||
}
|
}
|
||||||
@ -179,12 +183,7 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, item := range itemsToAdd {
|
for _, item := range itemsToAdd {
|
||||||
// copy to ensure exploding doesn't modify the original node
|
newContent = append(newContent, item.Copy())
|
||||||
itemCopy := item.Copy()
|
|
||||||
if err := explodeNode(itemCopy, Context{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newContent = append(newContent, itemCopy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -198,6 +198,15 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!map)::{a: 42}\n",
|
"D0, P[], (!!map)::{a: 42}\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Nested merge anchor with inline map",
|
||||||
|
document: `{<<: {<<: {a: 42}}}`,
|
||||||
|
expression: `explode(.)`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::{a: 42}\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
description: "Merge anchor with sequence with inline map",
|
description: "Merge anchor with sequence with inline map",
|
||||||
|
|||||||
@ -46,9 +46,9 @@ func containsObject(lhs *CandidateNode, rhs *CandidateNode) (bool, error) {
|
|||||||
rhsKey := rhs.Content[index]
|
rhsKey := rhs.Content[index]
|
||||||
rhsValue := rhs.Content[index+1]
|
rhsValue := rhs.Content[index+1]
|
||||||
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
|
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
|
||||||
lhsKeyIndex := findInArray(lhs, rhsKey)
|
lhsKeyIndex := findKeyInMap(lhs, rhsKey)
|
||||||
log.Debugf("index is %v", lhsKeyIndex)
|
log.Debugf("index is %v", lhsKeyIndex)
|
||||||
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 {
|
if lhsKeyIndex < 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
lhsValue := lhs.Content[lhsKeyIndex+1]
|
lhsValue := lhs.Content[lhsKeyIndex+1]
|
||||||
|
|||||||
@ -65,6 +65,16 @@ var containsOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!bool)::false\n",
|
"D0, P[], (!!bool)::false\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression: findInArray could match a null key against a null
|
||||||
|
// value at an earlier odd index, producing a false negative.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "? 1\n: ~\n? ~\n: x",
|
||||||
|
expression: `contains({~: "x"})`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "String contains substring",
|
description: "String contains substring",
|
||||||
document: `"foobar"`,
|
document: `"foobar"`,
|
||||||
|
|||||||
@ -155,8 +155,10 @@ func repeatString(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
} else if count < 0 {
|
} else if count < 0 {
|
||||||
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
|
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
|
||||||
} else if count > 10000000 {
|
}
|
||||||
return nil, fmt.Errorf("cannot repeat string by more than 100 million (%v)", count)
|
maxResultLen := 10 * 1024 * 1024 // 10 MiB
|
||||||
|
if count > 0 && len(stringNode.Value) > maxResultLen/count {
|
||||||
|
return nil, fmt.Errorf("result of repeating string (%v bytes) by %v would exceed %v bytes", len(stringNode.Value), count, maxResultLen)
|
||||||
}
|
}
|
||||||
target.Value = strings.Repeat(stringNode.Value, count)
|
target.Value = strings.Repeat(stringNode.Value, count)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package yqlib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/bits"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -237,12 +238,11 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
expectedError: "cannot repeat string by a negative number (-4)",
|
expectedError: "cannot repeat string by a negative number (-4)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Multiply string X by more than 100 million",
|
description: "Multiply string by count that exceeds result size limit",
|
||||||
// very large string.repeats causes a panic
|
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
document: `n: 100000001`,
|
document: `n: 100000001`,
|
||||||
expression: `"banana" * .n`,
|
expression: `"banana" * .n`,
|
||||||
expectedError: "cannot repeat string by more than 100 million (100000001)",
|
expectedError: "result of repeating string (6 bytes) by 100000001 would exceed 10485760 bytes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Multiply int node X string",
|
description: "Multiply int node X string",
|
||||||
@ -554,7 +554,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
document: document,
|
document: document,
|
||||||
expression: `.b * .c`,
|
expression: `.b * .c`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[b], (!!map)::{name: dog, <<: *cat}\n",
|
"D0, P[b], (!!map)::{name: dog, \"<<\": *cat}\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -693,6 +693,29 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!null)::null\n",
|
"D0, P[], (!!null)::null\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/418818862
|
||||||
|
// Large repeat count with a long string must not panic.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `"abc" * 99999999`,
|
||||||
|
expectedError: "result of repeating string (3 bytes) by 99999999 would exceed 10485760 bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383195001
|
||||||
|
// Product of string length * repeat count must be bounded.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `"x" * 99999999`,
|
||||||
|
expectedError: "result of repeating string (1 bytes) by 99999999 would exceed 10485760 bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Pick a count whose product with len("ab") overflows int on
|
||||||
|
// any architecture: 2^30 on 32-bit, 2^62 on 64-bit. Doubling
|
||||||
|
// either yields MaxInt+1, which wraps to MinInt and bypasses
|
||||||
|
// a naive len*count guard.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: fmt.Sprintf(`"ab" * %d`, 1<<(bits.UintSize-2)),
|
||||||
|
expectedError: fmt.Sprintf("result of repeating string (2 bytes) by %d would exceed 10485760 bytes", 1<<(bits.UintSize-2)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiplyOperatorScenarios(t *testing.T) {
|
func TestMultiplyOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
@ -16,6 +16,39 @@ func getSliceNumber(d *dataTreeNavigator, context Context, node *CandidateNode,
|
|||||||
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
|
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clampSliceIndex resolves a possibly-negative slice index against
|
||||||
|
// length and clamps the result to [0, length].
|
||||||
|
func clampSliceIndex(index, length int) int {
|
||||||
|
if index < 0 {
|
||||||
|
index += length
|
||||||
|
}
|
||||||
|
if index < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if index > length {
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceStringNode(lhsNode *CandidateNode, firstNumber int, secondNumber int) *CandidateNode {
|
||||||
|
runes := []rune(lhsNode.Value)
|
||||||
|
length := len(runes)
|
||||||
|
|
||||||
|
relativeFirstNumber := clampSliceIndex(firstNumber, length)
|
||||||
|
relativeSecondNumber := clampSliceIndex(secondNumber, length)
|
||||||
|
if relativeSecondNumber < relativeFirstNumber {
|
||||||
|
relativeSecondNumber = relativeFirstNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("sliceStringNode: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
||||||
|
|
||||||
|
slicedString := string(runes[relativeFirstNumber:relativeSecondNumber])
|
||||||
|
replacement := lhsNode.CreateReplacement(ScalarNode, lhsNode.Tag, slicedString)
|
||||||
|
replacement.Style = lhsNode.Style
|
||||||
|
return replacement
|
||||||
|
}
|
||||||
|
|
||||||
func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
|
||||||
log.Debug("slice array operator!")
|
log.Debug("slice array operator!")
|
||||||
@ -28,27 +61,23 @@ func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *E
|
|||||||
lhsNode := el.Value.(*CandidateNode)
|
lhsNode := el.Value.(*CandidateNode)
|
||||||
|
|
||||||
firstNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.LHS)
|
firstNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.LHS)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Context{}, err
|
return Context{}, err
|
||||||
}
|
}
|
||||||
relativeFirstNumber := firstNumber
|
|
||||||
if relativeFirstNumber < 0 {
|
|
||||||
relativeFirstNumber = len(lhsNode.Content) + firstNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
secondNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.RHS)
|
secondNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.RHS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Context{}, err
|
return Context{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
relativeSecondNumber := secondNumber
|
if lhsNode.Kind == ScalarNode && lhsNode.guessTagFromCustomType() == "!!str" {
|
||||||
if relativeSecondNumber < 0 {
|
results.PushBack(sliceStringNode(lhsNode, firstNumber, secondNumber))
|
||||||
relativeSecondNumber = len(lhsNode.Content) + secondNumber
|
continue
|
||||||
} else if relativeSecondNumber > len(lhsNode.Content) {
|
|
||||||
relativeSecondNumber = len(lhsNode.Content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relativeFirstNumber := clampSliceIndex(firstNumber, len(lhsNode.Content))
|
||||||
|
relativeSecondNumber := clampSliceIndex(secondNumber, len(lhsNode.Content))
|
||||||
|
|
||||||
log.Debugf("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
log.Debugf("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
||||||
|
|
||||||
var newResults []*CandidateNode
|
var newResults []*CandidateNode
|
||||||
|
|||||||
@ -98,6 +98,115 @@ var sliceArrayScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!seq)::- cat1\n",
|
"D0, P[], (!!seq)::- cat1\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/438776028
|
||||||
|
// Negative second index that underflows after adjustment must
|
||||||
|
// clamp to zero, yielding an empty sequence.
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[a, b, c]`,
|
||||||
|
expression: `.[0:-99999]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!seq)::[]\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// First-index underflow: without clamping, the loop starts at a
|
||||||
|
// negative index and panics on Content access.
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[a, b, c]`,
|
||||||
|
expression: `.[-99999:3]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!seq)::- a\n- b\n- c\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Both indices underflow: both clamp to zero, yielding an empty
|
||||||
|
// sequence.
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[a, b, c]`,
|
||||||
|
expression: `.[-99999:-99998]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!seq)::[]\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slicing strings",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[0:5]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::Austr\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slicing strings - without the second number",
|
||||||
|
subdescription: "Finishes at the end of the string",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[5:]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::alia\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slicing strings - without the first number",
|
||||||
|
subdescription: "Starts from the start of the string",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[:5]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::Austr\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slicing strings - use negative numbers to count backwards from the end",
|
||||||
|
subdescription: "Negative indices count from the end of the string",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[-5:]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::ralia\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[1:-1]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::ustrali\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[:]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::Australia\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "second index beyond string length clamps",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[:100]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::Australia\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "first index beyond string length returns empty string",
|
||||||
|
document: `country: Australia`,
|
||||||
|
expression: `.country[100:]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[country], (!!str)::\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slicing strings - Unicode",
|
||||||
|
subdescription: "Indices are rune-based, so multi-byte characters are handled correctly",
|
||||||
|
document: `greeting: héllo`,
|
||||||
|
expression: `.greeting[1:3]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[greeting], (!!str)::él\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSliceOperatorScenarios(t *testing.T) {
|
func TestSliceOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
146
pkg/yqlib/operator_system.go
Normal file
146
pkg/yqlib/operator_system.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveSystemArgs(argsNode *CandidateNode) ([]string, error) {
|
||||||
|
if argsNode == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if argsNode.Kind == SequenceNode {
|
||||||
|
args := make([]string, 0, len(argsNode.Content))
|
||||||
|
for _, child := range argsNode.Content {
|
||||||
|
// Only non-null scalar children are valid arguments.
|
||||||
|
if child == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if child.Kind != ScalarNode || child.Tag == "!!null" {
|
||||||
|
return nil, fmt.Errorf("system operator: argument must be a non-null scalar; got kind=%v tag=%v", child.Kind, child.Tag)
|
||||||
|
}
|
||||||
|
args = append(args, child.Value)
|
||||||
|
}
|
||||||
|
if len(args) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-argument case: only accept a non-null scalar node.
|
||||||
|
if argsNode.Tag == "!!null" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if argsNode.Kind != ScalarNode {
|
||||||
|
return nil, fmt.Errorf("system operator: args must be a non-null scalar or sequence of non-null scalars; got kind=%v tag=%v", argsNode.Kind, argsNode.Tag)
|
||||||
|
}
|
||||||
|
return []string{argsNode.Value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCommandNode(commandNodes Context) (string, error) {
|
||||||
|
if commandNodes.MatchingNodes.Front() == nil {
|
||||||
|
return "", fmt.Errorf("system operator: command expression returned no results")
|
||||||
|
}
|
||||||
|
if commandNodes.MatchingNodes.Len() > 1 {
|
||||||
|
log.Debugf("system operator: command expression returned %d results, using first", commandNodes.MatchingNodes.Len())
|
||||||
|
}
|
||||||
|
cmdNode := commandNodes.MatchingNodes.Front().Value.(*CandidateNode)
|
||||||
|
if cmdNode.Kind != ScalarNode || cmdNode.guessTagFromCustomType() != "!!str" {
|
||||||
|
return "", fmt.Errorf("system operator: command must be a string scalar")
|
||||||
|
}
|
||||||
|
if cmdNode.Value == "" {
|
||||||
|
return "", fmt.Errorf("system operator: command must be a non-empty string")
|
||||||
|
}
|
||||||
|
return cmdNode.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||||
|
if !ConfiguredSecurityPreferences.EnableSystemOps {
|
||||||
|
return Context{}, fmt.Errorf("system operations are disabled, use --security-enable-system-operator to enable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine at parse time whether we have (command; args) or just (command)
|
||||||
|
hasArgs := expressionNode.RHS.Operation.OperationType == blockOpType
|
||||||
|
|
||||||
|
var results = list.New()
|
||||||
|
|
||||||
|
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||||
|
candidate := el.Value.(*CandidateNode)
|
||||||
|
nodeContext := context.SingleReadonlyChildContext(candidate)
|
||||||
|
|
||||||
|
var command string
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
if hasArgs {
|
||||||
|
block := expressionNode.RHS
|
||||||
|
commandNodes, err := d.GetMatchingNodes(nodeContext, block.LHS)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
command, err = resolveCommandNode(commandNodes)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
argsNodes, err := d.GetMatchingNodes(nodeContext, block.RHS)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
if argsNodes.MatchingNodes.Len() > 1 {
|
||||||
|
log.Debugf("system operator: args expression returned %d results, using first", argsNodes.MatchingNodes.Len())
|
||||||
|
}
|
||||||
|
if argsNodes.MatchingNodes.Front() != nil {
|
||||||
|
args, err = resolveSystemArgs(argsNodes.MatchingNodes.Front().Value.(*CandidateNode))
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commandNodes, err := d.GetMatchingNodes(nodeContext, expressionNode.RHS)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
command, err = resolveCommandNode(commandNodes)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdin bytes.Buffer
|
||||||
|
encoded, err := encodeToYamlString(candidate)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
stdin.WriteString(encoded)
|
||||||
|
|
||||||
|
// #nosec G204 - intentional: user must explicitly enable this operator
|
||||||
|
cmd := exec.Command(command, args...)
|
||||||
|
cmd.Stdin = &stdin
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
stderrStr := strings.TrimSpace(stderr.String())
|
||||||
|
if stderrStr != "" {
|
||||||
|
return Context{}, fmt.Errorf("system command '%v' failed: %w\nstderr: %v", command, err, stderrStr)
|
||||||
|
}
|
||||||
|
return Context{}, fmt.Errorf("system command '%v' failed: %w", command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := string(output)
|
||||||
|
if strings.HasSuffix(result, "\r\n") {
|
||||||
|
result = result[:len(result)-2]
|
||||||
|
} else if strings.HasSuffix(result, "\n") {
|
||||||
|
result = result[:len(result)-1]
|
||||||
|
}
|
||||||
|
newNode := candidate.CreateReplacement(ScalarNode, "!!str", result)
|
||||||
|
results.PushBack(newNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.ChildContext(results), nil
|
||||||
|
}
|
||||||
123
pkg/yqlib/operator_system_test.go
Normal file
123
pkg/yqlib/operator_system_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findExec(t *testing.T, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
path, err := exec.LookPath(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping: %v not found: %v", name, err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemOperatorDisabledScenarios = []expressionScenario{
|
||||||
|
{
|
||||||
|
description: "system operator returns error when disabled",
|
||||||
|
subdescription: "Use `--security-enable-system-operator` to enable the system operator.",
|
||||||
|
document: "country: Australia",
|
||||||
|
expression: `.country = system("/usr/bin/echo"; "test")`,
|
||||||
|
expectedError: "system operations are disabled, use --security-enable-system-operator to enable",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemOperatorDisabledScenarios(t *testing.T) {
|
||||||
|
originalEnableSystemOps := ConfiguredSecurityPreferences.EnableSystemOps
|
||||||
|
defer func() {
|
||||||
|
ConfiguredSecurityPreferences.EnableSystemOps = originalEnableSystemOps
|
||||||
|
}()
|
||||||
|
|
||||||
|
ConfiguredSecurityPreferences.EnableSystemOps = false
|
||||||
|
|
||||||
|
for _, tt := range systemOperatorDisabledScenarios {
|
||||||
|
testScenario(t, &tt)
|
||||||
|
}
|
||||||
|
documentOperatorScenarios(t, "system-operators", systemOperatorDisabledScenarios)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemOperatorEnabledScenarios(t *testing.T) {
|
||||||
|
echoPath := findExec(t, "echo")
|
||||||
|
falsePath := findExec(t, "false")
|
||||||
|
|
||||||
|
originalEnableSystemOps := ConfiguredSecurityPreferences.EnableSystemOps
|
||||||
|
defer func() {
|
||||||
|
ConfiguredSecurityPreferences.EnableSystemOps = originalEnableSystemOps
|
||||||
|
}()
|
||||||
|
|
||||||
|
ConfiguredSecurityPreferences.EnableSystemOps = true
|
||||||
|
|
||||||
|
scenarios := []expressionScenario{
|
||||||
|
{
|
||||||
|
description: "Run a command with an argument",
|
||||||
|
subdescription: "Use `--security-enable-system-operator` to enable the system operator.",
|
||||||
|
yqFlags: "--security-enable-system-operator",
|
||||||
|
document: "country: Australia",
|
||||||
|
expression: `.country = system("` + echoPath + `"; "test")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::country: test\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Run a command without arguments",
|
||||||
|
subdescription: "Omit the semicolon and args to run the command with no extra arguments.",
|
||||||
|
yqFlags: "--security-enable-system-operator",
|
||||||
|
document: "a: hello",
|
||||||
|
expression: `.a = system("` + echoPath + `")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::a: \"\"\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Run a command with multiple arguments",
|
||||||
|
subdescription: "Pass an array of arguments.",
|
||||||
|
skipDoc: true,
|
||||||
|
document: "a: hello",
|
||||||
|
expression: `.a = system("` + echoPath + `"; ["foo", "bar"])`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::a: foo bar\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Command and args are evaluated per matched node",
|
||||||
|
skipDoc: true,
|
||||||
|
document: "cmd: " + echoPath + "\narg: hello",
|
||||||
|
expression: `.result = system(.cmd; .arg)`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::cmd: " + echoPath + "\narg: hello\nresult: hello\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Command failure returns error",
|
||||||
|
skipDoc: true,
|
||||||
|
document: "a: hello",
|
||||||
|
expression: `.a = system("` + falsePath + `")`,
|
||||||
|
expectedError: "system command '" + falsePath + "' failed: exit status 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Null command returns error",
|
||||||
|
skipDoc: true,
|
||||||
|
document: "a: hello",
|
||||||
|
expression: `.a = system(null)`,
|
||||||
|
expectedError: "system operator: command must be a string scalar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "System operator processes multiple matched nodes",
|
||||||
|
skipDoc: true,
|
||||||
|
document: "a: first",
|
||||||
|
document2: "a: second",
|
||||||
|
expression: `.a = system("` + echoPath + `"; "replaced")`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::a: replaced\n",
|
||||||
|
"D0, P[], (!!map)::a: replaced\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range scenarios {
|
||||||
|
testScenario(t, &tt)
|
||||||
|
}
|
||||||
|
appendOperatorDocumentScenario(t, "system-operators", scenarios)
|
||||||
|
}
|
||||||
@ -36,9 +36,33 @@ func traversePathOperator(_ *dataTreeNavigator, context Context, expressionNode
|
|||||||
return context.ChildContext(matches), nil
|
return context.ChildContext(matches), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveAliasChain follows an alias chain iteratively, returning the
|
||||||
|
// first non-alias node. Returns an error if a cycle is detected.
|
||||||
|
func resolveAliasChain(node *CandidateNode) (*CandidateNode, error) {
|
||||||
|
if node.Kind != AliasNode {
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
visited := map[*CandidateNode]bool{}
|
||||||
|
for node.Kind == AliasNode {
|
||||||
|
if visited[node] {
|
||||||
|
return nil, fmt.Errorf("alias cycle detected")
|
||||||
|
}
|
||||||
|
visited[node] = true
|
||||||
|
log.Debug("its an alias!")
|
||||||
|
node = node.Alias
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
func traverse(context Context, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
|
func traverse(context Context, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
|
||||||
log.Debugf("Traversing %v", NodeToString(matchingNode))
|
log.Debugf("Traversing %v", NodeToString(matchingNode))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
matchingNode, err = resolveAliasChain(matchingNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if matchingNode.Tag == "!!null" && operation.Value != "[]" && !context.DontAutoCreate {
|
if matchingNode.Tag == "!!null" && operation.Value != "[]" && !context.DontAutoCreate {
|
||||||
log.Debugf("Guessing kind")
|
log.Debugf("Guessing kind")
|
||||||
// we must have added this automatically, lets guess what it should be now
|
// we must have added this automatically, lets guess what it should be now
|
||||||
@ -62,10 +86,6 @@ func traverse(context Context, matchingNode *CandidateNode, operation *Operation
|
|||||||
log.Debugf("its a sequence of %v things!", len(matchingNode.Content))
|
log.Debugf("its a sequence of %v things!", len(matchingNode.Content))
|
||||||
return traverseArray(matchingNode, operation, operation.Preferences.(traversePreferences))
|
return traverseArray(matchingNode, operation, operation.Preferences.(traversePreferences))
|
||||||
|
|
||||||
case AliasNode:
|
|
||||||
log.Debug("its an alias!")
|
|
||||||
matchingNode = matchingNode.Alias
|
|
||||||
return traverse(context, matchingNode, operation)
|
|
||||||
default:
|
default:
|
||||||
return list.New(), nil
|
return list.New(), nil
|
||||||
}
|
}
|
||||||
@ -79,7 +99,11 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode
|
|||||||
log.Debugf("--traverseArrayOperator")
|
log.Debugf("--traverseArrayOperator")
|
||||||
|
|
||||||
if expressionNode.RHS != nil && expressionNode.RHS.RHS != nil && expressionNode.RHS.RHS.Operation.OperationType == createMapOpType {
|
if expressionNode.RHS != nil && expressionNode.RHS.RHS != nil && expressionNode.RHS.RHS.Operation.OperationType == createMapOpType {
|
||||||
return sliceArrayOperator(d, context, expressionNode.RHS.RHS)
|
lhsContext, err := d.GetMatchingNodes(context, expressionNode.LHS)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, err
|
||||||
|
}
|
||||||
|
return sliceArrayOperator(d, lhsContext, expressionNode.RHS.RHS)
|
||||||
}
|
}
|
||||||
|
|
||||||
lhs, err := d.GetMatchingNodes(context, expressionNode.LHS)
|
lhs, err := d.GetMatchingNodes(context, expressionNode.LHS)
|
||||||
@ -125,7 +149,13 @@ func traverseNodesWithArrayIndices(context Context, indicesToTraverse []*Candida
|
|||||||
return context.ChildContext(matchingNodeMap), nil
|
return context.ChildContext(matchingNodeMap), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesToTraverse []*CandidateNode, prefs traversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse
|
func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesToTraverse []*CandidateNode, prefs traversePreferences) (*list.List, error) {
|
||||||
|
var err error
|
||||||
|
matchingNode, err = resolveAliasChain(matchingNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if matchingNode.Tag == "!!null" {
|
if matchingNode.Tag == "!!null" {
|
||||||
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
|
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
|
||||||
// auto vivification
|
// auto vivification
|
||||||
@ -138,9 +168,6 @@ func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesT
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch matchingNode.Kind {
|
switch matchingNode.Kind {
|
||||||
case AliasNode:
|
|
||||||
matchingNode = matchingNode.Alias
|
|
||||||
return traverseArrayIndices(context, matchingNode, indicesToTraverse, prefs)
|
|
||||||
case SequenceNode:
|
case SequenceNode:
|
||||||
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
|
|||||||
@ -665,6 +665,16 @@ var traversePathOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[a], (!!null)::null\n",
|
"D0, P[a], (!!null)::null\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/390467412
|
||||||
|
// go-yaml accepts cross-document alias references (invalid per
|
||||||
|
// YAML spec). A nested assignment on such an alias can create a
|
||||||
|
// circular alias node, which must not cause a stack overflow.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "&-- a\n---\n*--",
|
||||||
|
expression: ". = (.x = 1)",
|
||||||
|
expectedError: "alias cycle detected",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTraversePathOperatorScenarios(t *testing.T) {
|
func TestTraversePathOperatorScenarios(t *testing.T) {
|
||||||
@ -682,3 +692,58 @@ func TestTraversePathOperatorAlignedToSpecScenarios(t *testing.T) {
|
|||||||
appendOperatorDocumentScenario(t, "traverse-read", fixedTraversePathOperatorScenarios)
|
appendOperatorDocumentScenario(t, "traverse-read", fixedTraversePathOperatorScenarios)
|
||||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/390467412
|
||||||
|
// A circular alias (alias pointing back to itself) must not cause a
|
||||||
|
// stack overflow. resolveAliasChain should detect the cycle and return
|
||||||
|
// an error; both traverse() and traverseArrayIndices() use it.
|
||||||
|
func TestTraverseAliasCycle(t *testing.T) {
|
||||||
|
aliasNode := &CandidateNode{
|
||||||
|
Kind: AliasNode,
|
||||||
|
}
|
||||||
|
aliasNode.Alias = aliasNode // A -> A
|
||||||
|
|
||||||
|
op := &Operation{
|
||||||
|
OperationType: traversePathOpType,
|
||||||
|
Value: "key",
|
||||||
|
StringValue: "key",
|
||||||
|
Preferences: traversePreferences{},
|
||||||
|
}
|
||||||
|
_, err := traverse(Context{}, aliasNode, op)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same cycle must be caught through the array traversal path.
|
||||||
|
_, err = traverseArrayIndices(Context{}, aliasNode, nil, traversePreferences{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle via traverseArrayIndices, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraverseAliasCycleChain(t *testing.T) {
|
||||||
|
nodeA := &CandidateNode{Kind: AliasNode}
|
||||||
|
nodeB := &CandidateNode{Kind: AliasNode}
|
||||||
|
nodeA.Alias = nodeB
|
||||||
|
nodeB.Alias = nodeA // A -> B -> A
|
||||||
|
|
||||||
|
op := &Operation{
|
||||||
|
OperationType: traversePathOpType,
|
||||||
|
Value: "key",
|
||||||
|
StringValue: "key",
|
||||||
|
Preferences: traversePreferences{},
|
||||||
|
}
|
||||||
|
_, err := traverse(Context{}, nodeA, op)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle chain, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ type expressionScenario struct {
|
|||||||
dontFormatInputForDoc bool // dont format input doc for documentation generation
|
dontFormatInputForDoc bool // dont format input doc for documentation generation
|
||||||
requiresFormat string
|
requiresFormat string
|
||||||
skipForGoccy bool
|
skipForGoccy bool
|
||||||
|
yqFlags string // extra yq flags to include in generated doc command snippets
|
||||||
}
|
}
|
||||||
|
|
||||||
var goccyTesting = false
|
var goccyTesting = false
|
||||||
@ -356,14 +357,22 @@ func documentInput(w *bufio.Writer, s expressionScenario) (string, string) {
|
|||||||
|
|
||||||
writeOrPanic(w, "then\n")
|
writeOrPanic(w, "then\n")
|
||||||
|
|
||||||
|
flagsPrefix := ""
|
||||||
|
if s.yqFlags != "" {
|
||||||
|
flagsPrefix = s.yqFlags + " "
|
||||||
|
}
|
||||||
if s.expression != "" {
|
if s.expression != "" {
|
||||||
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v'%v' %v\n```\n", envCommand, command, strings.ReplaceAll(s.expression, "'", `'\''`), files))
|
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v%v'%v' %v\n```\n", envCommand, flagsPrefix, command, strings.ReplaceAll(s.expression, "'", `'\''`), files))
|
||||||
} else {
|
} else {
|
||||||
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v%v\n```\n", envCommand, command, files))
|
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v%v%v\n```\n", envCommand, flagsPrefix, command, files))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writeOrPanic(w, "Running\n")
|
writeOrPanic(w, "Running\n")
|
||||||
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v--null-input '%v'\n```\n", envCommand, command, s.expression))
|
flagsPrefix := ""
|
||||||
|
if s.yqFlags != "" {
|
||||||
|
flagsPrefix = s.yqFlags + " "
|
||||||
|
}
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```bash\n%vyq %v%v--null-input '%v'\n```\n", envCommand, flagsPrefix, command, s.expression))
|
||||||
}
|
}
|
||||||
return formattedDoc, formattedDoc2
|
return formattedDoc, formattedDoc2
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,6 +202,37 @@ var propertyScenarios = []formatScenario{
|
|||||||
expected: expectedDecodedYaml,
|
expected: expectedDecodedYaml,
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Decode properties with array brackets",
|
||||||
|
input: `user.credentials[0].username=user1
|
||||||
|
user.credentials[0].password=$2b$08$...
|
||||||
|
user.credentials[1].username=user2
|
||||||
|
user.credentials[1].password=$2b$08$...
|
||||||
|
user.credentials[2].username=user3
|
||||||
|
user.credentials[2].password=$2b$10$...`,
|
||||||
|
expected: `user:
|
||||||
|
credentials:
|
||||||
|
- username: user1
|
||||||
|
password: $2b$08$...
|
||||||
|
- username: user2
|
||||||
|
password: $2b$08$...
|
||||||
|
- username: user3
|
||||||
|
password: $2b$10$...
|
||||||
|
`,
|
||||||
|
scenarioType: "decode-array-brackets",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Decode properties with nested array brackets",
|
||||||
|
input: `user.clowns[0][1] = "cool"`,
|
||||||
|
expected: `user:
|
||||||
|
clowns:
|
||||||
|
- - null
|
||||||
|
- '"cool"'
|
||||||
|
`,
|
||||||
|
scenarioType: "decode-array-brackets",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
@ -442,6 +473,12 @@ func TestPropertyScenarios(t *testing.T) {
|
|||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(ConfiguredPropertiesPreferences)), s.description)
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(ConfiguredPropertiesPreferences)), s.description)
|
||||||
case "decode":
|
case "decode":
|
||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description)
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description)
|
||||||
|
case "decode-array-brackets":
|
||||||
|
previousPreferences := ConfiguredPropertiesPreferences.Copy()
|
||||||
|
ConfiguredPropertiesPreferences.UseArrayBrackets = true
|
||||||
|
actual := mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))
|
||||||
|
ConfiguredPropertiesPreferences = previousPreferences
|
||||||
|
test.AssertResultWithContext(t, s.expected, actual, s.description)
|
||||||
case "encode-wrapped":
|
case "encode-wrapped":
|
||||||
prefs := ConfiguredPropertiesPreferences.Copy()
|
prefs := ConfiguredPropertiesPreferences.Copy()
|
||||||
prefs.UnwrapScalar = false
|
prefs.UnwrapScalar = false
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
type SecurityPreferences struct {
|
type SecurityPreferences struct {
|
||||||
DisableEnvOps bool
|
DisableEnvOps bool
|
||||||
DisableFileOps bool
|
DisableFileOps bool
|
||||||
|
EnableSystemOps bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfiguredSecurityPreferences = SecurityPreferences{
|
var ConfiguredSecurityPreferences = SecurityPreferences{
|
||||||
DisableEnvOps: false,
|
DisableEnvOps: false,
|
||||||
DisableFileOps: false,
|
DisableFileOps: false,
|
||||||
|
EnableSystemOps: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,6 +209,50 @@ address = "12 cat st"
|
|||||||
var rtEmptyArray = `A = []
|
var rtEmptyArray = `A = []
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var rtEmptyArrayInTable = `[features]
|
||||||
|
my-feature = []
|
||||||
|
`
|
||||||
|
|
||||||
|
var rtMixedEmptyArraysInTable = `[features]
|
||||||
|
my-other-feature = []
|
||||||
|
my-feature = ["my-other-feature"]
|
||||||
|
`
|
||||||
|
|
||||||
|
var yamlEmptyArrayInTable = `features:
|
||||||
|
my-feature: []
|
||||||
|
`
|
||||||
|
|
||||||
|
var expectedTomlEmptyArrayInTable = `[features]
|
||||||
|
my-feature = []
|
||||||
|
`
|
||||||
|
|
||||||
|
var yamlMixedEmptyArraysInTable = `features:
|
||||||
|
my-other-feature: []
|
||||||
|
my-feature:
|
||||||
|
- my-other-feature
|
||||||
|
`
|
||||||
|
|
||||||
|
var expectedTomlMixedEmptyArraysInTable = `[features]
|
||||||
|
my-other-feature = []
|
||||||
|
my-feature = ["my-other-feature"]
|
||||||
|
`
|
||||||
|
|
||||||
|
var issue2688SampleToml = `[project]
|
||||||
|
name = "some-project"
|
||||||
|
version = "0.5.1"
|
||||||
|
authors = [{name = "Author", email = "author@example.com"}]
|
||||||
|
license = { file = "LICENSE" }
|
||||||
|
readme = "README.md"
|
||||||
|
`
|
||||||
|
|
||||||
|
var issue2688SampleExpected = `[project]
|
||||||
|
name = "some-project"
|
||||||
|
version = "0.5.2"
|
||||||
|
authors = [{ name = "Author", email = "author@example.com" }]
|
||||||
|
license = { file = "LICENSE" }
|
||||||
|
readme = "README.md"
|
||||||
|
`
|
||||||
|
|
||||||
var rtSampleTable = `var = "x"
|
var rtSampleTable = `var = "x"
|
||||||
|
|
||||||
[owner.contact]
|
[owner.contact]
|
||||||
@ -287,6 +331,18 @@ var expectedSubArrays = `array:
|
|||||||
- {}
|
- {}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Keys with special characters that require quoting in TOML
|
||||||
|
var rtSpecialKeyInlineTable = `host = { "http://sealos.hub:5000" = { capabilities = ["pull", "resolve", "push"], skip_verify = true } }
|
||||||
|
`
|
||||||
|
|
||||||
|
var rtSpecialKeyTableSection = `["/tmp/blah"]
|
||||||
|
value = "hello"
|
||||||
|
`
|
||||||
|
|
||||||
|
var rtSpecialKeyDottedTableSection = `[servers."http://localhost:8080"]
|
||||||
|
ip = "127.0.0.1"
|
||||||
|
`
|
||||||
|
|
||||||
var tomlScenarios = []formatScenario{
|
var tomlScenarios = []formatScenario{
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
@ -551,6 +607,36 @@ var tomlScenarios = []formatScenario{
|
|||||||
expected: rtEmptyArray,
|
expected: rtEmptyArray,
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue #2674: roundtrip empty array in table",
|
||||||
|
input: rtEmptyArrayInTable,
|
||||||
|
expression: ".",
|
||||||
|
expected: rtEmptyArrayInTable,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue #2674: roundtrip mixed empty and non-empty arrays in table",
|
||||||
|
input: rtMixedEmptyArraysInTable,
|
||||||
|
expression: ".",
|
||||||
|
expected: rtMixedEmptyArraysInTable,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue #2674: encode empty array in table",
|
||||||
|
input: yamlEmptyArrayInTable,
|
||||||
|
expected: expectedTomlEmptyArrayInTable,
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue #2674: encode mixed empty and non-empty arrays in table",
|
||||||
|
input: yamlMixedEmptyArraysInTable,
|
||||||
|
expected: expectedTomlMixedEmptyArraysInTable,
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Roundtrip: sample table",
|
description: "Roundtrip: sample table",
|
||||||
input: rtSampleTable,
|
input: rtSampleTable,
|
||||||
@ -614,6 +700,80 @@ var tomlScenarios = []formatScenario{
|
|||||||
expected: tomlTableWithComments,
|
expected: tomlTableWithComments,
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
|
// Encode (YAML → TOML) scenarios - verify readable table sections are produced
|
||||||
|
{
|
||||||
|
description: "Encode: Simple mapping produces table section",
|
||||||
|
input: "arg:\n hello: foo\n",
|
||||||
|
expected: "[arg]\nhello = \"foo\"\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Encode: Nested mappings produce nested table sections",
|
||||||
|
input: "a:\n b:\n c: val\n",
|
||||||
|
expected: "[a.b]\nc = \"val\"\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Encode: Mixed scalars and nested mapping",
|
||||||
|
input: "a:\n hello: foo\n nested:\n key: val\n",
|
||||||
|
expected: "[a]\nhello = \"foo\"\n\n[a.nested]\nkey = \"val\"\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Encode: YAML flow mapping produces table section (same as block mapping)",
|
||||||
|
input: "arg: {hello: foo}\n",
|
||||||
|
expected: "[arg]\nhello = \"foo\"\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue: JSON auto-detected via YAML decoder produces table sections",
|
||||||
|
input: `{"arg":{"hello": "foo"}}`,
|
||||||
|
expected: "[arg]\nhello = \"foo\"\n",
|
||||||
|
scenarioType: "encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue: JSON via JSON decoder produces table sections",
|
||||||
|
input: `{"arg":{"hello": "foo"}}`,
|
||||||
|
expected: "[arg]\nhello = \"foo\"\n",
|
||||||
|
scenarioType: "encode-json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Issue 2688: inline table arrays do not change following table scope",
|
||||||
|
input: issue2688SampleToml,
|
||||||
|
expression: `.project.version = "0.5.2"`,
|
||||||
|
expected: issue2688SampleExpected,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Roundtrip: key with special characters in inline table",
|
||||||
|
input: rtSpecialKeyInlineTable,
|
||||||
|
expression: ".",
|
||||||
|
expected: rtSpecialKeyInlineTable,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Roundtrip: key with special characters in table section",
|
||||||
|
input: rtSpecialKeyTableSection,
|
||||||
|
expression: ".",
|
||||||
|
expected: rtSpecialKeyTableSection,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipDoc: true,
|
||||||
|
description: "Roundtrip: special character key in dotted table section header",
|
||||||
|
input: rtSpecialKeyDottedTableSection,
|
||||||
|
expression: ".",
|
||||||
|
expected: rtSpecialKeyDottedTableSection,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTomlScenario(t *testing.T, s formatScenario) {
|
func testTomlScenario(t *testing.T, s formatScenario) {
|
||||||
@ -629,6 +789,10 @@ func testTomlScenario(t *testing.T, s formatScenario) {
|
|||||||
}
|
}
|
||||||
case "roundtrip":
|
case "roundtrip":
|
||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewTomlDecoder(), NewTomlEncoder()), s.description)
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewTomlDecoder(), NewTomlEncoder()), s.description)
|
||||||
|
case "encode":
|
||||||
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder()), s.description)
|
||||||
|
case "encode-json":
|
||||||
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewJSONDecoder(), NewTomlEncoder()), s.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,6 +818,28 @@ func documentTomlDecodeScenario(w *bufio.Writer, s formatScenario) {
|
|||||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewTomlDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
|
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewTomlDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func documentTomlEncodeScenario(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 toml '%v' sample.yml\n```\n", expression))
|
||||||
|
writeOrPanic(w, "will output\n")
|
||||||
|
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```toml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder())))
|
||||||
|
}
|
||||||
|
|
||||||
func documentTomlRoundtripScenario(w *bufio.Writer, s formatScenario) {
|
func documentTomlRoundtripScenario(w *bufio.Writer, s formatScenario) {
|
||||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||||
|
|
||||||
@ -687,6 +873,8 @@ func documentTomlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
|||||||
documentTomlDecodeScenario(w, s)
|
documentTomlDecodeScenario(w, s)
|
||||||
case "roundtrip":
|
case "roundtrip":
|
||||||
documentTomlRoundtripScenario(w, s)
|
documentTomlRoundtripScenario(w, s)
|
||||||
|
case "encode", "encode-json":
|
||||||
|
documentTomlEncodeScenario(w, s)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||||
@ -704,6 +892,60 @@ func TestTomlScenarios(t *testing.T) {
|
|||||||
documentScenarios(t, "usage", "toml", genericScenarios, documentTomlScenario)
|
documentScenarios(t, "usage", "toml", genericScenarios, documentTomlScenario)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTomlEncodeJsonKeepsRootArrayBeforeTables(t *testing.T) {
|
||||||
|
scenario := formatScenario{
|
||||||
|
description: "Encode: JSON root array stays outside later tables",
|
||||||
|
input: `{
|
||||||
|
"_source": {
|
||||||
|
"cookie": [
|
||||||
|
{
|
||||||
|
"Domain": "",
|
||||||
|
"Expires": "0001-01-01T00:00:00Z",
|
||||||
|
"HttpOnly": false,
|
||||||
|
"MaxAge": 0,
|
||||||
|
"Name": "name",
|
||||||
|
"Path": "",
|
||||||
|
"Raw": "",
|
||||||
|
"RawExpires": "",
|
||||||
|
"SameSite": 0,
|
||||||
|
"Secure": false,
|
||||||
|
"Unparsed": null,
|
||||||
|
"Value": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"highlight": {
|
||||||
|
"did": [
|
||||||
|
"did"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sort": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
expected: `sort = [1]
|
||||||
|
|
||||||
|
[[_source.cookie]]
|
||||||
|
Domain = ""
|
||||||
|
Expires = "0001-01-01T00:00:00Z"
|
||||||
|
HttpOnly = false
|
||||||
|
MaxAge = 0
|
||||||
|
Name = "name"
|
||||||
|
Path = ""
|
||||||
|
Raw = ""
|
||||||
|
RawExpires = ""
|
||||||
|
SameSite = 0
|
||||||
|
Secure = false
|
||||||
|
Value = "value"
|
||||||
|
|
||||||
|
[highlight]
|
||||||
|
did = ["did"]
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
test.AssertResultWithContext(t, scenario.expected, mustProcessFormatScenario(scenario, NewJSONDecoder(), NewTomlEncoder()), scenario.description)
|
||||||
|
}
|
||||||
|
|
||||||
// TestTomlColourization tests that colourization correctly distinguishes
|
// TestTomlColourization tests that colourization correctly distinguishes
|
||||||
// between table section headers and inline arrays
|
// between table section headers and inline arrays
|
||||||
func TestTomlColourization(t *testing.T) {
|
func TestTomlColourization(t *testing.T) {
|
||||||
|
|||||||
@ -298,4 +298,9 @@ subsubarray
|
|||||||
Ffile
|
Ffile
|
||||||
Fquery
|
Fquery
|
||||||
coverpkg
|
coverpkg
|
||||||
gsub
|
gsub
|
||||||
|
ralia
|
||||||
|
Austr
|
||||||
|
ustrali
|
||||||
|
héllo
|
||||||
|
alia
|
||||||
|
|||||||
@ -8,6 +8,15 @@
|
|||||||
- git push --tags
|
- git push --tags
|
||||||
- use github actions to publish docker and make github release
|
- use github actions to publish docker and make github release
|
||||||
- check github updated yq action in marketplace
|
- check github updated yq action in marketplace
|
||||||
|
- update github-action/Dockerfile to pin the newly published docker image digest:
|
||||||
|
skopeo inspect docker://docker.io/mikefarah/yq:4 | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Digest'])"
|
||||||
|
then update the FROM line in github-action/Dockerfile with the new digest:
|
||||||
|
FROM mikefarah/yq:4@sha256:<new-digest>
|
||||||
|
|
||||||
|
// release artifacts are signed with cosign keyless signing (Sigstore)
|
||||||
|
// users can verify with:
|
||||||
|
// cosign verify-blob --bundle checksums.bundle checksums
|
||||||
|
// install cosign: brew install cosign OR go install github.com/sigstore/cosign/v2/cmd/cosign@latest
|
||||||
|
|
||||||
|
|
||||||
- snapcraft
|
- snapcraft
|
||||||
|
|||||||
@ -1,3 +1,19 @@
|
|||||||
|
4.53.2:
|
||||||
|
- Fixing release process
|
||||||
|
|
||||||
|
4.53.1:
|
||||||
|
- Releases and tags now signed and immutable!
|
||||||
|
- Add system(command; args) operator (disabled by default) (#2640)
|
||||||
|
- TOML encoder: prefer readable table sections over inline tables (#2649)
|
||||||
|
- Fix TOML encoder to quote keys containing special characters (#2648)
|
||||||
|
- Add string slicing support (#2639)
|
||||||
|
- Fix findInArray misuse on MappingNodes in equality and contains (#2645) Thanks @jandubois!
|
||||||
|
- Fix panic on negative slice indices that underflow after adjustment (#2646) Thanks @jandubois!
|
||||||
|
- Fix stack overflow from circular alias in traverse (#2647) Thanks @jandubois!
|
||||||
|
- Fix panic and OOM in repeatString for large repeat counts (#2644) Thanks @jandubois!
|
||||||
|
- Bumped dependencies
|
||||||
|
|
||||||
|
|
||||||
4.52.5:
|
4.52.5:
|
||||||
- Fix: reset TOML decoder state between files (#2634) thanks @terminalchai
|
- Fix: reset TOML decoder state between files (#2634) thanks @terminalchai
|
||||||
- Fix: preserve original filename when using --front-matter (#2613) thanks @cobyfrombrooklyn-bot
|
- Fix: preserve original filename when using --front-matter (#2613) thanks @cobyfrombrooklyn-bot
|
||||||
|
|||||||
@ -49,5 +49,5 @@ fi
|
|||||||
|
|
||||||
git add cmd/version.go snap/snapcraft.yaml
|
git add cmd/version.go snap/snapcraft.yaml
|
||||||
git commit -m 'Bumping version'
|
git commit -m 'Bumping version'
|
||||||
git tag $version
|
git tag $version -m "releasing"
|
||||||
git tag -f v4
|
git tag -f v4 -m "releasing $version"
|
||||||
@ -4,7 +4,7 @@ set -eo pipefail
|
|||||||
|
|
||||||
# You may need to go install github.com/goreleaser/goreleaser/v2@latest first
|
# You may need to go install github.com/goreleaser/goreleaser/v2@latest first
|
||||||
GORELEASER="goreleaser build --clean"
|
GORELEASER="goreleaser build --clean"
|
||||||
if [ -z "$CI" ]; then
|
if [ -z "$CI" ] || [[ "${GITHUB_REF_NAME:-}" == draft-* ]]; then
|
||||||
GORELEASER+=" --snapshot"
|
GORELEASER+=" --snapshot"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yq
|
name: yq
|
||||||
version: 'v4.52.5'
|
version: 'v4.53.2'
|
||||||
summary: A lightweight and portable command-line data file processor
|
summary: A lightweight and portable command-line data file processor
|
||||||
description: |
|
description: |
|
||||||
`yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files.
|
`yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files.
|
||||||
@ -32,6 +32,6 @@ parts:
|
|||||||
build-environment:
|
build-environment:
|
||||||
- CGO_ENABLED: 0
|
- CGO_ENABLED: 0
|
||||||
source: https://github.com/mikefarah/yq.git
|
source: https://github.com/mikefarah/yq.git
|
||||||
source-tag: v4.52.5
|
source-tag: v4.53.2
|
||||||
build-snaps:
|
build-snaps:
|
||||||
- go/latest/stable
|
- go/latest/stable
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user