Compare commits

...

112 Commits

Author SHA1 Message Date
dependabot[bot]
e2f1d5ccf7
Bump go.yaml.in/yaml/v4 from 4.0.0-rc.5 to 4.0.0-rc.6 (#2759)
Bumps [go.yaml.in/yaml/v4](https://github.com/yaml/go-yaml) from 4.0.0-rc.5 to 4.0.0-rc.6.
- [Commits](https://github.com/yaml/go-yaml/compare/v4.0.0-rc.5...v4.0.0-rc.6)

---
updated-dependencies:
- dependency-name: go.yaml.in/yaml/v4
  dependency-version: 4.0.0-rc.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:54:05 +10:00
dependabot[bot]
16f149b351
Bump github.com/pelletier/go-toml/v2 from 2.4.0 to 2.4.2 (#2762)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.4.0 to 2.4.2.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.4.0...v2.4.2)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:53:39 +10:00
dependabot[bot]
5da9215306
Bump actions/setup-go from 6.4.0 to 6.5.0 (#2763)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.4.0 to 6.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4a3601121d...924ae3a1cd)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:53:18 +10:00
dependabot[bot]
e95bb7e472
Bump golang.org/x/net from 0.55.0 to 0.56.0 (#2740)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0.
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 11:11:00 +10:00
dependabot[bot]
2074319595
Bump golang.org/x/mod from 0.36.0 to 0.37.0 (#2741)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/mod/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:04:36 +10:00
dependabot[bot]
be992d8add
Bump alpine from a2d49ea to 28bd5fe (#2752)
Bumps alpine from `a2d49ea` to `28bd5fe`.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:04:28 +10:00
dependabot[bot]
637bb1fecd
Bump golang from 11fd8f7 to 792443b (#2753)
Bumps golang from `11fd8f7` to `792443b`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:04:05 +10:00
dependabot[bot]
bc23b42789
Bump github.com/pelletier/go-toml/v2 from 2.3.1 to 2.4.0 (#2754)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.3.1...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:03:55 +10:00
dependabot[bot]
8e2c9b612d
Bump golang from 68cb6d6 to 11fd8f7 (#2738)
Bumps golang from `68cb6d6` to `11fd8f7`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:46:10 +10:00
dependabot[bot]
0970cd4b05
Bump alpine from 5b10f43 to a2d49ea (#2739)
Bumps alpine from `5b10f43` to `a2d49ea`.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:46:03 +10:00
dependabot[bot]
bf3591a234
Bump golang.org/x/text from 0.37.0 to 0.38.0 (#2742)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.37.0 to 0.38.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:45:37 +10:00
dependabot[bot]
09f1565d51
Bump github/codeql-action from 4.35.2 to 4.36.2 (#2743)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.36.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](95e58e9a2c...8aad20d150)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:29:01 +10:00
dependabot[bot]
13d340ff51
Bump sigstore/cosign-installer from 3.10.1 to 4.1.2 (#2744)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.10.1 to 4.1.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](7e8b541eb2...6f9f177880)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:28:52 +10:00
Mike Farah
5cf0adcc5b Adding some tests 2026-06-09 14:23:11 +10:00
William Floyd
30e16a33c3
Fix for #2677 (#2705)
* Update docs given https://github.com/yaml/go-yaml/pull/348

* Fix for https://github.com/mikefarah/yq/issues/2677

Depends on https://github.com/yaml/go-yaml/pull/348

* Test for https://github.com/mikefarah/yq/issues/2677

* Remove redundant check and add test case for explicit `!!merge` on `*+` traversal

* Bump go.yaml.in/yaml/v4 from 4.0.0-rc.4 to 4.0.0-rc.5
2026-06-09 14:08:47 +10:00
Mike Farah
25dfcf280f Switch to typos from cspell - no more npm 2026-06-07 20:09:44 +10:00
Mike Farah
91a166e8d8 Pinning using hashes 2026-06-07 19:57:15 +10:00
Mike Farah
f9b0d7e45d Pinning using hashes 2026-06-07 19:49:39 +10:00
Mike Farah
48a851bf57 Fixing toml docs 2026-06-07 19:47:11 +10:00
Mike Farah
131aa0b7cc pinning deps 2026-06-07 19:46:58 +10:00
Mike Farah
ef3c14f806 project words 2026-06-07 18:21:59 +10:00
Mike Farah
26434e221e Process for SHA-pin github action 2026-06-07 18:19:13 +10:00
Mike Farah
0eebc242fb New workflow for github action 2026-06-07 08:56:21 +10:00
Mike Farah
87a62da881 New workflow for github action 2026-06-07 08:54:17 +10:00
Mike Farah
ef507264e1 New workflow for github action 2026-06-07 08:50:24 +10:00
Mike Farah
2a40eb3d04 Bumping github action docker file 2026-06-07 08:45:27 +10:00
Mike Farah
e3cb1dc7c6 Fixing build 2026-06-07 07:03:26 +10:00
Mike Farah
1b9b4ac518 Bumping version 2026-06-06 20:59:47 +10:00
Mike Farah
9b67d655f1 Portable bump version 2026-06-06 20:59:43 +10:00
Mike Farah
f8850c043c Preparting release 2026-06-06 20:57:28 +10:00
Mike Farah
c5a342359d Refining 2026-06-06 19:14:59 +10:00
Mike Farah
196a99e912 Agent disclosure 2026-06-06 19:13:33 +10:00
Mike Farah
fa99bf12c3 Merging agents 2026-06-06 18:38:30 +10:00
Mike Farah
71117613d6
docs: add AGENTS.md with Cursor Cloud development instructions (#2735)
* docs: add AGENTS.md with Cursor Cloud development instructions

Co-authored-by: Mike Farah <mikefarah@users.noreply.github.com>

* docs: fix AGENTS.md spelling and note spellcheck before PR

Co-authored-by: Mike Farah <mikefarah@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mike Farah <mikefarah@users.noreply.github.com>
2026-06-06 18:25:36 +10:00
Mike Farah
0cb1bbe698 Fixing test 2026-06-06 18:25:12 +10:00
Mike Farah
8fc8eedd3b Minor doc update 2026-06-06 18:23:24 +10:00
Xie Benyi
7620a727b5
fix: reset INI decoder state on init (#2719) 2026-06-06 16:21:13 +10:00
Tony
6f9201a0ca
feat: add --ini-preserve-quotes flag for INI round-trip quote preservation (#2728)
Add INIPreferences.PreserveSurroundedQuote option that wires through to
go-ini/ini's LoadOptions.PreserveSurroundedQuote. When enabled, existing
surrounding quotes on INI values are preserved during decode/encode
round-trips.

Fixes #2456

Co-authored-by: toller892 <toller892@gmail.com>
2026-06-06 16:20:29 +10:00
dependabot[bot]
5b19ddbcd0
Bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#2724)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](4d04d5d948...d7f5e7f509)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 16:02:50 +10:00
dependabot[bot]
1ac2a3d96a
Bump docker/setup-qemu-action from 4.0.0 to 4.1.0 (#2726)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](ce360397dd...06116385d9)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 16:00:51 +10:00
dependabot[bot]
4a12c3f908
Bump golang.org/x/net from 0.54.0 to 0.55.0 (#2725)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 16:00:38 +10:00
dependabot[bot]
41377138ef
Bump docker/login-action from 4.1.0 to 4.2.0 (#2727)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](4907a6ddec...650006c6eb)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:53:24 +10:00
dependabot[bot]
ae5cf9ff04
Bump golang from 1.26.3 to 1.26.4 (#2731)
Bumps golang from 1.26.3 to 1.26.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:53:06 +10:00
dependabot[bot]
fe449b956a
Bump actions/checkout from 6.0.2 to 6.0.3 (#2732)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...df4cb1c069)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-06 15:52:53 +10:00
Rayan Salhab
8f3291d316
fix: decode properties array bracket paths (#2693)
* fix: decode properties array bracket paths

* test: add nested array bracket properties decode case

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-15 22:22:06 +10:00
ChrisJr404
2861815f71
fix(json): preserve floats with trailing zero when encoding YAML to JSON (#2701)
YAML scalars tagged `!!float` were round-tripped through `float64` and
re-serialized by Go's JSON encoder, which strips the decimal part of
whole-number floats. As a result, `50.0` came out as `50` and a
sequence like `[50.0, 95.0, 99.0, 99.9]` became `[50,95,99,99.9]`,
turning a uniform array of floats into a mixed int/float array that
downstream consumers (Horreum, JSON Schema validators, jq, etc.)
reject.

The JSON spec does not distinguish ints from floats, but every common
JSON library (Go's `encoding/json`, Python's `json`, jq) preserves the
fractional form of values that came in as floats. yq's YAML decoder
already parses these as `!!float` with the original text intact, so we
can emit them verbatim instead of round-tripping.

`MarshalJSON` for `ScalarNode` now special-cases `!!float`:
- if `Value` is already a JSON-shaped number literal containing a `.`
  or exponent, emit it verbatim (e.g. `50.0`, `99.9`, `1.5e-3`, `-7.0`);
- if `Value` is an integer-shaped string tagged `!!float` (e.g.
  `!!float 5`), format the parsed float and append `.0` so it stays a
  JSON number with a fractional part;
- otherwise (empty value, parse error, or non-finite result), fall back
  to the existing encoding path so behaviour for `.inf` / `.nan` and
  anything unusual is unchanged.

`!!int` nodes still encode as JSON integers.

Closes #2683

Signed-off-by: ChrisJr404 <chris@hacknow.com>
2026-05-14 20:00:34 +10:00
梦曦·花已落
fcb79822dd
feat(toml): fix JSON to TOML root scope and null handling (#2689)
Ensure root-level TOML attributes are emitted before table sections so fields like sort remain root-scoped. Skip null-valued object fields during TOML encoding
    instead of converting them to empty strings.
2026-05-14 19:57:42 +10:00
dependabot[bot]
e9acb9b734
Bump golang.org/x/mod from 0.35.0 to 0.36.0 (#2709)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/mod/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 19:56:57 +10:00
dependabot[bot]
83b282c413
Bump golang.org/x/net from 0.53.0 to 0.54.0 (#2707)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 19:38:12 +10:00
dependabot[bot]
54fa4324ea
Bump golang from 1.26.2 to 1.26.3 (#2706)
Bumps golang from 1.26.2 to 1.26.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 19:38:00 +10:00
Terminal Chai
ee6c30dac2
fix: reset TOML decoder finished flag on Init to fix multi-doc evaluation (#2704)
* fix: reset TOML decoder between files

* test: fix TOML regression fixture spelling
2026-05-14 19:37:43 +10:00
Rayan Salhab
722c9aa16c
Fix nested inline YAML merge explode (#2699)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-14 19:33:50 +10:00
dependabot[bot]
702dd16048
Bump github.com/pelletier/go-toml/v2 from 2.3.0 to 2.3.1 (#2695)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 19:31:08 +10:00
Rayan Salhab
d1dff4661b
fix: preserve TOML inline table array scope (#2694)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-14 19:30:52 +10:00
Copilot
cb97935554
fix: TOML encoder uses inline tables for YAML FlowStyle mappings, inconsistent with explicit JSON parsing (#2687)
* Initial plan

* fix: TOML encoder no longer treats YAML FlowStyle as inline tables

Remove FlowStyle checks from the TOML encoder. YAML flow-style mappings
are a YAML-specific rendering hint and should not influence TOML output.
Only nodes explicitly marked with EncodeHintInline (set by the TOML
decoder for actual TOML inline tables) will produce TOML inline table
syntax.

This fixes the bug where JSON auto-detected via the YAML parser (which
parses {} as flow-style mappings) would produce inline TOML tables
instead of readable table sections, while explicitly parsing with
-p json produced correct table sections.

Updated tests: YAML flow mappings now produce table sections (same as
block mappings), consistent with the fix. Added new test cases for the
JSON → TOML conversion via both YAML decoder (auto-detection) and JSON
decoder.

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/3e504870-b585-4998-af9c-a451e2f6a6a3

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-28 19:32:07 +10:00
Rayan Salhab
cfe2eee7e6
Preserve empty TOML arrays in tables (#2686)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-04-27 19:12:30 +10:00
dependabot[bot]
1a433d1035
Bump actions/upload-artifact from 4.6.1 to 7.0.1 (#2663)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.1 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](4cec3d8aa0...043fb46d1a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:40:39 +10:00
dependabot[bot]
1c0d8b9da9
Bump actions/checkout from 4.2.2 to 6.0.2 (#2668)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.2.2...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:40:25 +10:00
dependabot[bot]
0110a3cea8
Bump golang.org/x/net from 0.52.0 to 0.53.0 (#2669)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0.
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:39:45 +10:00
dependabot[bot]
54482d44b3
Bump golang from 2a2b4b5 to 5f3787b (#2664)
Bumps golang from `2a2b4b5` to `5f3787b`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:19:59 +10:00
dependabot[bot]
33f3351c01
Bump ossf/scorecard-action from 2.4.1 to 2.4.3 (#2665)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.1 to 2.4.3.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](f49aabe0b5...4eaacf0543)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:19:38 +10:00
dependabot[bot]
6cb656ced0
Bump alpine from 2510918 to 5b10f43 (#2667)
Bumps alpine from `2510918` to `5b10f43`.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:19:22 +10:00
Terminal Chai
ecc43d7c9e
fix: reset TOML decoder between files when evaluating all at once (#2685)
* fix: reset TOML decoder between files

* test: fix TOML regression fixture spelling
2026-04-26 09:18:45 +10:00
Jan Dubois
1deec5e450
Fix repeatString overflow test on 32-bit platforms (#2680)
The test literal "ab" * 4611686018427387904 (2^62) exceeds MaxInt32,
so parseInt rejects it before the size guard runs. Compute the count
with 1 << (bits.UintSize - 2) to yield 2^30 on 32-bit and 2^62 on
64-bit. Both values, when doubled by len("ab"), wrap past MaxInt and
bypass a naive len*count guard, exercising the division-safe check
added in #2644.

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 09:18:34 +10:00
dependabot[bot]
ff45fad14c
Bump github.com/zclconf/go-cty from 1.18.0 to 1.18.1 (#2682)
Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/zclconf/go-cty/releases)
- [Changelog](https://github.com/zclconf/go-cty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zclconf/go-cty/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/zclconf/go-cty
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:17:34 +10:00
dependabot[bot]
6679d3c02b
Bump github/codeql-action from 3 to 4 (#2671)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 21:04:18 +10:00
dependabot[bot]
54a7fc8f0c
Bump softprops/action-gh-release from 2.6.2 to 3.0.0 (#2672)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.6.2 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](3bb12739c2...b430933298)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 21:03:55 +10:00
dependabot[bot]
0d3ab07928
Bump golang.org/x/text from 0.35.0 to 0.36.0 (#2670)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.35.0 to 0.36.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 21:02:08 +10:00
Mike Farah
d93987a93a
release notes 2026-04-17 16:04:18 +10:00
Mike Farah
751d8ad57b
Bumping version 2026-04-17 16:03:30 +10:00
Mike Farah
6dd681a7c0
Fixing release signing 2026-04-17 16:03:18 +10:00
Mike Farah
fc7c337d8f
Updating bump version script 2026-04-17 15:36:35 +10:00
Mike Farah
e969dd789f
Bumping version 2026-04-17 15:36:22 +10:00
Mike Farah
dc4b4ea1df
Preparing release notes 2026-04-17 15:31:40 +10:00
Mike Farah
602586d8fd
Create scorecard.yml
Signed-off-by: Mike Farah <mikefarah@gmail.com>
2026-04-14 18:43:11 +10:00
Copilot
9a0335abb2
fix: restrict GitHub Actions workflow token permissions (OSSF least-privilege) (#2662)
* Initial plan

* fix: add least-privilege token permissions to GitHub workflows (OSSF)

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/1b5db5e2-af78-4289-a6e0-2e972fc68ef1

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-13 19:11:10 +10:00
Mike Farah
838c51691c
Trying to test release 2026-04-12 19:54:34 +10:00
Mike Farah
c8f6c1a042 Updating release to sign checksums 2026-04-12 19:39:01 +10:00
Copilot
0e803833fb
chore: pin GitHub Actions and Docker base images to full-length hashes (OSSF scorecard) (#2658)
* Initial plan

* chore: pin GitHub Actions dependencies to specific commit SHAs (OSSF)

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/cbd03f0a-f2dc-4da4-b01c-7dd06ad83ee9

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* chore: pin Dockerfile base images to specific SHA digests (OSSF)

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/7a8f6690-37fb-42ab-b3dc-0dd23c270fbe

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* chore: revert yq pins in test-yq.yml; add release note for github-action/Dockerfile SHA

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/e1b35d79-92a3-47d5-b4ac-a2efe2fd58ce

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-12 19:31:32 +10:00
Copilot
30ca9ffde7
Add SECURITY.md security policy (#2660)
* Initial plan

* Add SECURITY.md with security policy

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/f9ff8a4c-addc-485b-abb8-4103394851a4

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-12 18:57:11 +10:00
Copilot
2927a28283
TOML encoder: prefer readable table sections over inline tables (#2649)
* Initial plan

* Fix TOML encoder to prefer readable table sections over inline tables

When converting from YAML/JSON to TOML, the encoder now always uses
readable TOML table section syntax ([section]) instead of compact inline
hash table syntax (key = { ... }), which better matches TOML's goal as
a human-focused configuration format.

Changes:
- decoder_toml.go: Mark inline TOML tables with FlowStyle so round-trips
  can be distinguished from YAML flow mappings
- encoder_toml.go:
  - encodeTopLevelEntry: use FlowStyle check instead of EncodeSeparate to
    decide inline vs table section (all block mappings now become tables)
  - encodeSeparateMapping: count FlowStyle children as attributes; use
    recursive encodeSeparateMapping for nested non-flow mappings
  - encodeMappingBodyWithPath: emit non-flow child mappings as sub-table
    sections instead of inline tables
- toml_test.go: add encode (YAML→TOML) test scenarios, update roundtrip
  expectations for inline tables (now expanded to table sections)

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/4824a219-6d5e-42e7-bca1-a8a277bf8c6a

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Fix TOML roundtrip: use TomlInline flag instead of FlowStyle to preserve inline tables

FlowStyle affected YAML decode output (causing inline tables to appear as
YAML flow mappings). Replace it with a new TOML-specific TomlInline bool
on CandidateNode that:
- Is set by the TOML decoder for inline tables (not FlowStyle)
- Is copied by UpdateAttributesFrom so it survives DeeplyAssign merges
- Is checked by the TOML encoder alongside FlowStyle (for YAML flow maps)
- Has no effect on the YAML encoder, preserving existing TOML→YAML output

TOML roundtrip tests are restored to their original expected values (inline
tables stay inline, table sections stay as sections).

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/f59bdf62-6d16-4664-991b-38eb87c9d81c

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Refactor EncodeSeparate+TomlInline into a single EncodeHint enum

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/24db9a8f-601d-4ccf-ada7-129ed3226bb6

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Fix stale comment in hasStructuralChildren

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/24db9a8f-601d-4ccf-ada7-129ed3226bb6

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Remove unused hasStructuralChildren method from tomlEncoder

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/2c234b77-28e9-4995-ba6f-9d213ec551a0

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-12 18:36:43 +10:00
Copilot
c47fe40a30
Fix TOML encoder to quote keys containing special characters (#2648)
* Initial plan

* Fix TOML encoder to quote keys with special characters

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/b2b52954-d13f-4e67-831a-16fdd3378de5

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Add test for dotted table section header with special character key

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/12c783dd-8b7f-43bf-b71a-e7a0b5e55fea

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Apply De Morgan's law to tomlKey condition to fix staticcheck QF1001

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/eeab0316-309f-418f-b357-11bbacffb471

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-12 14:27:20 +10:00
dependabot[bot]
8c018da9c9
Bump go.yaml.in/yaml/v4 from 4.0.0-rc.3 to 4.0.0-rc.4 (#2579)
* Bump go.yaml.in/yaml/v4 from 4.0.0-rc.3 to 4.0.0-rc.4

Bumps [go.yaml.in/yaml/v4](https://github.com/yaml/go-yaml) from 4.0.0-rc.3 to 4.0.0-rc.4.
- [Commits](https://github.com/yaml/go-yaml/compare/v4.0.0-rc.3...v4.0.0-rc.4)

---
updated-dependencies:
- dependency-name: go.yaml.in/yaml/v4
  dependency-version: 4.0.0-rc.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix test expectations for go.yaml.in/yaml/v4 rc.4 error message changes

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/e172bcc4-f547-4c9f-bcc5-ba61849d37e5

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-04-12 14:26:15 +10:00
Copilot
44c55c8a54
Add system(command; args) operator (disabled by default) (#2640)
* Initial plan

* Add system(command; args) operator with --enable-system-operator flag

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/8a11e9a0-10d2-4f2a-ae29-4e9d0bfc266f

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Update pkg/yqlib/operator_system.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Evaluate system command/args per matched node using SingleReadonlyChildContext

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/dca841eb-3f63-4f23-adeb-556431560420

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Add yqFlags to expressionScenario for doc command snippets; fix system op docs

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/3f8a5375-25fd-4428-a8e6-b630194c36b2

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Update pkg/yqlib/doc/operators/headers/system-operators.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update pkg/yqlib/doc/operators/system-operators.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Validate command node type and handle multiple results with debug log

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/928aabc5-ad71-41d8-94ab-403942e3f92d

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove deprecated --enable-system-operator alias; use --security-enable-system-operator consistently

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/286b95e9-b6d7-4ab8-b401-2d7a03853922

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address deep review feedback: error on disabled, strict arg/cmd validation, debug logs, docs

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/fbfba2db-60ea-4c20-a4c2-0fd396b80c81

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Co-authored-by: Mike Farah <mikefarah@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 20:06:46 +10:00
dependabot[bot]
22e609b2d9
Bump golang from 1.26.1 to 1.26.2 (#2654)
Bumps golang from 1.26.1 to 1.26.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 18:57:30 +10:00
Copilot
3b2423e871
Add string slicing support (#2639)
* Initial plan

* Add string slicing support to yq

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/a8525fbb-77a7-4bb0-a3a7-b24f99ae8710

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Fix sliceStringNode signature and fix test descriptions/expressions

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/58726b13-68ae-4f93-971f-eb70459edcf4

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Update pkg/yqlib/operator_slice.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix array slice out-of-bounds panic with very negative indices

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/7c146762-d251-45fd-8555-2488f59fc57b

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* S2-S4: tighten lexer condition, fix doc header, add Unicode example

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/ec06083e-e20a-45d2-bf7e-4e1fa7be1073

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Fix spelling: multibyte -> multi-byte in Unicode test subdescription

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/6e7b304b-5b52-4e89-8bad-ba22813305c7

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Co-authored-by: Mike Farah <mikefarah@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-06 19:29:07 +10:00
dependabot[bot]
68f0322ba3
Bump softprops/action-gh-release from 1 to 2 (#1978)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 18:53:44 +10:00
dependabot[bot]
d69c7d1a36
Bump github.com/yuin/gopher-lua from 1.1.1 to 1.1.2 (#2642)
Bumps [github.com/yuin/gopher-lua](https://github.com/yuin/gopher-lua) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/yuin/gopher-lua/releases)
- [Commits](https://github.com/yuin/gopher-lua/compare/v1.1.1...v1.1.2)

---
updated-dependencies:
- dependency-name: github.com/yuin/gopher-lua
  dependency-version: 1.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 18:45:44 +10:00
Jan Dubois
b0ba9589d7
Fix findInArray misuse on MappingNodes in equality and contains (#2645)
recurseNodeObjectEqual and containsObject both used findInArray to
locate keys in a MappingNode's Content array. findInArray steps by 1,
so it matches against both keys (even indices) and values (odd indices).

In recurseNodeObjectEqual, when a null key in the LHS matched a null
value in the RHS at the last position, rhs.Content[indexInRHS+1]
accessed an out-of-bounds index, causing a panic.

In containsObject, a %2 guard prevented the panic but introduced false
negatives: when a null value appeared before the actual null key,
findInArray returned the value's odd index, the guard rejected it, and
the function reported the key as missing.

Both functions now use findKeyInMap, which steps by 2 and compares only
key positions. The %2 guard in containsObject is removed.

Reproducer for the panic (recurseNodeObjectEqual):

    echo '? [{~: ~}]
    : v1
    ? [{2: ~}]
    : v2' | yq '. += .'

Reproducer for the false negative (containsObject):

    printf '? 1\n: ~\n? ~\n: x\n' | yq 'contains({~: "x"})'

Found by OSS-Fuzz via the lima project's FuzzEvaluateExpression target.
https://issues.oss-fuzz.com/issues/383860504

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:30:44 +10:00
Jan Dubois
80139ae1cc
Fix panic on negative slice indices that underflow after adjustment (#2646)
sliceArrayOperator adjusts negative indices by adding Content length,
but does not clamp the result. When the absolute value of a negative
index exceeds Content length (e.g. .[-99999:3] on a 3-element array),
the adjusted index remains negative and causes an out-of-bounds access
in the Content slice loop.

Extract the adjust-and-clamp logic into clampSliceIndex and use it for
both index positions.

Reproducer (panics before this fix, returns full array after):

    echo '[a, b, c]' | yq '.[-99999:3]'

Found by OSS-Fuzz via the lima project's FuzzEvaluateExpression target.
https://issues.oss-fuzz.com/issues/438776028

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:27:02 +10:00
Jan Dubois
0374ad6b4b
Fix stack overflow from circular alias in traverse (#2647)
go-yaml accepts cross-document alias references, which the YAML spec
forbids (anchors are scoped to a single document). When a nested
assignment targets such an alias, UpdateFrom copies the Alias field
between nodes, creating a self-referencing AliasNode. Both traverse()
and traverseArrayIndices() then follow this cycle indefinitely.

Extract resolveAliasChain(), which follows aliases iteratively with a
visited set and returns an error on cycles. Both traverse() and
traverseArrayIndices() now call it, eliminating the recursive alias
handling in both code paths.

Note: traverseMergeAnchor() also dereferences aliases (lines 358 and
371) but with single-step assignment, not recursion. A self-referencing
alias there falls through the kind switch silently rather than
crashing. Using resolveAliasChain() in that function would produce a
clear error instead of silently dropping the node.

Reproducer (stack overflow before this fix, returns error after):

    echo '&-- a
    ---
    *--' | yq eval-all '. = (.x = 1)'

Found by OSS-Fuzz via the lima project's FuzzEvaluateExpression target.
https://issues.oss-fuzz.com/issues/390467412

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:25:13 +10:00
Jan Dubois
2ef934281e
Fix panic and OOM in repeatString for large repeat counts (#2644)
The existing check (count > 10 million) does not account for string
length. A 68-byte string repeated 35 trillion times passes the count
check but panics in strings.Repeat with "makeslice: len out of range".
Smaller counts (e.g. 10 million * 6-byte string = 60 MB) cause OOM on
memory-constrained environments like OSS-Fuzz (2560 MB limit).

Replace the count-only check with a result size check: the product of
string length and repeat count must not exceed 10 MiB. Use division
(len > limit/count) instead of multiplication (len*count > limit) to
avoid integer overflow — a large count can wrap the product to a
negative value, bypassing the guard entirely.

Fixes at least four OSS-Fuzz bugs found via Lima's FuzzEvaluateExpression:
  https://issues.oss-fuzz.com/issues/418818862 (makeslice overflow)
  https://issues.oss-fuzz.com/issues/422001683 (timeout from huge alloc)
  https://issues.oss-fuzz.com/issues/383195001 (OOM, 3 GB allocation)
  https://issues.oss-fuzz.com/issues/385180606 (OOM, 97 TB allocation)

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:22:46 +10:00
dependabot[bot]
17f66dc6c6
Bump github.com/goccy/go-json from 0.10.5 to 0.10.6 (#2636)
Bumps [github.com/goccy/go-json](https://github.com/goccy/go-json) from 0.10.5 to 0.10.6.
- [Release notes](https://github.com/goccy/go-json/releases)
- [Changelog](https://github.com/goccy/go-json/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-json/compare/v0.10.5...v0.10.6)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-json
  dependency-version: 0.10.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 20:42:41 +11:00
dependabot[bot]
dcb9c2a543
Bump github.com/pelletier/go-toml/v2 from 2.2.4 to 2.3.0 (#2637)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.4 to 2.3.0.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.4...v2.3.0)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 20:42:19 +11:00
dependabot[bot]
8f5d876bf0
Bump github.com/fatih/color from 1.18.0 to 1.19.0 (#2638)
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 20:42:06 +11:00
Copilot
7d8d3ab902
Replace gopkg.in/op/go-logging.v1 with log/slog (#2635)
* Initial plan

* Replace gopkg.in/op/go-logging.v1 with log/slog

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/aa9c12f4-21b9-4633-9868-6b56585b247f

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2026-03-26 20:41:54 +11:00
Mike Farah
11f4dc1a03 Bumping version 2026-03-26 10:08:43 +11:00
Mike Farah
0f4fb8d35e Bumping version 2026-03-26 10:03:01 +11:00
Mike Farah
80c319aa0c Fixing tests with latest linting rules 2026-03-26 09:29:51 +11:00
Terminal Chai
b25ae78545
fix: reset TOML decoder state between files (#2634)
* fix: reset TOML decoder between files

* test: fix TOML regression fixture spelling
2026-03-26 09:16:21 +11:00
cobyfrombrooklyn-bot
b151522485
fix: preserve original filename when using --front-matter (#2613)
When using --front-matter, yq creates a temporary file for the
extracted YAML content but replaces the original filename in args
with the temp file path. This caused the 'filename' operator to
return the temp file path instead of the original filename.

Added a filename alias mechanism: when front matter processing
replaces the file path, it registers the original filename as an
alias. The readDocuments and stream evaluator functions resolve
aliases before setting candidateNode.filename.

Fixes #2538

Co-authored-by: cobyfrombrooklyn-bot <cobyfrombrooklyn@gmail.com>
2026-03-26 09:06:20 +11:00
dependabot[bot]
c5cbf9760b
Bump golang.org/x/net from 0.50.0 to 0.52.0 (#2628)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.52.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 14:03:38 +11:00
dependabot[bot]
b5cb9a2f20
Bump github.com/zclconf/go-cty from 1.17.0 to 1.18.0 (#2616)
Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/zclconf/go-cty/releases)
- [Changelog](https://github.com/zclconf/go-cty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zclconf/go-cty/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/zclconf/go-cty
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:19:36 +11:00
dependabot[bot]
133ba767a6
Bump golang.org/x/mod from 0.33.0 to 0.34.0 (#2629)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/mod/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:19:22 +11:00
dependabot[bot]
5db3dcf394
Bump golang.org/x/text from 0.34.0 to 0.35.0 (#2630)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.34.0 to 0.35.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:11:12 +11:00
Oleksandr Redko
4c148178e2
Fix typo in filename (#2611) 2026-03-21 09:29:07 +11:00
dependabot[bot]
4df6e46f95
Bump docker/setup-buildx-action from 3 to 4 (#2627)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 09:28:17 +11:00
Mike Farah
6a965bc39a Bumping golint 2026-03-21 09:25:31 +11:00
dependabot[bot]
34d3a29308
Bump golang from 1.26.0 to 1.26.1 (#2626)
Bumps golang from 1.26.0 to 1.26.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 20:30:05 +11:00
dependabot[bot]
16e4df2304
Bump docker/login-action from 3 to 4 (#2620)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 13:48:13 +11:00
dependabot[bot]
79a92d0478
Bump docker/setup-qemu-action from 3 to 4 (#2621)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 13:47:56 +11:00
Mike Farah
88a31ae8c6 updating release notes 2026-02-14 18:43:51 +11:00
138 changed files with 2612 additions and 855 deletions

View File

@ -20,6 +20,8 @@ on:
schedule: schedule:
- cron: '24 3 * * 1' - cron: '24 3 * * 1'
permissions: {}
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@ -38,11 +40,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -53,7 +55,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v4 uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -67,4 +69,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2

View File

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

View File

@ -7,23 +7,28 @@ on:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
permissions: {}
jobs: jobs:
publishDocker: publishDocker:
environment: dockerhub environment: dockerhub
env: env:
IMAGE_NAME: mikefarah/yq IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
with: with:
platforms: all platforms: all
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with: with:
version: latest version: latest
@ -31,13 +36,13 @@ jobs:
run: echo ${{ steps.buildx.outputs.platforms }} && docker version run: echo ${{ steps.buildx.outputs.platforms }} && docker version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -75,26 +80,3 @@ 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,25 +5,51 @@ 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@v6 uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with: with:
go-version: '^1.20' go-version: '^1.20'
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v6 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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 https://raw.githubusercontent.com/golang/dep/master/install.sh | sh curl -sSfL https://raw.githubusercontent.com/golang/dep/1f7c19e5f52f49ffb9f956f64c010be14683468b/install.sh | env DEP_RELEASE_TAG=v0.5.4 sh
dep ensure dep ensure
fi fi

View File

@ -5,12 +5,17 @@ on:
- 'v4.*' - 'v4.*'
- 'draft-*' - 'draft-*'
permissions: {}
jobs: jobs:
publishGitRelease: publishGitRelease:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@v6 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with: with:
go-version: '^1.20' go-version: '^1.20'
check-latest: true check-latest: true
@ -24,7 +29,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 uses: docker://pandoc/core:2.14.2@sha256:04e127c6642a2b9d447c26fe0ac6a5932efa8f508eda9f07da51b6e621dd7c19
id: gen-man-page id: gen-man-page
with: with:
args: >- args: >-
@ -37,14 +42,22 @@ 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@latest go install github.com/goreleaser/goreleaser/v2@v2.16.0
./scripts/xcompile.sh ./scripts/xcompile.sh
- name: Sign checksums
run: |
cosign sign-blob --yes --bundle build/checksums.bundle build/checksums
cosign sign-blob --yes --bundle build/checksums-bsd.bundle build/checksums-bsd
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with: with:
files: build/* files: build/*
draft: true draft: true

78
.github/workflows/scorecard.yml vendored Normal file
View File

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

View File

@ -7,19 +7,23 @@ on:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
permissions: {}
jobs: jobs:
buildSnap: buildSnap:
environment: snap environment: snap
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: snapcore/action-build@v1 - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
id: build id: build
env: env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with: with:
snapcraft-args: "remote-build --launchpad-accept-public-upload" snapcraft-args: "remote-build --launchpad-accept-public-upload"
- uses: snapcore/action-publish@v1 - uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
env: env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with: with:

View File

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

View File

@ -1,3 +1,79 @@
# 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 # General rules
✅ **DO:** ✅ **DO:**
- You can use ./yq with the `--debug-node-info` flag to get a deeper understanding of the ast. - You can use ./yq with the `--debug-node-info` flag to get a deeper understanding of the ast.
@ -5,10 +81,12 @@
- Add comprehensive tests to cover the changes - Add comprehensive tests to cover the changes
- Run test suite to ensure there is no regression - Run test suite to ensure there is no regression
- Use UK english spelling - Use UK english spelling
- **Follow the mandatory GitHub agent disclosure rule above** on every GitHub action — no exceptions
❌ **DON'T:** ❌ **DON'T:**
- Git add or commit - Git add or commit
- Add comments to functions that are self-explanatory - 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)

View File

@ -1,4 +1,4 @@
FROM golang:1.26.0 AS builder FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3 AS builder
WORKDIR /go/src/mikefarah/yq WORKDIR /go/src/mikefarah/yq
@ -10,7 +10,7 @@ RUN ./scripts/acceptance.sh
# Choose alpine as a base image to make this useful for CI, as many # Choose alpine as a base image to make this useful for CI, as many
# CI tools expect an interactive shell inside the container # CI tools expect an interactive shell inside the container
FROM alpine:3 AS production FROM alpine:3@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b 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,8 +1,4 @@
FROM golang:1.26.0 FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3
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

@ -42,7 +42,7 @@ 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 tmp/dev_image_id: Dockerfile.dev scripts/devtools.sh _typos.toml
@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 .

26
SECURITY.md Normal file
View File

@ -0,0 +1,26 @@
# Security Policy
## Reporting a Vulnerability
Please **do not** report security vulnerabilities through public GitHub issues.
Instead, use GitHub's private vulnerability reporting feature:
👉 https://github.com/mikefarah/yq/security
This allows vulnerabilities to be triaged and addressed confidentially before any public disclosure.
## Scope
### HTTP / TLS / Network vulnerabilities
yq is a command-line YAML/JSON/TOML processor that reads from files or standard input and writes to standard output. **yq does not include any HTTP or network libraries** and makes no network connections at runtime. CVEs related to HTTP, TLS, or networking are therefore **not applicable** to yq.
### Dependency version bumps
yq uses [Dependabot](https://docs.github.com/en/code-security/dependabot) to automatically raise pull requests for:
- Go module dependencies
- Go toolchain version
- Docker base images
Please **do not** raise pull requests or issues solely to bump dependency or Go versions — Dependabot handles this automatically and the maintainers merge those PRs regularly.

20
_typos.toml Normal file
View File

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

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' image: 'docker://mikefarah/yq:4-githubaction@sha256:e1b8c865f299ea6b02910a7ddf147d5d431244d4cc116f89c2148c9f53822906'
args: args:
- ${{ inputs.cmd }} - ${{ inputs.cmd }}

View File

@ -101,12 +101,15 @@ 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,6 +13,7 @@ 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

@ -122,12 +122,15 @@ 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,6 +13,7 @@ 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,30 +68,21 @@ 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") != ""
if verbose && forceNoColor { level := slog.LevelWarn
level = logging.DEBUG if verbose {
stringFormat = `[%{level:5.5s}] %{time:15:04:05} %{shortfile:-33s} %{shortfunc:-25s} %{message}` level = slog.LevelDebug
} 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}`
} }
var format = logging.MustStringFormatter(stringFormat) yqlib.GetLogger().SetLevel(level)
var backend = logging.AddModuleLevel( opts := &slog.HandlerOptions{Level: level, AddSource: verbose}
logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format)) handler := slog.NewTextHandler(os.Stderr, opts)
yqlib.GetLogger().SetSlogger(slog.New(handler))
backend.SetLevel(level, "")
logging.SetBackend(backend)
yqlib.InitExpressionParser() yqlib.InitExpressionParser()
return nil return nil
@ -165,6 +156,8 @@ 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)")
@ -221,6 +214,7 @@ yq -P -oy sample.json
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.") rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableFileOps, "security-disable-file-ops", "", false, "Disable file related operations (e.g. load)") rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableFileOps, "security-disable-file-ops", "", false, "Disable file related operations (e.g. load)")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.EnableSystemOps, "security-enable-system-operator", "", false, "Enable system operator to allow execution of external commands.")
rootCmd.AddCommand( rootCmd.AddCommand(
createEvaluateSequenceCommand(), createEvaluateSequenceCommand(),

View File

@ -195,6 +195,7 @@ 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().Debug("Using input format %v", inputFormat) yqlib.GetLogger().Debugf("Using input format %v", inputFormat)
yqlib.GetLogger().Debug("Using output format %v", outputFormat) yqlib.GetLogger().Debugf("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().Debug("Unknown file format extension '%v', defaulting to yaml", inputFormat) yqlib.GetLogger().Debugf("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().Warning("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat) yqlib.GetLogger().Warningf("yq default output is now 'auto' (based on the filename extension). Normally yq would output '%v', but for backwards compatibility 'yaml' has been set. Please use -oy to specify yaml, or drop the -p flag.", outputFormat)
} }
outputFormat = "yaml" outputFormat = "yaml"
} }
@ -235,7 +235,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(logging.DEBUG) { if yqlib.GetLogger().IsEnabledFor(slog.LevelDebug) {
if err != nil { if err != nil {
yqlib.GetLogger().Debugf("error: %v", err) yqlib.GetLogger().Debugf("error: %v", err)
} else { } else {
@ -280,7 +280,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().Debug("Assuming arg %v is an expression file", args[0]) yqlib.GetLogger().Debugf("Assuming arg %v is an expression file", args[0])
expressionFile = args[0] expressionFile = args[0]
args = args[1:] args = args[1:]
} }
@ -296,7 +296,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().Debug("assuming expression is '%v'", args[0]) yqlib.GetLogger().Debugf("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] { if a[i] != b[i] { //nolint:gosec // G602 false positive: b length equality is checked above
return false return 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.52.4" Version = "v4.53.3"
// 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,14 +0,0 @@
---
$schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json
version: '0.2'
language: en-GB
dictionaryDefinitions:
- name: project-words
path: './project-words.txt'
addWords: true
dictionaries:
- project-words
ignorePaths:
- 'vendor'
- 'bin'
- '/project-words.txt'

View File

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

29
go.mod
View File

@ -6,24 +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.18.0 github.com/fatih/color v1.19.0
github.com/go-ini/ini v1.67.0 github.com/go-ini/ini v1.67.0
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.6
github.com/goccy/go-yaml v1.19.2 github.com/goccy/go-yaml v1.19.2
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.2.4 github.com/pelletier/go-toml/v2 v2.4.2
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1 github.com/yuin/gopher-lua v1.1.2
github.com/zclconf/go-cty v1.17.0 github.com/zclconf/go-cty v1.18.1
go.yaml.in/yaml/v4 v4.0.0-rc.3 go.yaml.in/yaml/v4 v4.0.0-rc.6
golang.org/x/mod v0.33.0 golang.org/x/mod v0.37.0
golang.org/x/net v0.50.0 golang.org/x/net v0.56.0
golang.org/x/text v0.34.0 golang.org/x/text v0.38.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
) )
require ( require (
@ -34,11 +33,9 @@ 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.19.0 // indirect golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.45.0 // indirect
) )
go 1.24.0 go 1.25.0
toolchain go1.24.1

50
go.sum
View File

@ -18,14 +18,14 @@ 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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e 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,30 +61,28 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/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,3 +1,5 @@
//go:build goinstall
package main package main
import ( import (
@ -11,6 +13,10 @@ import (
// TestGoInstallCompatibility ensures the module can be zipped for go install. // TestGoInstallCompatibility ensures the module can be zipped for go install.
// This is an integration test that uses the same zip.CreateFromDir function // 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. // 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 // See: https://github.com/mikefarah/yq/issues/2587
func TestGoInstallCompatibility(t *testing.T) { func TestGoInstallCompatibility(t *testing.T) {
mod := module.Version{ mod := module.Version{

View File

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

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.Debug("its a head comment %v", comment.String()) log.Debugf("its a head comment %v", comment.String())
case yaml.CommentLinePosition: case yaml.CommentLinePosition:
o.LineComment = comment.String() o.LineComment = comment.String()
log.Debug("its a line comment %v", comment.String()) log.Debugf("its a line comment %v", comment.String())
case yaml.CommentFootPosition: case yaml.CommentFootPosition:
o.FootComment = comment.String() o.FootComment = comment.String()
log.Debug("its a foot comment %v", comment.String()) log.Debugf("its a foot comment %v", comment.String())
} }
} }
} }
@ -93,8 +93,8 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, an
log.Debugf("folded Type %v", astLiteral.Start.Type) log.Debugf("folded Type %v", astLiteral.Start.Type)
o.Style = FoldedStyle o.Style = FoldedStyle
} }
log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Value) log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.Value)
log.Debug("start value: %v ", node.(*ast.LiteralNode).Start.Type) log.Debugf("start value: %v ", node.(*ast.LiteralNode).Start.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.Debug("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key) log.Debugf("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key)
// AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling // 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.Debug("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value) log.Debugf("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value)
if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil { if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil {
return err return err
} }

View File

@ -7,6 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"strconv"
"strings"
"github.com/goccy/go-json" "github.com/goccy/go-json"
) )
@ -120,7 +123,7 @@ func (o *CandidateNode) UnmarshalJSON(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
log.Debug("UnmarshalJSON - scalar is %v", scalar) log.Debugf("UnmarshalJSON - scalar is %v", scalar)
return o.setScalarFromJson(scalar) return o.setScalarFromJson(scalar)
@ -140,6 +143,12 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
return buf.Bytes(), err return buf.Bytes(), err
case ScalarNode: case ScalarNode:
log.Debugf("MarshalJSON ScalarNode") log.Debugf("MarshalJSON ScalarNode")
if o.guessTagFromCustomType() == "!!float" {
if raw, ok := jsonFloatLiteral(o.Value); ok {
buf.WriteString(raw)
return buf.Bytes(), nil
}
}
value, err := o.GetValueRep() value, err := o.GetValueRep()
if err != nil { if err != nil {
return buf.Bytes(), err return buf.Bytes(), err
@ -177,3 +186,85 @@ func (o *CandidateNode) MarshalJSON() ([]byte, error) {
return buf.Bytes(), err return buf.Bytes(), err
} }
} }
// jsonFloatLiteral returns a JSON-shaped representation of a YAML !!float scalar
// value, preserving the original textual form (e.g. "50.0" stays "50.0") whenever
// possible. The second return value is false when the value cannot be safely
// rendered as a JSON number (e.g. ".inf", ".nan", or anything that parses to a
// non-finite float); callers should fall back to the normal encoding path in
// that case, which preserves the existing behaviour for those inputs.
func jsonFloatLiteral(raw string) (string, bool) {
if raw == "" {
return "", false
}
f, err := strconv.ParseFloat(raw, 64)
if err != nil {
return "", false
}
if math.IsInf(f, 0) || math.IsNaN(f) {
return "", false
}
if isJSONNumberLiteral(raw) {
return raw, true
}
formatted := strconv.FormatFloat(f, 'f', -1, 64)
if !strings.ContainsAny(formatted, ".eE") {
formatted += ".0"
}
return formatted, true
}
// isJSONNumberLiteral reports whether s is already a valid JSON number literal
// representing a fractional value (i.e. contains a "." or an exponent), so it
// can be emitted verbatim without round-tripping through a float64.
func isJSONNumberLiteral(s string) bool {
if s == "" {
return false
}
i := 0
if s[i] == '-' {
i++
if i == len(s) {
return false
}
}
// integer part: 0 or [1-9][0-9]*
if s[i] == '0' {
i++
} else if s[i] >= '1' && s[i] <= '9' {
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
} else {
return false
}
hasFraction := false
if i < len(s) && s[i] == '.' {
hasFraction = true
i++
if i == len(s) || s[i] < '0' || s[i] > '9' {
return false
}
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
}
hasExponent := false
if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
hasExponent = true
i++
if i < len(s) && (s[i] == '+' || s[i] == '-') {
i++
}
if i == len(s) || s[i] < '0' || s[i] > '9' {
return false
}
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
}
if i != len(s) {
return false
}
return hasFraction || hasExponent
}

View File

@ -106,6 +106,31 @@ func TestCreateScalarNodeScenarios(t *testing.T) {
} }
} }
type createStringScalarNodeScenario struct {
stringValue string
expectedTag string
}
var createStringScalarNodeScenarios = []createStringScalarNodeScenario{
{
stringValue: "yourKey",
expectedTag: "!!str",
},
{
stringValue: "<<",
expectedTag: "!!merge",
},
}
func TestCreateStringScalarNodeScenarios(t *testing.T) {
for _, tt := range createStringScalarNodeScenarios {
actual := createStringScalarNode(tt.stringValue)
test.AssertResultWithContext(t, tt.stringValue, actual.Value, fmt.Sprintf("Value for: %v", tt.stringValue))
test.AssertResultWithContext(t, tt.expectedTag, actual.Tag, fmt.Sprintf("Tag for: %v", tt.stringValue))
test.AssertResultWithContext(t, ScalarNode, actual.Kind, fmt.Sprintf("Kind for: %v", tt.stringValue))
}
}
func TestGetKeyForMapValue(t *testing.T) { 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.Debug("set anchor %v to %v", o.Anchor, NodeToString(o)) log.Debugf("set anchor %v to %v", o.Anchor, NodeToString(o))
} }
// its a single alias // 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.Debug("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor])) log.Debugf("set alias to %v", NodeToString(anchorMap[node.Alias.Anchor]))
} }
o.HeadComment = node.HeadComment o.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.Debug("UnmarshalYAML - alias from yaml: %v", o.Tag) log.Debugf("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.Debug("MarshalYAML to yaml: %v", o.Tag) log.Debugf("MarshalYAML to yaml: %v", o.Tag)
switch o.Kind { switch o.Kind {
case AliasNode: case AliasNode:
log.Debug("MarshalYAML - alias to yaml: %v", o.Tag) log.Debugf("MarshalYAML - alias to yaml: %v", o.Tag)
target := &yaml.Node{Kind: yaml.AliasNode} target := &yaml.Node{Kind: yaml.AliasNode}
o.copyToYamlNode(target) o.copyToYamlNode(target)
return target, nil return target, nil
case ScalarNode: case ScalarNode:
log.Debug("MarshalYAML - scalar: %v", o.Value) log.Debugf("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

@ -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.Info("Skipping chown: %v", err) log.Infof("Skipping chown: %v", err)
} }
} }
return nil return nil

View File

@ -3,9 +3,8 @@ 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 {
@ -75,7 +74,7 @@ func (n *Context) ChildContext(results *list.List) Context {
} }
func (n *Context) ToString() string { func (n *Context) ToString() string {
if !log.IsEnabledFor(logging.DEBUG) { if !log.IsEnabledFor(slog.LevelDebug) {
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
logging.SetLevel(logging.DEBUG, "") GetLogger().SetLevel(slog.LevelDebug)
defer logging.SetLevel(logging.INFO, "") // Reset to default defer GetLogger().SetLevel(slog.LevelWarn) // 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,8 +2,7 @@ package yqlib
import ( import (
"fmt" "fmt"
"log/slog"
logging "gopkg.in/op/go-logging.v1"
) )
type DataTreeNavigator interface { type DataTreeNavigator interface {
@ -55,7 +54,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(logging.DEBUG) { if log.IsEnabledFor(slog.LevelDebug) {
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.EncodeSeparate = true typeNode.EncodeHint = EncodeHintSeparateBlock
} }
} }
current = typeNode current = typeNode

View File

@ -12,17 +12,20 @@ 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() Decoder { func NewINIDecoder(prefs INIPreferences) Decoder {
return &iniDecoder{ return &iniDecoder{
finished: false, // Initialise the flag as false finished: false, // Initialise 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
} }
@ -39,7 +42,10 @@ func (dec *iniDecoder) Decode() (*CandidateNode, error) {
} }
// Parse the INI content // Parse the INI content
cfg, err := ini.Load(content) loadOpts := ini.LoadOptions{
PreserveSurroundedQuote: dec.prefs.PreserveSurroundedQuote,
}
cfg, err := ini.LoadSources(loadOpts, content)
if err != nil { 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,10 +16,11 @@ type propertiesDecoder struct {
reader io.Reader reader io.Reader
finished bool finished bool
d DataTreeNavigator d DataTreeNavigator
prefs PropertiesPreferences
} }
func NewPropertiesDecoder() Decoder { func NewPropertiesDecoder() Decoder {
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false} return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false, prefs: ConfiguredPropertiesPreferences.Copy()}
} }
func (dec *propertiesDecoder) Init(reader io.Reader) error { func (dec *propertiesDecoder) Init(reader io.Reader) error {
@ -28,20 +29,56 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error {
return nil return nil
} }
func parsePropKey(key string) []interface{} { func parsePropKey(key string, prefs PropertiesPreferences) []interface{} {
pathStrArray := strings.Split(key, ".") pathStrArray := strings.Split(key, ".")
path := make([]interface{}, len(pathStrArray)) path := make([]interface{}, 0, len(pathStrArray))
for i, pathStr := range pathStrArray { for _, pathStr := range pathStrArray {
num, err := strconv.ParseInt(pathStr, 10, 32) path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets)
if err == nil {
path[i] = num
} else {
path[i] = pathStr
}
} }
return path return path
} }
func appendPropKeySegment(path []interface{}, segment string, useArrayBrackets bool) []interface{} {
if useArrayBrackets && strings.Contains(segment, "[") {
bracketPath, ok := parsePropKeyArrayBracketSegment(segment)
if ok {
return append(path, bracketPath...)
}
}
num, err := strconv.ParseInt(segment, 10, 32)
if err == nil {
return append(path, num)
}
return append(path, segment)
}
func parsePropKeyArrayBracketSegment(segment string) ([]interface{}, bool) {
path := []interface{}{}
bracketIndex := strings.Index(segment, "[")
if bracketIndex > 0 {
path = append(path, segment[:bracketIndex])
}
remaining := segment[bracketIndex:]
for remaining != "" {
if !strings.HasPrefix(remaining, "[") {
return nil, false
}
closingBracket := strings.Index(remaining, "]")
if closingBracket < 0 {
return nil, false
}
arrayIndex, err := strconv.ParseInt(remaining[1:closingBracket], 10, 32)
if err != nil {
return nil, false
}
path = append(path, arrayIndex)
remaining = remaining[closingBracket+1:]
}
return path, true
}
func (dec *propertiesDecoder) processComment(c string) string { func (dec *propertiesDecoder) processComment(c string) string {
if c == "" { if c == "" {
return "" return ""
@ -75,7 +112,7 @@ func (dec *propertiesDecoder) applyPropertyComments(context Context, path []inte
func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error { func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error {
value, _ := properties.Get(key) value, _ := properties.Get(key)
path := parsePropKey(key) path := parsePropKey(key, dec.prefs)
propertyComments := properties.GetComments(key) propertyComments := properties.GetComments(key)
if len(propertyComments) > 0 { if len(propertyComments) > 0 {

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.Error("Bad scenario %v: %w", s.description, err) log.Errorf("Bad scenario %v: %v", s.description, err)
return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error()) return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error())
} }
return result return result

View File

@ -44,6 +44,7 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
} }
dec.pendingComments = make([]string, 0) dec.pendingComments = make([]string, 0)
dec.firstContentSeen = false dec.firstContentSeen = false
dec.finished = false
return nil return nil
} }
@ -105,7 +106,7 @@ func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode
for dec.parser.NextExpression() { for dec.parser.NextExpression() {
nextItem := dec.parser.Expression() nextItem := dec.parser.Expression()
log.Debug("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind) log.Debugf("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind)
switch nextItem.Kind { switch nextItem.Kind {
case toml.KeyValue: case toml.KeyValue:
@ -117,7 +118,7 @@ func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode
dec.pendingComments = append(dec.pendingComments, string(nextItem.Data)) dec.pendingComments = append(dec.pendingComments, string(nextItem.Data))
default: default:
// run out of key values // run out of key values
log.Debug("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind) log.Debugf("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind)
return true, nil return true, nil
} }
} }
@ -149,9 +150,10 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
} }
return &CandidateNode{ return &CandidateNode{
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
Content: content, EncodeHint: EncodeHintInline,
Content: content,
}, nil }, nil
} }
@ -270,7 +272,7 @@ func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
currentNode := dec.parser.Expression() currentNode := dec.parser.Expression()
log.Debug("currentNode: %v ", currentNode.Kind) log.Debugf("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
@ -297,7 +299,7 @@ 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.Debug("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap)) log.Debugf("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
switch currentNode.Kind { switch currentNode.Kind {
case toml.Comment: case toml.Comment:
// Collect comment to attach to next element // Collect comment to attach to next element
@ -325,7 +327,7 @@ func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode) runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode)
} }
log.Debug("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap)) log.Debugf("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap))
return runAgainstCurrentExp, err return runAgainstCurrentExp, err
} }
@ -333,7 +335,7 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
log.Debug("Enter processTable") log.Debug("Enter processTable")
child := currentNode.Child() child := currentNode.Child()
fullPath := dec.getFullPath(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)
@ -344,10 +346,10 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
} }
tableNodeValue := &CandidateNode{ tableNodeValue := &CandidateNode{
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
Content: make([]*CandidateNode, 0), Content: make([]*CandidateNode, 0),
EncodeSeparate: true, EncodeHint: EncodeHintSeparateBlock,
} }
// Attach pending head comments to the table // Attach pending head comments to the table
@ -401,7 +403,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.Debug("arrayAppend to path: %v,%v", path, NodeToString(rhsNode)) log.Debugf("arrayAppend to path: %v,%v", path, NodeToString(rhsNode))
rhsCandidateNode := &CandidateNode{ rhsCandidateNode := &CandidateNode{
Kind: SequenceNode, Kind: SequenceNode,
Tag: "!!seq", Tag: "!!seq",
@ -426,7 +428,7 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
log.Debug("Enter processArrayTable") log.Debug("Enter processArrayTable")
child := currentNode.Child() child := currentNode.Child()
fullPath := dec.getFullPath(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)
@ -441,9 +443,9 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
hasValue := dec.parser.NextExpression() hasValue := dec.parser.NextExpression()
tableNodeValue := &CandidateNode{ tableNodeValue := &CandidateNode{
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
EncodeSeparate: true, EncodeHint: EncodeHintSeparateBlock,
} }
// Attach pending head comments to the array table // Attach pending head comments to the array table

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.Debug("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment) log.Debugf("createMap: headC: %v, lineC: %v, footC: %v", n.HeadComment, n.LineComment, n.FootComment)
yamlNode := &CandidateNode{Kind: MappingNode, Tag: "!!map"} 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.Debug("len of children in %v is %v", label, len(children)) log.Debugf("len of children in %v is %v", label, len(children))
if len(children) > 1 { 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.Debug("scalar comment hack, currentlabel [%v]", labelNode.HeadComment) log.Debugf("scalar comment hack, currentlabel [%v]", labelNode.HeadComment)
labelNode.HeadComment = joinComments([]string{labelNode.HeadComment, strings.TrimSpace(children[0].HeadComment)}, "\n") 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.Debug("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment) log.Debugf("scalar (%v), headC: %v, lineC: %v, footC: %v", scalar.Tag, n.HeadComment, n.LineComment, n.FootComment)
scalar.HeadComment = dec.processComment(n.HeadComment) scalar.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.Debug("looking for %s", s) log.Debugf("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.Debug("found it, appending an entry%s", s) log.Debugf("found it, appending an entry%s", s)
childEntry.V = append(childEntry.V, c) childEntry.V = append(childEntry.V, c)
log.Debug("yay len of children in %v is %v", s, len(childEntry.V)) log.Debugf("yay len of children in %v is %v", s, len(childEntry.V))
return return
} }
} }
log.Debug("not there, making a new one %s", s) log.Debugf("not there, making a new one %s", s)
n.Children = append(n.Children, &xmlChildrenKv{K: s, V: []*xmlNode{c}}) 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.Debug("start element %v", se.Name.Local) log.Debugf("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.Debug("chardata [%v] for %v", elem.n.Data, elem.label) log.Debugf("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.Debug("end element %v", elem.label) log.Debugf("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.Debug("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr) log.Debugf("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ") elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ")
default: default:
log.Debug("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr) log.Debugf("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.HeadComment = joinComments([]string{elem.n.HeadComment, commentStr}, " ") 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.Debug("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr) log.Debugf("got a foot comment, putting on last child for %v: [%v]", childKv.K, commentStr)
// if it's an array of scalars, put the foot comment on the scalar itself // if 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.Debug("got a foot comment for %v: [%v]", elem.label, commentStr) log.Debugf("got a foot comment for %v: [%v]", elem.label, commentStr)
elem.n.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ") elem.n.FootComment = joinComments([]string{elem.n.FootComment, commentStr}, " ")
} }
} }

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 aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag. `yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`.
## NOTE --yaml-fix-merge-anchor-to-spec flag ## NOTE --yaml-fix-merge-anchor-to-spec flag
@ -32,7 +32,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- !!merge <<: *CENTRE - <<: *CENTRE
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
!!merge <<: *item_value <<: *item_value
thingTwo: thingTwo:
name: item_2 name: item_2
!!merge <<: *item_value <<: *item_value
``` ```
then then
```bash ```bash
@ -231,7 +231,7 @@ thingOne:
value: false value: false
thingTwo: thingTwo:
name: item_2 name: item_2
!!merge <<: *item_value <<: *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
!!merge <<: *foo <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -298,7 +298,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- !!merge <<: - <<:
- *CENTRE - *CENTRE
- *BIG - *BIG
``` ```
@ -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
!!merge <<: *foo <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then
@ -411,7 +411,7 @@ Given a sample.yml file of:
r: 10 r: 10
- &SMALL - &SMALL
r: 1 r: 1
- !!merge <<: - <<:
- *CENTRE - *CENTRE
- *BIG - *BIG
``` ```
@ -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

@ -55,7 +55,7 @@ yq '.a = .a / 0 | .b = .b / 0' sample.yml
``` ```
will output will output
```yaml ```yaml
a: !!float +Inf a: +Inf
b: !!float -Inf b: -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 aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag. `yq` supports merge keys (like `<<: *blah`) from YAML 1.1. These are no longer part of the YAML 1.2 standard, but remain common in practice. Plain `<<:` keys are recognised as merge keys and round-trip as `<<:` without an explicit `!!merge` tag. When the source uses an explicit `!!merge` tag, that is preserved on output. Internally, when `yq` synthesises a `<<` map key (for example during merge operations), it tags the key as `!!merge` rather than `!!str`.
## NOTE --yaml-fix-merge-anchor-to-spec flag ## NOTE --yaml-fix-merge-anchor-to-spec flag

View File

@ -1,5 +1,5 @@
# Slice/Splice Array # Slice Array or String
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array. The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.
You may leave out the first or second number, which will refer to the start or end of the array respectively. You may leave out the first or second number, which will refer to the start or end of the array or string respectively.

View File

@ -0,0 +1,27 @@
# System Operators
The `system` operator allows you to run an external command and use its output as a value in your expression.
**Security warning**: The system operator is disabled by default. You must explicitly pass `--security-enable-system-operator` to use it.
**Note:** When enabled, the system operator can replicate the functionality of `env` and `load`
operators via external commands. Enabling it effectively overrides `--security-disable-env-ops`
and `--security-disable-file-ops`.
## Usage
```bash
yq --security-enable-system-operator --null-input '.field = system("command"; "arg1")'
```
The operator takes:
- A command string (required)
- An argument (or an array of arguments), separated from the command by `;` (optional)
The current matched node's value is serialised and piped to the command via stdin. The command's stdout (with trailing newline stripped) is returned as a string.
## Disabling the system operator
The system operator is disabled by default. When disabled, an error is returned instead of running the command, consistent with `--security-disable-env-ops` and `--security-disable-file-ops`.
Use `--security-enable-system-operator` flag to enable it.

View File

@ -34,7 +34,7 @@ yq '.a = .a % .b' sample.yml
``` ```
will output will output
```yaml ```yaml
a: !!float 2 a: 2
b: 2.5 b: 2.5
``` ```
@ -69,7 +69,7 @@ yq '.a = .a % .b' sample.yml
``` ```
will output will output
```yaml ```yaml
a: !!float NaN a: 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
!!merge <<: *foo <<: *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

@ -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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *foo
thing: foobar_thing thing: foobar_thing
- foobar_c - foobar_c
- *foo - *foo

View File

@ -1,8 +1,8 @@
# Slice/Splice Array # Slice Array or String
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array. The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.
You may leave out the first or second number, which will refer to the start or end of the array respectively. You may leave out the first or second number, which will refer to the start or end of the array or string respectively.
## Slicing arrays ## Slicing arrays
Given a sample.yml file of: Given a sample.yml file of:
@ -103,3 +103,81 @@ will output
- cow - cow
``` ```
## Slicing strings
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[0:5]' sample.yml
```
will output
```yaml
Austr
```
## Slicing strings - without the second number
Finishes at the end of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[5:]' sample.yml
```
will output
```yaml
alia
```
## Slicing strings - without the first number
Starts from the start of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[:5]' sample.yml
```
will output
```yaml
Austr
```
## Slicing strings - use negative numbers to count backwards from the end
Negative indices count from the end of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[-5:]' sample.yml
```
will output
```yaml
ralia
```
## Slicing strings - Unicode
Indices are rune-based, so multi-byte characters are handled correctly
Given a sample.yml file of:
```yaml
greeting: héllo
```
then
```bash
yq '.greeting[1:3]' sample.yml
```
will output
```yaml
él
```

View File

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

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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *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
!!merge <<: *foo <<: *foo
thing: foobar_thing thing: foobar_thing
``` ```
then then

View File

@ -125,6 +125,22 @@ will output
{"whatever":"cat"} {"whatever":"cat"}
``` ```
## Encode json: preserve floats with trailing zero
Whole-number floats keep their decimal point so downstream consumers see a JSON number with a fractional part (matches Go's encoding/json, Python's json, and jq).
Given a sample.yml file of:
```yaml
percentiles: [50.0, 95.0, 99.0, 99.9]
```
then
```bash
yq -o=json -I=0 '.' sample.yml
```
will output
```json
{"percentiles":[50.0,95.0,99.0,99.9]}
```
## Roundtrip JSON Lines / NDJSON ## Roundtrip JSON Lines / NDJSON
Given a sample.json file of: Given a sample.json file of:
```json ```json

View File

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

View File

@ -1,6 +1,6 @@
# TOML # TOML
Decode from TOML. Note that `yq` does not yet support outputting in TOML format (and therefore it cannot roundtrip) Encode and decode to and from TOML.
## Parse: Simple ## Parse: Simple
@ -384,3 +384,20 @@ ip = "10.0.0.2"
role = "backend" role = "backend"
``` ```
## Encode: Simple mapping produces table section
Given a sample.yml file of:
```yaml
arg:
hello: foo
```
then
```bash
yq -o toml '.' sample.yml
```
will output
```toml
[arg]
hello = "foo"
```

View File

@ -449,8 +449,8 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
return false return false
} }
// If EncodeSeparate is set, emit children as separate blocks regardless of label extraction // If EncodeHintSeparateBlock is set, emit children as separate blocks regardless of label extraction
if valueNode.EncodeSeparate { if valueNode.EncodeHint == EncodeHintSeparateBlock {
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled { if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true return true
} }
@ -537,9 +537,9 @@ func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockTy
return false, nil return false, nil
} }
// Only emit as separate blocks if EncodeSeparate is true // Only emit as separate blocks if EncodeHintSeparateBlock is set
// This allows the encoder to respect the original block structure preserved by the decoder // This allows the encoder to respect the original block structure preserved by the decoder
if !valueNode.EncodeSeparate { if valueNode.EncodeHint != EncodeHintSeparateBlock {
return false, nil return false, nil
} }

View File

@ -32,7 +32,7 @@ func (ke *kyamlEncoder) PrintLeadingContent(writer io.Writer, content string) er
} }
func (ke *kyamlEncoder) Encode(writer io.Writer, node *CandidateNode) error { func (ke *kyamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debug("encoderKYaml - going to print %v", NodeToString(node)) log.Debugf("encoderKYaml - going to print %v", NodeToString(node))
if node.Kind == ScalarNode && ke.prefs.UnwrapScalar { if node.Kind == ScalarNode && ke.prefs.UnwrapScalar {
return writeString(writer, node.Value+"\n") return writeString(writer, node.Value+"\n")
} }

View File

@ -69,6 +69,27 @@ func (te *tomlEncoder) CanHandleAliases() bool {
// ---- helpers ---- // ---- helpers ----
// tomlKey returns the key quoted if it contains characters that are not valid
// in a TOML bare key. TOML bare keys may only contain ASCII letters, ASCII
// digits, underscores, and dashes.
func tomlKey(key string) string {
for _, r := range key {
if (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '_' && r != '-' {
return fmt.Sprintf("%q", key)
}
}
return key
}
// tomlDottedKey joins path components, quoting any that require it.
func tomlDottedKey(path []string) string {
parts := make([]string, len(path))
for i, p := range path {
parts[i] = tomlKey(p)
}
return strings.Join(parts, ".")
}
func (te *tomlEncoder) writeComment(w io.Writer, comment string) error { func (te *tomlEncoder) writeComment(w io.Writer, comment string) error {
if comment == "" { if comment == "" {
return nil return nil
@ -111,12 +132,23 @@ func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error
} }
} }
// Preserve existing order by iterating Content
for i := 0; i < len(node.Content); i += 2 { for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i] keyNode := node.Content[i]
valNode := node.Content[i+1] valNode := node.Content[i+1]
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil { if isTomlAttribute(valNode) {
return err if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
return err
}
}
}
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valNode := node.Content[i+1]
if !isTomlAttribute(valNode) {
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
return err
}
} }
} }
return nil return nil
@ -148,9 +180,15 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
} }
if allMaps { if allMaps {
key := path[len(path)-1] key := path[len(path)-1]
quotedKey := tomlKey(key)
if te.wroteRootAttr {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
te.wroteRootAttr = false
}
for _, it := range node.Content { for _, it := range node.Content {
// [[key]] then body if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err return err
} }
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil { if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
@ -162,12 +200,12 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
// Regular array attribute // Regular array attribute
return te.writeArrayAttribute(w, path[len(path)-1], node) return te.writeArrayAttribute(w, path[len(path)-1], node)
case MappingNode: case MappingNode:
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path // Use inline table syntax only for nodes explicitly marked as TOML inline tables.
if !node.EncodeSeparate { // YAML flow-style mappings are not treated as inline tables; the FlowStyle attribute
// If children contain mappings or arrays of mappings, prefer separate sections // is a YAML-specific rendering hint and should not affect TOML output. This ensures
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) { // that auto-detected JSON input (parsed as YAML flow style) produces readable table
return te.encodeSeparateMapping(w, path, node) // sections, consistent with explicitly parsed JSON input.
} if node.EncodeHint == EncodeHintInline {
return te.writeInlineTableAttribute(w, path[len(path)-1], node) return te.writeInlineTableAttribute(w, path[len(path)-1], node)
} }
return te.encodeSeparateMapping(w, path, node) return te.encodeSeparateMapping(w, path, node)
@ -176,7 +214,30 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
} }
} }
func isTomlArrayOfTables(seq *CandidateNode) bool {
if len(seq.Content) == 0 {
return false
}
for _, it := range seq.Content {
if it.Kind != MappingNode || it.EncodeHint == EncodeHintInline {
return false
}
}
return true
}
func isTomlAttribute(node *CandidateNode) bool {
if node.Kind == ScalarNode {
return true
}
return node.Kind == SequenceNode && !isTomlArrayOfTables(node)
}
func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error { func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error {
if value.Tag == "!!null" {
return nil
}
te.wroteRootAttr = true // Mark that we wrote a root attribute te.wroteRootAttr = true // Mark that we wrote a root attribute
// Write head comment before the attribute // Write head comment before the attribute
@ -185,7 +246,7 @@ func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateN
} }
// Write the attribute // Write the attribute
line := key + " = " + te.formatScalar(value) line := tomlKey(key) + " = " + te.formatScalar(value)
// Add line comment if present // Add line comment if present
if value.LineComment != "" { if value.LineComment != "" {
@ -210,7 +271,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
// Handle empty arrays // Handle empty arrays
if len(seq.Content) == 0 { if len(seq.Content) == 0 {
line := key + " = []" line := tomlKey(key) + " = []"
if seq.LineComment != "" { if seq.LineComment != "" {
lineComment := strings.TrimSpace(seq.LineComment) lineComment := strings.TrimSpace(seq.LineComment)
if !strings.HasPrefix(lineComment, "#") { if !strings.HasPrefix(lineComment, "#") {
@ -233,7 +294,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
if hasElementComments { if hasElementComments {
// Write multiline array format with comments // Write multiline array format with comments
if _, err := w.Write([]byte(key + " = [\n")); err != nil { if _, err := w.Write([]byte(tomlKey(key) + " = [\n")); err != nil {
return err return err
} }
@ -324,7 +385,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
} }
} }
line := key + " = [" + strings.Join(items, ", ") + "]" line := tomlKey(key) + " = [" + strings.Join(items, ", ") + "]"
// Add line comment if present // Add line comment if present
if seq.LineComment != "" { if seq.LineComment != "" {
@ -372,21 +433,24 @@ func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
v := m.Content[i+1] v := m.Content[i+1]
switch v.Kind { switch v.Kind {
case ScalarNode: case ScalarNode:
parts = append(parts, fmt.Sprintf("%s = %s", k, te.formatScalar(v))) if v.Tag == "!!null" {
continue
}
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), te.formatScalar(v)))
case SequenceNode: case SequenceNode:
// inline array in inline table // inline array in inline table
arr, err := te.sequenceToInlineArray(v) arr, err := te.sequenceToInlineArray(v)
if err != nil { if err != nil {
return "", err return "", err
} }
parts = append(parts, fmt.Sprintf("%s = %s", k, arr)) parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), arr))
case MappingNode: case MappingNode:
// nested inline table // nested inline table
inline, err := te.mappingToInlineTable(v) inline, err := te.mappingToInlineTable(v)
if err != nil { if err != nil {
return "", err return "", err
} }
parts = append(parts, fmt.Sprintf("%s = %s", k, inline)) parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), inline))
default: default:
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind) return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
} }
@ -399,7 +463,7 @@ func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *Can
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte(key + " = " + inline + "\n")) _, err = w.Write([]byte(tomlKey(key) + " = " + inline + "\n"))
return err return err
} }
@ -421,7 +485,7 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
} }
// Write table header [a.b.c] // Write table header [a.b.c]
header := "[" + strings.Join(path, ".") + "]\n" header := "[" + tomlDottedKey(path) + "]\n"
_, err := w.Write([]byte(header)) _, err := w.Write([]byte(header))
return err return err
} }
@ -429,24 +493,21 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
// encodeSeparateMapping handles a mapping that should be encoded as table sections. // encodeSeparateMapping handles a mapping that should be encoded as table sections.
// It emits the table header for this mapping if it has any content, then processes children. // It emits the table header for this mapping if it has any content, then processes children.
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error { func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes) // Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
// Inline mapping children also count as attributes since they render as key = { ... }.
hasAttrs := false hasAttrs := false
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1] v := m.Content[i+1]
if v.Kind == ScalarNode { if v.Kind == ScalarNode && v.Tag != "!!null" {
hasAttrs = true
break
}
if v.Kind == MappingNode && v.EncodeHint == EncodeHintInline {
hasAttrs = true hasAttrs = true
break break
} }
if v.Kind == SequenceNode { if v.Kind == SequenceNode {
// Check if it's NOT an array of tables if !isTomlArrayOfTables(v) {
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if !allMaps {
hasAttrs = true hasAttrs = true
break break
} }
@ -464,31 +525,26 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
return nil return nil
} }
// No attributes, just nested structures - process children // No attributes, just nested table structures - process children recursively
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value k := m.Content[i].Value
v := m.Content[i+1] v := m.Content[i+1]
switch v.Kind { switch v.Kind {
case MappingNode: case MappingNode:
// Emit [path.k]
newPath := append(append([]string{}, path...), k) newPath := append(append([]string{}, path...), k)
if err := te.writeTableHeader(w, newPath, v); err != nil { if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
return err return err
} }
case SequenceNode: case SequenceNode:
// If sequence of maps, emit [[path.k]] per element // If sequence of maps, emit [[path.k]] per element
allMaps := true if isTomlArrayOfTables(v) {
for _, it := range v.Content { key := tomlDottedKey(append(append([]string{}, path...), k))
if it.Kind != MappingNode { if te.wroteRootAttr {
allMaps = false if _, err := w.Write([]byte("\n")); err != nil {
break return err
}
te.wroteRootAttr = false
} }
}
if allMaps {
key := strings.Join(append(append([]string{}, path...), k), ".")
for _, it := range v.Content { for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err return err
@ -513,42 +569,9 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
return nil return nil
} }
func (te *tomlEncoder) hasEncodeSeparateChild(m *CandidateNode) bool {
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
if v.Kind == MappingNode && v.EncodeSeparate {
return true
}
}
return false
}
func (te *tomlEncoder) hasStructuralChildren(m *CandidateNode) bool {
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
// Only consider it structural if mapping has EncodeSeparate or is non-empty
if v.Kind == MappingNode && v.EncodeSeparate {
return true
}
if v.Kind == SequenceNode {
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
return true
}
}
}
return false
}
// encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context // encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context
func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error { func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error {
// First, attributes (scalars and non-map arrays) // First, attributes (scalars, inline mappings, and non-map arrays)
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value k := m.Content[i].Value
v := m.Content[i+1] v := m.Content[i+1]
@ -557,15 +580,14 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
if err := te.writeAttribute(w, k, v); err != nil { if err := te.writeAttribute(w, k, v); err != nil {
return err return err
} }
case SequenceNode: case MappingNode:
allMaps := true if v.EncodeHint == EncodeHintInline {
for _, it := range v.Content { if err := te.writeInlineTableAttribute(w, k, v); err != nil {
if it.Kind != MappingNode { return err
allMaps = false
break
} }
} }
if !allMaps { case SequenceNode:
if !isTomlArrayOfTables(v) {
if err := te.writeArrayAttribute(w, k, v); err != nil { if err := te.writeArrayAttribute(w, k, v); err != nil {
return err return err
} }
@ -578,15 +600,8 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
k := m.Content[i].Value k := m.Content[i].Value
v := m.Content[i+1] v := m.Content[i+1]
if v.Kind == SequenceNode { if v.Kind == SequenceNode {
allMaps := true if isTomlArrayOfTables(v) {
for _, it := range v.Content { dotted := tomlDottedKey(append(append([]string{}, path...), k))
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
dotted := strings.Join(append(append([]string{}, path...), k), ".")
for _, it := range v.Content { for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
return err return err
@ -599,12 +614,14 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
} }
} }
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes // Finally, child mappings: inline-hint ones were emitted above as attributes,
// while all others are emitted as separate sub-table sections.
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value k := m.Content[i].Value
v := m.Content[i+1] v := m.Content[i+1]
if v.Kind == MappingNode && !v.EncodeSeparate { if v.Kind == MappingNode && v.EncodeHint != EncodeHintInline {
if err := te.writeInlineTableAttribute(w, k, v); err != nil { subPath := append(append([]string{}, path...), k)
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
return err return err
} }
} }
@ -660,7 +677,7 @@ func (te *tomlEncoder) colorizeToml(input []byte) []byte {
// Table sections - [section] or [[array]] // Table sections - [section] or [[array]]
// Only treat '[' as a table section if it appears at the start of the line // Only treat '[' as a table section if it appears at the start of the line
// (possibly after whitespace). This avoids mis-colouring inline arrays like // (possibly after whitespace). This avoids incorrectly colouring inline arrays like
// "ports = [8000, 8001]" as table sections. // "ports = [8000, 8001]" as table sections.
if ch == '[' { if ch == '[' {
isSectionHeader := true isSectionHeader := true

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.Warning("Unable to write newline, skipping: %w", err) log.Warningf("Unable to write newline, skipping: %v", 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.Warning("Unable to write newline, skipping: %w", err) log.Warningf("Unable to write newline, skipping: %v", 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.Warning("Unable to write newline, skipping: %w", err) log.Warningf("Unable to write newline, skipping: %v", err)
} }
} else { } else {

View File

@ -29,7 +29,7 @@ func (ye *yamlEncoder) PrintLeadingContent(writer io.Writer, content string) err
} }
func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error { func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debug("encoderYaml - going to print %v", NodeToString(node)) log.Debugf("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") {

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.Debug("Parsing expression: [%v]", expression) log.Debugf("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,6 +95,7 @@ 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,8 +3,7 @@ package yqlib
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
logging "gopkg.in/op/go-logging.v1"
) )
type expressionPostFixer interface { type expressionPostFixer interface {
@ -134,7 +133,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(logging.DEBUG) { if log.IsEnabledFor(slog.LevelDebug) {
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

@ -30,7 +30,7 @@ func tryRenameFile(from string, to string) error {
} }
func tryRemoveTempFile(filename string) { func tryRemoveTempFile(filename string) {
log.Debug("Removing temp file: %v", filename) log.Debugf("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 +40,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 thar directory. // and ensuring that it's not possible to give a path to a file outside that directory.
in, err := os.Open(src) // #nosec in, err := os.Open(src) // #nosec
if err != nil { if err != nil {
@ -68,8 +68,7 @@ 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.Error("Error closing file!") log.Errorf("Error closing file %v: %v", file.Name(), err)
log.Error(err.Error())
} }
} }

View File

@ -90,7 +90,7 @@ 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() }, func() Decoder { return NewINIDecoder(ConfiguredINIPreferences) },
} }
var Formats = []*Format{ var Formats = []*Format{

View File

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

View File

@ -108,6 +108,55 @@ 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,8 +230,7 @@ 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",
// fine to have !!merge as that's what the current impl does expected: "a: &remember\n c: mike\nb:\n <<: *remember\n",
expected: "a: &remember\n c: mike\nb:\n !!merge <<: *remember\n",
}, },
{ {
description: "custom tag", description: "custom tag",

View File

@ -1,18 +1,21 @@
package yqlib package yqlib
type INIPreferences struct { type INIPreferences struct {
ColorsEnabled bool ColorsEnabled bool
PreserveSurroundedQuote bool
} }
func NewDefaultINIPreferences() INIPreferences { func NewDefaultINIPreferences() INIPreferences {
return INIPreferences{ return INIPreferences{
ColorsEnabled: false, ColorsEnabled: false,
PreserveSurroundedQuote: false,
} }
} }
func (p *INIPreferences) Copy() INIPreferences { func (p *INIPreferences) Copy() INIPreferences {
return INIPreferences{ return INIPreferences{
ColorsEnabled: p.ColorsEnabled, ColorsEnabled: p.ColorsEnabled,
PreserveSurroundedQuote: p.PreserveSurroundedQuote,
} }
} }

View File

@ -5,6 +5,7 @@ package yqlib
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/mikefarah/yq/v4/test" "github.com/mikefarah/yq/v4/test"
@ -22,6 +23,16 @@ const expectedSimpleINIYaml = `section:
key: value key: value
` `
const quotedINIInput = `[section]
color_theme = "Default"
theme_background = "False"
`
const expectedQuotedINIOutput = `[section]
color_theme = "Default"
theme_background = "False"
`
var iniScenarios = []formatScenario{ var iniScenarios = []formatScenario{
{ {
description: "Parse INI: simple", description: "Parse INI: simple",
@ -49,6 +60,22 @@ var iniScenarios = []formatScenario{
}, },
} }
// iniPreserveQuotesPrefs returns INIPreferences with PreserveSurroundedQuote enabled.
func iniPreserveQuotesPrefs() INIPreferences {
prefs := NewDefaultINIPreferences()
prefs.PreserveSurroundedQuote = true
return prefs
}
var iniPreserveQuotesScenarios = []formatScenario{
{
description: "Roundtrip INI: preserve quotes",
input: quotedINIInput,
expected: expectedQuotedINIOutput,
scenarioType: "roundtrip",
},
}
func documentRoundtripINIScenario(w *bufio.Writer, s formatScenario) { func documentRoundtripINIScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description)) writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
@ -70,7 +97,7 @@ func documentRoundtripINIScenario(w *bufio.Writer, s formatScenario) {
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder()))) writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder())))
} }
func documentDecodeINIScenario(w *bufio.Writer, s formatScenario) { func documentDecodeINIScenario(w *bufio.Writer, s formatScenario) {
@ -94,7 +121,7 @@ func documentDecodeINIScenario(w *bufio.Writer, s formatScenario) {
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)))) writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewYamlEncoder(ConfiguredYamlPreferences))))
} }
func testINIScenario(t *testing.T, s formatScenario) { func testINIScenario(t *testing.T, s formatScenario) {
@ -102,11 +129,11 @@ func testINIScenario(t *testing.T, s formatScenario) {
case "encode": case "encode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewINIEncoder()), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewINIEncoder()), s.description)
case "decode": case "decode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewYamlEncoder(ConfiguredYamlPreferences)), s.description)
case "roundtrip": case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder()), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder()), s.description)
case "decode-error": case "decode-error":
result, err := processFormatScenario(s, NewINIDecoder(), NewINIEncoder()) result, err := processFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder())
if err == nil { if err == nil {
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result) t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
} else { } else {
@ -175,6 +202,21 @@ func documentDecodeErrorINIScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n\n", s.expectedError)) writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n\n", s.expectedError))
} }
func TestINIDecoderInitResetsFinished(t *testing.T) {
decoder := NewINIDecoder(NewDefaultINIPreferences())
firstDocuments, err := readDocuments(strings.NewReader("[first]\nkey = value\n"), "first.ini", 0, decoder)
if err != nil {
t.Fatal(err)
}
test.AssertResult(t, 1, firstDocuments.Len())
secondDocuments, err := readDocuments(strings.NewReader("[second]\nkey = value\n"), "second.ini", 1, decoder)
if err != nil {
t.Fatal(err)
}
test.AssertResult(t, 1, secondDocuments.Len())
}
func TestINIScenarios(t *testing.T) { func TestINIScenarios(t *testing.T) {
for _, tt := range iniScenarios { for _, tt := range iniScenarios {
testINIScenario(t, tt) testINIScenario(t, tt)
@ -185,3 +227,19 @@ func TestINIScenarios(t *testing.T) {
} }
documentScenarios(t, "usage", "convert", genericScenarios, documentINIScenario) documentScenarios(t, "usage", "convert", genericScenarios, documentINIScenario)
} }
func testINIPreserveQuotesScenario(t *testing.T, s formatScenario) {
prefs := iniPreserveQuotesPrefs()
switch s.scenarioType {
case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(prefs), NewINIEncoder()), s.description)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func TestINIPreserveQuotesScenarios(t *testing.T) {
for _, tt := range iniPreserveQuotesScenarios {
testINIPreserveQuotesScenario(t, tt)
}
}

View File

@ -220,6 +220,54 @@ var jsonScenarios = []formatScenario{
expected: "{\"stuff\":\"cool\"}\n{\"whatever\":\"cat\"}\n", expected: "{\"stuff\":\"cool\"}\n{\"whatever\":\"cat\"}\n",
scenarioType: "encode", scenarioType: "encode",
}, },
{
description: "Encode json: preserve floats with trailing zero",
subdescription: "Whole-number floats keep their decimal point so downstream consumers see a JSON number with a fractional part (matches Go's encoding/json, Python's json, and jq).",
input: `percentiles: [50.0, 95.0, 99.0, 99.9]`,
indent: 0,
expected: "{\"percentiles\":[50.0,95.0,99.0,99.9]}\n",
scenarioType: "encode",
},
{
description: "Encode json: ints stay ints",
skipDoc: true,
input: `a: 50`,
indent: 0,
expected: "{\"a\":50}\n",
scenarioType: "encode",
},
{
description: "Encode json: !!float tagged whole number gets .0",
skipDoc: true,
input: `a: !!float 5`,
indent: 0,
expected: "{\"a\":5.0}\n",
scenarioType: "encode",
},
{
description: "Encode json: scientific notation float preserved",
skipDoc: true,
input: `a: 1.5e-3`,
indent: 0,
expected: "{\"a\":1.5e-3}\n",
scenarioType: "encode",
},
{
description: "Encode json: negative float preserved",
skipDoc: true,
input: `a: -7.0`,
indent: 0,
expected: "{\"a\":-7.0}\n",
scenarioType: "encode",
},
{
description: "Encode json: mixed int and float array",
skipDoc: true,
input: `a: [1, 2.0, 3, 4.5]`,
indent: 0,
expected: "{\"a\":[1,2.0,3,4.5]}\n",
scenarioType: "encode",
},
{ {
description: "Roundtrip JSON Lines / NDJSON", description: "Roundtrip JSON Lines / NDJSON",
input: sampleNdJson, input: sampleNdJson,

View File

@ -105,7 +105,7 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
skipNextToken = false skipNextToken = false
currentToken := tokens[index] currentToken := tokens[index]
log.Debug("processing %v", currentToken.toString(true)) log.Debugf("processing %v", currentToken.toString(true))
if currentToken.TokenType == traverseArrayCollect { if currentToken.TokenType == traverseArrayCollect {
// `.[exp]`` works by creating a traversal array of [self, exp] and piping that into the traverse array operator // `.[exp]`` works by creating a traversal array of [self, exp] and piping that into the traverse array operator
@ -131,6 +131,11 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
log.Debugf("previous token is : traverseArrayOpType") log.Debugf("previous token is : traverseArrayOpType")
// need to put the number 0 before this token, as that is implied // need to put the number 0 before this token, as that is implied
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")}) postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
} else if index >= 2 && tokens[index-1].TokenType == openCollect &&
(tokens[index-2].TokenType == operationToken || tokens[index-2].TokenType == closeCollect || tokens[index-2].TokenType == closeCollectObject) {
log.Debugf("previous token is : openCollect following a traversal, implying 0 start")
// need to put the number 0 before this token, as that is implied
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
} }
} }

View File

@ -96,6 +96,8 @@ var participleYqRules = []*participleYqRule{
simpleOp("load_?str|str_?load", loadStringOpType), simpleOp("load_?str|str_?load", loadStringOpType),
{"LoadYaml", `load`, loadOp(NewYamlDecoder(LoadYamlPreferences)), 0}, {"LoadYaml", `load`, loadOp(NewYamlDecoder(LoadYamlPreferences)), 0},
simpleOp("system", systemOpType),
{"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0}, {"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0},
simpleOp("select", selectOpType), simpleOp("select", selectOpType),
@ -283,7 +285,7 @@ func pathToken(wrapped bool) yqAction {
if wrapped { if wrapped {
value = unwrap(value) value = unwrap(value)
} }
log.Debug("PathToken %v", value) log.Debugf("PathToken %v", value)
op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: prefs} op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op, CheckForPostTraverse: true}, nil return &token{TokenType: operationToken, Operation: op, CheckForPostTraverse: true}, nil
} }
@ -336,7 +338,7 @@ func flattenWithDepth() yqAction {
func assignAllCommentsOp(updateAssign bool) yqAction { func assignAllCommentsOp(updateAssign bool) yqAction {
return func(rawToken lexer.Token) (*token, error) { return func(rawToken lexer.Token) (*token, error) {
log.Debug("assignAllCommentsOp %v", rawToken.Value) log.Debugf("assignAllCommentsOp %v", rawToken.Value)
value := rawToken.Value value := rawToken.Value
op := &Operation{ op := &Operation{
OperationType: assignCommentOpType, OperationType: assignCommentOpType,
@ -351,7 +353,7 @@ func assignAllCommentsOp(updateAssign bool) yqAction {
func assignOpToken(updateAssign bool) yqAction { func assignOpToken(updateAssign bool) yqAction {
return func(rawToken lexer.Token) (*token, error) { return func(rawToken lexer.Token) (*token, error) {
log.Debug("assignOpToken %v", rawToken.Value) log.Debugf("assignOpToken %v", rawToken.Value)
value := rawToken.Value value := rawToken.Value
prefs := assignPreferences{DontOverWriteAnchor: true} prefs := assignPreferences{DontOverWriteAnchor: true}
if strings.Contains(value, "c") { if strings.Contains(value, "c") {
@ -376,9 +378,9 @@ func nullValue() yqAction {
func stringValue() yqAction { func stringValue() yqAction {
return func(rawToken lexer.Token) (*token, error) { return func(rawToken lexer.Token) (*token, error) {
log.Debug("rawTokenvalue: %v", rawToken.Value) log.Debugf("rawTokenvalue: %v", rawToken.Value)
value := unwrap(rawToken.Value) value := unwrap(rawToken.Value)
log.Debug("unwrapped: %v", value) log.Debugf("unwrapped: %v", value)
value = processEscapeCharacters(value) value = processEscapeCharacters(value)
return &token{TokenType: operationToken, Operation: &Operation{ return &token{TokenType: operationToken, Operation: &Operation{
OperationType: stringInterpolationOpType, OperationType: stringInterpolationOpType,

View File

@ -4,11 +4,10 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt" "fmt"
"log/slog"
"math" "math"
"strconv" "strconv"
"strings" "strings"
logging "gopkg.in/op/go-logging.v1"
) )
var ExpressionParser ExpressionParserInterface var ExpressionParser ExpressionParserInterface
@ -19,17 +18,17 @@ func InitExpressionParser() {
} }
} }
var log = logging.MustGetLogger("yq-lib") var log = newLogger()
var PrettyPrintExp = `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""` var PrettyPrintExp = `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`
// GetLogger returns the yq logger instance. // GetLogger returns the yq logger instance.
func GetLogger() *logging.Logger { func GetLogger() *Logger {
return log return log
} }
func getContentValueByKey(content []*CandidateNode, key string) *CandidateNode { func getContentValueByKey(content []*CandidateNode, key string) *CandidateNode {
for index := 0; index < len(content); index = index + 2 { for index := 0; index < len(content)-1; index = index + 2 {
keyNode := content[index] keyNode := content[index]
valueNode := content[index+1] valueNode := content[index+1]
if keyNode.Value == key { if keyNode.Value == key {
@ -81,7 +80,7 @@ func recurseNodeObjectEqual(lhs *CandidateNode, rhs *CandidateNode) bool {
key := lhs.Content[index] key := lhs.Content[index]
value := lhs.Content[index+1] value := lhs.Content[index+1]
indexInRHS := findInArray(rhs, key) indexInRHS := findKeyInMap(rhs, key)
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) { if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
return false return false
@ -251,7 +250,7 @@ func processEscapeCharacters(original string) string {
value := result.String() value := result.String()
if value != original { if value != original {
log.Debug("processEscapeCharacters from [%v] to [%v]", original, value) log.Debugf("processEscapeCharacters from [%v] to [%v]", original, value)
} }
return value return value
} }
@ -274,7 +273,7 @@ func footComment(node *CandidateNode) string {
// use for debugging only // use for debugging only
func NodesToString(collection *list.List) string { func NodesToString(collection *list.List) string {
if !log.IsEnabledFor(logging.DEBUG) { if !log.IsEnabledFor(slog.LevelDebug) {
return "" return ""
} }
@ -286,7 +285,7 @@ func NodesToString(collection *list.List) string {
} }
func NodeToString(node *CandidateNode) string { func NodeToString(node *CandidateNode) string {
if !log.IsEnabledFor(logging.DEBUG) { if !log.IsEnabledFor(slog.LevelDebug) {
return "" return ""
} }
if node == nil { if node == nil {
@ -304,7 +303,7 @@ func NodeToString(node *CandidateNode) string {
} }
func NodeContentToString(node *CandidateNode, depth int) string { func NodeContentToString(node *CandidateNode, depth int) string {
if !log.IsEnabledFor(logging.DEBUG) { if !log.IsEnabledFor(slog.LevelDebug) {
return "" return ""
} }
var sb strings.Builder var sb strings.Builder

View File

@ -24,7 +24,7 @@ type parseSnippetScenario struct {
var parseSnippetScenarios = []parseSnippetScenario{ var parseSnippetScenarios = []parseSnippetScenario{
{ {
snippet: ":", snippet: ":",
expectedError: "yaml: did not find expected key", expectedError: "go-yaml load error in parser (while parsing a block mapping) at L1.C1: did not find expected key",
}, },
{ {
snippet: "", snippet: "",
@ -300,6 +300,24 @@ func TestRecurseNodeObjectEqual(t *testing.T) {
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2)) test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3)) test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4)) test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
// A null key must not match a null value in the other map.
// Regression test for https://issues.oss-fuzz.com/issues/383860504
nullKey := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
nullVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
intKey := createScalarNode(2, "2")
intKey.Tag = "!!int"
intVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
mapWithNullKey := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{nullKey, nullVal},
}
mapWithIntKey := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{intKey, intVal},
}
test.AssertResult(t, false, recurseNodeObjectEqual(mapWithNullKey, mapWithIntKey))
} }
func TestParseInt(t *testing.T) { func TestParseInt(t *testing.T) {

77
pkg/yqlib/logger.go Normal file
View File

@ -0,0 +1,77 @@
package yqlib
import (
"fmt"
"log/slog"
"os"
)
// Logger wraps log/slog providing a printf-style interface used throughout yq.
type Logger struct {
levelVar slog.LevelVar
slogger *slog.Logger
}
func newLogger() *Logger {
l := &Logger{}
l.levelVar.Set(slog.LevelWarn)
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: &l.levelVar})
l.slogger = slog.New(handler)
return l
}
// SetLevel sets the minimum log level.
func (l *Logger) SetLevel(level slog.Level) {
l.levelVar.Set(level)
}
// GetLevel returns the current log level.
func (l *Logger) GetLevel() slog.Level {
return l.levelVar.Level()
}
// IsEnabledFor returns true if the given level is enabled.
func (l *Logger) IsEnabledFor(level slog.Level) bool {
return l.levelVar.Level() <= level
}
// SetSlogger replaces the underlying slog.Logger (e.g. to configure output format).
func (l *Logger) SetSlogger(logger *slog.Logger) {
l.slogger = logger
}
func (l *Logger) Debug(msg string) {
if l.IsEnabledFor(slog.LevelDebug) {
l.slogger.Debug(msg)
}
}
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.IsEnabledFor(slog.LevelDebug) {
l.slogger.Debug(fmt.Sprintf(format, args...))
}
}
func (l *Logger) Info(msg string) {
l.slogger.Info(msg)
}
func (l *Logger) Infof(format string, args ...interface{}) {
l.slogger.Info(fmt.Sprintf(format, args...))
}
func (l *Logger) Warning(msg string) {
l.slogger.Warn(msg)
}
func (l *Logger) Warningf(format string, args ...interface{}) {
l.slogger.Warn(fmt.Sprintf(format, args...))
}
func (l *Logger) Error(msg string) {
l.slogger.Error(msg)
}
func (l *Logger) Errorf(format string, args ...interface{}) {
l.slogger.Error(fmt.Sprintf(format, args...))
}

View File

@ -2,7 +2,7 @@
package yqlib package yqlib
func NewINIDecoder() Decoder { func NewINIDecoder(prefs INIPreferences) Decoder {
return nil return nil
} }

View File

@ -25,7 +25,7 @@ var valueToStringFunc = func(p *Operation) string {
} }
func createValueOperation(value interface{}, stringValue string) *Operation { func createValueOperation(value interface{}, stringValue string) *Operation {
log.Debug("creating value op for string %v", stringValue) log.Debugf("creating value op for string %v", stringValue)
var node = createScalarNode(value, stringValue) var node = createScalarNode(value, stringValue)
return &Operation{ return &Operation{
@ -164,6 +164,8 @@ var stringInterpolationOpType = &operationType{Type: "STRING_INT", NumArgs: 0, P
var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 52, Handler: loadOperator, CheckForPostTraverse: true} var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 52, Handler: loadOperator, CheckForPostTraverse: true}
var loadStringOpType = &operationType{Type: "LOAD_STRING", NumArgs: 1, Precedence: 52, Handler: loadStringOperator} var loadStringOpType = &operationType{Type: "LOAD_STRING", NumArgs: 1, Precedence: 52, Handler: loadStringOperator}
var systemOpType = &operationType{Type: "SYSTEM", NumArgs: 1, Precedence: 50, Handler: systemOperator}
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 52, Handler: keysOperator, CheckForPostTraverse: true} var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 52, Handler: keysOperator, CheckForPostTraverse: true}
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator} var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}

View File

@ -195,9 +195,9 @@ func addMaps(target *CandidateNode, lhsC *CandidateNode, rhsC *CandidateNode) {
for index := 0; index < len(rhs.Content); index = index + 2 { for index := 0; index < len(rhs.Content); index = index + 2 {
key := rhs.Content[index] key := rhs.Content[index]
value := rhs.Content[index+1] value := rhs.Content[index+1]
log.Debug("finding %v", key.Value) log.Debugf("finding %v", key.Value)
indexInLHS := findKeyInMap(target, key) indexInLHS := findKeyInMap(target, key)
log.Debug("indexInLhs %v", indexInLHS) log.Debugf("indexInLhs %v", indexInLHS)
if indexInLHS < 0 { if indexInLHS < 0 {
// not in there, append it // not in there, append it
target.AddKeyValueChild(key, value) target.AddKeyValueChild(key, value)

View File

@ -527,6 +527,18 @@ var addOperatorScenarios = []expressionScenario{
expression: `.a += [2]`, expression: `.a += [2]`,
expectedError: "!!seq () cannot be added to a !!str (a)", expectedError: "!!seq () cannot be added to a !!str (a)",
}, },
{
// Regression test for https://issues.oss-fuzz.com/issues/383860504
// Adding a map to itself must not panic when sequence keys contain
// single-entry mappings with a null key in one and a non-null key
// in the other.
skipDoc: true,
document: "? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2",
expression: `. += .`,
expected: []string{
"D0, P[], (!!map)::? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2\n",
},
},
} }
func TestAddOperatorScenarios(t *testing.T) { func TestAddOperatorScenarios(t *testing.T) {

View File

@ -170,6 +170,10 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
if mergeNodeSeq.Kind == AliasNode { if mergeNodeSeq.Kind == AliasNode {
mergeNodeSeq = mergeNodeSeq.Alias mergeNodeSeq = mergeNodeSeq.Alias
} }
mergeNodeSeq = mergeNodeSeq.Copy()
if err := explodeNode(mergeNodeSeq, Context{}); err != nil {
return err
}
if mergeNodeSeq.Kind != MappingNode { if mergeNodeSeq.Kind != MappingNode {
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag) return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag)
} }
@ -179,12 +183,7 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
}) })
for _, item := range itemsToAdd { for _, item := range itemsToAdd {
// copy to ensure exploding doesn't modify the original node newContent = append(newContent, item.Copy())
itemCopy := item.Copy()
if err := explodeNode(itemCopy, Context{}); err != nil {
return err
}
newContent = append(newContent, itemCopy)
} }
} }
} }
@ -256,7 +255,7 @@ func explodeNode(node *CandidateNode, context Context) error {
node.Value = node.Alias.Value node.Value = node.Alias.Value
node.Alias = nil node.Alias = nil
} }
log.Debug("now I'm %v", NodeToString(node)) log.Debugf("now I'm %v", NodeToString(node))
return nil return nil
case MappingNode: case MappingNode:
// //check the map has an alias in it // //check the map has an alias in it
@ -304,7 +303,7 @@ func applyAlias(node *CandidateNode, alias *CandidateNode, aliasIndex int, newCo
if alias == nil { if alias == nil {
return nil return nil
} }
log.Debug("alias: %v", NodeToString(alias)) log.Debugf("alias: %v", NodeToString(alias))
if alias.Kind != MappingNode { if alias.Kind != MappingNode {
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", alias.Tag) return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", alias.Tag)
} }

View File

@ -31,7 +31,7 @@ thingOne:
value: false value: false
thingTwo: thingTwo:
name: item_2 name: item_2
!!merge <<: *item_value <<: *item_value
` `
var explodeMergeAnchorsFixedExpected = `D0, P[], (!!map)::foo: var explodeMergeAnchorsFixedExpected = `D0, P[], (!!map)::foo:
@ -198,6 +198,15 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::{a: 42}\n", "D0, P[], (!!map)::{a: 42}\n",
}, },
}, },
{
skipDoc: true,
description: "Nested merge anchor with inline map",
document: `{<<: {<<: {a: 42}}}`,
expression: `explode(.)`,
expected: []string{
"D0, P[], (!!map)::{a: 42}\n",
},
},
{ {
skipDoc: true, skipDoc: true,
description: "Merge anchor with sequence with inline map", description: "Merge anchor with sequence with inline map",
@ -279,7 +288,63 @@ var badAnchorOperatorScenarios = []expressionScenario{
}, },
} }
var mixedMergeTagStyleDocument = `
constants:
errorResponse: &errorResponse
status: 200
endpoints:
- condition: true
!!merge <<: *errorResponse
- condition: false
<<: *errorResponse
other:
!!merge <<: *errorResponse
somethingElse:
<<: *errorResponse
`
var mixedMergeTagStyleExplodedDocument = `
constants:
errorResponse:
status: 200
endpoints:
- condition: true
status: 200
- condition: false
status: 200
other:
status: 200
somethingElse:
status: 200
`
var anchorOperatorScenarios = []expressionScenario{ var anchorOperatorScenarios = []expressionScenario{
{
// mergeObjects previously skipped all !!merge-tagged nodes. Since !!merge only appears on
// << map keys, this meant applyAssignment was never called for the << key. It was later
// autocreated by createStringScalarNode("<<") with tag !!str, silently dropping !!merge.
// DontFollowAlias:true already prevents aliases being followed, so the skip was redundant.
// Old (buggy) output: "D0, P[], (!!map)::base: &base\n x: 1\ndest:\n <<: *base\n"
skipDoc: true,
description: "direct *+ preserves explicit !!merge tag on << key (regression for issue 2677)",
document: "base: &base\n x: 1\ndest:\n !!merge <<: *base\n",
expression: `. as $d | {} *+ $d`,
expected: []string{"D0, P[], (!!map)::base: &base\n x: 1\ndest:\n !!merge <<: *base\n"},
},
{
skipDoc: true,
description: "explicit !!merge tag on << key is preserved through ireduce merge",
document: mixedMergeTagStyleDocument,
expression: `. as $item ireduce ({}; . *+ $item)`,
expected: []string{"D0, P[], (!!map)::" + mixedMergeTagStyleDocument},
},
{
skipDoc: true,
description: "explode expands << merge keys regardless of explicit tag style (!!merge or plain)",
document: mixedMergeTagStyleDocument,
expression: `explode(.)`,
expected: []string{"D0, P[], (!!map)::" + mixedMergeTagStyleExplodedDocument},
},
{ {
skipDoc: true, skipDoc: true,
description: "merge anchor to alias alias", description: "merge anchor to alias alias",

View File

@ -37,7 +37,7 @@ func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode
prefs := getAssignPreferences(expressionNode.Operation.Preferences) prefs := getAssignPreferences(expressionNode.Operation.Preferences)
log.Debug("assignUpdateOperator prefs: %v", prefs) log.Debugf("assignUpdateOperator prefs: %v", prefs)
if !expressionNode.Operation.UpdateAssign { if !expressionNode.Operation.UpdateAssign {
// this works because we already ran against LHS with an editable context. // this works because we already ran against LHS with an editable context.

View File

@ -142,7 +142,7 @@ func notOperator(_ *dataTreeNavigator, context Context, _ *ExpressionNode) (Cont
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
log.Debug("notOperation checking %v", candidate) log.Debugf("notOperation checking %v", candidate)
truthy := isTruthyNode(candidate) truthy := isTruthyNode(candidate)
result := createBooleanCandidate(candidate, !truthy) result := createBooleanCandidate(candidate, !truthy)
results.PushBack(result) results.PushBack(result)

View File

@ -46,9 +46,9 @@ func containsObject(lhs *CandidateNode, rhs *CandidateNode) (bool, error) {
rhsKey := rhs.Content[index] rhsKey := rhs.Content[index]
rhsValue := rhs.Content[index+1] rhsValue := rhs.Content[index+1]
log.Debugf("Looking for %v in the lhs", rhsKey.Value) log.Debugf("Looking for %v in the lhs", rhsKey.Value)
lhsKeyIndex := findInArray(lhs, rhsKey) lhsKeyIndex := findKeyInMap(lhs, rhsKey)
log.Debugf("index is %v", lhsKeyIndex) log.Debugf("index is %v", lhsKeyIndex)
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 { if lhsKeyIndex < 0 {
return false, nil return false, nil
} }
lhsValue := lhs.Content[lhsKeyIndex+1] lhsValue := lhs.Content[lhsKeyIndex+1]

View File

@ -65,6 +65,16 @@ var containsOperatorScenarios = []expressionScenario{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
}, },
}, },
{
// Regression: findInArray could match a null key against a null
// value at an earlier odd index, producing a false negative.
skipDoc: true,
document: "? 1\n: ~\n? ~\n: x",
expression: `contains({~: "x"})`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{ {
description: "String contains substring", description: "String contains substring",
document: `"foobar"`, document: `"foobar"`,

View File

@ -80,7 +80,7 @@ func formatDateTime(d *dataTreeNavigator, context Context, expressionNode *Expre
node, errorReading := parseSnippet(formattedTimeStr) node, errorReading := parseSnippet(formattedTimeStr)
if errorReading != nil { if errorReading != nil {
log.Debugf("could not parse %v - lets just leave it as a string: %w", formattedTimeStr, errorReading) log.Debugf("could not parse %v - lets just leave it as a string: %v", formattedTimeStr, errorReading)
node = &CandidateNode{ node = &CandidateNode{
Kind: ScalarNode, Kind: ScalarNode,
Tag: "!!str", Tag: "!!str",

View File

@ -45,7 +45,7 @@ func removeFromContext(context Context, candidate *CandidateNode) (Context, erro
if nodeInContext != candidate { if nodeInContext != candidate {
newResults.PushBack(nodeInContext) newResults.PushBack(nodeInContext)
} else { } else {
log.Info("Need to delete this %v", NodeToString(nodeInContext)) log.Infof("Need to delete this %v", NodeToString(nodeInContext))
} }
} }
return context.ChildContext(newResults), nil return context.ChildContext(newResults), nil

View File

@ -45,7 +45,7 @@ var divideOperatorScenarios = []expressionScenario{
document: `{a: 1, b: -1}`, document: `{a: 1, b: -1}`,
expression: `.a = .a / 0 | .b = .b / 0`, expression: `.a = .a / 0 | .b = .b / 0`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: !!float +Inf, b: !!float -Inf}\n", "D0, P[], (!!map)::{a: +Inf, b: -Inf}\n",
}, },
}, },
{ {

View File

@ -33,7 +33,7 @@ func configureEncoder(format *Format, indent int) Encoder {
func encodeToString(candidate *CandidateNode, prefs encoderPreferences) (string, error) { func encodeToString(candidate *CandidateNode, prefs encoderPreferences) (string, error) {
var output bytes.Buffer var output bytes.Buffer
log.Debug("printing with indent: %v", prefs.indent) log.Debugf("printing with indent: %v", prefs.indent)
encoder := configureEncoder(prefs.format, prefs.indent) encoder := configureEncoder(prefs.format, prefs.indent)
if encoder == nil { if encoder == nil {

View File

@ -157,10 +157,10 @@ func withEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
log.Debug("candidate %v", NodeToString(candidate)) log.Debugf("candidate %v", NodeToString(candidate))
log.Debug("candidate leading content: %v", candidate.LeadingContent) log.Debugf("candidate leading content: %v", candidate.LeadingContent)
collected.LeadingContent = candidate.LeadingContent collected.LeadingContent = candidate.LeadingContent
log.Debug("candidate FootComment: [%v]", candidate.FootComment) log.Debugf("candidate FootComment: [%v]", candidate.FootComment)
collected.HeadComment = candidate.HeadComment collected.HeadComment = candidate.HeadComment
collected.FootComment = candidate.FootComment collected.FootComment = candidate.FootComment

View File

@ -21,7 +21,7 @@ func envOperator(_ *dataTreeNavigator, context Context, expressionNode *Expressi
return Context{}, fmt.Errorf("env operations have been disabled") return Context{}, fmt.Errorf("env operations have been disabled")
} }
envName := expressionNode.Operation.CandidateNode.Value envName := expressionNode.Operation.CandidateNode.Value
log.Debug("EnvOperator, env name:", envName) log.Debugf("EnvOperator, env name: %v", envName)
rawValue := os.Getenv(envName) rawValue := os.Getenv(envName)
@ -49,9 +49,9 @@ func envOperator(_ *dataTreeNavigator, context Context, expressionNode *Expressi
} }
} }
log.Debug("ENV tag", node.Tag) log.Debugf("ENV tag: %v", node.Tag)
log.Debug("ENV value", node.Value) log.Debugf("ENV value: %v", node.Value)
log.Debug("ENV Kind", node.Kind) log.Debugf("ENV Kind: %v", node.Kind)
return context.SingleChildContext(node), nil return context.SingleChildContext(node), nil
} }
@ -78,7 +78,7 @@ func envsubstOperator(_ *dataTreeNavigator, context Context, expressionNode *Exp
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
node := el.Value.(*CandidateNode) node := el.Value.(*CandidateNode)
if node.Tag != "!!str" { if node.Tag != "!!str" {
log.Warning("EnvSubstOperator, env name:", node.Tag, node.Value) log.Warningf("EnvSubstOperator, env name: %v %v", node.Tag, node.Value)
return Context{}, fmt.Errorf("cannot substitute with %v, can only substitute strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag) return Context{}, fmt.Errorf("cannot substitute with %v, can only substitute strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag)
} }

View File

@ -77,7 +77,7 @@ var loadScenarios = []expressionScenario{
document: `{something: {file: "thing.yml"}, over: {here: [{file: "thing.yml"}]}}`, document: `{something: {file: "thing.yml"}, over: {here: [{file: "thing.yml"}]}}`,
expression: `(.. | select(has("file"))) |= load("../../examples/" + .file)`, expression: `(.. | select(has("file"))) |= load("../../examples/" + .file)`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{something: {a: apple is included, b: cool.}, over: {here: [{a: apple is included, b: cool.}]}}\n", "D0, P[], (!!map)::{something: {a: apple is included, b: cool.}, over: {here: [{a: apple is included,\n b: cool.}]}}\n",
}, },
}, },
{ {

View File

@ -47,8 +47,8 @@ func mapOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
} }
result, err := d.GetMatchingNodes(splatted, expressionNode.RHS) result, err := d.GetMatchingNodes(splatted, expressionNode.RHS)
log.Debug("expressionNode.Rhs %v", expressionNode.RHS.Operation.OperationType) log.Debugf("expressionNode.Rhs %v", expressionNode.RHS.Operation.OperationType)
log.Debug("result %v", result) log.Debugf("result %v", result)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -37,7 +37,7 @@ var moduloOperatorScenarios = []expressionScenario{
document: `{a: 12, b: 2.5}`, document: `{a: 12, b: 2.5}`,
expression: `.a = .a % .b`, expression: `.a = .a % .b`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: !!float 2, b: 2.5}\n", "D0, P[], (!!map)::{a: 2, b: 2.5}\n",
}, },
}, },
{ {
@ -53,7 +53,7 @@ var moduloOperatorScenarios = []expressionScenario{
document: `{a: 1.1, b: 0}`, document: `{a: 1.1, b: 0}`,
expression: `.a = .a % .b`, expression: `.a = .a % .b`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: !!float NaN, b: 0}\n", "D0, P[], (!!map)::{a: NaN, b: 0}\n",
}, },
}, },
{ {
@ -70,7 +70,7 @@ var moduloOperatorScenarios = []expressionScenario{
document: "a: 2\nb: !goat 2.3", document: "a: 2\nb: !goat 2.3",
expression: `.a = .a % .b`, expression: `.a = .a % .b`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::a: !!float 2\nb: !goat 2.3\n", "D0, P[], (!!map)::a: 2\nb: !goat 2.3\n",
}, },
}, },
{ {

View File

@ -155,8 +155,10 @@ func repeatString(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error
return nil, err return nil, err
} else if count < 0 { } else if count < 0 {
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count) return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
} else if count > 10000000 { }
return nil, fmt.Errorf("cannot repeat string by more than 100 million (%v)", count) maxResultLen := 10 * 1024 * 1024 // 10 MiB
if count > 0 && len(stringNode.Value) > maxResultLen/count {
return nil, fmt.Errorf("result of repeating string (%v bytes) by %v would exceed %v bytes", len(stringNode.Value), count, maxResultLen)
} }
target.Value = strings.Repeat(stringNode.Value, count) target.Value = strings.Repeat(stringNode.Value, count)
@ -187,10 +189,6 @@ func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs
log.Debugf("going to applied assignment to LHS: %v with RHS: %v", NodeToString(lhs), NodeToString(candidate)) log.Debugf("going to applied assignment to LHS: %v with RHS: %v", NodeToString(lhs), NodeToString(candidate))
if candidate.Tag == "!!merge" {
continue
}
err := applyAssignment(d, context, pathIndexToStartFrom, lhs, candidate, preferences) err := applyAssignment(d, context, pathIndexToStartFrom, lhs, candidate, preferences)
if err != nil { if err != nil {
return nil, err return nil, err

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