Compare commits

..

No commits in common. "master" and "v4.50.1" have entirely different histories.

185 changed files with 930 additions and 6744 deletions

View File

@ -34,13 +34,13 @@ The command you ran:
yq eval-all 'select(fileIndex==0) | .a.b.c' data1.yml data2.yml yq eval-all 'select(fileIndex==0) | .a.b.c' data1.yml data2.yml
``` ```
**Actual behaviour** **Actual behavior**
```yaml ```yaml
cat: meow cat: meow
``` ```
**Expected behaviour** **Expected behavior**
```yaml ```yaml
this: should really work this: should really work

View File

@ -1 +0,0 @@
When you find a bug - make sure to include a new test that exposes the bug, as well as the fix for the bug itself.

View File

@ -20,8 +20,6 @@ on:
schedule: schedule:
- cron: '24 3 * * 1' - cron: '24 3 * * 1'
permissions: {}
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@ -40,11 +38,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/init@v4
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.
@ -55,7 +53,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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/autobuild@v4
# 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
@ -69,4 +67,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/analyze@v4

View File

@ -1,107 +0,0 @@
name: Release Docker GitHub Action
on:
workflow_dispatch:
permissions: {}
jobs:
publishGithubActionDocker:
environment: dockerhub
env:
IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
version: latest
- name: Verify Dockerfile base image digest matches yq:4
run: |
PINNED_DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' github-action/Dockerfile | head -1)
if [ -z "${PINNED_DIGEST}" ]; then
echo "::error::Could not find a sha256 digest in github-action/Dockerfile"
exit 1
fi
LATEST_DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:4" --format '{{printf "%s" .Manifest.Digest}}')
echo "Dockerfile pins: ${PINNED_DIGEST}"
echo "mikefarah/yq:4 is: ${LATEST_DIGEST}"
if [ "${PINNED_DIGEST}" != "${LATEST_DIGEST}" ]; then
echo "::error::github-action/Dockerfile digest does not match the current mikefarah/yq:4 image"
echo "Update the FROM line in github-action/Dockerfile to:"
echo " FROM mikefarah/yq:4@${LATEST_DIGEST}"
exit 1
fi
- name: Resolve version from yq:4
run: |
IMAGE_VERSION=$(docker run --rm "${IMAGE_NAME}:4" --version | awk '{print $NF}' | sed 's/^v//')
if [ -z "${IMAGE_VERSION}" ]; then
echo "::error::Could not determine yq version from ${IMAGE_NAME}:4"
exit 1
fi
echo "Resolved yq version: ${IMAGE_VERSION}"
echo "IMAGE_VERSION=${IMAGE_VERSION}" >> "${GITHUB_ENV}"
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push github-action image
working-directory: github-action
run: |
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64,linux/arm/v7,linux/s390x"
echo "Building and pushing github-action image for version ${IMAGE_VERSION}"
docker buildx build \
--label "org.opencontainers.image.authors=https://github.com/mikefarah/yq/graphs/contributors" \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--label "org.opencontainers.image.description=yq is a portable command-line data file processor" \
--label "org.opencontainers.image.documentation=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.licenses=MIT" \
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
--label "org.opencontainers.image.source=https://github.com/mikefarah/yq" \
--label "org.opencontainers.image.title=yq" \
--label "org.opencontainers.image.url=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.version=${IMAGE_VERSION}" \
--platform "${PLATFORMS}" \
--pull \
--push \
-t "${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "${IMAGE_NAME}:4-githubaction" \
-t "${IMAGE_NAME}:latest-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
.
- name: Report action.yml digest to pin
run: |
GITHUBACTION_DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:4-githubaction" --format '{{printf "%s" .Manifest.Digest}}')
echo "Published ${IMAGE_NAME}:4-githubaction at ${GITHUBACTION_DIGEST}"
echo "Update action.yml image to:"
echo " docker://${IMAGE_NAME}:4-githubaction@${GITHUBACTION_DIGEST}"

View File

@ -7,28 +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:
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 uses: docker/setup-qemu-action@v3
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@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 uses: docker/setup-buildx-action@v3
with: with:
version: latest version: latest
@ -36,13 +31,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@v3
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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -80,3 +75,26 @@ jobs:
-t "ghcr.io/${IMAGE_NAME}:4" \ -t "ghcr.io/${IMAGE_NAME}:4" \
-t "ghcr.io/${IMAGE_NAME}:latest" \ -t "ghcr.io/${IMAGE_NAME}:latest" \
. .
cd github-action
docker buildx build \
--label "org.opencontainers.image.authors=https://github.com/mikefarah/yq/graphs/contributors" \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--label "org.opencontainers.image.description=yq is a portable command-line data file processor" \
--label "org.opencontainers.image.documentation=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.licenses=MIT" \
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
--label "org.opencontainers.image.source=https://github.com/mikefarah/yq" \
--label "org.opencontainers.image.title=yq" \
--label "org.opencontainers.image.url=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.version=${IMAGE_VERSION}" \
--platform "${PLATFORMS}" \
--pull \
--push \
-t "${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "${IMAGE_NAME}:4-githubaction" \
-t "${IMAGE_NAME}:latest-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
.

View File

@ -5,51 +5,25 @@ permissions:
jobs: jobs:
verify-action-digest:
name: Verify action.yml image digest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Verify action.yml digest matches published image
run: |
PINNED_DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' action.yml | head -1)
if [ -z "${PINNED_DIGEST}" ]; then
echo "::error::action.yml does not pin the runtime image by digest"
exit 1
fi
LATEST_DIGEST=$(docker buildx imagetools inspect docker.io/mikefarah/yq:4-githubaction --format '{{printf "%s" .Manifest.Digest}}')
echo "action.yml pins: ${PINNED_DIGEST}"
echo "mikefarah/yq:4-githubaction: ${LATEST_DIGEST}"
if [ "${PINNED_DIGEST}" != "${LATEST_DIGEST}" ]; then
echo "::error::action.yml digest does not match the current mikefarah/yq:4-githubaction image"
echo "Update the image line in action.yml to:"
echo " docker://mikefarah/yq:4-githubaction@${LATEST_DIGEST}"
exit 1
fi
build: build:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@v5
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@v6
- name: Get dependencies - name: Get dependencies
run: | run: |
go get -v -t -d ./... go get -v -t -d ./...
if [ -f Gopkg.toml ]; then if [ -f Gopkg.toml ]; then
curl -sSfL https://raw.githubusercontent.com/golang/dep/1f7c19e5f52f49ffb9f956f64c010be14683468b/install.sh | env DEP_RELEASE_TAG=v0.5.4 sh curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure dep ensure
fi fi

View File

@ -5,17 +5,12 @@ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - uses: actions/setup-go@v5
with: with:
go-version: '^1.20' go-version: '^1.20'
check-latest: true check-latest: true
@ -29,7 +24,7 @@ jobs:
run: echo "VERSION=${GITHUB_REF##*/}" >> "${GITHUB_OUTPUT}" run: echo "VERSION=${GITHUB_REF##*/}" >> "${GITHUB_OUTPUT}"
- name: Generate man page - name: Generate man page
uses: docker://pandoc/core:2.14.2@sha256:04e127c6642a2b9d447c26fe0ac6a5932efa8f508eda9f07da51b6e621dd7c19 uses: docker://pandoc/core:2.14.2
id: gen-man-page id: gen-man-page
with: with:
args: >- args: >-
@ -42,22 +37,14 @@ jobs:
--output=yq.1 --output=yq.1
man.md man.md
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- 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@v2.16.0 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 uses: softprops/action-gh-release@v1
with: with:
files: build/* files: build/*
draft: true draft: true

View File

@ -1,78 +0,0 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '39 7 * * 2'
push:
branches: [ "master" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "Checkout code"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
# file_mode: git
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: results.sarif

View File

@ -7,23 +7,19 @@ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@v6
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 - uses: snapcore/action-build@v1
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@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0 - uses: snapcore/action-publish@v1
env: env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with: with:

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@v6
- name: Get test - name: Get test
id: get_value id: get_value
uses: mikefarah/yq@master uses: mikefarah/yq@master

6
.gitignore vendored
View File

@ -43,11 +43,9 @@ yq*.snap
test.yml test.yml
test*.yml test*.yml
test*.tf
test*.xml test*.xml
test*.toml test*.toml
test*.yaml test*.yaml
*.kyaml
test_dir1/ test_dir1/
test_dir2/ test_dir2/
0.yml 0.yml
@ -70,7 +68,3 @@ debian/files
.vscode .vscode
yq3 yq3
# Golang
.gomodcache/
.gocache/

View File

@ -14,11 +14,6 @@ linters:
- unconvert - unconvert
- unparam - unparam
settings: settings:
misspell:
locale: UK
ignore-rules:
- color
- colors
depguard: depguard:
rules: rules:
prevent_unmaintained_packages: prevent_unmaintained_packages:

View File

@ -39,6 +39,7 @@ builds:
- openbsd_amd64 - openbsd_amd64
- windows_386 - windows_386
- windows_amd64 - windows_amd64
- windows_arm
- windows_arm64 - windows_arm64
no_unique_dist_dir: true no_unique_dist_dir: true

View File

@ -11,7 +11,7 @@ appearance, race, religion, or sexual identity and orientation.
## Our Standards ## Our Standards
Examples of behaviour that contributes to creating a positive environment Examples of behavior that contributes to creating a positive environment
include: include:
* Using welcoming and inclusive language * Using welcoming and inclusive language
@ -20,7 +20,7 @@ include:
* Focusing on what is best for the community * Focusing on what is best for the community
* Showing empathy towards other community members * Showing empathy towards other community members
Examples of unacceptable behaviour by participants include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or * The use of sexualized language or imagery and unwelcome sexual attention or
advances advances
@ -34,13 +34,13 @@ Examples of unacceptable behaviour by participants include:
## Our Responsibilities ## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable Project maintainers are responsible for clarifying the standards of acceptable
behaviour and are expected to take appropriate and fair corrective action in behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behaviour. response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviours that they deem inappropriate, permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful. threatening, offensive, or harmful.
## Scope ## Scope
@ -54,7 +54,7 @@ further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behaviour may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at mikefarah@gmail.com. All reported by contacting the project team at mikefarah@gmail.com. All
complaints will be reviewed and investigated and will result in a response that complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is is deemed necessary and appropriate to the circumstances. The project team is

View File

@ -197,21 +197,6 @@ Note: PRs with small changes (e.g. minor typos) may not be merged (see https://j
make [local] test # Run in Docker container make [local] test # Run in Docker container
``` ```
- **Problem**: Tests fail with a VCS error:
```bash
error obtaining VCS status: exit status 128
Use -buildvcs=false to disable VCS stamping.
```
- **Solution**:
Git security mechanisms prevent Golang from detecting the Git details inside
the container; either build with the `local` option, or pass GOFLAGS to
disable Golang buildvcs behaviour.
```bash
make local test
# OR
make test GOFLAGS='-buildvcs=true'
```
### Documentation Generation Issues ### Documentation Generation Issues
- **Problem**: Generated docs don't update after test changes - **Problem**: Generated docs don't update after test changes
- **Solution**: - **Solution**:

View File

@ -1,4 +1,4 @@
FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3 AS builder FROM golang:1.25.5 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@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS production FROM alpine:3 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

View File

@ -1,4 +1,8 @@
FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3 FROM golang:1.25.5
RUN apt-get update && \
apt-get install -y npm && \
npm install -g npx cspell@latest
COPY scripts/devtools.sh /opt/devtools.sh COPY scripts/devtools.sh /opt/devtools.sh

View File

@ -35,14 +35,13 @@ clean:
## prefix before other make targets to run in your local dev environment ## prefix before other make targets to run in your local dev environment
local: | quiet local: | quiet
@$(eval ENGINERUN= ) @$(eval ENGINERUN= )
@$(eval GOFLAGS="$(GOFLAGS)" )
@mkdir -p tmp @mkdir -p tmp
@touch tmp/dev_image_id @touch tmp/dev_image_id
quiet: # this is silly but shuts up 'Nothing to be done for `local`' quiet: # this is silly but shuts up 'Nothing to be done for `local`'
@: @:
prepare: tmp/dev_image_id prepare: tmp/dev_image_id
tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh _typos.toml tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh
@mkdir -p tmp @mkdir -p tmp
@${ENGINE} rmi -f ${DEV_IMAGE} > /dev/null 2>&1 || true @${ENGINE} rmi -f ${DEV_IMAGE} > /dev/null 2>&1 || true
@${ENGINE} build -t ${DEV_IMAGE} -f Dockerfile.dev . @${ENGINE} build -t ${DEV_IMAGE} -f Dockerfile.dev .

View File

@ -4,7 +4,6 @@ IMPORT_PATH := github.com/mikefarah/${PROJECT}
export GIT_COMMIT = $(shell git rev-parse --short HEAD) export GIT_COMMIT = $(shell git rev-parse --short HEAD)
export GIT_DIRTY = $(shell test -n "$$(git status --porcelain)" && echo "+CHANGES" || true) export GIT_DIRTY = $(shell test -n "$$(git status --porcelain)" && echo "+CHANGES" || true)
export GIT_DESCRIBE = $(shell git describe --tags --always) export GIT_DESCRIBE = $(shell git describe --tags --always)
GOFLAGS :=
LDFLAGS := LDFLAGS :=
LDFLAGS += -X main.GitCommit=${GIT_COMMIT}${GIT_DIRTY} LDFLAGS += -X main.GitCommit=${GIT_COMMIT}${GIT_DIRTY}
LDFLAGS += -X main.GitDescribe=${GIT_DESCRIBE} LDFLAGS += -X main.GitDescribe=${GIT_DESCRIBE}
@ -27,15 +26,13 @@ ifeq ($(CYG_CHECK),1)
else else
# all non-windows environments # all non-windows environments
ROOT := $(shell pwd) ROOT := $(shell pwd)
# Deliberately use `command -v` instead of `which` to be POSIX compliant SELINUX := $(shell which getenforce 2>&1 >/dev/null && echo :z)
SELINUX := $(shell command -v getenforce >/dev/null 2>&1 && echo :z)
endif endif
DEV_IMAGE := ${PROJECT}_dev DEV_IMAGE := ${PROJECT}_dev
ENGINERUN := ${ENGINE} run --rm \ ENGINERUN := ${ENGINE} run --rm \
-e LDFLAGS="${LDFLAGS}" \ -e LDFLAGS="${LDFLAGS}" \
-e GOFLAGS="${GOFLAGS}" \
-e GITHUB_TOKEN="${GITHUB_TOKEN}" \ -e GITHUB_TOKEN="${GITHUB_TOKEN}" \
-v ${ROOT}/vendor:/go/src${SELINUX} \ -v ${ROOT}/vendor:/go/src${SELINUX} \
-v ${ROOT}:/${PROJECT}/src/${IMPORT_PATH}${SELINUX} \ -v ${ROOT}:/${PROJECT}/src/${IMPORT_PATH}${SELINUX} \

View File

@ -3,7 +3,7 @@
![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq) ![CodeQL](https://github.com/mikefarah/yq/workflows/CodeQL/badge.svg) ![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq) ![CodeQL](https://github.com/mikefarah/yq/workflows/CodeQL/badge.svg)
A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, kyaml, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously. A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
yq is written in Go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below. yq is written in Go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below.
@ -363,8 +363,6 @@ gah install yq
- [Load content from other files](https://mikefarah.gitbook.io/yq/operators/load) - [Load content from other files](https://mikefarah.gitbook.io/yq/operators/load)
- [Convert to/from json/ndjson](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert) - [Convert to/from json/ndjson](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
- [Convert to/from xml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/xml) - [Convert to/from xml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/xml)
- [Convert to/from hcl (terraform)](https://mikefarah.gitbook.io/yq/v/v4.x/usage/hcl)
- [Convert to/from toml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/toml)
- [Convert to/from properties](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties) - [Convert to/from properties](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties)
- [Convert to/from csv/tsv](https://mikefarah.gitbook.io/yq/usage/csv-tsv) - [Convert to/from csv/tsv](https://mikefarah.gitbook.io/yq/usage/csv-tsv)
- [General shell completion scripts (bash/zsh/fish/powershell)](https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion) - [General shell completion scripts (bash/zsh/fish/powershell)](https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion)
@ -415,7 +413,7 @@ Flags:
-h, --help help for yq -h, --help help for yq
-I, --indent int sets indent level for output (default 2) -I, --indent int sets indent level for output (default 2)
-i, --inplace update the file in place of first file given. -i, --inplace update the file in place of first file given.
-p, --input-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|lua|l|ini|i] parse format for input. (default "auto") -p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l|ini|i] parse format for input. (default "auto")
--lua-globals output keys as top-level global variables --lua-globals output keys as top-level global variables
--lua-prefix string prefix (default "return ") --lua-prefix string prefix (default "return ")
--lua-suffix string suffix (default ";\n") --lua-suffix string suffix (default ";\n")
@ -424,7 +422,7 @@ Flags:
-N, --no-doc Don't print document separators (---) -N, --no-doc Don't print document separators (---)
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char. -0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch. -n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
-o, --output-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|shell|s|lua|l|ini|i] output format type. (default "auto") -o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l|ini|i] output format type. (default "auto")
-P, --prettyPrint pretty print, shorthand for '... style = ""' -P, --prettyPrint pretty print, shorthand for '... style = ""'
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot) --properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
--properties-separator string separator to use between keys and values (default " = ") --properties-separator string separator to use between keys and values (default " = ")

View File

@ -1,26 +0,0 @@
# 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.

View File

@ -1,20 +0,0 @@
[files]
extend-exclude = ["vendor", "bin"]
[default]
locale = "en"
extend-ignore-identifiers-re = [
"NdJson",
]
[default.extend-identifiers]
AttributeIDSupressMenu = "AttributeIDSupressMenu"
[default.extend-words]
Teh = "Teh"
teh = "teh"
Supress = "Supress"
HashiCorp = "HashiCorp"
Hashi = "Hashi"
fot = "fot"
nd = "nd"

View File

@ -6,7 +6,6 @@ setUp() {
rm test*.csv 2>/dev/null || true rm test*.csv 2>/dev/null || true
rm test*.tsv 2>/dev/null || true rm test*.tsv 2>/dev/null || true
rm test*.xml 2>/dev/null || true rm test*.xml 2>/dev/null || true
rm test*.tf 2>/dev/null || true
} }
testInputProperties() { testInputProperties() {
@ -154,37 +153,6 @@ EOM
assertEquals "$expected" "$X" assertEquals "$expected" "$X"
} }
testInputKYaml() {
cat >test.kyaml <<'EOL'
# leading
{
a: 1, # a line
# head b
b: 2,
c: [
# head d
"d", # d line
],
}
EOL
read -r -d '' expected <<'EOM'
# leading
a: 1 # a line
# head b
b: 2
c:
# head d
- d # d line
EOM
X=$(./yq e -p=kyaml -P test.kyaml)
assertEquals "$expected" "$X"
X=$(./yq ea -p=kyaml -P test.kyaml)
assertEquals "$expected" "$X"
}
@ -287,61 +255,4 @@ EOM
assertEquals "$expected" "$X" assertEquals "$expected" "$X"
} }
testInputTerraform() { source ./scripts/shunit2
cat >test.tf <<EOL
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
tags = {
Environment = "Dev"
Project = "Test"
}
}
EOL
read -r -d '' expected << EOM
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
tags = {
Environment = "Dev"
Project = "Test"
}
}
EOM
X=$(./yq test.tf)
assertEquals "$expected" "$X"
X=$(./yq ea test.tf)
assertEquals "$expected" "$X"
}
testInputTerraformGithubAction() {
cat >test.tf <<EOL
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
tags = {
Environment = "Dev"
Project = "Test"
}
}
EOL
read -r -d '' expected << EOM
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
tags = {
Environment = "Dev"
Project = "Test"
}
}
EOM
X=$(cat /dev/null | ./yq test.tf)
assertEquals "$expected" "$X"
X=$(cat /dev/null | ./yq ea test.tf)
assertEquals "$expected" "$X"
}
source ./scripts/shunit2

View File

@ -280,55 +280,6 @@ EOM
assertEquals "$expected" "$X" assertEquals "$expected" "$X"
} }
testOutputKYaml() {
cat >test.yml <<'EOL'
# leading
a: 1 # a line
# head b
b: 2
c:
# head d
- d # d line
EOL
read -r -d '' expected <<'EOM'
# leading
{
a: 1, # a line
# head b
b: 2,
c: [
# head d
"d", # d line
],
}
EOM
X=$(./yq e --output-format=kyaml test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea --output-format=kyaml test.yml)
assertEquals "$expected" "$X"
}
testOutputKYamlShort() {
cat >test.yml <<EOL
a: b
EOL
read -r -d '' expected <<'EOM'
{
a: "b",
}
EOM
X=$(./yq e -o=ky test.yml)
assertEquals "$expected" "$X"
X=$(./yq ea -o=ky test.yml)
assertEquals "$expected" "$X"
}
testOutputXmComplex() { testOutputXmComplex() {
cat >test.yml <<EOL cat >test.yml <<EOL
a: {b: {c: ["cat", "dog"], +@f: meow}} a: {b: {c: ["cat", "dog"], +@f: meow}}

View File

@ -12,6 +12,6 @@ outputs:
description: "The complete result from the yq command being run" description: "The complete result from the yq command being run"
runs: runs:
using: 'docker' using: 'docker'
image: 'docker://mikefarah/yq:4-githubaction@sha256:e1b8c865f299ea6b02910a7ddf147d5d431244d4cc116f89c2148c9f53822906' image: 'docker://mikefarah/yq:4-githubaction'
args: args:
- ${{ inputs.cmd }} - ${{ inputs.cmd }}

View File

@ -1,95 +1,3 @@
# yq — agent instructions
## ⚠️ MANDATORY: GitHub agent disclosure
**Always required. No exceptions.**
Whenever you perform **any** GitHub action on behalf of the user, you **must** disclose that an AI agent (Cursor) wrote the content and is acting on the user's behalf — **not the user personally**. Do this **before** submitting; never post first and add the disclosure later.
Applies to **all** GitHub interactions, including:
- Pull requests (titles, descriptions, and reviews)
- PR comments and inline review comments
- Issues (new issues, comments, and updates)
- Any other post or reply on GitHub
**How to disclose:** Put it prominently at the **top** of every PR description, review body, comment, or issue. Use wording like:
Inline review comments must include a short disclosure too (e.g. `> Generated by Cursor acting on the user's behalf, not the user personally.`).
**Never** submit a GitHub action without this disclosure.
---
Always run the spellcheck before raising a PR:
```bash
bash scripts/spelling.sh
```
This is also included in the full CI pipeline via `make local test`.
## Cursor Cloud specific instructions
### Overview
**yq** is a Go CLI for querying and transforming YAML, JSON, XML, INI, and other structured formats. There are no long-running services — development is build-and-test against a local `./yq` binary.
### Prerequisites
- **Go ≥ 1.25** (see `go.mod`)
- **Bash** (acceptance tests)
- **Docker/Podman** is optional; use `make local <target>` to run natively when containers are unavailable
### PATH
After `scripts/devtools.sh`, add Go tool binaries to PATH:
```bash
export PATH="$HOME/go/bin:$PATH"
```
`golangci-lint` and `typos` install to `$HOME/go/bin`; `gosec` installs to `./bin/gosec` in the repo root.
### Common commands (local, no Docker)
| Task | Command |
|------|---------|
| Install dev tools | `bash scripts/devtools.sh` |
| Vendor dependencies | `make local vendor` |
| Build binary | `go build -o yq .` or `make local build` |
| Format | `make local format` |
| Lint | `make local check` |
| Unit tests | `make local test` or `bash scripts/test.sh` |
| Acceptance (E2E) | `bash scripts/acceptance.sh` (requires `./yq` built first) |
`make local build` runs the full CI chain (format → spelling → gosec → lint → unit tests → build → acceptance). For a faster loop, build with `go build -o yq .` and run `bash scripts/acceptance.sh`.
### Caveats
- **`make` without `local`** tries Docker/Podman (`Dockerfile.dev`). In Cloud Agent VMs without Docker, always prefix with `make local`.
- **Spelling step** uses `typos` (installed by `scripts/devtools.sh`).
- **`make local test` / `scripts/check.sh`** require `golangci-lint` on PATH (`devtools.sh`).
---
# General rules
✅ **DO:**
- You can use ./yq with the `--debug-node-info` flag to get a deeper understanding of the ast.
- run ./scripts/format.sh to format the code; then ./scripts/check.sh lint and finally ./scripts/spelling.sh to check spelling.
- Add comprehensive tests to cover the changes
- Run test suite to ensure there is no regression
- Use UK english spelling
- **Follow the mandatory GitHub agent disclosure rule above** on every GitHub action — no exceptions
❌ **DON'T:**
- Git add or commit
- Add comments to functions that are self-explanatory
- **Post to GitHub without the mandatory agent disclosure** (PRs, reviews, comments, issues, or any other GitHub interaction)
# Adding a New Encoder/Decoder # Adding a New Encoder/Decoder
This guide explains how to add support for a new format (encoder/decoder) to yq without modifying `candidate_node.go`. This guide explains how to add support for a new format (encoder/decoder) to yq without modifying `candidate_node.go`.
@ -161,7 +69,6 @@ Create a test file `pkg/yqlib/<format>_test.go` using the `formatScenario` patte
- `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation) - `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation)
- Create a helper function `test<Format>Scenario()` that switches on `scenarioType` - Create a helper function `test<Format>Scenario()` that switches on `scenarioType`
- Create main test function `Test<Format>FormatScenarios()` that iterates over scenarios - Create main test function `Test<Format>FormatScenarios()` that iterates over scenarios
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
Test coverage must include: Test coverage must include:
- Basic data types (scalars, arrays, objects/maps) - Basic data types (scalars, arrays, objects/maps)
@ -276,6 +183,14 @@ Tests must be implemented in `<format>_test.go` following the `formatScenario` p
## Common Patterns ## Common Patterns
### Scalar-Only Formats
Some formats only work with scalars (like base64, uri):
```go
if node.guessTagFromCustomType() != "!!str" {
return fmt.Errorf("cannot encode %v as <format>, can only operate on strings", node.Tag)
}
```
### Format with Indentation ### Format with Indentation
Use preferences to control output formatting: Use preferences to control output formatting:
```go ```go
@ -417,7 +332,6 @@ Create `pkg/yqlib/operator_<type>_test.go` using the `expressionScenario` patter
- Include `subdescription` for longer test names - Include `subdescription` for longer test names
- Set `expectedError` if testing error cases - Set `expectedError` if testing error cases
- Create main test function that iterates over scenarios - Create main test function that iterates over scenarios
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
Test coverage must include: Test coverage must include:
- Basic data types and nested structures - Basic data types and nested structures

View File

@ -60,7 +60,7 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
out := cmd.OutOrStdout() out := cmd.OutOrStdout()
if writeInplace { if writeInplace {
// only use colours if its forced // only use colors if its forced
colorsEnabled = forceColor colorsEnabled = forceColor
writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0]) writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0])
out, err = writeInPlaceHandler.CreateTempFile() out, err = writeInPlaceHandler.CreateTempFile()
@ -101,15 +101,12 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
} }
if frontMatter != "" { if frontMatter != "" {
originalFilename := args[0]
frontMatterHandler := yqlib.NewFrontMatterHandler(args[0]) frontMatterHandler := yqlib.NewFrontMatterHandler(args[0])
err = frontMatterHandler.Split() err = frontMatterHandler.Split()
if err != nil { if err != nil {
return err return err
} }
args[0] = frontMatterHandler.GetYamlFrontMatterFilename() args[0] = frontMatterHandler.GetYamlFrontMatterFilename()
yqlib.SetFilenameAlias(args[0], originalFilename)
defer yqlib.ClearFilenameAliases()
if frontMatter == "process" { if frontMatter == "process" {
reader := frontMatterHandler.GetContentReader() reader := frontMatterHandler.GetContentReader()

View File

@ -13,7 +13,6 @@ func TestCreateEvaluateAllCommand(t *testing.T) {
if cmd == nil { if cmd == nil {
t.Fatal("createEvaluateAllCommand returned nil") t.Fatal("createEvaluateAllCommand returned nil")
return
} }
// Test basic command properties // Test basic command properties

View File

@ -74,7 +74,7 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
} }
if writeInplace { if writeInplace {
// only use colours if its forced // only use colors if its forced
colorsEnabled = forceColor colorsEnabled = forceColor
writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0]) writeInPlaceHandler := yqlib.NewWriteInPlaceHandler(args[0])
out, err = writeInPlaceHandler.CreateTempFile() out, err = writeInPlaceHandler.CreateTempFile()
@ -122,15 +122,12 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
if frontMatter != "" { if frontMatter != "" {
yqlib.GetLogger().Debug("using front matter handler") yqlib.GetLogger().Debug("using front matter handler")
originalFilename := args[0]
frontMatterHandler := yqlib.NewFrontMatterHandler(args[0]) frontMatterHandler := yqlib.NewFrontMatterHandler(args[0])
err = frontMatterHandler.Split() err = frontMatterHandler.Split()
if err != nil { if err != nil {
return err return err
} }
args[0] = frontMatterHandler.GetYamlFrontMatterFilename() args[0] = frontMatterHandler.GetYamlFrontMatterFilename()
yqlib.SetFilenameAlias(args[0], originalFilename)
defer yqlib.ClearFilenameAliases()
if frontMatter == "process" { if frontMatter == "process" {
reader := frontMatterHandler.GetContentReader() reader := frontMatterHandler.GetContentReader()

View File

@ -13,7 +13,6 @@ func TestCreateEvaluateSequenceCommand(t *testing.T) {
if cmd == nil { if cmd == nil {
t.Fatal("createEvaluateSequenceCommand returned nil") t.Fatal("createEvaluateSequenceCommand returned nil")
return
} }
// Test basic command properties // Test basic command properties

View File

@ -2,12 +2,12 @@ package cmd
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"strings" "strings"
"github.com/mikefarah/yq/v4/pkg/yqlib" "github.com/mikefarah/yq/v4/pkg/yqlib"
"github.com/spf13/cobra" "github.com/spf13/cobra"
logging "gopkg.in/op/go-logging.v1"
) )
type runeValue rune type runeValue rune
@ -68,21 +68,30 @@ yq -P -oy sample.json
}, },
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
cmd.SetOut(cmd.OutOrStdout()) cmd.SetOut(cmd.OutOrStdout())
level := logging.WARNING
stringFormat := `[%{level}] %{color}%{time:15:04:05}%{color:reset} %{message}`
// when NO_COLOR environment variable presents and not an empty string the coloured output should be disabled; // when NO_COLOR environment variable presents and not an empty string the coloured output should be disabled;
// refer to no-color.org // refer to no-color.org
forceNoColor = forceNoColor || os.Getenv("NO_COLOR") != "" forceNoColor = forceNoColor || os.Getenv("NO_COLOR") != ""
level := slog.LevelWarn if verbose && forceNoColor {
if verbose { level = logging.DEBUG
level = slog.LevelDebug stringFormat = `[%{level:5.5s}] %{time:15:04:05} %{shortfile:-33s} %{shortfunc:-25s} %{message}`
} else if verbose {
level = logging.DEBUG
stringFormat = `[%{level:5.5s}] %{color}%{time:15:04:05}%{color:bold} %{shortfile:-33s} %{shortfunc:-25s}%{color:reset} %{message}`
} else if forceNoColor {
stringFormat = `[%{level}] %{time:15:04:05} %{message}`
} }
yqlib.GetLogger().SetLevel(level) var format = logging.MustStringFormatter(stringFormat)
opts := &slog.HandlerOptions{Level: level, AddSource: verbose} var backend = logging.AddModuleLevel(
handler := slog.NewTextHandler(os.Stderr, opts) logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format))
yqlib.GetLogger().SetSlogger(slog.New(handler))
backend.SetLevel(level, "")
logging.SetBackend(backend)
yqlib.InitExpressionParser() yqlib.InitExpressionParser()
return nil return nil
@ -156,8 +165,6 @@ yq -P -oy sample.json
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})")
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables")
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "ini-preserve-quotes", yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "preserve surrounding quotes on INI values during round-trip")
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values") rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values")
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)")
@ -177,7 +184,7 @@ yq -P -oy sample.json
} }
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit") rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
rootCmd.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the file in place of first file given.") rootCmd.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the file in place of first file given.")
rootCmd.PersistentFlags().VarP(unwrapScalarFlag, "unwrapScalar", "r", "unwrap scalar, print the value with no quotes, colours or comments. Defaults to true for yaml") rootCmd.PersistentFlags().VarP(unwrapScalarFlag, "unwrapScalar", "r", "unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml")
rootCmd.PersistentFlags().Lookup("unwrapScalar").NoOptDefVal = "true" rootCmd.PersistentFlags().Lookup("unwrapScalar").NoOptDefVal = "true"
rootCmd.PersistentFlags().BoolVarP(&nulSepOutput, "nul-output", "0", false, "Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.") rootCmd.PersistentFlags().BoolVarP(&nulSepOutput, "nul-output", "0", false, "Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.")
@ -196,7 +203,6 @@ yq -P -oy sample.json
} }
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.") rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025") rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.CompactSequenceIndent, "yaml-compact-seq-indent", "c", false, "Use compact sequence indentation where '- ' is considered part of the indentation.")
rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.") rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.")
if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil { if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil {
@ -214,7 +220,6 @@ 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(),

View File

@ -195,7 +195,6 @@ func TestNew(t *testing.T) {
if rootCmd == nil { if rootCmd == nil {
t.Fatal("New() returned nil") t.Fatal("New() returned nil")
return
} }
// Test basic command properties // Test basic command properties

View File

@ -3,12 +3,12 @@ package cmd
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"strings" "strings"
"github.com/mikefarah/yq/v4/pkg/yqlib" "github.com/mikefarah/yq/v4/pkg/yqlib"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/op/go-logging.v1"
) )
func isAutomaticOutputFormat() bool { func isAutomaticOutputFormat() bool {
@ -104,8 +104,8 @@ func configureFormats(args []string) error {
return err return err
} }
yqlib.GetLogger().Debugf("Using input format %v", inputFormat) yqlib.GetLogger().Debug("Using input format %v", inputFormat)
yqlib.GetLogger().Debugf("Using output format %v", outputFormat) yqlib.GetLogger().Debug("Using output format %v", outputFormat)
return nil return nil
} }
@ -117,7 +117,7 @@ func configureInputFormat(inputFilename string) error {
_, err := yqlib.FormatFromString(inputFormat) _, err := yqlib.FormatFromString(inputFormat)
if err != nil { if err != nil {
// unknown file type, default to yaml // unknown file type, default to yaml
yqlib.GetLogger().Debugf("Unknown file format extension '%v', defaulting to yaml", inputFormat) yqlib.GetLogger().Debug("Unknown file format extension '%v', defaulting to yaml", inputFormat)
inputFormat = "yaml" inputFormat = "yaml"
if isAutomaticOutputFormat() { if isAutomaticOutputFormat() {
outputFormat = "yaml" outputFormat = "yaml"
@ -132,7 +132,7 @@ func configureInputFormat(inputFilename string) error {
// //
outputFormat = yqlib.FormatStringFromFilename(inputFilename) outputFormat = yqlib.FormatStringFromFilename(inputFilename)
if inputFilename != "-" { if inputFilename != "-" {
yqlib.GetLogger().Warningf("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat) yqlib.GetLogger().Warning("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat)
} }
outputFormat = "yaml" outputFormat = "yaml"
} }
@ -166,9 +166,6 @@ func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {
} }
yqlib.ConfiguredYamlPreferences.EvaluateTogether = evaluateTogether yqlib.ConfiguredYamlPreferences.EvaluateTogether = evaluateTogether
if format.DecoderFactory == nil {
return nil, fmt.Errorf("no support for %s input format", inputFormat)
}
yqlibDecoder := format.DecoderFactory() yqlibDecoder := format.DecoderFactory()
if yqlibDecoder == nil { if yqlibDecoder == nil {
return nil, fmt.Errorf("no support for %s input format", inputFormat) return nil, fmt.Errorf("no support for %s input format", inputFormat)
@ -200,23 +197,17 @@ func configureEncoder() (yqlib.Encoder, error) {
} }
yqlib.ConfiguredXMLPreferences.Indent = indent yqlib.ConfiguredXMLPreferences.Indent = indent
yqlib.ConfiguredYamlPreferences.Indent = indent yqlib.ConfiguredYamlPreferences.Indent = indent
yqlib.ConfiguredKYamlPreferences.Indent = indent
yqlib.ConfiguredJSONPreferences.Indent = indent yqlib.ConfiguredJSONPreferences.Indent = indent
yqlib.ConfiguredYamlPreferences.UnwrapScalar = unwrapScalar yqlib.ConfiguredYamlPreferences.UnwrapScalar = unwrapScalar
yqlib.ConfiguredKYamlPreferences.UnwrapScalar = unwrapScalar
yqlib.ConfiguredPropertiesPreferences.UnwrapScalar = unwrapScalar yqlib.ConfiguredPropertiesPreferences.UnwrapScalar = unwrapScalar
yqlib.ConfiguredJSONPreferences.UnwrapScalar = unwrapScalar yqlib.ConfiguredJSONPreferences.UnwrapScalar = unwrapScalar
yqlib.ConfiguredShellVariablesPreferences.UnwrapScalar = unwrapScalar
yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredKYamlPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredTomlPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators
yqlib.ConfiguredKYamlPreferences.PrintDocSeparators = !noDocSeparators
encoder := yqlibOutputFormat.EncoderFactory() encoder := yqlibOutputFormat.EncoderFactory()
@ -235,7 +226,7 @@ func maybeFile(str string) bool {
yqlib.GetLogger().Debugf("checking '%v' is a file", str) yqlib.GetLogger().Debugf("checking '%v' is a file", str)
stat, err := os.Stat(str) // #nosec stat, err := os.Stat(str) // #nosec
result := err == nil && !stat.IsDir() result := err == nil && !stat.IsDir()
if yqlib.GetLogger().IsEnabledFor(slog.LevelDebug) { if yqlib.GetLogger().IsEnabledFor(logging.DEBUG) {
if err != nil { if err != nil {
yqlib.GetLogger().Debugf("error: %v", err) yqlib.GetLogger().Debugf("error: %v", err)
} else { } else {
@ -280,7 +271,7 @@ func processArgs(originalArgs []string) (string, []string, error) {
if expressionFile == "" && maybeFirstArgIsAFile && strings.HasSuffix(args[0], ".yq") { if expressionFile == "" && maybeFirstArgIsAFile && strings.HasSuffix(args[0], ".yq") {
// lets check if an expression file was given // lets check if an expression file was given
yqlib.GetLogger().Debugf("Assuming arg %v is an expression file", args[0]) yqlib.GetLogger().Debug("Assuming arg %v is an expression file", args[0])
expressionFile = args[0] expressionFile = args[0]
args = args[1:] args = args[1:]
} }
@ -296,7 +287,7 @@ func processArgs(originalArgs []string) (string, []string, error) {
yqlib.GetLogger().Debugf("processed args: %v", args) yqlib.GetLogger().Debugf("processed args: %v", args)
if expression == "" && len(args) > 0 && args[0] != "-" && !maybeFile(args[0]) { if expression == "" && len(args) > 0 && args[0] != "-" && !maybeFile(args[0]) {
yqlib.GetLogger().Debugf("assuming expression is '%v'", args[0]) yqlib.GetLogger().Debug("assuming expression is '%v'", args[0])
expression = args[0] expression = args[0]
args = args[1:] args = args[1:]
} }

View File

@ -911,7 +911,7 @@ func stringsEqual(a, b []string) bool {
return false return false
} }
for i := range a { for i := range a {
if a[i] != b[i] { //nolint:gosec // G602 false positive: b length equality is checked above if a[i] != b[i] {
return false return false
} }
} }
@ -926,13 +926,13 @@ func TestSetupColors(t *testing.T) {
expectColors bool expectColors bool
}{ }{
{ {
name: "force colour enabled", name: "force color enabled",
forceColor: true, forceColor: true,
forceNoColor: false, forceNoColor: false,
expectColors: true, expectColors: true,
}, },
{ {
name: "force no colour enabled", name: "force no color enabled",
forceColor: false, forceColor: false,
forceNoColor: true, forceNoColor: true,
expectColors: false, expectColors: false,

View File

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

View File

@ -1,9 +1,6 @@
package cmd package cmd
import ( import "testing"
"strings"
"testing"
)
func TestGetVersionDisplay(t *testing.T) { func TestGetVersionDisplay(t *testing.T) {
var expectedVersion = ProductName + " (https://github.com/mikefarah/yq/) version " + Version var expectedVersion = ProductName + " (https://github.com/mikefarah/yq/) version " + Version
@ -28,18 +25,6 @@ func TestGetVersionDisplay(t *testing.T) {
} }
func Test_getHumanVersion(t *testing.T) { func Test_getHumanVersion(t *testing.T) {
// Save original values
origGitDescribe := GitDescribe
origGitCommit := GitCommit
origVersionPrerelease := VersionPrerelease
// Restore after test
defer func() {
GitDescribe = origGitDescribe
GitCommit = origGitCommit
VersionPrerelease = origVersionPrerelease
}()
GitDescribe = "e42813d" GitDescribe = "e42813d"
GitCommit = "e42813d+CHANGES" GitCommit = "e42813d+CHANGES"
var wanted string var wanted string
@ -64,118 +49,3 @@ func Test_getHumanVersion(t *testing.T) {
} }
} }
} }
func Test_getHumanVersion_NoGitDescribe(t *testing.T) {
// Save original values
origGitDescribe := GitDescribe
origGitCommit := GitCommit
origVersionPrerelease := VersionPrerelease
// Restore after test
defer func() {
GitDescribe = origGitDescribe
GitCommit = origGitCommit
VersionPrerelease = origVersionPrerelease
}()
GitDescribe = ""
GitCommit = ""
VersionPrerelease = ""
got := getHumanVersion()
if got != Version {
t.Errorf("getHumanVersion() = %v, want %v", got, Version)
}
}
func Test_getHumanVersion_WithPrerelease(t *testing.T) {
// Save original values
origGitDescribe := GitDescribe
origGitCommit := GitCommit
origVersionPrerelease := VersionPrerelease
// Restore after test
defer func() {
GitDescribe = origGitDescribe
GitCommit = origGitCommit
VersionPrerelease = origVersionPrerelease
}()
GitDescribe = ""
GitCommit = "abc123"
VersionPrerelease = "beta"
got := getHumanVersion()
expected := Version + "-beta (abc123)"
if got != expected {
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
}
}
func Test_getHumanVersion_PrereleaseInVersion(t *testing.T) {
// Save original values
origGitDescribe := GitDescribe
origGitCommit := GitCommit
origVersionPrerelease := VersionPrerelease
// Restore after test
defer func() {
GitDescribe = origGitDescribe
GitCommit = origGitCommit
VersionPrerelease = origVersionPrerelease
}()
GitDescribe = "v1.2.3-rc1"
GitCommit = "xyz789"
VersionPrerelease = "rc1"
got := getHumanVersion()
// Should not duplicate "rc1" since it's already in GitDescribe
expected := "v1.2.3-rc1 (xyz789)"
if got != expected {
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
}
}
func Test_getHumanVersion_StripSingleQuotes(t *testing.T) {
// Save original values
origGitDescribe := GitDescribe
origGitCommit := GitCommit
origVersionPrerelease := VersionPrerelease
// Restore after test
defer func() {
GitDescribe = origGitDescribe
GitCommit = origGitCommit
VersionPrerelease = origVersionPrerelease
}()
GitDescribe = "'v1.2.3'"
GitCommit = "'commit123'"
VersionPrerelease = ""
got := getHumanVersion()
// Should strip single quotes
if strings.Contains(got, "'") {
t.Errorf("getHumanVersion() = %v, should not contain single quotes", got)
}
expected := "v1.2.3"
if got != expected {
t.Errorf("getHumanVersion() = %v, want %v", got, expected)
}
}
func TestProductName(t *testing.T) {
if ProductName != "yq" {
t.Errorf("ProductName = %v, want yq", ProductName)
}
}
func TestVersionIsSet(t *testing.T) {
if Version == "" {
t.Error("Version should not be empty")
}
if !strings.HasPrefix(Version, "v") {
t.Errorf("Version %v should start with 'v'", Version)
}
}

14
cspell.config.yaml Normal file
View File

@ -0,0 +1,14 @@
---
$schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json
version: '0.2'
language: en-GB
dictionaryDefinitions:
- name: project-words
path: './project-words.txt'
addWords: true
dictionaries:
- project-words
ignorePaths:
- 'vendor'
- 'bin'
- '/project-words.txt'

View File

@ -1,10 +0,0 @@
# leading
{
a: 1, # a line
# head b
b: 2,
c: [
# head d
"d", # d line
],
}

View File

@ -1,7 +0,0 @@
# leading
a: 1 # a line
# head b
b: 2
c:
# head d
- d # d line

View File

@ -1,27 +0,0 @@
# main.tf
# Define required providers and minimum Terraform version
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.2"
}
# Configure the AWS provider
provider "aws" {
region = var.aws_region
}
# Define an S3 bucket resource
resource "aws_s3_bucket" "example_bucket" {
bucket = var.bucket_name
tags = {
Environment = "Development"
Project = "TerraformExample"
}
}

View File

@ -1,26 +1,6 @@
[[fruits]]
[animals]
# This is a TOML document [[fruits.varieties]] # nested array of tables
name = "red delicious"
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
[servers]
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"

View File

@ -1,4 +1,4 @@
FROM mikefarah/yq:4@sha256:11a1f0b604b13dbbdc662260d8db6f644b22d8553122a25c1b5b2e8713ca6977 FROM mikefarah/yq:4
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh

31
go.mod
View File

@ -6,23 +6,23 @@ require (
github.com/alecthomas/repr v0.5.2 github.com/alecthomas/repr v0.5.2
github.com/dimchansky/utfbom v1.1.1 github.com/dimchansky/utfbom v1.1.1
github.com/elliotchance/orderedmap v1.8.0 github.com/elliotchance/orderedmap v1.8.0
github.com/fatih/color v1.19.0 github.com/fatih/color v1.18.0
github.com/go-ini/ini v1.67.0 github.com/go-ini/ini v1.67.0
github.com/goccy/go-json v0.10.6 github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.19.2 github.com/goccy/go-yaml v1.19.0
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.4.2 github.com/pelletier/go-toml/v2 v2.2.4
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.2 github.com/yuin/gopher-lua v1.1.1
github.com/zclconf/go-cty v1.18.1 github.com/zclconf/go-cty v1.17.0
go.yaml.in/yaml/v4 v4.0.0-rc.6 go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/mod v0.37.0 golang.org/x/net v0.48.0
golang.org/x/net v0.56.0 golang.org/x/text v0.32.0
golang.org/x/text v0.38.0 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
) )
require ( require (
@ -33,9 +33,12 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
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.21.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/tools v0.39.0 // indirect
) )
go 1.25.0 go 1.24.0
toolchain go1.24.1

54
go.sum
View File

@ -18,16 +18,16 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw= github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=
github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
@ -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.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/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,30 @@ 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.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
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.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
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/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 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=

View File

@ -1,30 +0,0 @@
//go:build goinstall
package main
import (
"io"
"testing"
"golang.org/x/mod/module"
"golang.org/x/mod/zip"
)
// TestGoInstallCompatibility ensures the module can be zipped for go install.
// This is an integration test that uses the same zip.CreateFromDir function
// that go install uses internally. If this test fails, go install will fail.
//
// Built with the goinstall tag and run after the main test suite (see scripts/test.sh)
// so it does not race with pkg/yqlib tests that rewrite doc/*.md during execution.
//
// See: https://github.com/mikefarah/yq/issues/2587
func TestGoInstallCompatibility(t *testing.T) {
mod := module.Version{
Path: "github.com/mikefarah/yq/v4",
Version: "v4.0.0", // the actual version doesn't matter for validation
}
if err := zip.CreateFromDir(io.Discard, mod, "."); err != nil {
t.Fatalf("Module cannot be zipped for go install: %v", err)
}
}

View File

@ -54,25 +54,3 @@ func TestAllAtOnceEvaluateNodes(t *testing.T) {
test.AssertResultComplex(t, tt.expected, resultsToString(t, list)) test.AssertResultComplex(t, tt.expected, resultsToString(t, list))
} }
} }
func TestTomlDecoderCanBeReinitializedAcrossDocuments(t *testing.T) {
decoder := NewTomlDecoder()
firstDocuments, err := ReadDocuments(strings.NewReader("id = \"Foobar\"\n"), decoder)
if err != nil {
t.Fatalf("failed to read first TOML document: %v", err)
}
if firstDocuments.Len() != 1 {
t.Fatalf("expected first document count to be 1, got %d", firstDocuments.Len())
}
test.AssertResult(t, "Foobar", firstDocuments.Front().Value.(*CandidateNode).Content[1].Value)
secondDocuments, err := ReadDocuments(strings.NewReader("id = \"Banana\"\n"), decoder)
if err != nil {
t.Fatalf("failed to read second TOML document: %v", err)
}
if secondDocuments.Len() != 1 {
t.Fatalf("expected second document count to be 1, got %d", secondDocuments.Len())
}
test.AssertResult(t, "Banana", secondDocuments.Front().Value.(*CandidateNode).Content[1].Value)
}

View File

@ -27,30 +27,10 @@ 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
if stringValue == "<<" { node.Tag = "!!str"
node.Tag = "!!merge"
} else {
node.Tag = "!!str"
}
return node return node
} }
@ -117,9 +97,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
// EncodeHint controls how a mapping node is serialised by format-specific encoders // For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables
// (e.g. TOML, HCL) that support both inline and block/section representations. // rather than consolidated into nested mappings (default behaviour)
EncodeHint EncodeHint EncodeSeparate bool
} }
func (n *CandidateNode) CreateChild() *CandidateNode { func (n *CandidateNode) CreateChild() *CandidateNode {
@ -300,7 +280,7 @@ func (n *CandidateNode) AddChild(rawChild *CandidateNode) {
func (n *CandidateNode) AddChildren(children []*CandidateNode) { func (n *CandidateNode) AddChildren(children []*CandidateNode) {
if n.Kind == MappingNode { if n.Kind == MappingNode {
for i := 0; i < len(children)-1; i += 2 { for i := 0; i < len(children); i += 2 {
key := children[i] key := children[i]
value := children[i+1] value := children[i+1]
n.AddKeyValueChild(key, value) n.AddKeyValueChild(key, value)
@ -343,11 +323,11 @@ func (n *CandidateNode) guessTagFromCustomType() string {
dataBucket, errorReading := parseSnippet(n.Value) dataBucket, errorReading := parseSnippet(n.Value)
if errorReading != nil { if errorReading != nil {
log.Debugf("guessTagFromCustomType: could not guess underlying tag type %v", errorReading) log.Debug("guessTagFromCustomType: could not guess underlying tag type %v", errorReading)
return n.Tag return n.Tag
} }
guessedTag := dataBucket.Tag guessedTag := dataBucket.Tag
log.Infof("im guessing the tag %v is a %v", n.Tag, guessedTag) log.Info("im guessing the tag %v is a %v", n.Tag, guessedTag)
return guessedTag return guessedTag
} }
@ -431,7 +411,7 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
EvaluateTogether: n.EvaluateTogether, EvaluateTogether: n.EvaluateTogether,
IsMapKey: n.IsMapKey, IsMapKey: n.IsMapKey,
EncodeHint: n.EncodeHint, EncodeSeparate: n.EncodeSeparate,
} }
if cloneContent { if cloneContent {
@ -465,7 +445,7 @@ func (n *CandidateNode) UpdateFrom(other *CandidateNode, prefs assignPreferences
} }
func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignPreferences) { func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignPreferences) {
log.Debugf("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other)) log.Debug("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other))
if n.Kind != other.Kind { if n.Kind != other.Kind {
// clear out the contents when switching to a different type // clear out the contents when switching to a different type
// e.g. map to array // e.g. map to array
@ -485,9 +465,6 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
n.Anchor = other.Anchor n.Anchor = other.Anchor
} }
// Preserve EncodeHint for format-specific encoding hints
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

View File

@ -36,13 +36,13 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
switch commentMapComment.Position { switch commentMapComment.Position {
case yaml.CommentHeadPosition: case yaml.CommentHeadPosition:
o.HeadComment = comment.String() o.HeadComment = comment.String()
log.Debugf("its a head comment %v", comment.String()) log.Debug("its a head comment %v", comment.String())
case yaml.CommentLinePosition: case yaml.CommentLinePosition:
o.LineComment = comment.String() o.LineComment = comment.String()
log.Debugf("its a line comment %v", comment.String()) log.Debug("its a line comment %v", comment.String())
case yaml.CommentFootPosition: case yaml.CommentFootPosition:
o.FootComment = comment.String() o.FootComment = comment.String()
log.Debugf("its a foot comment %v", comment.String()) log.Debug("its a foot comment %v", comment.String())
} }
} }
} }
@ -93,8 +93,8 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
log.Debugf("folded Type %v", astLiteral.Start.Type) log.Debugf("folded Type %v", astLiteral.Start.Type)
o.Style = FoldedStyle o.Style = FoldedStyle
} }
log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.Value) log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Value)
log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.Type) log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Type)
// TODO: here I could put the original value with line breaks // TODO: here I could put the original value with line breaks
// to solve the multiline > problem // to solve the multiline > problem
o.Value = astLiteral.Value.Value o.Value = astLiteral.Value.Value
@ -187,7 +187,7 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
} }
func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error { func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error {
log.Debugf("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key) log.Debug("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key)
// AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling // AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling
// particularly for the anchorMap // particularly for the anchorMap
@ -197,7 +197,7 @@ func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingVa
return err return err
} }
log.Debugf("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value) log.Debug("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value)
if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil { if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil {
return err return err
} }

View File

@ -106,31 +106,6 @@ func TestCreateScalarNodeScenarios(t *testing.T) {
} }
} }
type createStringScalarNodeScenario struct {
stringValue string
expectedTag string
}
var createStringScalarNodeScenarios = []createStringScalarNodeScenario{
{
stringValue: "yourKey",
expectedTag: "!!str",
},
{
stringValue: "<<",
expectedTag: "!!merge",
},
}
func TestCreateStringScalarNodeScenarios(t *testing.T) {
for _, tt := range createStringScalarNodeScenarios {
actual := createStringScalarNode(tt.stringValue)
test.AssertResultWithContext(t, tt.stringValue, actual.Value, fmt.Sprintf("Value for: %v", tt.stringValue))
test.AssertResultWithContext(t, tt.expectedTag, actual.Tag, fmt.Sprintf("Tag for: %v", tt.stringValue))
test.AssertResultWithContext(t, ScalarNode, actual.Kind, fmt.Sprintf("Kind for: %v", tt.stringValue))
}
}
func TestGetKeyForMapValue(t *testing.T) { func TestGetKeyForMapValue(t *testing.T) {
key := createStringScalarNode("yourKey") key := createStringScalarNode("yourKey")
n := CandidateNode{Key: key, Value: "meow", document: 3} n := CandidateNode{Key: key, Value: "meow", document: 3}

View File

@ -55,13 +55,13 @@ func (o *CandidateNode) copyFromYamlNode(node *yaml.Node, anchorMap map[string]*
if o.Anchor != "" { if o.Anchor != "" {
anchorMap[o.Anchor] = o anchorMap[o.Anchor] = o
log.Debugf("set anchor %v to %v", o.Anchor, NodeToString(o)) log.Debug("set anchor %v to %v", o.Anchor, NodeToString(o))
} }
// its a single alias // its a single alias
if node.Alias != nil && node.Alias.Anchor != "" { if node.Alias != nil && node.Alias.Anchor != "" {
o.Alias = anchorMap[node.Alias.Anchor] o.Alias = anchorMap[node.Alias.Anchor]
log.Debugf("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor])) log.Debug("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor]))
} }
o.HeadComment = node.HeadComment o.HeadComment = node.HeadComment
o.LineComment = node.LineComment o.LineComment = node.LineComment
@ -106,7 +106,7 @@ func (o *CandidateNode) UnmarshalYAML(node *yaml.Node, anchorMap map[string]*Can
log.Debugf("UnmarshalYAML %v", node.Tag) log.Debugf("UnmarshalYAML %v", node.Tag)
switch node.Kind { switch node.Kind {
case yaml.AliasNode: case yaml.AliasNode:
log.Debugf("UnmarshalYAML - alias from yaml: %v", o.Tag) log.Debug("UnmarshalYAML - alias from yaml: %v", o.Tag)
o.Kind = AliasNode o.Kind = AliasNode
o.copyFromYamlNode(node, anchorMap) o.copyFromYamlNode(node, anchorMap)
return nil return nil
@ -176,15 +176,15 @@ func (o *CandidateNode) UnmarshalYAML(node *yaml.Node, anchorMap map[string]*Can
} }
func (o *CandidateNode) MarshalYAML() (*yaml.Node, error) { func (o *CandidateNode) MarshalYAML() (*yaml.Node, error) {
log.Debugf("MarshalYAML to yaml: %v", o.Tag) log.Debug("MarshalYAML to yaml: %v", o.Tag)
switch o.Kind { switch o.Kind {
case AliasNode: case AliasNode:
log.Debugf("MarshalYAML - alias to yaml: %v", o.Tag) log.Debug("MarshalYAML - alias to yaml: %v", o.Tag)
target := &yaml.Node{Kind: yaml.AliasNode} target := &yaml.Node{Kind: yaml.AliasNode}
o.copyToYamlNode(target) o.copyToYamlNode(target)
return target, nil return target, nil
case ScalarNode: case ScalarNode:
log.Debugf("MarshalYAML - scalar: %v", o.Value) log.Debug("MarshalYAML - scalar: %v", o.Value)
target := &yaml.Node{Kind: yaml.ScalarNode} target := &yaml.Node{Kind: yaml.ScalarNode}
o.copyToYamlNode(target) o.copyToYamlNode(target)
return target, nil return target, nil

View File

@ -7,9 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"strconv"
"strings"
"github.com/goccy/go-json" "github.com/goccy/go-json"
) )
@ -123,7 +120,7 @@ func (o *CandidateNode) UnmarshalJSON(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
log.Debugf("UnmarshalJSON - scalar is %v", scalar) log.Debug("UnmarshalJSON - scalar is %v", scalar)
return o.setScalarFromJson(scalar) return o.setScalarFromJson(scalar)
@ -143,12 +140,6 @@ 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
@ -186,85 +177,3 @@ 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
}

View File

@ -18,7 +18,7 @@ func changeOwner(info fs.FileInfo, file *os.File) error {
// this happens with snap confinement // this happens with snap confinement
// not really a big issue as users can chown // not really a big issue as users can chown
// the file themselves if required. // the file themselves if required.
log.Infof("Skipping chown: %v", err) log.Info("Skipping chown: %v", err)
} }
} }
return nil return nil

View File

@ -3,8 +3,9 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt" "fmt"
"log/slog"
"time" "time"
logging "gopkg.in/op/go-logging.v1"
) )
type Context struct { type Context struct {
@ -74,7 +75,7 @@ func (n *Context) ChildContext(results *list.List) Context {
} }
func (n *Context) ToString() string { func (n *Context) ToString() string {
if !log.IsEnabledFor(slog.LevelDebug) { if !log.IsEnabledFor(logging.DEBUG) {
return "" return ""
} }
result := fmt.Sprintf("Context\nDontAutoCreate: %v\n", n.DontAutoCreate) result := fmt.Sprintf("Context\nDontAutoCreate: %v\n", n.DontAutoCreate)

View File

@ -2,11 +2,11 @@ package yqlib
import ( import (
"container/list" "container/list"
"log/slog"
"strings" "strings"
"testing" "testing"
"github.com/mikefarah/yq/v4/test" "github.com/mikefarah/yq/v4/test"
logging "gopkg.in/op/go-logging.v1"
) )
func TestChildContext(t *testing.T) { func TestChildContext(t *testing.T) {
@ -155,8 +155,8 @@ func TestToString(t *testing.T) {
test.AssertResultComplex(t, "", result) test.AssertResultComplex(t, "", result)
// Test with debug logging enabled // Test with debug logging enabled
GetLogger().SetLevel(slog.LevelDebug) logging.SetLevel(logging.DEBUG, "")
defer GetLogger().SetLevel(slog.LevelWarn) // Reset to default defer logging.SetLevel(logging.INFO, "") // Reset to default
result2 := context.ToString() result2 := context.ToString()
test.AssertResultComplex(t, true, len(result2) > 0) test.AssertResultComplex(t, true, len(result2) > 0)

View File

@ -2,7 +2,8 @@ package yqlib
import ( import (
"fmt" "fmt"
"log/slog"
logging "gopkg.in/op/go-logging.v1"
) )
type DataTreeNavigator interface { type DataTreeNavigator interface {
@ -54,7 +55,7 @@ func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *Ex
return context, nil return context, nil
} }
log.Debugf("Processing Op: %v", expressionNode.Operation.toString()) log.Debugf("Processing Op: %v", expressionNode.Operation.toString())
if log.IsEnabledFor(slog.LevelDebug) { if log.IsEnabledFor(logging.DEBUG) {
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
log.Debug(NodeToString(el.Value.(*CandidateNode))) log.Debug(NodeToString(el.Value.(*CandidateNode)))
} }

View File

@ -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.EncodeHint = EncodeHintSeparateBlock typeNode.EncodeSeparate = true
} }
} }
current = typeNode current = typeNode

View File

@ -12,20 +12,17 @@ import (
type iniDecoder struct { type iniDecoder struct {
reader io.Reader reader io.Reader
finished bool // Flag to signal completion of processing finished bool // Flag to signal completion of processing
prefs INIPreferences
} }
func NewINIDecoder(prefs INIPreferences) Decoder { func NewINIDecoder() Decoder {
return &iniDecoder{ return &iniDecoder{
finished: false, // Initialise the flag as false finished: false, // Initialize the flag as false
prefs: prefs,
} }
} }
func (dec *iniDecoder) Init(reader io.Reader) error { func (dec *iniDecoder) Init(reader io.Reader) error {
// Store the reader for use in Decode // Store the reader for use in Decode
dec.reader = reader dec.reader = reader
dec.finished = false
return nil return nil
} }
@ -42,10 +39,7 @@ func (dec *iniDecoder) Decode() (*CandidateNode, error) {
} }
// Parse the INI content // Parse the INI content
loadOpts := ini.LoadOptions{ cfg, err := ini.Load(content)
PreserveSurroundedQuote: dec.prefs.PreserveSurroundedQuote,
}
cfg, err := ini.LoadSources(loadOpts, content)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse INI content: %w", err) return nil, fmt.Errorf("failed to parse INI content: %w", err)
} }

View File

@ -16,11 +16,10 @@ 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, prefs: ConfiguredPropertiesPreferences.Copy()} return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
} }
func (dec *propertiesDecoder) Init(reader io.Reader) error { func (dec *propertiesDecoder) Init(reader io.Reader) error {
@ -29,56 +28,20 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error {
return nil return nil
} }
func parsePropKey(key string, prefs PropertiesPreferences) []interface{} { func parsePropKey(key string) []interface{} {
pathStrArray := strings.Split(key, ".") pathStrArray := strings.Split(key, ".")
path := make([]interface{}, 0, len(pathStrArray)) path := make([]interface{}, len(pathStrArray))
for _, pathStr := range pathStrArray { for i, pathStr := range pathStrArray {
path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets) num, err := strconv.ParseInt(pathStr, 10, 32)
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 ""
@ -112,7 +75,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, dec.prefs) path := parsePropKey(key)
propertyComments := properties.GetComments(key) propertyComments := properties.GetComments(key)
if len(propertyComments) > 0 { if len(propertyComments) > 0 {

View File

@ -68,7 +68,7 @@ func mustProcessFormatScenario(s formatScenario, decoder Decoder, encoder Encode
result, err := processFormatScenario(s, decoder, encoder) result, err := processFormatScenario(s, decoder, encoder)
if err != nil { if err != nil {
log.Errorf("Bad scenario %v: %v", s.description, err) log.Error("Bad scenario %v: %w", s.description, err)
return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error()) return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error())
} }
return result return result

View File

@ -8,19 +8,16 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
"time" "time"
toml "github.com/pelletier/go-toml/v2/unstable" toml "github.com/pelletier/go-toml/v2/unstable"
) )
type tomlDecoder struct { type tomlDecoder struct {
parser toml.Parser parser toml.Parser
finished bool finished bool
d DataTreeNavigator d DataTreeNavigator
rootMap *CandidateNode rootMap *CandidateNode
pendingComments []string // Head comments collected from Comment nodes
firstContentSeen bool // Track if we've processed the first non-comment node
} }
func NewTomlDecoder() Decoder { func NewTomlDecoder() Decoder {
@ -31,7 +28,7 @@ func NewTomlDecoder() Decoder {
} }
func (dec *tomlDecoder) Init(reader io.Reader) error { func (dec *tomlDecoder) Init(reader io.Reader) error {
dec.parser = toml.Parser{KeepComments: true} dec.parser = toml.Parser{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
_, err := buf.ReadFrom(reader) _, err := buf.ReadFrom(reader)
if err != nil { if err != nil {
@ -42,24 +39,9 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
} }
dec.pendingComments = make([]string, 0)
dec.firstContentSeen = false
dec.finished = false
return nil return nil
} }
func (dec *tomlDecoder) attachOrphanedCommentsToNode(tableNodeValue *CandidateNode) {
if len(dec.pendingComments) > 0 {
comments := strings.Join(dec.pendingComments, "\n")
if tableNodeValue.HeadComment == "" {
tableNodeValue.HeadComment = comments
} else {
tableNodeValue.HeadComment = tableNodeValue.HeadComment + "\n" + comments
}
dec.pendingComments = make([]string, 0)
}
}
func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} { func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
path := make([]interface{}, 0) path := make([]interface{}, 0)
for { for {
@ -74,24 +56,13 @@ func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error { func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error {
value := tomlNode.Value() value := tomlNode.Value()
path := dec.getFullPath(value.Next()) path := dec.getFullPath(value.Next())
log.Debug("processKeyValueIntoMap: %v", path)
valueNode, err := dec.decodeNode(value) valueNode, err := dec.decodeNode(value)
if err != nil { if err != nil {
return err return err
} }
// Attach pending head comments
if len(dec.pendingComments) > 0 {
valueNode.HeadComment = strings.Join(dec.pendingComments, "\n")
dec.pendingComments = make([]string, 0)
}
// Check for inline comment chained to the KeyValue node
nextNode := tomlNode.Next()
if nextNode != nil && nextNode.Kind == toml.Comment {
valueNode.LineComment = string(nextNode.Data)
}
context := Context{} context := Context{}
context = context.SingleChildContext(rootMap) context = context.SingleChildContext(rootMap)
@ -106,19 +77,15 @@ func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode
for dec.parser.NextExpression() { for dec.parser.NextExpression() {
nextItem := dec.parser.Expression() nextItem := dec.parser.Expression()
log.Debugf("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind) log.Debug("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind)
switch nextItem.Kind { if nextItem.Kind == toml.KeyValue {
case toml.KeyValue:
if err := dec.processKeyValueIntoMap(rootMap, nextItem); err != nil { if err := dec.processKeyValueIntoMap(rootMap, nextItem); err != nil {
return false, err return false, err
} }
case toml.Comment: } else {
// Standalone comment - add to pending for next element
dec.pendingComments = append(dec.pendingComments, string(nextItem.Data))
default:
// run out of key values // run out of key values
log.Debugf("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind) log.Debug("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind)
return true, nil return true, nil
} }
} }
@ -150,39 +117,21 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
} }
return &CandidateNode{ return &CandidateNode{
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
EncodeHint: EncodeHintInline, Content: content,
Content: content,
}, nil }, nil
} }
func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) { func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) {
content := make([]*CandidateNode, 0) content := make([]*CandidateNode, 0)
var pendingArrayComments []string
iterator := tomlNode.Children() iterator := tomlNode.Children()
for iterator.Next() { for iterator.Next() {
child := iterator.Node() child := iterator.Node()
// Handle comments within arrays
if child.Kind == toml.Comment {
// Collect comments to attach to the next array element
pendingArrayComments = append(pendingArrayComments, string(child.Data))
continue
}
yamlNode, err := dec.decodeNode(child) yamlNode, err := dec.decodeNode(child)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Attach any pending comments to this array element
if len(pendingArrayComments) > 0 {
yamlNode.HeadComment = strings.Join(pendingArrayComments, "\n")
pendingArrayComments = make([]string, 0)
}
content = append(content, yamlNode) content = append(content, yamlNode)
} }
@ -272,7 +221,7 @@ func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
currentNode := dec.parser.Expression() currentNode := dec.parser.Expression()
log.Debugf("currentNode: %v ", currentNode.Kind) log.Debug("currentNode: %v ", currentNode.Kind)
runAgainstCurrentExp, err = dec.processTopLevelNode(currentNode) runAgainstCurrentExp, err = dec.processTopLevelNode(currentNode)
if err != nil { if err != nil {
return dec.rootMap, err return dec.rootMap, err
@ -299,43 +248,24 @@ func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error) { func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error) {
var runAgainstCurrentExp bool var runAgainstCurrentExp bool
var err error var err error
log.Debugf("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap)) log.Debug("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
switch currentNode.Kind { switch currentNode.Kind {
case toml.Comment:
// Collect comment to attach to next element
commentText := string(currentNode.Data)
// If we haven't seen any content yet, accumulate comments for root
if !dec.firstContentSeen {
if dec.rootMap.HeadComment == "" {
dec.rootMap.HeadComment = commentText
} else {
dec.rootMap.HeadComment = dec.rootMap.HeadComment + "\n" + commentText
}
} else {
// We've seen content, so these comments are for the next element
dec.pendingComments = append(dec.pendingComments, commentText)
}
return false, nil
case toml.Table: case toml.Table:
dec.firstContentSeen = true
runAgainstCurrentExp, err = dec.processTable(currentNode) runAgainstCurrentExp, err = dec.processTable(currentNode)
case toml.ArrayTable: case toml.ArrayTable:
dec.firstContentSeen = true
runAgainstCurrentExp, err = dec.processArrayTable(currentNode) runAgainstCurrentExp, err = dec.processArrayTable(currentNode)
default: default:
dec.firstContentSeen = true
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode) runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode)
} }
log.Debugf("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap)) log.Debug("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap))
return runAgainstCurrentExp, err return runAgainstCurrentExp, err
} }
func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) { func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
log.Debug("Enter processTable") log.Debug("Enter processTable")
child := currentNode.Child() fullPath := dec.getFullPath(currentNode.Child())
fullPath := dec.getFullPath(child) log.Debug("fullpath: %v", fullPath)
log.Debugf("fullpath: %v", fullPath)
c := Context{} c := Context{}
c = c.SingleChildContext(dec.rootMap) c = c.SingleChildContext(dec.rootMap)
@ -346,53 +276,27 @@ 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),
EncodeHint: EncodeHintSeparateBlock,
}
// Attach pending head comments to the table
if len(dec.pendingComments) > 0 {
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
dec.pendingComments = make([]string, 0)
} }
var tableValue *toml.Node var tableValue *toml.Node
runAgainstCurrentExp := false runAgainstCurrentExp := false
sawKeyValue := false hasValue := dec.parser.NextExpression()
for dec.parser.NextExpression() { // check to see if there is any table data
if hasValue {
tableValue = dec.parser.Expression() tableValue = dec.parser.Expression()
// Allow standalone comments inside the table before the first key-value. // next expression is not table data, so we are done
// These should be associated with the next element in the table (usually the first key-value),
// not treated as "end of table" (which would cause subsequent key-values to be parsed at root).
if tableValue.Kind == toml.Comment {
dec.pendingComments = append(dec.pendingComments, string(tableValue.Data))
continue
}
// next expression is not table data, so we are done (but we need to re-process it at top-level)
if tableValue.Kind != toml.KeyValue { if tableValue.Kind != toml.KeyValue {
log.Debug("got an empty table (or reached next section)") log.Debug("got an empty table")
// If the table had only comments, attach them to the table itself so they don't leak to the next node.
if !sawKeyValue {
dec.attachOrphanedCommentsToNode(tableNodeValue)
}
runAgainstCurrentExp = true runAgainstCurrentExp = true
break } else {
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
if err != nil && !errors.Is(err, io.EOF) {
return false, err
}
} }
sawKeyValue = true
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
if err != nil && !errors.Is(err, io.EOF) {
return false, err
}
break
}
// If we hit EOF after only seeing comments inside this table, attach them to the table itself
// so they don't leak to whatever comes next.
if !sawKeyValue {
dec.attachOrphanedCommentsToNode(tableNodeValue)
} }
err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue) err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue)
@ -403,7 +307,7 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
} }
func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode *CandidateNode) error { func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode *CandidateNode) error {
log.Debugf("arrayAppend to path: %v,%v", path, NodeToString(rhsNode)) log.Debug("arrayAppend to path: %v,%v", path, NodeToString(rhsNode))
rhsCandidateNode := &CandidateNode{ rhsCandidateNode := &CandidateNode{
Kind: SequenceNode, Kind: SequenceNode,
Tag: "!!seq", Tag: "!!seq",
@ -426,9 +330,8 @@ func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode
func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) { func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) {
log.Debug("Enter processArrayTable") log.Debug("Enter processArrayTable")
child := currentNode.Child() fullPath := dec.getFullPath(currentNode.Child())
fullPath := dec.getFullPath(child) log.Debug("Fullpath: %v", fullPath)
log.Debugf("Fullpath: %v", fullPath)
c := Context{} c := Context{}
c = c.SingleChildContext(dec.rootMap) c = c.SingleChildContext(dec.rootMap)
@ -443,64 +346,23 @@ 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",
EncodeHint: EncodeHintSeparateBlock,
} }
// Attach pending head comments to the array table
if len(dec.pendingComments) > 0 {
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
dec.pendingComments = make([]string, 0)
}
runAgainstCurrentExp := false runAgainstCurrentExp := false
sawKeyValue := false // if the next value is a ArrayTable or Table, then its not part of this declaration (not a key value pair)
if hasValue { // so lets leave that expression for the next round of parsing
for { if hasValue && (dec.parser.Expression().Kind == toml.ArrayTable || dec.parser.Expression().Kind == toml.Table) {
exp := dec.parser.Expression() runAgainstCurrentExp = true
// Allow standalone comments inside array tables before the first key-value. } else if hasValue {
if exp.Kind == toml.Comment { // otherwise, if there is a value, it must be some key value pairs of the
dec.pendingComments = append(dec.pendingComments, string(exp.Data)) // first object in the array!
hasValue = dec.parser.NextExpression() tableValue := dec.parser.Expression()
if !hasValue { runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
break if err != nil && !errors.Is(err, io.EOF) {
} return false, err
continue
}
// if the next value is a ArrayTable or Table, then its not part of this declaration (not a key value pair)
// so lets leave that expression for the next round of parsing
if exp.Kind == toml.ArrayTable || exp.Kind == toml.Table {
// If this array-table entry had only comments, attach them to the entry so they don't leak.
if !sawKeyValue {
dec.attachOrphanedCommentsToNode(tableNodeValue)
}
runAgainstCurrentExp = true
break
}
sawKeyValue = true
// otherwise, if there is a value, it must be some key value pairs of the
// first object in the array!
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, exp)
if err != nil && !errors.Is(err, io.EOF) {
return false, err
}
break
} }
} }
// If we hit EOF after only seeing comments inside this array-table entry, attach them to the entry
// so they don't leak to whatever comes next.
if !sawKeyValue && len(dec.pendingComments) > 0 {
comments := strings.Join(dec.pendingComments, "\n")
if tableNodeValue.HeadComment == "" {
tableNodeValue.HeadComment = comments
} else {
tableNodeValue.HeadComment = tableNodeValue.HeadComment + "\n" + comments
}
dec.pendingComments = make([]string, 0)
}
// += function // += function
err = dec.arrayAppend(c, fullPath, tableNodeValue) err = dec.arrayAppend(c, fullPath, tableNodeValue)
@ -513,42 +375,23 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
// Because TOML. So we'll inject the last index into the path. // Because TOML. So we'll inject the last index into the path.
func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) { func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) {
// We need to check the entire path (except the last element), not just the immediate parent, pathToCheck := fullPath
// because we may have nested array tables like [[array.subarray.subsubarray]] if len(fullPath) >= 1 {
// where both 'array' and 'subarray' are arrays that already exist. pathToCheck = fullPath[:len(fullPath)-1]
if len(fullPath) == 0 {
return fullPath, nil
} }
readOp := createTraversalTree(pathToCheck, traversePreferences{DontAutoCreate: true}, false)
resultPath := make([]interface{}, 0, len(fullPath)*2) // preallocate with extra space for indices resultContext, err := dec.d.GetMatchingNodes(c, readOp)
if err != nil {
// Process all segments except the last one return nil, err
for i := 0; i < len(fullPath)-1; i++ { }
resultPath = append(resultPath, fullPath[i]) if resultContext.MatchingNodes.Len() >= 1 {
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
// Check if the current path segment points to an array // path refers to an array, we need to add this to the last element in the array
readOp := createTraversalTree(resultPath, traversePreferences{DontAutoCreate: true}, false) if match.Kind == SequenceNode {
resultContext, err := dec.d.GetMatchingNodes(c, readOp) fullPath = append(pathToCheck, len(match.Content)-1, fullPath[len(fullPath)-1])
if err != nil { log.Debugf("Adding to end of %v array, using path: %v", pathToCheck, fullPath)
return nil, err
}
if resultContext.MatchingNodes.Len() >= 1 {
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
// If this segment points to an array, we need to add the last index
// before continuing with the rest of the path
if match.Kind == SequenceNode && len(match.Content) > 0 {
lastIndex := len(match.Content) - 1
resultPath = append(resultPath, lastIndex)
log.Debugf("Path segment %v is an array, injecting index %d", resultPath[:len(resultPath)-1], lastIndex)
}
} }
} }
return fullPath, err
// Add the last segment
resultPath = append(resultPath, fullPath[len(fullPath)-1])
log.Debugf("getPathToUse: original path %v -> result path %v", fullPath, resultPath)
return resultPath, nil
} }

View File

@ -1,160 +0,0 @@
//go:build !yq_nouri
package yqlib
import (
"io"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func TestUriDecoder_Init(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("test")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
}
func TestUriDecoder_DecodeSimpleString(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("hello%20world")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "!!str", node.Tag)
test.AssertResult(t, "hello world", node.Value)
}
func TestUriDecoder_DecodeSpecialCharacters(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("hello%21%40%23%24%25")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "hello!@#$%", node.Value)
}
func TestUriDecoder_DecodeUTF8(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("%E2%9C%93%20check")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "✓ check", node.Value)
}
func TestUriDecoder_DecodePlusSign(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("a+b")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
// Note: url.QueryUnescape does NOT convert + to space
// That's only for form encoding (url.ParseQuery)
test.AssertResult(t, "a b", node.Value)
}
func TestUriDecoder_DecodeEmptyString(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "", node.Value)
// Second decode should return EOF
node, err = decoder.Decode()
test.AssertResult(t, io.EOF, err)
test.AssertResult(t, (*CandidateNode)(nil), node)
}
func TestUriDecoder_DecodeMultipleCalls(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("test")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
// First decode
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "test", node.Value)
// Second decode should return EOF since we've consumed all input
node, err = decoder.Decode()
test.AssertResult(t, io.EOF, err)
test.AssertResult(t, (*CandidateNode)(nil), node)
}
func TestUriDecoder_DecodeInvalidEscape(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("test%ZZ")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
_, err = decoder.Decode()
// Should return an error for invalid escape sequence
if err == nil {
t.Error("Expected error for invalid escape sequence, got nil")
}
}
func TestUriDecoder_DecodeSlashAndQuery(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("path%2Fto%2Ffile%3Fquery%3Dvalue")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "path/to/file?query=value", node.Value)
}
func TestUriDecoder_DecodePercent(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("100%25")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "100%", node.Value)
}
func TestUriDecoder_DecodeNoEscaping(t *testing.T) {
decoder := NewUriDecoder()
reader := strings.NewReader("simple_text-123")
err := decoder.Init(reader)
test.AssertResult(t, nil, err)
node, err := decoder.Decode()
test.AssertResult(t, nil, err)
test.AssertResult(t, "simple_text-123", node.Value)
}
// Mock reader that returns an error
type errorReader struct{}
func (e *errorReader) Read(_ []byte) (n int, err error) {
return 0, io.ErrUnexpectedEOF
}
func TestUriDecoder_DecodeReadError(t *testing.T) {
decoder := NewUriDecoder()
err := decoder.Init(&errorReader{})
test.AssertResult(t, nil, err)
_, err = decoder.Decode()
test.AssertResult(t, io.ErrUnexpectedEOF, err)
}

View File

@ -64,7 +64,7 @@ func (dec *xmlDecoder) processComment(c string) string {
} }
func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) { func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
log.Debugf("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment) log.Debug("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment)
yamlNode := &CandidateNode{Kind: MappingNode, Tag: "!!map"} yamlNode := &CandidateNode{Kind: MappingNode, Tag: "!!map"}
if len(n.Data) > 0 { if len(n.Data) > 0 {
@ -92,7 +92,7 @@ func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
log.Debugf("label=%v, i=%v, keyValuePair.FootComment: %v", label, i, keyValuePair.FootComment) log.Debugf("label=%v, i=%v, keyValuePair.FootComment: %v", label, i, keyValuePair.FootComment)
labelNode.FootComment = dec.processComment(keyValuePair.FootComment) labelNode.FootComment = dec.processComment(keyValuePair.FootComment)
log.Debugf("len of children in %v is %v", label, len(children)) log.Debug("len of children in %v is %v", label, len(children))
if len(children) > 1 { if len(children) > 1 {
valueNode, err = dec.createSequence(children) valueNode, err = dec.createSequence(children)
if err != nil { if err != nil {
@ -105,7 +105,7 @@ func (dec *xmlDecoder) createMap(n *xmlNode) (*CandidateNode, error) {
if len(children[0].Children) == 0 && children[0].HeadComment != "" { if len(children[0].Children) == 0 && children[0].HeadComment != "" {
if len(children[0].Data) > 0 { if len(children[0].Data) > 0 {
log.Debugf("scalar comment hack, currentlabel [%v]", labelNode.HeadComment) log.Debug("scalar comment hack, currentlabel [%v]", labelNode.HeadComment)
labelNode.HeadComment = joinComments([]string{labelNode.HeadComment, strings.TrimSpace(children[0].HeadComment)}, "\n") labelNode.HeadComment = joinComments([]string{labelNode.HeadComment, strings.TrimSpace(children[0].HeadComment)}, "\n")
children[0].HeadComment = "" children[0].HeadComment = ""
} else { } else {
@ -151,7 +151,7 @@ func (dec *xmlDecoder) convertToYamlNode(n *xmlNode) (*CandidateNode, error) {
scalar := dec.createValueNodeFromData(n.Data) scalar := dec.createValueNodeFromData(n.Data)
log.Debugf("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment) log.Debug("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment)
scalar.HeadComment = dec.processComment(n.HeadComment) scalar.HeadComment = dec.processComment(n.HeadComment)
scalar.LineComment = dec.processComment(n.LineComment) scalar.LineComment = dec.processComment(n.LineComment)
if scalar.Tag == "!!seq" { if scalar.Tag == "!!seq" {
@ -211,17 +211,17 @@ func (n *xmlNode) AddChild(s string, c *xmlNode) {
if n.Children == nil { if n.Children == nil {
n.Children = make([]*xmlChildrenKv, 0) n.Children = make([]*xmlChildrenKv, 0)
} }
log.Debugf("looking for %s", s) log.Debug("looking for %s", s)
// see if we can find an existing entry to add to // see if we can find an existing entry to add to
for _, childEntry := range n.Children { for _, childEntry := range n.Children {
if childEntry.K == s { if childEntry.K == s {
log.Debugf("found it, appending an entry%s", s) log.Debug("found it, appending an entry%s", s)
childEntry.V = append(childEntry.V, c) childEntry.V = append(childEntry.V, c)
log.Debugf("yay len of children in %v is %v", s, len(childEntry.V)) log.Debug("yay len of children in %v is %v", s, len(childEntry.V))
return return
} }
} }
log.Debugf("not there, making a new one %s", s) log.Debug("not there, making a new one %s", s)
n.Children = append(n.Children, &xmlChildrenKv{K: s, V: []*xmlNode{c}}) n.Children = append(n.Children, &xmlChildrenKv{K: s, V: []*xmlNode{c}})
} }
@ -267,7 +267,7 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
switch se := t.(type) { switch se := t.(type) {
case xml.StartElement: case xml.StartElement:
log.Debugf("start element %v", se.Name.Local) log.Debug("start element %v", se.Name.Local)
elem.state = "started" elem.state = "started"
// Build new a new current element and link it to its parent // Build new a new current element and link it to its parent
var label = se.Name.Local var label = se.Name.Local
@ -302,14 +302,14 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
if len(newBit) > 0 { if len(newBit) > 0 {
elem.n.Data = append(elem.n.Data, newBit) elem.n.Data = append(elem.n.Data, newBit)
elem.state = "chardata" elem.state = "chardata"
log.Debugf("chardata [%v] for %v", elem.n.Data, elem.label) log.Debug("chardata [%v] for %v", elem.n.Data, elem.label)
} }
case xml.EndElement: case xml.EndElement:
if elem == nil { if elem == nil {
log.Debug("no element, probably bad xml") log.Debug("no element, probably bad xml")
continue continue
} }
log.Debugf("end element %v", elem.label) log.Debug("end element %v", elem.label)
elem.state = "finished" elem.state = "finished"
// And add it to its parent list // And add it to its parent list
if elem.parent != nil { if elem.parent != nil {
@ -326,10 +326,10 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
applyFootComment(elem, commentStr) applyFootComment(elem, commentStr)
case "chardata": case "chardata":
log.Debugf("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr) log.Debug("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ") elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ")
default: default:
log.Debugf("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr) log.Debug("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.HeadComment = joinComments([]string{elem.n.HeadComment, commentStr}, " ") elem.n.HeadComment = joinComments([]string{elem.n.HeadComment, commentStr}, " ")
} }
@ -354,7 +354,7 @@ func applyFootComment(elem *element, commentStr string) {
if len(elem.n.Children) > 0 { if len(elem.n.Children) > 0 {
lastChildIndex := len(elem.n.Children) - 1 lastChildIndex := len(elem.n.Children) - 1
childKv := elem.n.Children[lastChildIndex] childKv := elem.n.Children[lastChildIndex]
log.Debugf("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr) log.Debug("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr)
// if it's an array of scalars, put the foot comment on the scalar itself // if it's an array of scalars, put the foot comment on the scalar itself
if len(childKv.V) > 0 && len(childKv.V[0].Children) == 0 { if len(childKv.V) > 0 && len(childKv.V[0].Children) == 0 {
nodeToUpdate := childKv.V[len(childKv.V)-1] nodeToUpdate := childKv.V[len(childKv.V)-1]
@ -363,7 +363,7 @@ func applyFootComment(elem *element, commentStr string) {
childKv.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ") childKv.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ")
} }
} else { } else {
log.Debugf("got a foot comment for %v: [%v]", elem.label, commentStr) log.Debug("got a foot comment for %v: [%v]", elem.label, commentStr)
elem.n.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ") elem.n.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ")
} }
} }

View File

@ -78,7 +78,7 @@ func (dec *yamlDecoder) processReadStream(reader *bufio.Reader) (io.Reader, stri
if separatorPrefixRe.MatchString(line) { if separatorPrefixRe.MatchString(line) {
match := separatorPrefixRe.FindString(line) match := separatorPrefixRe.FindString(line)
remainder := line[len(match):] remainder := line[len(match):]
// normalise separator newline: if original had none, default to LF // normalize separator newline: if original had none, default to LF
sepNewline := newline sepNewline := newline
if sepNewline == "" { if sepNewline == "" {
sepNewline = "\n" sepNewline = "\n"

View File

@ -2,7 +2,7 @@
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names). Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names).
`yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`. `yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
## NOTE --yaml-fix-merge-anchor-to-spec flag ## NOTE --yaml-fix-merge-anchor-to-spec flag
@ -22,7 +22,7 @@ see https://yaml.org/type/merge.html
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- &CENTRE - &CENTER
x: 1 x: 1
y: 2 y: 2
- &LEFT - &LEFT
@ -32,7 +32,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- <<: *CENTRE - !!merge <<: *CENTER
r: 10 r: 10
``` ```
then then
@ -213,10 +213,10 @@ item_value: &item_value
value: true value: true
thingOne: thingOne:
name: item_1 name: item_1
<<: *item_value !!merge <<: *item_value
thingTwo: thingTwo:
name: item_2 name: item_2
<<: *item_value !!merge <<: *item_value
``` ```
then then
```bash ```bash
@ -231,7 +231,7 @@ thingOne:
value: false value: false
thingTwo: thingTwo:
name: item_2 name: item_2
<<: *item_value !!merge <<: *item_value
``` ```
## LEGACY: Explode with merge anchors ## LEGACY: Explode with merge anchors
@ -249,13 +249,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -288,7 +288,7 @@ see https://yaml.org/type/merge.html. This has the correct data, but the wrong k
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- &CENTRE - &CENTER
x: 1 x: 1
y: 2 y: 2
- &LEFT - &LEFT
@ -298,8 +298,8 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- <<: - !!merge <<:
- *CENTRE - *CENTER
- *BIG - *BIG
``` ```
then then
@ -318,7 +318,7 @@ see https://yaml.org/type/merge.html. This has the correct data, but the wrong k
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- &CENTRE - &CENTER
x: 1 x: 1
y: 2 y: 2
- &LEFT - &LEFT
@ -328,7 +328,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- <<: - !!merge <<:
- *BIG - *BIG
- *LEFT - *LEFT
- *SMALL - *SMALL
@ -361,13 +361,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -401,7 +401,7 @@ Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- &CENTRE - &CENTER
x: 1 x: 1
y: 2 y: 2
- &LEFT - &LEFT
@ -411,8 +411,8 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- <<: - !!merge <<:
- *CENTRE - *CENTER
- *BIG - *BIG
``` ```
then then
@ -432,7 +432,7 @@ Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- &CENTRE - &CENTER
x: 1 x: 1
y: 2 y: 2
- &LEFT - &LEFT
@ -442,7 +442,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- <<: - !!merge <<:
- *BIG - *BIG
- *LEFT - *LEFT
- *SMALL - *SMALL
@ -467,7 +467,7 @@ Given a sample.yml file of:
```yaml ```yaml
a: a:
b: &b 42 b: &b 42
<<: !!merge <<:
c: *b c: *b
``` ```
then then

View File

@ -2,7 +2,7 @@
Various operators for parsing and manipulating dates. Various operators for parsing and manipulating dates.
## Date time formatting ## Date time formattings
This uses Golang's built in time library for parsing and formatting date times. This uses Golang's built in time library for parsing and formatting date times.
When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing. When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing.

View File

@ -55,7 +55,7 @@ yq '.a = .a / 0 | .b = .b / 0' sample.yml
``` ```
will output will output
```yaml ```yaml
a: +Inf a: !!float +Inf
b: -Inf b: !!float -Inf
``` ```

View File

@ -2,7 +2,7 @@
Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names). Use the `alias` and `anchor` operators to read and write yaml aliases and anchors. The `explode` operator normalises a yaml file (dereference (or expands) aliases and remove anchor names).
`yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`. `yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
## NOTE --yaml-fix-merge-anchor-to-spec flag ## NOTE --yaml-fix-merge-anchor-to-spec flag

View File

@ -2,7 +2,7 @@
Various operators for parsing and manipulating dates. Various operators for parsing and manipulating dates.
## Date time formatting ## Date time formattings
This uses Golang's built in time library for parsing and formatting date times. This uses Golang's built in time library for parsing and formatting date times.
When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing. When not specified, the RFC3339 standard is assumed `2006-01-02T15:04:05Z07:00` for parsing.

View File

@ -1,5 +1,5 @@
# Slice Array or String # Slice/Splice 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. 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.
You may leave out the first or second number, which will refer to the start or end of the array or string respectively. You may leave out the first or second number, which will refer to the start or end of the array respectively.

View File

@ -1,27 +0,0 @@
# 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.

View File

@ -34,7 +34,7 @@ yq '.a = .a % .b' sample.yml
``` ```
will output will output
```yaml ```yaml
a: 2 a: !!float 2
b: 2.5 b: 2.5
``` ```
@ -69,7 +69,7 @@ yq '.a = .a % .b' sample.yml
``` ```
will output will output
```yaml ```yaml
a: NaN a: !!float NaN
b: 0 b: 0
``` ```

View File

@ -471,13 +471,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -487,7 +487,7 @@ yq '.foobar * .foobarList' sample.yml
will output will output
```yaml ```yaml
c: foobarList_c c: foobarList_c
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
thing: foobar_thing thing: foobar_thing

View File

@ -79,46 +79,6 @@ will output
c: cat c: cat
``` ```
## Get the top (root) parent
Use negative numbers to get the top parents. You can think of this as indexing into the 'parents' array above
Given a sample.yml file of:
```yaml
a:
b:
c: cat
```
then
```bash
yq '.a.b.c | parent(-1)' sample.yml
```
will output
```yaml
a:
b:
c: cat
```
## Root
Alias for parent(-1), returns the top level parent. This is usually the document node.
Given a sample.yml file of:
```yaml
a:
b:
c: cat
```
then
```bash
yq '.a.b.c | root' sample.yml
```
will output
```yaml
a:
b:
c: cat
```
## N-th parent ## N-th parent
You can optionally supply the number of levels to go up for the parent, the default being 1. You can optionally supply the number of levels to go up for the parent, the default being 1.
@ -156,25 +116,6 @@ a:
c: cat c: cat
``` ```
## N-th negative
Similarly, use negative numbers to index backwards from the parents array
Given a sample.yml file of:
```yaml
a:
b:
c: cat
```
then
```bash
yq '.a.b.c | parent(-2)' sample.yml
```
will output
```yaml
b:
c: cat
```
## No parent ## No parent
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml

View File

@ -131,13 +131,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -147,7 +147,7 @@ yq '.foobar | [..]' sample.yml
will output will output
```yaml ```yaml
- c: foobar_c - c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
- foobar_c - foobar_c
- *foo - *foo

View File

@ -1,8 +1,8 @@
# Slice Array or String # Slice/Splice 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. 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.
You may leave out the first or second number, which will refer to the start or end of the array or string respectively. You may leave out the first or second number, which will refer to the start or end of the array respectively.
## Slicing arrays ## Slicing arrays
Given a sample.yml file of: Given a sample.yml file of:
@ -103,81 +103,3 @@ 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
```

View File

@ -1,76 +0,0 @@
# System Operators
The `system` operator allows you to run an external command and use its output as a value in your expression.
**Security warning**: The system operator is disabled by default. You must explicitly pass `--security-enable-system-operator` to use it.
**Note:** When enabled, the system operator can replicate the functionality of `env` and `load`
operators via external commands. Enabling it effectively overrides `--security-disable-env-ops`
and `--security-disable-file-ops`.
## Usage
```bash
yq --security-enable-system-operator --null-input '.field = system("command"; "arg1")'
```
The operator takes:
- A command string (required)
- An argument (or an array of arguments), separated from the command by `;` (optional)
The current matched node's value is serialised and piped to the command via stdin. The command's stdout (with trailing newline stripped) is returned as a string.
## Disabling the system operator
The system operator is disabled by default. When disabled, an error is returned instead of running the command, consistent with `--security-disable-env-ops` and `--security-disable-file-ops`.
Use `--security-enable-system-operator` flag to enable it.
## system operator returns error when disabled
Use `--security-enable-system-operator` to enable the system operator.
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country = system("/usr/bin/echo"; "test")' sample.yml
```
will output
```bash
Error: system operations are disabled, use --security-enable-system-operator to enable
```
## Run a command with an argument
Use `--security-enable-system-operator` to enable the system operator.
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq --security-enable-system-operator '.country = system("/bin/echo"; "test")' sample.yml
```
will output
```yaml
country: test
```
## Run a command without arguments
Omit the semicolon and args to run the command with no extra arguments.
Given a sample.yml file of:
```yaml
a: hello
```
then
```bash
yq --security-enable-system-operator '.a = system("/bin/echo")' sample.yml
```
will output
```yaml
a: ""
```

View File

@ -294,13 +294,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -325,13 +325,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -376,13 +376,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -409,13 +409,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -442,13 +442,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -477,13 +477,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -513,13 +513,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -546,13 +546,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -579,13 +579,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -614,13 +614,13 @@ bar: &bar
c: bar_c c: bar_c
foobarList: foobarList:
b: foobarList_b b: foobarList_b
<<: !!merge <<:
- *foo - *foo
- *bar - *bar
c: foobarList_c c: foobarList_c
foobar: foobar:
c: foobar_c c: foobar_c
<<: *foo !!merge <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then

View File

@ -125,22 +125,6 @@ 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

View File

@ -7,7 +7,7 @@ HCL is commonly used in HashiCorp tools like Terraform for configuration files.
- String interpolation and expressions (preserved without quotes) - String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments) - Comments (leading, head, and line comments)
- Nested structures (maps and lists) - Nested structures (maps and lists)
- Syntax colorisation when enabled - Syntax colorization when enabled
## Parse HCL ## Parse HCL

View File

@ -7,5 +7,5 @@ HCL is commonly used in HashiCorp tools like Terraform for configuration files.
- String interpolation and expressions (preserved without quotes) - String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments) - Comments (leading, head, and line comments)
- Nested structures (maps and lists) - Nested structures (maps and lists)
- Syntax colorisation when enabled - Syntax colorization when enabled

View File

@ -1,9 +0,0 @@
# KYaml
Encode and decode to and from KYaml (a restricted subset of YAML that uses flow-style collections).
KYaml is useful when you want YAML data rendered in a compact, JSON-like form while still supporting YAML features like comments.
Notes:
- Strings are always double-quoted in KYaml output.
- Anchors and aliases are expanded (KYaml output does not emit them).

View File

@ -1,4 +1,4 @@
# TOML # TOML
Encode and decode to and from TOML. Decode from TOML. Note that `yq` does not yet support outputting in TOML format (and therefore it cannot roundtrip)

View File

@ -1,253 +0,0 @@
# KYaml
Encode and decode to and from KYaml (a restricted subset of YAML that uses flow-style collections).
KYaml is useful when you want YAML data rendered in a compact, JSON-like form while still supporting YAML features like comments.
Notes:
- Strings are always double-quoted in KYaml output.
- Anchors and aliases are expanded (KYaml output does not emit them).
## Encode kyaml: plain string scalar
Strings are always double-quoted in KYaml output.
Given a sample.yml file of:
```yaml
cat
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
"cat"
```
## encode flow mapping and sequence
Given a sample.yml file of:
```yaml
a: b
c:
- d
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
a: "b",
c: [
"d",
],
}
```
## encode non-string scalars
Given a sample.yml file of:
```yaml
a: 12
b: true
c: null
d: "true"
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
a: 12,
b: true,
c: null,
d: "true",
}
```
## quote non-identifier keys
Given a sample.yml file of:
```yaml
"1a": b
"has space": c
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
"1a": "b",
"has space": "c",
}
```
## escape quoted strings
Given a sample.yml file of:
```yaml
a: "line1\nline2\t\"q\""
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
a: "line1\nline2\t\"q\"",
}
```
## preserve comments when encoding
Given a sample.yml file of:
```yaml
# leading
a: 1 # a line
# head b
b: 2
c:
# head d
- d # d line
- e
# trailing
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
# leading
{
a: 1, # a line
# head b
b: 2,
c: [
# head d
"d", # d line
"e",
],
# trailing
}
```
## Encode kyaml: anchors and aliases
KYaml output does not support anchors/aliases; they are expanded to concrete values.
Given a sample.yml file of:
```yaml
base: &base
a: b
copy: *base
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
base: {
a: "b",
},
copy: {
a: "b",
},
}
```
## Encode kyaml: yaml to kyaml shows formatting differences
KYaml uses flow-style collections (braces/brackets) and explicit commas.
Given a sample.yml file of:
```yaml
person:
name: John
pets:
- cat
- dog
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
{
person: {
name: "John",
pets: [
"cat",
"dog",
],
},
}
```
## Encode kyaml: nested lists of objects
Lists and objects can be nested arbitrarily; KYaml always uses flow-style collections.
Given a sample.yml file of:
```yaml
- name: a
items:
- id: 1
tags:
- k: x
v: y
- k: x2
v: y2
- id: 2
tags:
- k: z
v: w
```
then
```bash
yq -o=kyaml '.' sample.yml
```
will output
```yaml
[
{
name: "a",
items: [
{
id: 1,
tags: [
{
k: "x",
v: "y",
},
{
k: "x2",
v: "y2",
},
],
},
{
id: 2,
tags: [
{
k: "z",
v: "w",
},
],
},
],
},
]
```

View File

@ -1,6 +1,6 @@
# TOML # TOML
Encode and decode to and from TOML. Decode from TOML. Note that `yq` does not yet support outputting in TOML format (and therefore it cannot roundtrip)
## Parse: Simple ## Parse: Simple
@ -141,263 +141,3 @@ will output
dependencies: {} dependencies: {}
``` ```
## Roundtrip: inline table attribute
Given a sample.toml file of:
```toml
name = { first = "Tom", last = "Preston-Werner" }
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
name = { first = "Tom", last = "Preston-Werner" }
```
## Roundtrip: table section
Given a sample.toml file of:
```toml
[owner.contact]
name = "Tom"
age = 36
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
[owner.contact]
name = "Tom"
age = 36
```
## Roundtrip: array of tables
Given a sample.toml file of:
```toml
[[fruits]]
name = "apple"
[[fruits.varieties]]
name = "red delicious"
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
[[fruits]]
name = "apple"
[[fruits.varieties]]
name = "red delicious"
```
## Roundtrip: arrays and scalars
Given a sample.toml file of:
```toml
A = ["hello", ["world", "again"]]
B = 12
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
A = ["hello", ["world", "again"]]
B = 12
```
## Roundtrip: simple
Given a sample.toml file of:
```toml
A = "hello"
B = 12
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
A = "hello"
B = 12
```
## Roundtrip: deep paths
Given a sample.toml file of:
```toml
[person]
name = "hello"
address = "12 cat st"
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
[person]
name = "hello"
address = "12 cat st"
```
## Roundtrip: empty array
Given a sample.toml file of:
```toml
A = []
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
A = []
```
## Roundtrip: sample table
Given a sample.toml file of:
```toml
var = "x"
[owner.contact]
name = "Tom Preston-Werner"
age = 36
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
var = "x"
[owner.contact]
name = "Tom Preston-Werner"
age = 36
```
## Roundtrip: empty table
Given a sample.toml file of:
```toml
[dependencies]
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
[dependencies]
```
## Roundtrip: comments
Given a sample.toml file of:
```toml
# This is a comment
A = "hello" # inline comment
B = 12
# Table comment
[person]
name = "Tom" # name comment
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
# This is a comment
A = "hello" # inline comment
B = 12
# Table comment
[person]
name = "Tom" # name comment
```
## Roundtrip: sample from web
Given a sample.toml file of:
```toml
# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
# [servers] yq can't do this one yet
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
# [servers] yq can't do this one yet
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
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"
```

View File

@ -1,12 +1,7 @@
package yqlib package yqlib
import ( import (
"bufio"
"errors"
"io" "io"
"strings"
"github.com/fatih/color"
) )
type Encoder interface { type Encoder interface {
@ -30,63 +25,3 @@ func mapKeysToStrings(node *CandidateNode) {
mapKeysToStrings(child) mapKeysToStrings(child)
} }
} }
// Some funcs are shared between encoder_yaml and encoder_kyaml
func PrintYAMLDocumentSeparator(writer io.Writer, PrintDocSeparators bool) error {
if PrintDocSeparators {
log.Debug("writing doc sep")
if err := writeString(writer, "---\n"); err != nil {
return err
}
}
return nil
}
func PrintYAMLLeadingContent(writer io.Writer, content string, PrintDocSeparators bool, ColorsEnabled bool) error {
reader := bufio.NewReader(strings.NewReader(content))
// reuse precompiled package-level regex
// (declared in decoder_yaml.go)
for {
readline, errReading := reader.ReadString('\n')
if errReading != nil && !errors.Is(errReading, io.EOF) {
return errReading
}
if strings.Contains(readline, "$yqDocSeparator$") {
// Preserve the original line ending (CRLF or LF)
lineEnding := "\n"
if strings.HasSuffix(readline, "\r\n") {
lineEnding = "\r\n"
}
if PrintDocSeparators {
if err := writeString(writer, "---"+lineEnding); err != nil {
return err
}
}
} else {
if len(readline) > 0 && readline != "\n" && readline[0] != '%' && !commentLineRe.MatchString(readline) {
readline = "# " + readline
}
if ColorsEnabled && strings.TrimSpace(readline) != "" {
readline = format(color.FgHiBlack) + readline + format(color.Reset)
}
if err := writeString(writer, readline); err != nil {
return err
}
}
if errors.Is(errReading, io.EOF) {
if readline != "" {
// the last comment we read didn't have a newline, put one in
if err := writeString(writer, "\n"); err != nil {
return err
}
}
break
}
}
return nil
}

View File

@ -43,9 +43,6 @@ func (he *hclEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error { func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("I need to encode %v", NodeToString(node)) log.Debugf("I need to encode %v", NodeToString(node))
if node.Kind == ScalarNode {
return writeString(writer, node.Value+"\n")
}
f := hclwrite.NewEmptyFile() f := hclwrite.NewEmptyFile()
body := f.Body() body := f.Body()
@ -66,8 +63,8 @@ func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
finalOutput := he.injectComments(compactOutput, commentMap) finalOutput := he.injectComments(compactOutput, commentMap)
if he.prefs.ColorsEnabled { if he.prefs.ColorsEnabled {
colourized := he.colorizeHcl(finalOutput) colorized := he.colorizeHcl(finalOutput)
_, err := writer.Write(colourized) _, err := writer.Write(colorized)
return err return err
} }
@ -173,18 +170,19 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string
return []byte(result) return []byte(result)
} }
// colorizeHcl applies syntax highlighting to HCL output using fatih/color
func (he *hclEncoder) colorizeHcl(input []byte) []byte { func (he *hclEncoder) colorizeHcl(input []byte) []byte {
hcl := string(input) hcl := string(input)
result := strings.Builder{} result := strings.Builder{}
// Create colour functions for different token types // Create color functions for different token types
commentColor := color.New(color.FgHiBlack).SprintFunc() commentColor := color.New(color.FgHiBlack).SprintFunc()
stringColor := color.New(color.FgGreen).SprintFunc() stringColor := color.New(color.FgGreen).SprintFunc()
numberColor := color.New(color.FgHiMagenta).SprintFunc() numberColor := color.New(color.FgHiMagenta).SprintFunc()
keyColor := color.New(color.FgCyan).SprintFunc() keyColor := color.New(color.FgCyan).SprintFunc()
boolColor := color.New(color.FgHiMagenta).SprintFunc() boolColor := color.New(color.FgHiMagenta).SprintFunc()
// Simple tokenization for HCL colouring // Simple tokenization for HCL coloring
i := 0 i := 0
for i < len(hcl) { for i < len(hcl) {
ch := hcl[i] ch := hcl[i]
@ -449,8 +447,8 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
return false return false
} }
// If EncodeHintSeparateBlock is set, emit children as separate blocks regardless of label extraction // If EncodeSeparate is set, emit children as separate blocks regardless of label extraction
if valueNode.EncodeHint == EncodeHintSeparateBlock { if valueNode.EncodeSeparate {
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled { if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true return true
} }
@ -475,12 +473,12 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
} }
} }
// If all child values are mappings, treat each child key as a labelled instance of this block type // If all child values are mappings, treat each child key as a labeled instance of this block type
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled { if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true return true
} }
// No labels detected, render as unlabelled block // No labels detected, render as unlabeled block
block := body.AppendNewBlock(key, nil) block := body.AppendNewBlock(key, nil)
if err := he.encodeNodeAttributes(block.Body(), valueNode); err == nil { if err := he.encodeNodeAttributes(block.Body(), valueNode); err == nil {
return true return true
@ -492,7 +490,7 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
// encodeNode encodes a CandidateNode directly to HCL, preserving style information // encodeNode encodes a CandidateNode directly to HCL, preserving style information
func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error { func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error {
if node.Kind != MappingNode { if node.Kind != MappingNode {
return fmt.Errorf("HCL encoder expects a mapping at the root level, got %v", kindToString(node.Kind)) return fmt.Errorf("HCL encoder expects a mapping at the root level")
} }
for i := 0; i < len(node.Content); i += 2 { for i := 0; i < len(node.Content); i += 2 {
@ -537,9 +535,9 @@ func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockTy
return false, nil return false, nil
} }
// Only emit as separate blocks if EncodeHintSeparateBlock is set // Only emit as separate blocks if EncodeSeparate is true
// 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.EncodeHint != EncodeHintSeparateBlock { if !valueNode.EncodeSeparate {
return false, nil return false, nil
} }

View File

@ -1,318 +0,0 @@
//go:build !yq_nokyaml
package yqlib
import (
"bytes"
"io"
"regexp"
"strconv"
"strings"
)
type kyamlEncoder struct {
prefs KYamlPreferences
}
func NewKYamlEncoder(prefs KYamlPreferences) Encoder {
return &kyamlEncoder{prefs: prefs}
}
func (ke *kyamlEncoder) CanHandleAliases() bool {
// KYAML is a restricted subset; avoid emitting anchors/aliases.
return false
}
func (ke *kyamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
return PrintYAMLDocumentSeparator(writer, ke.prefs.PrintDocSeparators)
}
func (ke *kyamlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
return PrintYAMLLeadingContent(writer, content, ke.prefs.PrintDocSeparators, ke.prefs.ColorsEnabled)
}
func (ke *kyamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("encoderKYaml - going to print %v", NodeToString(node))
if node.Kind == ScalarNode && ke.prefs.UnwrapScalar {
return writeString(writer, node.Value+"\n")
}
destination := writer
tempBuffer := bytes.NewBuffer(nil)
if ke.prefs.ColorsEnabled {
destination = tempBuffer
}
// Mirror the YAML encoder behaviour: trailing comments on the document root
// are stored in FootComment and need to be printed after the document.
trailingContent := node.FootComment
if err := ke.writeCommentBlock(destination, node.HeadComment, 0); err != nil {
return err
}
if err := ke.writeNode(destination, node, 0); err != nil {
return err
}
if err := ke.writeInlineComment(destination, node.LineComment); err != nil {
return err
}
if err := writeString(destination, "\n"); err != nil {
return err
}
if err := ke.PrintLeadingContent(destination, trailingContent); err != nil {
return err
}
if ke.prefs.ColorsEnabled {
return colorizeAndPrint(tempBuffer.Bytes(), writer)
}
return nil
}
func (ke *kyamlEncoder) writeNode(writer io.Writer, node *CandidateNode, indent int) error {
switch node.Kind {
case MappingNode:
return ke.writeMapping(writer, node, indent)
case SequenceNode:
return ke.writeSequence(writer, node, indent)
case ScalarNode:
return writeString(writer, ke.formatScalar(node))
case AliasNode:
// Should have been exploded by the printer, but handle defensively.
if node.Alias == nil {
return writeString(writer, "null")
}
return ke.writeNode(writer, node.Alias, indent)
default:
return writeString(writer, "null")
}
}
func (ke *kyamlEncoder) writeMapping(writer io.Writer, node *CandidateNode, indent int) error {
if len(node.Content) == 0 {
return writeString(writer, "{}")
}
if err := writeString(writer, "{\n"); err != nil {
return err
}
for i := 0; i+1 < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
entryIndent := indent + ke.prefs.Indent
if err := ke.writeCommentBlock(writer, keyNode.HeadComment, entryIndent); err != nil {
return err
}
if valueNode.HeadComment != "" && valueNode.HeadComment != keyNode.HeadComment {
if err := ke.writeCommentBlock(writer, valueNode.HeadComment, entryIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, entryIndent); err != nil {
return err
}
if err := writeString(writer, ke.formatKey(keyNode)); err != nil {
return err
}
if err := writeString(writer, ": "); err != nil {
return err
}
if err := ke.writeNode(writer, valueNode, entryIndent); err != nil {
return err
}
// Always emit a trailing comma; KYAML encourages explicit separators,
// and this ensures all quoted strings have a trailing `",` as requested.
if err := writeString(writer, ","); err != nil {
return err
}
inline := valueNode.LineComment
if inline == "" {
inline = keyNode.LineComment
}
if err := ke.writeInlineComment(writer, inline); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
foot := valueNode.FootComment
if foot == "" {
foot = keyNode.FootComment
}
if err := ke.writeCommentBlock(writer, foot, entryIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
return writeString(writer, "}")
}
func (ke *kyamlEncoder) writeSequence(writer io.Writer, node *CandidateNode, indent int) error {
if len(node.Content) == 0 {
return writeString(writer, "[]")
}
if err := writeString(writer, "[\n"); err != nil {
return err
}
for _, child := range node.Content {
itemIndent := indent + ke.prefs.Indent
if err := ke.writeCommentBlock(writer, child.HeadComment, itemIndent); err != nil {
return err
}
if err := ke.writeIndent(writer, itemIndent); err != nil {
return err
}
if err := ke.writeNode(writer, child, itemIndent); err != nil {
return err
}
if err := writeString(writer, ","); err != nil {
return err
}
if err := ke.writeInlineComment(writer, child.LineComment); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
if err := ke.writeCommentBlock(writer, child.FootComment, itemIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
return writeString(writer, "]")
}
func (ke *kyamlEncoder) writeIndent(writer io.Writer, indent int) error {
if indent <= 0 {
return nil
}
return writeString(writer, strings.Repeat(" ", indent))
}
func (ke *kyamlEncoder) formatKey(keyNode *CandidateNode) string {
// KYAML examples use bare keys. Quote keys only when needed.
key := keyNode.Value
if isValidKYamlBareKey(key) {
return key
}
return `"` + escapeDoubleQuotedString(key) + `"`
}
func (ke *kyamlEncoder) formatScalar(node *CandidateNode) string {
switch node.Tag {
case "!!null":
return "null"
case "!!bool":
return strings.ToLower(node.Value)
case "!!int", "!!float":
return node.Value
case "!!str":
return `"` + escapeDoubleQuotedString(node.Value) + `"`
default:
// Fall back to a string representation to avoid implicit typing surprises.
return `"` + escapeDoubleQuotedString(node.Value) + `"`
}
}
var kyamlBareKeyRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]*$`)
func isValidKYamlBareKey(s string) bool {
// Conservative: require an identifier-like key; otherwise quote.
if s == "" {
return false
}
return kyamlBareKeyRe.MatchString(s)
}
func escapeDoubleQuotedString(s string) string {
var b strings.Builder
b.Grow(len(s) + 2)
for _, r := range s {
switch r {
case '\\':
b.WriteString(`\\`)
case '"':
b.WriteString(`\"`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
if r < 0x20 {
// YAML double-quoted strings support \uXXXX escapes.
b.WriteString(`\u`)
hex := "0000" + strings.ToUpper(strconv.FormatInt(int64(r), 16))
b.WriteString(hex[len(hex)-4:])
} else {
b.WriteRune(r)
}
}
}
return b.String()
}
func (ke *kyamlEncoder) writeCommentBlock(writer io.Writer, comment string, indent int) error {
if strings.TrimSpace(comment) == "" {
return nil
}
lines := strings.Split(strings.ReplaceAll(comment, "\r\n", "\n"), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
toWrite := line
if !commentLineRe.MatchString(toWrite) {
toWrite = "# " + toWrite
}
if err := writeString(writer, toWrite); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
}
return nil
}
func (ke *kyamlEncoder) writeInlineComment(writer io.Writer, comment string) error {
comment = strings.TrimSpace(strings.ReplaceAll(comment, "\r\n", "\n"))
if comment == "" {
return nil
}
lines := strings.Split(comment, "\n")
first := strings.TrimSpace(lines[0])
if first == "" {
return nil
}
if !strings.HasPrefix(first, "#") {
first = "# " + first
}
if err := writeString(writer, " "); err != nil {
return err
}
return writeString(writer, first)
}

View File

@ -57,13 +57,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
// let's just pick a fallback key to use if we are encoding a single scalar // let's just pick a fallback key to use if we are encoding a single scalar
nonemptyPath = "value" nonemptyPath = "value"
} }
var valueString string _, err := io.WriteString(*w, nonemptyPath+"="+quoteValue(node.Value)+"\n")
if pe.prefs.UnwrapScalar {
valueString = node.Value
} else {
valueString = quoteValue(node.Value)
}
_, err := io.WriteString(*w, nonemptyPath+"="+valueString+"\n")
return err return err
case SequenceNode: case SequenceNode:
for index, child := range node.Content { for index, child := range node.Content {

View File

@ -135,36 +135,3 @@ func TestShellVariablesEncoderCustomSeparatorArray(t *testing.T) {
func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) { func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) {
assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X") assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X")
} }
func assertEncodesToUnwrapped(t *testing.T, yaml string, shellvars string) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
originalUnwrapScalar := ConfiguredShellVariablesPreferences.UnwrapScalar
defer func() {
ConfiguredShellVariablesPreferences.UnwrapScalar = originalUnwrapScalar
}()
ConfiguredShellVariablesPreferences.UnwrapScalar = true
var encoder = NewShellVariablesEncoder()
inputs, err := readDocuments(strings.NewReader(yaml), "test.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
if err != nil {
panic(err)
}
node := inputs.Front().Value.(*CandidateNode)
err = encoder.Encode(writer, node)
if err != nil {
panic(err)
}
writer.Flush()
test.AssertResult(t, shellvars, strings.TrimSuffix(output.String(), "\n"))
}
func TestShellVariablesEncoderUnwrapScalar(t *testing.T) {
assertEncodesToUnwrapped(t, "a: Lewis Carroll", "a=Lewis Carroll")
assertEncodesToUnwrapped(t, "b: 123", "b=123")
assertEncodesToUnwrapped(t, "c: true", "c=true")
assertEncodesToUnwrapped(t, "d: value with spaces", "d=value with spaces")
}

View File

@ -1,58 +1,22 @@
//go:build !yq_notoml
package yqlib package yqlib
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"strings"
"github.com/fatih/color"
) )
type tomlEncoder struct { type tomlEncoder struct {
wroteRootAttr bool // Track if we wrote root-level attributes before tables
prefs TomlPreferences
} }
func NewTomlEncoder() Encoder { func NewTomlEncoder() Encoder {
return NewTomlEncoderWithPrefs(ConfiguredTomlPreferences) return &tomlEncoder{}
}
func NewTomlEncoderWithPrefs(prefs TomlPreferences) Encoder {
return &tomlEncoder{prefs: prefs}
} }
func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error { func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
if node.Kind != MappingNode { if node.Kind == ScalarNode {
// For standalone selections, TOML tests expect raw value for scalars return writeString(writer, node.Value+"\n")
if node.Kind == ScalarNode {
return writeString(writer, node.Value+"\n")
}
return fmt.Errorf("TOML encoder expects a mapping at the root level")
} }
return fmt.Errorf("only scalars (e.g. strings, numbers, booleans) are supported for TOML output at the moment. Please use yaml output format (-oy) until the encoder has been fully implemented")
// Encode to a buffer first if colors are enabled
var buf bytes.Buffer
var targetWriter io.Writer
targetWriter = writer
if te.prefs.ColorsEnabled {
targetWriter = &buf
}
// Encode a root mapping as a sequence of attributes, tables, and arrays of tables
if err := te.encodeRootMapping(targetWriter, node); err != nil {
return err
}
if te.prefs.ColorsEnabled {
colourised := te.colorizeToml(buf.Bytes())
_, err := writer.Write(colourised)
return err
}
return nil
} }
func (te *tomlEncoder) PrintDocumentSeparator(_ io.Writer) error { func (te *tomlEncoder) PrintDocumentSeparator(_ io.Writer) error {
@ -66,742 +30,3 @@ func (te *tomlEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
func (te *tomlEncoder) CanHandleAliases() bool { func (te *tomlEncoder) CanHandleAliases() bool {
return false return false
} }
// ---- 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 {
if comment == "" {
return nil
}
lines := strings.Split(comment, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "#") {
line = "# " + line
}
if _, err := w.Write([]byte(line + "\n")); err != nil {
return err
}
}
return nil
}
func (te *tomlEncoder) formatScalar(node *CandidateNode) string {
switch node.Tag {
case "!!str":
// Quote strings per TOML spec
return fmt.Sprintf("%q", node.Value)
case "!!bool", "!!int", "!!float":
return node.Value
case "!!null":
// TOML does not have null; encode as empty string
return `""`
default:
return node.Value
}
}
func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error {
te.wroteRootAttr = false // Reset state
// Write root head comment if present (at the very beginning, no leading blank line)
if node.HeadComment != "" {
if err := te.writeComment(w, node.HeadComment); 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
}
}
}
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
}
// encodeTopLevelEntry encodes a key/value at the root, dispatching to attribute, table, or array-of-tables
func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *CandidateNode) error {
if len(path) == 0 {
return fmt.Errorf("cannot encode TOML entry with empty path")
}
switch node.Kind {
case ScalarNode:
// key = value
return te.writeAttribute(w, path[len(path)-1], node)
case SequenceNode:
// Empty arrays should be encoded as [] attributes
if len(node.Content) == 0 {
return te.writeArrayAttribute(w, path[len(path)-1], node)
}
// If all items are mappings => array of tables; else => array attribute
allMaps := true
for _, it := range node.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
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 {
if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
return err
}
}
return nil
}
// Regular array attribute
return te.writeArrayAttribute(w, path[len(path)-1], node)
case MappingNode:
// Use inline table syntax only for nodes explicitly marked as TOML inline tables.
// YAML flow-style mappings are not treated as inline tables; the FlowStyle attribute
// is a YAML-specific rendering hint and should not affect TOML output. This ensures
// that auto-detected JSON input (parsed as YAML flow style) produces readable table
// sections, consistent with explicitly parsed JSON input.
if node.EncodeHint == EncodeHintInline {
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
}
return te.encodeSeparateMapping(w, path, node)
default:
return fmt.Errorf("unsupported node kind for TOML: %v", node.Kind)
}
}
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 {
if value.Tag == "!!null" {
return nil
}
te.wroteRootAttr = true // Mark that we wrote a root attribute
// Write head comment before the attribute
if err := te.writeComment(w, value.HeadComment); err != nil {
return err
}
// Write the attribute
line := tomlKey(key) + " = " + te.formatScalar(value)
// Add line comment if present
if value.LineComment != "" {
lineComment := strings.TrimSpace(value.LineComment)
if !strings.HasPrefix(lineComment, "#") {
lineComment = "# " + lineComment
}
line += " " + lineComment
}
_, err := w.Write([]byte(line + "\n"))
return err
}
func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *CandidateNode) error {
te.wroteRootAttr = true // Mark that we wrote a root attribute
// Write head comment before the array
if err := te.writeComment(w, seq.HeadComment); err != nil {
return err
}
// Handle empty arrays
if len(seq.Content) == 0 {
line := tomlKey(key) + " = []"
if seq.LineComment != "" {
lineComment := strings.TrimSpace(seq.LineComment)
if !strings.HasPrefix(lineComment, "#") {
lineComment = "# " + lineComment
}
line += " " + lineComment
}
_, err := w.Write([]byte(line + "\n"))
return err
}
// Check if any array elements have head comments - if so, use multiline format
hasElementComments := false
for _, it := range seq.Content {
if it.HeadComment != "" {
hasElementComments = true
break
}
}
if hasElementComments {
// Write multiline array format with comments
if _, err := w.Write([]byte(tomlKey(key) + " = [\n")); err != nil {
return err
}
for i, it := range seq.Content {
// Write head comment for this element
if it.HeadComment != "" {
commentLines := strings.Split(it.HeadComment, "\n")
for _, commentLine := range commentLines {
if strings.TrimSpace(commentLine) != "" {
if !strings.HasPrefix(strings.TrimSpace(commentLine), "#") {
commentLine = "# " + commentLine
}
if _, err := w.Write([]byte(" " + commentLine + "\n")); err != nil {
return err
}
}
}
}
// Write the element value
var itemStr string
switch it.Kind {
case ScalarNode:
itemStr = te.formatScalar(it)
case SequenceNode:
nested, err := te.sequenceToInlineArray(it)
if err != nil {
return err
}
itemStr = nested
case MappingNode:
inline, err := te.mappingToInlineTable(it)
if err != nil {
return err
}
itemStr = inline
case AliasNode:
return fmt.Errorf("aliases are not supported in TOML")
default:
return fmt.Errorf("unsupported array item kind: %v", it.Kind)
}
// Always add trailing comma in multiline arrays
itemStr += ","
if _, err := w.Write([]byte(" " + itemStr + "\n")); err != nil {
return err
}
// Add blank line between elements (except after the last one)
if i < len(seq.Content)-1 {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
}
}
if _, err := w.Write([]byte("]\n")); err != nil {
return err
}
return nil
}
// Join scalars or nested arrays recursively into TOML array syntax
items := make([]string, 0, len(seq.Content))
for _, it := range seq.Content {
switch it.Kind {
case ScalarNode:
items = append(items, te.formatScalar(it))
case SequenceNode:
// Nested arrays: encode inline
nested, err := te.sequenceToInlineArray(it)
if err != nil {
return err
}
items = append(items, nested)
case MappingNode:
// Inline table inside array
inline, err := te.mappingToInlineTable(it)
if err != nil {
return err
}
items = append(items, inline)
case AliasNode:
return fmt.Errorf("aliases are not supported in TOML")
default:
return fmt.Errorf("unsupported array item kind: %v", it.Kind)
}
}
line := tomlKey(key) + " = [" + strings.Join(items, ", ") + "]"
// Add line comment if present
if seq.LineComment != "" {
lineComment := strings.TrimSpace(seq.LineComment)
if !strings.HasPrefix(lineComment, "#") {
lineComment = "# " + lineComment
}
line += " " + lineComment
}
_, err := w.Write([]byte(line + "\n"))
return err
}
func (te *tomlEncoder) sequenceToInlineArray(seq *CandidateNode) (string, error) {
items := make([]string, 0, len(seq.Content))
for _, it := range seq.Content {
switch it.Kind {
case ScalarNode:
items = append(items, te.formatScalar(it))
case SequenceNode:
nested, err := te.sequenceToInlineArray(it)
if err != nil {
return "", err
}
items = append(items, nested)
case MappingNode:
inline, err := te.mappingToInlineTable(it)
if err != nil {
return "", err
}
items = append(items, inline)
default:
return "", fmt.Errorf("unsupported array item kind: %v", it.Kind)
}
}
return "[" + strings.Join(items, ", ") + "]", nil
}
func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
// key = { a = 1, b = "x" }
parts := make([]string, 0, len(m.Content)/2)
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
switch v.Kind {
case ScalarNode:
if v.Tag == "!!null" {
continue
}
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), te.formatScalar(v)))
case SequenceNode:
// inline array in inline table
arr, err := te.sequenceToInlineArray(v)
if err != nil {
return "", err
}
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), arr))
case MappingNode:
// nested inline table
inline, err := te.mappingToInlineTable(v)
if err != nil {
return "", err
}
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), inline))
default:
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
}
}
return "{ " + strings.Join(parts, ", ") + " }", nil
}
func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *CandidateNode) error {
inline, err := te.mappingToInlineTable(m)
if err != nil {
return err
}
_, err = w.Write([]byte(tomlKey(key) + " = " + inline + "\n"))
return err
}
func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *CandidateNode) error {
// Add blank line before table header (or before comment if present) if we wrote root attributes
needsBlankLine := te.wroteRootAttr
if needsBlankLine {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
te.wroteRootAttr = false // Only add once
}
// Write head comment before the table header
if m.HeadComment != "" {
if err := te.writeComment(w, m.HeadComment); err != nil {
return err
}
}
// Write table header [a.b.c]
header := "[" + tomlDottedKey(path) + "]\n"
_, err := w.Write([]byte(header))
return err
}
// 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.
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).
// Inline mapping children also count as attributes since they render as key = { ... }.
hasAttrs := false
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
if v.Kind == ScalarNode && v.Tag != "!!null" {
hasAttrs = true
break
}
if v.Kind == MappingNode && v.EncodeHint == EncodeHintInline {
hasAttrs = true
break
}
if v.Kind == SequenceNode {
if !isTomlArrayOfTables(v) {
hasAttrs = true
break
}
}
}
// If there are attributes or if the mapping is empty, emit the table header
if hasAttrs || len(m.Content) == 0 {
if err := te.writeTableHeader(w, path, m); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, path, m); err != nil {
return err
}
return nil
}
// No attributes, just nested table structures - process children recursively
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
switch v.Kind {
case MappingNode:
newPath := append(append([]string{}, path...), k)
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
return err
}
case SequenceNode:
// If sequence of maps, emit [[path.k]] per element
if isTomlArrayOfTables(v) {
key := tomlDottedKey(append(append([]string{}, path...), k))
if te.wroteRootAttr {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
te.wroteRootAttr = false
}
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, append(append([]string{}, path...), k), it); err != nil {
return err
}
}
} else {
// Regular array attribute under the current table path
if err := te.writeArrayAttribute(w, k, v); err != nil {
return err
}
}
case ScalarNode:
// Attributes directly under the current table path
if err := te.writeAttribute(w, k, v); err != nil {
return err
}
}
}
return nil
}
// 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 {
// First, attributes (scalars, inline mappings, and non-map arrays)
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
switch v.Kind {
case ScalarNode:
if err := te.writeAttribute(w, k, v); err != nil {
return err
}
case MappingNode:
if v.EncodeHint == EncodeHintInline {
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
return err
}
}
case SequenceNode:
if !isTomlArrayOfTables(v) {
if err := te.writeArrayAttribute(w, k, v); err != nil {
return err
}
}
}
}
// Then, nested arrays of tables with full path
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
if v.Kind == SequenceNode {
if isTomlArrayOfTables(v) {
dotted := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, append(append([]string{}, path...), k), it); err != nil {
return err
}
}
}
}
}
// 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 {
k := m.Content[i].Value
v := m.Content[i+1]
if v.Kind == MappingNode && v.EncodeHint != EncodeHintInline {
subPath := append(append([]string{}, path...), k)
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
return err
}
}
}
return nil
}
// colorizeToml applies syntax highlighting to TOML output using fatih/color
func (te *tomlEncoder) colorizeToml(input []byte) []byte {
toml := string(input)
result := strings.Builder{}
// Force color output (don't check for TTY)
color.NoColor = false
// Create color functions for different token types
// Use EnableColor() to ensure colors work even when NO_COLOR env is set
commentColorObj := color.New(color.FgHiBlack)
commentColorObj.EnableColor()
stringColorObj := color.New(color.FgGreen)
stringColorObj.EnableColor()
numberColorObj := color.New(color.FgHiMagenta)
numberColorObj.EnableColor()
keyColorObj := color.New(color.FgCyan)
keyColorObj.EnableColor()
boolColorObj := color.New(color.FgHiMagenta)
boolColorObj.EnableColor()
sectionColorObj := color.New(color.FgYellow, color.Bold)
sectionColorObj.EnableColor()
commentColor := commentColorObj.SprintFunc()
stringColor := stringColorObj.SprintFunc()
numberColor := numberColorObj.SprintFunc()
keyColor := keyColorObj.SprintFunc()
boolColor := boolColorObj.SprintFunc()
sectionColor := sectionColorObj.SprintFunc()
// Simple tokenization for TOML colouring
i := 0
for i < len(toml) {
ch := toml[i]
// Comments - from # to end of line
if ch == '#' {
end := i
for end < len(toml) && toml[end] != '\n' {
end++
}
result.WriteString(commentColor(toml[i:end]))
i = end
continue
}
// Table sections - [section] or [[array]]
// Only treat '[' as a table section if it appears at the start of the line
// (possibly after whitespace). This avoids incorrectly colouring inline arrays like
// "ports = [8000, 8001]" as table sections.
if ch == '[' {
isSectionHeader := true
if i > 0 {
isSectionHeader = false
j := i - 1
for j >= 0 && toml[j] != '\n' {
if toml[j] != ' ' && toml[j] != '\t' && toml[j] != '\r' {
// Found a non-whitespace character before this '[' on the same line,
// so this is not a table header.
break
}
j--
}
if j < 0 || toml[j] == '\n' {
// Reached the start of the string or a newline without encountering
// any non-whitespace, so '[' is at the logical start of the line.
isSectionHeader = true
}
}
if isSectionHeader {
end := i + 1
// Check for [[
if end < len(toml) && toml[end] == '[' {
end++
}
// Find closing ]
for end < len(toml) && toml[end] != ']' {
end++
}
// Include closing ]
if end < len(toml) {
end++
// Check for ]]
if end < len(toml) && toml[end] == ']' {
end++
}
}
result.WriteString(sectionColor(toml[i:end]))
i = end
continue
}
}
// Strings - quoted text (double or single quotes)
if ch == '"' || ch == '\'' {
quote := ch
end := i + 1
for end < len(toml) {
if toml[end] == quote {
break
}
if toml[end] == '\\' && end+1 < len(toml) {
// Skip the backslash and the escaped character
end += 2
continue
}
end++
}
if end < len(toml) {
end++ // include closing quote
}
result.WriteString(stringColor(toml[i:end]))
i = end
continue
}
// Numbers - sequences of digits, possibly with decimal point or minus
if (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < len(toml) && toml[i+1] >= '0' && toml[i+1] <= '9') {
end := i
if ch == '-' {
end++
}
for end < len(toml) {
c := toml[end]
if (c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' {
end++
} else if (c == '+' || c == '-') && end > 0 && (toml[end-1] == 'e' || toml[end-1] == 'E') {
// Only allow + or - immediately after 'e' or 'E' for scientific notation
end++
} else {
break
}
}
result.WriteString(numberColor(toml[i:end]))
i = end
continue
}
// Identifiers/keys - alphanumeric + underscore + dash
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' {
end := i
for end < len(toml) && ((toml[end] >= 'a' && toml[end] <= 'z') ||
(toml[end] >= 'A' && toml[end] <= 'Z') ||
(toml[end] >= '0' && toml[end] <= '9') ||
toml[end] == '_' || toml[end] == '-') {
end++
}
ident := toml[i:end]
// Check if this is a boolean/null keyword
switch ident {
case "true", "false":
result.WriteString(boolColor(ident))
default:
// Check if followed by = or whitespace then = (it's a key)
j := end
for j < len(toml) && (toml[j] == ' ' || toml[j] == '\t') {
j++
}
if j < len(toml) && toml[j] == '=' {
result.WriteString(keyColor(ident))
} else {
result.WriteString(ident) // plain text for other identifiers
}
}
i = end
continue
}
// Everything else (whitespace, operators, brackets) - no color
result.WriteByte(ch)
i++
}
return []byte(result.String())
}

View File

@ -59,7 +59,7 @@ func (e *xmlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
return err return err
} }
if _, err := e.writer.Write([]byte("\n")); err != nil { if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warningf("Unable to write newline, skipping: %v", err) log.Warning("Unable to write newline, skipping: %w", err)
} }
} }
} }
@ -131,7 +131,7 @@ func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *CandidateNode
return err return err
} }
if _, err := e.writer.Write([]byte("\n")); err != nil { if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warningf("Unable to write newline, skipping: %v", err) log.Warning("Unable to write newline, skipping: %w", err)
} }
} else if key.Value == e.prefs.DirectiveName { } else if key.Value == e.prefs.DirectiveName {
var directive xml.Directive = []byte(value.Value) var directive xml.Directive = []byte(value.Value)
@ -139,7 +139,7 @@ func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *CandidateNode
return err return err
} }
if _, err := e.writer.Write([]byte("\n")); err != nil { if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warningf("Unable to write newline, skipping: %v", err) log.Warning("Unable to write newline, skipping: %w", err)
} }
} else { } else {

View File

@ -1,10 +1,13 @@
package yqlib package yqlib
import ( import (
"bufio"
"bytes" "bytes"
"errors"
"io" "io"
"strings" "strings"
"github.com/fatih/color"
"go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4"
) )
@ -21,15 +24,67 @@ func (ye *yamlEncoder) CanHandleAliases() bool {
} }
func (ye *yamlEncoder) PrintDocumentSeparator(writer io.Writer) error { func (ye *yamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
return PrintYAMLDocumentSeparator(writer, ye.prefs.PrintDocSeparators) if ye.prefs.PrintDocSeparators {
log.Debug("writing doc sep")
if err := writeString(writer, "---\n"); err != nil {
return err
}
}
return nil
} }
func (ye *yamlEncoder) PrintLeadingContent(writer io.Writer, content string) error { func (ye *yamlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
return PrintYAMLLeadingContent(writer, content, ye.prefs.PrintDocSeparators, ye.prefs.ColorsEnabled) reader := bufio.NewReader(strings.NewReader(content))
// reuse precompiled package-level regex
// (declared in decoder_yaml.go)
for {
readline, errReading := reader.ReadString('\n')
if errReading != nil && !errors.Is(errReading, io.EOF) {
return errReading
}
if strings.Contains(readline, "$yqDocSeparator$") {
// Preserve the original line ending (CRLF or LF)
lineEnding := "\n"
if strings.HasSuffix(readline, "\r\n") {
lineEnding = "\r\n"
}
if ye.prefs.PrintDocSeparators {
if err := writeString(writer, "---"+lineEnding); err != nil {
return err
}
}
} else {
if len(readline) > 0 && readline != "\n" && readline[0] != '%' && !commentLineRe.MatchString(readline) {
readline = "# " + readline
}
if ye.prefs.ColorsEnabled && strings.TrimSpace(readline) != "" {
readline = format(color.FgHiBlack) + readline + format(color.Reset)
}
if err := writeString(writer, readline); err != nil {
return err
}
}
if errors.Is(errReading, io.EOF) {
if readline != "" {
// the last comment we read didn't have a newline, put one in
if err := writeString(writer, "\n"); err != nil {
return err
}
}
break
}
}
return nil
} }
func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error { func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("encoderYaml - going to print %v", NodeToString(node)) log.Debug("encoderYaml - going to print %v", NodeToString(node))
// Detect line ending style from LeadingContent // Detect line ending style from LeadingContent
lineEnding := "\n" lineEnding := "\n"
if strings.Contains(node.LeadingContent, "\r\n") { if strings.Contains(node.LeadingContent, "\r\n") {
@ -52,9 +107,6 @@ func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
var encoder = yaml.NewEncoder(destination) var encoder = yaml.NewEncoder(destination)
encoder.SetIndent(ye.prefs.Indent) encoder.SetIndent(ye.prefs.Indent)
if ye.prefs.CompactSequenceIndent {
encoder.CompactSeqIndent()
}
target, err := node.MarshalYAML() target, err := node.MarshalYAML()

View File

@ -26,7 +26,7 @@ func newExpressionParser() ExpressionParserInterface {
} }
func (p *expressionParserImpl) ParseExpression(expression string) (*ExpressionNode, error) { func (p *expressionParserImpl) ParseExpression(expression string) (*ExpressionNode, error) {
log.Debugf("Parsing expression: [%v]", expression) log.Debug("Parsing expression: [%v]", expression)
tokens, err := p.pathTokeniser.Tokenise(expression) tokens, err := p.pathTokeniser.Tokenise(expression)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -95,7 +95,6 @@ func TestParserSingleOperation(t *testing.T) {
test.AssertResultComplex(t, nil, err) test.AssertResultComplex(t, nil, err)
if result == nil { if result == nil {
t.Fatal("Expected non-nil result for single operation") t.Fatal("Expected non-nil result for single operation")
return
} }
if result.Operation == nil { if result.Operation == nil {
t.Fatal("Expected operation to be set") t.Fatal("Expected operation to be set")

View File

@ -3,7 +3,8 @@ package yqlib
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
logging "gopkg.in/op/go-logging.v1"
) )
type expressionPostFixer interface { type expressionPostFixer interface {
@ -133,7 +134,7 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
return nil, fmt.Errorf("bad expression - probably missing close bracket on %v", opStack[len(opStack)-1].toString(false)) return nil, fmt.Errorf("bad expression - probably missing close bracket on %v", opStack[len(opStack)-1].toString(false))
} }
if log.IsEnabledFor(slog.LevelDebug) { if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("PostFix Result:") log.Debugf("PostFix Result:")
for _, currentToken := range result { for _, currentToken := range result {
log.Debugf("> %v", currentToken.toString()) log.Debugf("> %v", currentToken.toString())

View File

@ -7,15 +7,7 @@ import (
) )
func tryRenameFile(from string, to string) error { func tryRenameFile(from string, to string) error {
if info, err := os.Lstat(to); err == nil && info.Mode()&os.ModeSymlink != 0 { if renameError := os.Rename(from, to); renameError != nil {
log.Debug("Target file is symlink, skipping rename and attempting to copy contents")
if copyError := copyFileContents(from, to); copyError != nil {
return fmt.Errorf("failed copying from %v to %v: %w", from, to, copyError)
}
tryRemoveTempFile(from)
return nil
} else if renameError := os.Rename(from, to); renameError != nil {
log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to) log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to)
log.Debug(renameError.Error()) log.Debug(renameError.Error())
log.Debug("going to try copying instead") log.Debug("going to try copying instead")
@ -30,7 +22,7 @@ func tryRenameFile(from string, to string) error {
} }
func tryRemoveTempFile(filename string) { func tryRemoveTempFile(filename string) {
log.Debugf("Removing temp file: %v", filename) log.Debug("Removing temp file: %v", filename)
removeErr := os.Remove(filename) removeErr := os.Remove(filename)
if removeErr != nil { if removeErr != nil {
log.Errorf("Failed to remove temp file: %v", filename) log.Errorf("Failed to remove temp file: %v", filename)
@ -40,7 +32,7 @@ func tryRemoveTempFile(filename string) {
// thanks https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang // thanks https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
func copyFileContents(src, dst string) (err error) { func copyFileContents(src, dst string) (err error) {
// ignore CWE-22 gosec issue - that's more targeted for http based apps that run in a public directory, // ignore CWE-22 gosec issue - that's more targeted for http based apps that run in a public directory,
// and ensuring that it's not possible to give a path to a file outside that directory. // and ensuring that it's not possible to give a path to a file outside thar directory.
in, err := os.Open(src) // #nosec in, err := os.Open(src) // #nosec
if err != nil { if err != nil {
@ -68,7 +60,8 @@ func SafelyCloseReader(reader io.Reader) {
func safelyCloseFile(file *os.File) { func safelyCloseFile(file *os.File) {
err := file.Close() err := file.Close()
if err != nil { if err != nil {
log.Errorf("Error closing file %v: %v", file.Name(), err) log.Error("Error closing file!")
log.Error(err.Error())
} }
} }

View File

@ -22,12 +22,6 @@ var YamlFormat = &Format{"yaml", []string{"y", "yml"},
func() Decoder { return NewYamlDecoder(ConfiguredYamlPreferences) }, func() Decoder { return NewYamlDecoder(ConfiguredYamlPreferences) },
} }
var KYamlFormat = &Format{"kyaml", []string{"ky"},
func() Encoder { return NewKYamlEncoder(ConfiguredKYamlPreferences) },
// KYaml is stricter YAML
func() Decoder { return NewYamlDecoder(ConfiguredYamlPreferences) },
}
var JSONFormat = &Format{"json", []string{"j"}, var JSONFormat = &Format{"json", []string{"j"},
func() Encoder { return NewJSONEncoder(ConfiguredJSONPreferences) }, func() Encoder { return NewJSONEncoder(ConfiguredJSONPreferences) },
func() Decoder { return NewJSONDecoder() }, func() Decoder { return NewJSONDecoder() },
@ -69,11 +63,11 @@ var ShFormat = &Format{"", nil,
} }
var TomlFormat = &Format{"toml", []string{}, var TomlFormat = &Format{"toml", []string{},
func() Encoder { return NewTomlEncoderWithPrefs(ConfiguredTomlPreferences) }, func() Encoder { return NewTomlEncoder() },
func() Decoder { return NewTomlDecoder() }, func() Decoder { return NewTomlDecoder() },
} }
var HclFormat = &Format{"hcl", []string{"h", "tf"}, var HclFormat = &Format{"hcl", []string{"h"},
func() Encoder { return NewHclEncoder(ConfiguredHclPreferences) }, func() Encoder { return NewHclEncoder(ConfiguredHclPreferences) },
func() Decoder { return NewHclDecoder() }, func() Decoder { return NewHclDecoder() },
} }
@ -90,12 +84,11 @@ var LuaFormat = &Format{"lua", []string{"l"},
var INIFormat = &Format{"ini", []string{"i"}, var INIFormat = &Format{"ini", []string{"i"},
func() Encoder { return NewINIEncoder() }, func() Encoder { return NewINIEncoder() },
func() Decoder { return NewINIDecoder(ConfiguredINIPreferences) }, func() Decoder { return NewINIDecoder() },
} }
var Formats = []*Format{ var Formats = []*Format{
YamlFormat, YamlFormat,
KYamlFormat,
JSONFormat, JSONFormat,
PropertiesFormat, PropertiesFormat,
CSVFormat, CSVFormat,

View File

@ -58,7 +58,7 @@ func (f *frontMatterHandlerImpl) Split() error {
return err return err
} }
f.yamlFrontMatterFilename = yamlTempFile.Name() f.yamlFrontMatterFilename = yamlTempFile.Name()
log.Debugf("yamlTempFile: %v", yamlTempFile.Name()) log.Debug("yamlTempFile: %v", yamlTempFile.Name())
lineCount := 0 lineCount := 0

View File

@ -108,55 +108,6 @@ yaml: doc
fmHandler.CleanUp() fmHandler.CleanUp()
} }
func TestFrontMatterFilenamePreserved(t *testing.T) {
// Regression test for https://github.com/mikefarah/yq/issues/2538
// When using --front-matter, the filename operator should return
// the original filename, not the path to the temporary file.
file := createTestFile(`---
name: john
---
Some content
`)
originalFilename := "/path/to/original/file.md"
fmHandler := NewFrontMatterHandler(file)
err := fmHandler.Split()
if err != nil {
panic(err)
}
tempFilename := fmHandler.GetYamlFrontMatterFilename()
// Register the alias (as the command code does)
SetFilenameAlias(tempFilename, originalFilename)
defer ClearFilenameAliases()
// Verify resolveFilename returns the original name
resolved := resolveFilename(tempFilename)
test.AssertResult(t, originalFilename, resolved)
// Read documents using the temp file, verify they get the original filename
reader, err := readStream(tempFilename)
if err != nil {
panic(err)
}
decoder := NewYamlDecoder(ConfiguredYamlPreferences)
docs, err := readDocuments(reader, tempFilename, 0, decoder)
if err != nil {
panic(err)
}
if docs.Len() == 0 {
t.Fatal("expected at least one document")
}
firstDoc := docs.Front().Value.(*CandidateNode)
test.AssertResult(t, originalFilename, firstDoc.filename)
tryRemoveTempFile(file)
fmHandler.CleanUp()
}
func TestFrontMatterSplitWithArray(t *testing.T) { func TestFrontMatterSplitWithArray(t *testing.T) {
file := createTestFile(`[1,2,3] file := createTestFile(`[1,2,3]
--- ---

View File

@ -230,7 +230,8 @@ var goccyYamlFormatScenarios = []formatScenario{
description: "merge anchor", description: "merge anchor",
skipDoc: true, skipDoc: true,
input: "a: &remember\n c: mike\nb:\n <<: *remember", input: "a: &remember\n c: mike\nb:\n <<: *remember",
expected: "a: &remember\n c: mike\nb:\n <<: *remember\n", // fine to have !!merge as that's what the current impl does
expected: "a: &remember\n c: mike\nb:\n !!merge <<: *remember\n",
}, },
{ {
description: "custom tag", description: "custom tag",

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