Compare commits

...

263 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
Mike Farah
5a7e72a743 Bumping version 2026-02-14 18:43:09 +11:00
Mike Farah
562531d936 Dropping windows/arm 2026-02-14 18:42:31 +11:00
Mike Farah
2c471b6498 Bumping version 2026-02-14 11:51:00 +11:00
Mike Farah
f4ef6ef3cf Release notes 2026-02-14 11:50:51 +11:00
dependabot[bot]
f49f2bd2d8
Bump golang.org/x/mod from 0.31.0 to 0.33.0 (#2606)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.31.0 to 0.33.0.
- [Commits](https://github.com/golang/mod/compare/v0.31.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.33.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-02-14 11:42:40 +11:00
dependabot[bot]
6ccc7b7797
Bump golang.org/x/net from 0.49.0 to 0.50.0 (#2604)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.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-02-14 11:32:58 +11:00
dependabot[bot]
b3e1fbb7d1
Bump golang from 1.25.6 to 1.26.0 (#2603)
Bumps golang from 1.25.6 to 1.26.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.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-02-14 11:32:47 +11:00
Mike Farah
288ca2d114
Fixing comments in TOML arrays #2592 (#2595) 2026-02-03 19:42:49 +11:00
Mike Farah
eb04fa87af More tests 2026-02-01 10:27:18 +11:00
Mike Farah
2be0094729 Bumping version 2026-02-01 09:19:54 +11:00
Mike Farah
3c18d5b035 Preparing release 2026-02-01 09:19:45 +11:00
Mike Farah
2dcc2293da Merge branch 'tomers-fix/toml-comments-table-scope-2588' 2026-02-01 09:14:32 +11:00
Mike Farah
eb4fde4ef8 Pulling out common code 2026-02-01 09:14:18 +11:00
Mike Farah
06ea4cf62e Fixing spelling 2026-02-01 09:10:48 +11:00
Mike Farah
37089d24af Merge branch 'fix/toml-comments-table-scope-2588' of github.com:tomers/yq into tomers-fix/toml-comments-table-scope-2588 2026-02-01 09:08:20 +11:00
Slava Ezhkin
7cf88a0291
Add regression test for go install compatibility #2587 (#2591) 2026-02-01 09:01:53 +11:00
Mike Farah
41adc1ad18 Fixing wrongly named instructions file 2026-02-01 08:53:12 +11:00
Tomer Shalev
b4b96f2a68 Fix TOML table parsing after standalone comments
Standalone TOML comments immediately inside a table/array-table no longer end the table scope, preventing subsequent keys from being flattened to the document root.
2026-01-31 14:41:30 +02:00
Mike Farah
2824d66a65 Multiply uses a readonly context #2558 2026-01-31 16:47:58 +11:00
Mike Farah
4bbffa9022 Fixed merge globbing wildcards in keys #2564 2026-01-31 15:44:50 +11:00
Mike Farah
bdeedbd275 Fixing TOML subarray parsing issue #2581 2026-01-31 15:25:11 +11:00
Mike Farah
3d918acc2a Bumping version 2026-01-31 15:03:32 +11:00
Mike Farah
01005cc8fd Preparing release notes 2026-01-31 15:03:23 +11:00
Mike Farah
c4468165f2 Formatting 2026-01-31 14:55:36 +11:00
sydarn
e35d32a0b6
buildfix: which -> command -v (#2582) 2026-01-31 14:51:56 +11:00
jfenal
78192a915b
feat: Add --yaml-compact-seq-indent / -c flag for compact sequence indentation (#2583)
Adds a new CLI flag that enables compact sequence indentation where '- ' is
considered part of the indentation. This leverages the CompactSeqIndent()
method from the underlying go.yaml.in/yaml/v4 library.

Example output with --yaml-compact-seq-indent:
  parent:
    items:
    - one
    - two

Instead of the default:
  parent:
    items:
      - one
      - two

Closes #1841
2026-01-31 14:50:01 +11:00
jfenal
c4f4e6d416
fix: TOML colorization now works when NO_COLOR env is set (#2584)
The colorizeToml function intended to force colors by setting
color.NoColor = false, but SprintFunc() still respects the NO_COLOR
environment variable. This caused TestTomlColourization to fail in
CI environments where NO_COLOR=1 is set.

Fixed by calling EnableColor() on each color object, which explicitly
forces colors regardless of environment settings.

Vibe-coded with Cursor (Claude Opus 4)
2026-01-31 14:49:42 +11:00
dependabot[bot]
5f90039bdc
Bump golang from 1.25.5 to 1.25.6 (#2580)
Bumps golang from 1.25.5 to 1.25.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.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-01-22 14:59:24 +11:00
Elias-elastisys
c6fa371d8d
Add symlink check to file rename util (#2576) 2026-01-22 13:43:32 +11:00
dependabot[bot]
3a27e39778
Bump actions/setup-go from 5 to 6 (#2471)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  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-01-22 13:42:54 +11:00
TJ Miller
414a085563
Fix default command used for __completeNoDesc alias (#2568) 2026-01-22 13:41:42 +11:00
dependabot[bot]
542801926f
Bump github.com/goccy/go-yaml from 1.19.1 to 1.19.2 (#2566)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.1 to 1.19.2.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.1...v1.19.2)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.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-01-21 10:17:43 +11:00
Robin H. Johnson
1bcc44ff9b
ci: ensure lint has goflags (#2570)
Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>
2026-01-21 10:17:26 +11:00
dependabot[bot]
a6f1b02340
Bump golang.org/x/net from 0.48.0 to 0.49.0 (#2575)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.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-01-21 10:15:44 +11:00
Flint Winters
f98028c925
Unwrap scalars in shell output mode. (#2548)
* feat: Add UnwrapScalar to ShellVariablesPreferences

- Add UnwrapScalar boolean field to ShellVariablesPreferences struct.
- Initialize UnwrapScalar to false in NewDefaultShellVariablesPreferences.
- This preference will control whether shell output should be quoted or raw.

* feat: Propagate unwrapScalar to ShellVariablesPreferences

- In configureEncoder function, set UnwrapScalar in ConfiguredShellVariablesPreferences.
- This ensures the -r flag's state is passed to the shell encoder for raw output control.

* feat: Implement conditional quoting in shellVariablesEncoder

- Modify doEncode method to check pe.prefs.UnwrapScalar.
- If UnwrapScalar is true, output raw node.Value.
- Otherwise, use quoteValue for shell-safe quoting.
- This enables quote-free output for Kubernetes workflows when -r is used.

* test: Add tests for UnwrapScalar in shell encoder

- Introduce assertEncodesToUnwrapped helper function.
- Add TestShellVariablesEncoderUnwrapScalar to verify quote-free output with -r.
- Add TestShellVariablesEncoderDefaultQuoting to confirm default quoting behavior without -r.
- Ensure comprehensive testing of conditional quoting logic for shell output.

* remove redundant test
2026-01-01 15:21:55 +11:00
Robin H. Johnson
c6029376a5
feat: K8S KYAML output format support (#2560)
* feat: K8S KYAML output format support

Reference: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/5295-kyaml/README.md
Co-authored-by: Codex <codex@openai.com>
Generated-with: OpenAI Codex CLI (partial)
Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>

* build: gomodcache/gocache should not be committed

Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>

* chore: fix spelling of behaviour

Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>

* build: pass GOFLAGS to docker to support buildvcs=false

In trying to develop the KYAML support, various tests gave false
positive results because they made assumptions about Git functionality
Make it possible to avoid that by passing GOFLAGS='-buildvcs=false' to
to Makefile.

Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>

* doc: cover documentScenarios for tests

Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>

* build: exclude go caches from gosec

Without tuning, gosec scans all of the vendor/gocache/gomodcache, taking
several minutes (3m35 here), whereas the core of the yq takes only 15
seconds to scan.

If we intend to remediate upstream issues in future; add a seperate
target to scan those.

Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>

---------

Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com>
Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>
Co-authored-by: Codex <codex@openai.com>
2026-01-01 15:14:53 +11:00
Mike Farah
23abf50fef Adding notoml directive to encoder 2025-12-26 11:08:24 +11:00
Mike Farah
64ec1f4aa7 Adding negative parent example 2025-12-26 10:53:42 +11:00
Mike Farah
4973c355e6 Bumping version 2025-12-20 19:21:32 +11:00
Mike Farah
ecbdcada9f Preparing release 2025-12-20 19:21:24 +11:00
Mike Farah
029ba68014 Bump gosec version 2025-12-20 19:15:36 +11:00
Mike Farah
4a06cce376
Switch to uk (#2557)
* Setting golangci to UK english (that's what we use in AU)

* Fixing more spelling

* Fixing

* Include MD files in spell checker
2025-12-20 19:11:48 +11:00
Mike Farah
37e48cea44 Refining agents.md 2025-12-20 16:04:09 +11:00
Mike Farah
207bec6b29 whitespace 2025-12-20 16:01:07 +11:00
Mike Farah
7198d16575 Merge branch 'master' into toml_encoder 2025-12-20 15:58:57 +11:00
copilot-swe-agent[bot]
5d6c2047cf Fix spelling: use British English Colourization
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-20 15:55:17 +11:00
copilot-swe-agent[bot]
7f60daad20 Add test for string escape bug and implement fix
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-20 15:55:17 +11:00
Mike Farah
b7cbe59fd7
Update pkg/yqlib/encoder_toml.go
Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
2025-12-20 15:37:55 +11:00
copilot-swe-agent[bot]
9fa353b123 Add test coverage for parent(0) and parent(-3) edge cases
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-20 15:36:49 +11:00
Mike Farah
c6ecad1546 Support negative parent indices 2025-12-20 15:36:49 +11:00
Mike Farah
56eb3655b8 Formatting 2025-12-20 15:35:41 +11:00
copilot-swe-agent[bot]
1de4ec59f2 Merge remote-tracking branch 'origin/pr/2552' into copilot/sub-pr-2552
# Conflicts:
#	pkg/yqlib/toml_test.go
2025-12-20 04:26:11 +00:00
copilot-swe-agent[bot]
c132c32731 Convert to UK English spelling (colourization, coloured)
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-20 04:17:39 +00:00
Mike Farah
0914121d29 Fixing number color issue 2025-12-20 15:12:30 +11:00
copilot-swe-agent[bot]
aa5134e645 Add test case and fix colorization bug for inline arrays in TOML
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-20 04:09:04 +00:00
Mike Farah
4d620bfa26
Update pkg/yqlib/encoder_toml.go
Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
2025-12-20 15:07:00 +11:00
Mike Farah
b8d90fd574
Update pkg/yqlib/candidate_node.go
Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
2025-12-20 15:05:03 +11:00
copilot-swe-agent[bot]
c1b81f1a03 Initial plan 2025-12-20 04:04:24 +00:00
Mike Farah
ea40e14fb1
Create *.instructions.md 2025-12-20 15:02:22 +11:00
Mike Farah
b974d973ee spelling 2025-12-20 09:55:29 +11:00
dependabot[bot]
66ec487792 Bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 10:19:44 +11:00
Mike Farah
161be10791 Comments! 2025-12-16 20:47:15 +11:00
Mike Farah
aa858520a8 Merge branch 'master' into toml_encoder 2025-12-16 14:27:22 +11:00
Mike Farah
ac2889c296 Fixed scalar encoding for HCL 2025-12-16 14:22:50 +11:00
Mike Farah
626624af7b Adding tf to hcl format names 2025-12-16 14:17:38 +11:00
Mike Farah
b0d2522f80 Readme update 2025-12-16 14:05:58 +11:00
Mike Farah
2ee38e15b6 Adding HCL to readme 2025-12-15 11:45:08 +11:00
Mike Farah
4e9d5e8e48 wip 2025-12-15 11:40:28 +11:00
Mike Farah
1338b521ff Colours! 2025-12-14 19:41:45 +11:00
Mike Farah
3a5323824f Handles comments! 2025-12-14 19:33:00 +11:00
Mike Farah
8780172b33 Added missing fix in release notes 2025-12-14 19:05:55 +11:00
Mike Farah
5f9bf8d241 wip toml encoder 2025-12-14 19:03:54 +11:00
Mike Farah
065b200af9 Bumping version 2025-12-14 17:07:57 +11:00
Mike Farah
745a7ffb3c Preparing release 2025-12-14 17:07:45 +11:00
dependabot[bot]
a305d706d4 Bump golang.org/x/net from 0.47.0 to 0.48.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.48.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-14 08:45:25 +11:00
dependabot[bot]
0671ccd2cc Bump github.com/zclconf/go-cty from 1.16.3 to 1.17.0
Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.16.3 to 1.17.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.16.3...v1.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-14 08:45:16 +11:00
dependabot[bot]
4d8cd450bd Bump golang.org/x/text from 0.31.0 to 0.32.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.31.0 to 0.32.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-13 22:37:33 +11:00
Mike Farah
d2d657eacc HCL improvements 2025-12-09 19:49:34 +11:00
Mike Farah
f4fd8c585a Better roundtriping of HCL 2025-12-08 21:09:21 +11:00
Mike Farah
e4bf8a1e0a Simplifying HCL decoder 2025-12-08 20:31:28 +11:00
copilot-swe-agent[bot]
fd405749f9 Add build tag to hcl_test.go to skip tests when HCL is disabled
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-08 20:30:47 +11:00
Mike Farah
51ddf8d357 Update pkg/yqlib/format.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:30:47 +11:00
Mike Farah
77eccfd3db Fixed comment parsing, added generated docs 2025-12-08 20:30:47 +11:00
Mike Farah
554bf5a2f2 colorise output 2025-12-08 20:30:47 +11:00
Mike Farah
8162f3a100 wip - comments 2025-12-08 20:30:47 +11:00
Mike Farah
48707369a0 wip - comments 2025-12-08 20:30:47 +11:00
Mike Farah
4f72c37de7 cleanup 2025-12-08 20:30:47 +11:00
Mike Farah
795f9c954c Fixing formatting 2025-12-08 20:30:47 +11:00
Mike Farah
3d35386ad9 Update pkg/yqlib/decoder_hcl.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:30:47 +11:00
Mike Farah
154a4ace01 Update pkg/yqlib/encoder_hcl.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:30:47 +11:00
Mike Farah
effdfe1221 Update agents.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:30:47 +11:00
Mike Farah
8af768a015 wip 2025-12-08 20:30:47 +11:00
Mike Farah
5f3dcb1ccf Fixed processing of block labels 2025-12-08 20:30:47 +11:00
Mike Farah
6270c29f54 Fixed processing of nested data 2025-12-08 20:30:47 +11:00
Mike Farah
df3101ce53 Refining 2025-12-08 20:30:47 +11:00
Mike Farah
65e79845d4 Refining 2025-12-08 20:30:47 +11:00
Mike Farah
b4d8131197 Adding HCL no build directive 2025-12-08 20:30:47 +11:00
Mike Farah
c75a2fad86 Use HCL encoder 2025-12-08 20:30:47 +11:00
Mike Farah
8d430cf41c Use HCL encoder 2025-12-08 20:30:47 +11:00
Mike Farah
2e96a28270 adding agents file 2025-12-08 20:30:47 +11:00
Mike Farah
656f07d0c2 wip 2025-12-08 20:30:47 +11:00
Mike Farah
1852073f29 hcl - sorted decoding 2025-12-08 20:30:47 +11:00
Mike Farah
7d2c774e8f maps and strings 2025-12-08 20:30:47 +11:00
Mike Farah
69076dfe81 wip 2025-12-08 20:30:47 +11:00
Mike Farah
9e17cd683f First cut 2025-12-08 20:30:47 +11:00
Mike Farah
eb3d0e63e3 Fixing handling of CRLF #2352 2025-12-06 19:08:37 +11:00
dependabot[bot]
2072808def Bump golang from 1.25.4 to 1.25.5
Bumps golang from 1.25.4 to 1.25.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:15 +11:00
dependabot[bot]
7d47b36b69 Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:08 +11:00
dependabot[bot]
53f10ae360 Bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:01 +11:00
dependabot[bot]
22510ab8d5 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:28:39 +11:00
Alexander
588d0bb3dd Bumped to core24 and removed riscv64 2025-11-26 09:31:58 +11:00
Mike Farah
7ccaf8e700 Bumping version 2025-11-25 10:45:39 +11:00
Mike Farah
a1a27b8536 Updating release notes 2025-11-25 10:45:35 +11:00
Mike Farah
1b91fc63ea Removing escape char processing from strenv #2517 2025-11-25 10:44:03 +11:00
Mike Farah
9e0c5fd3c9 Fixing escape charaters again 😢 #2517 2025-11-25 10:17:43 +11:00
Alexander
5d0481c0d2 Running build step on launchpad remote builder with supported architectures 2025-11-25 08:55:36 +11:00
Alexander
f91176a204 Fixed architecture builders 2025-11-25 08:55:36 +11:00
Mike Farah
8e86bdb876 Attempting to fix snap again 2025-11-22 18:33:44 +11:00
Mike Farah
fc164ca9c3 Updating README with latest yq help 2025-11-22 15:04:39 +11:00
Mike Farah
810e9d921e Syncing how-it-works from gitbook branch 2025-11-22 15:02:03 +11:00
Mike Farah
45be35c063 Bumping version 2025-11-22 14:52:48 +11:00
Mike Farah
39fbf01fa8 Fixing TOML ArrayTable parsing issues #1758 2025-11-22 14:49:49 +11:00
Mike Farah
306dc931a5 Fixing TOML ArrayTable parsing issues #1758 2025-11-22 14:35:07 +11:00
Mike Farah
f00852bc6c Added flags to disable env and file ops #2515 2025-11-22 09:40:03 +11:00
Mike Farah
c716d157f2 Fixing parsing of escaped characters in strenv #2506 2025-11-16 09:22:21 +11:00
Mike Farah
e49e588ab5 Fixing parsing of escaped characters #2506 2025-11-16 09:12:13 +11:00
Mike Farah
389486829d Updating release notes 2025-11-15 14:44:18 +11:00
Mike Farah
d32e71f25b Updating release notes 2025-11-15 14:37:33 +11:00
Mike Farah
796317b885 Bumping version 2025-11-15 14:32:46 +11:00
Mike Farah
258b84a05e Strip whitespace when decoding base64 #2507 2025-11-15 14:11:55 +11:00
dependabot[bot]
e056b91a00 Bump golang.org/x/net from 0.46.0 to 0.47.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.46.0 to 0.47.0.
- [Commits](https://github.com/golang/net/compare/v0.46.0...v0.47.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 14:11:03 +11:00
znley
85b0985a60 build: add linux/loong64 to release target
Signed-off-by: znley <shanjiantao@loongson.cn>
2025-11-15 11:36:53 +11:00
dependabot[bot]
874cbc4d3c Bump golang.org/x/text from 0.30.0 to 0.31.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 11:36:33 +11:00
Mike Farah
f6c780e793 Reverting test 2025-11-09 16:21:50 +11:00
Mike Farah
8c25f33df4 Merge in master 2025-11-09 16:19:04 +11:00
Mike Farah
2869919cb4 Merge branch 'master' into go-yaml-v4 2025-11-09 16:18:49 +11:00
Mike Farah
458d02f3ab Bumping to go-yaml 4-rc3 2025-11-09 16:15:36 +11:00
Mike Farah
877a47cb19 Improving first op docs 2025-11-09 16:12:17 +11:00
Mike Farah
3050ca5303 Adding first operator 2025-11-09 16:12:17 +11:00
Navid
49b6477c49 Fix out of range panic in yaml decoder 2025-11-09 16:12:17 +11:00
Mike Farah
78bc9baffd Added parents operator 2025-11-09 16:12:17 +11:00
Robert Lee
1f2b0fe76b Add --shell-key-separator flag for customizable shell output format
- Add ShellVariablesPreferences struct with KeySeparator field (default: '_')
- Update shellVariablesEncoder to use configurable separator
- Add --shell-key-separator CLI flag
- Add comprehensive tests for custom separator functionality
- Update documentation with example usage for custom separator

This feature allows users to specify a custom separator (e.g. '__') when
outputting shell variables, which helps disambiguate nested keys from
keys that contain underscores in their names.

Example:
  yq -o=shell --shell-key-separator='__' file.yaml

Fixes ambiguity when original YAML keys contain underscores.
2025-11-07 20:02:58 +11:00
dependabot[bot]
1228bcfa75 Bump golang from 1.25.2 to 1.25.4
Bumps golang from 1.25.2 to 1.25.4.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 19:44:03 +11:00
Mike Farah
7f72595a12 Cursor generated unit tests 2025-10-12 15:38:40 +11:00
Mike Farah
ff2c1c930c Adding more tests 2025-10-12 14:50:41 +11:00
Mike Farah
36d410b348 Updating readme 2025-10-12 14:45:10 +11:00
Mike Farah
6dfe002058 Updating contrib doc 2025-10-12 14:37:05 +11:00
Mike Farah
ed4f468c97 Release notes 2025-10-12 14:33:02 +11:00
Mike Farah
8b2ba41c6c Improving first op test 2025-10-12 14:32:28 +11:00
Mike Farah
02b28073bf Fixing error reporting 2025-09-09 20:16:49 +10:00
Mike Farah
6957399dc0 Updating go-yaml from v3 to v4 2025-09-09 20:05:58 +10:00
213 changed files with 13058 additions and 1115 deletions

View File

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

1
.github/instructions/instructions.md vendored Normal file
View File

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

View File

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

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
workflow_dispatch:
permissions: {}
jobs:
publishDocker:
environment: dockerhub
env:
IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
version: latest
@ -31,13 +36,13 @@ jobs:
run: echo ${{ steps.buildx.outputs.platforms }} && docker version
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -75,26 +80,3 @@ jobs:
-t "ghcr.io/${IMAGE_NAME}:4" \
-t "ghcr.io/${IMAGE_NAME}:latest" \
.
cd github-action
docker buildx build \
--label "org.opencontainers.image.authors=https://github.com/mikefarah/yq/graphs/contributors" \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--label "org.opencontainers.image.description=yq is a portable command-line data file processor" \
--label "org.opencontainers.image.documentation=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.licenses=MIT" \
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
--label "org.opencontainers.image.source=https://github.com/mikefarah/yq" \
--label "org.opencontainers.image.title=yq" \
--label "org.opencontainers.image.url=https://mikefarah.gitbook.io/yq/" \
--label "org.opencontainers.image.version=${IMAGE_VERSION}" \
--platform "${PLATFORMS}" \
--pull \
--push \
-t "${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "${IMAGE_NAME}:4-githubaction" \
-t "${IMAGE_NAME}:latest-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
.

View File

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

View File

@ -5,12 +5,17 @@ on:
- 'v4.*'
- 'draft-*'
permissions: {}
jobs:
publishGitRelease:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: '^1.20'
check-latest: true
@ -24,7 +29,7 @@ jobs:
run: echo "VERSION=${GITHUB_REF##*/}" >> "${GITHUB_OUTPUT}"
- name: Generate man page
uses: docker://pandoc/core:2.14.2
uses: docker://pandoc/core:2.14.2@sha256:04e127c6642a2b9d447c26fe0ac6a5932efa8f508eda9f07da51b6e621dd7c19
id: gen-man-page
with:
args: >-
@ -37,14 +42,22 @@ jobs:
--output=yq.1
man.md
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Cross compile
run: |
sudo apt-get install rhash -y
go install github.com/goreleaser/goreleaser/v2@latest
go install github.com/goreleaser/goreleaser/v2@v2.16.0
./scripts/xcompile.sh
- name: Sign checksums
run: |
cosign sign-blob --yes --bundle build/checksums.bundle build/checksums
cosign sign-blob --yes --bundle build/checksums-bsd.bundle build/checksums-bsd
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: build/*
draft: true

78
.github/workflows/scorecard.yml vendored Normal file
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,17 +7,25 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions: {}
jobs:
buildSnap:
environment: snap
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: snapcore/action-build@v1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
id: build
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snapcraft-args: "remote-build --launchpad-accept-public-upload"
- uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable
release: stable

View File

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

7
.gitignore vendored
View File

@ -25,6 +25,7 @@ _testmain.go
cover.out
coverage.out
coverage.html
coverage_sorted.txt
*.exe
*.test
*.prof
@ -42,9 +43,11 @@ yq*.snap
test.yml
test*.yml
test*.tf
test*.xml
test*.toml
test*.yaml
*.kyaml
test_dir1/
test_dir2/
0.yml
@ -67,3 +70,7 @@ debian/files
.vscode
yq3
# Golang
.gomodcache/
.gocache/

View File

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

View File

@ -23,6 +23,7 @@ builds:
- linux_amd64
- linux_arm
- linux_arm64
- linux_loong64
- linux_mips
- linux_mips64
- linux_mips64le
@ -38,7 +39,6 @@ builds:
- openbsd_amd64
- windows_386
- windows_amd64
- windows_arm
- windows_arm64
no_unique_dist_dir: true

500
AGENTS.md Normal file
View File

@ -0,0 +1,500 @@
# yq — agent instructions
## ⚠️ MANDATORY: GitHub agent disclosure
**Always required. No exceptions.**
Whenever you perform **any** GitHub action on behalf of the user, you **must** disclose that an AI agent (Cursor) wrote the content and is acting on the user's behalf — **not the user personally**. Do this **before** submitting; never post first and add the disclosure later.
Applies to **all** GitHub interactions, including:
- Pull requests (titles, descriptions, and reviews)
- PR comments and inline review comments
- Issues (new issues, comments, and updates)
- Any other post or reply on GitHub
**How to disclose:** Put it prominently at the **top** of every PR description, review body, comment, or issue. Use wording like:
Inline review comments must include a short disclosure too (e.g. `> Generated by Cursor acting on the user's behalf, not the user personally.`).
**Never** submit a GitHub action without this disclosure.
---
Always run the spellcheck before raising a PR:
```bash
bash scripts/spelling.sh
```
This is also included in the full CI pipeline via `make local test`.
## Cursor Cloud specific instructions
### Overview
**yq** is a Go CLI for querying and transforming YAML, JSON, XML, INI, and other structured formats. There are no long-running services — development is build-and-test against a local `./yq` binary.
### Prerequisites
- **Go ≥ 1.25** (see `go.mod`)
- **Bash** (acceptance tests)
- **Docker/Podman** is optional; use `make local <target>` to run natively when containers are unavailable
### PATH
After `scripts/devtools.sh`, add Go tool binaries to PATH:
```bash
export PATH="$HOME/go/bin:$PATH"
```
`golangci-lint` and `typos` install to `$HOME/go/bin`; `gosec` installs to `./bin/gosec` in the repo root.
### Common commands (local, no Docker)
| Task | Command |
|------|---------|
| Install dev tools | `bash scripts/devtools.sh` |
| Vendor dependencies | `make local vendor` |
| Build binary | `go build -o yq .` or `make local build` |
| Format | `make local format` |
| Lint | `make local check` |
| Unit tests | `make local test` or `bash scripts/test.sh` |
| Acceptance (E2E) | `bash scripts/acceptance.sh` (requires `./yq` built first) |
`make local build` runs the full CI chain (format → spelling → gosec → lint → unit tests → build → acceptance). For a faster loop, build with `go build -o yq .` and run `bash scripts/acceptance.sh`.
### Caveats
- **`make` without `local`** tries Docker/Podman (`Dockerfile.dev`). In Cloud Agent VMs without Docker, always prefix with `make local`.
- **Spelling step** uses `typos` (installed by `scripts/devtools.sh`).
- **`make local test` / `scripts/check.sh`** require `golangci-lint` on PATH (`devtools.sh`).
---
# General rules
✅ **DO:**
- You can use ./yq with the `--debug-node-info` flag to get a deeper understanding of the ast.
- run ./scripts/format.sh to format the code; then ./scripts/check.sh lint and finally ./scripts/spelling.sh to check spelling.
- Add comprehensive tests to cover the changes
- Run test suite to ensure there is no regression
- Use UK english spelling
- **Follow the mandatory GitHub agent disclosure rule above** on every GitHub action — no exceptions
❌ **DON'T:**
- Git add or commit
- Add comments to functions that are self-explanatory
- **Post to GitHub without the mandatory agent disclosure** (PRs, reviews, comments, issues, or any other GitHub interaction)
# Adding a New Encoder/Decoder
This guide explains how to add support for a new format (encoder/decoder) to yq without modifying `candidate_node.go`.
## Overview
The encoder/decoder architecture in yq is based on two main interfaces:
- **Encoder**: Converts a `CandidateNode` to output in a specific format
- **Decoder**: Reads input in a specific format and creates a `CandidateNode`
Each format is registered in `pkg/yqlib/format.go` and made available through factory functions.
## Architecture
### Key Files
- `pkg/yqlib/encoder.go` - Defines the `Encoder` interface
- `pkg/yqlib/decoder.go` - Defines the `Decoder` interface
- `pkg/yqlib/format.go` - Format registry and factory functions
- `pkg/yqlib/operator_encoder_decoder.go` - Encode/decode operators
- `pkg/yqlib/encoder_*.go` - Encoder implementations
- `pkg/yqlib/decoder_*.go` - Decoder implementations
### Interfaces
**Encoder Interface:**
```go
type Encoder interface {
Encode(writer io.Writer, node *CandidateNode) error
PrintDocumentSeparator(writer io.Writer) error
PrintLeadingContent(writer io.Writer, content string) error
CanHandleAliases() bool
}
```
**Decoder Interface:**
```go
type Decoder interface {
Init(reader io.Reader) error
Decode() (*CandidateNode, error)
}
```
## Step-by-Step: Adding a New Encoder/Decoder
### Step 1: Create the Encoder File
Create `pkg/yqlib/encoder_<format>.go` implementing the `Encoder` interface:
- `Encode()` - Convert a `CandidateNode` to your format and write to the output writer
- `PrintDocumentSeparator()` - Handle document separators if your format requires them
- `PrintLeadingContent()` - Handle leading content/comments if supported
- `CanHandleAliases()` - Return whether your format supports YAML aliases
See `encoder_json.go` or `encoder_base64.go` for examples.
### Step 2: Create the Decoder File
Create `pkg/yqlib/decoder_<format>.go` implementing the `Decoder` interface:
- `Init()` - Initialize the decoder with the input reader and set up any needed state
- `Decode()` - Decode one document from the input and return a `CandidateNode`, or `io.EOF` when finished
See `decoder_json.go` or `decoder_base64.go` for examples.
### Step 3: Create Tests (Mandatory)
Create a test file `pkg/yqlib/<format>_test.go` using the `formatScenario` pattern:
- Define test scenarios as `formatScenario` structs with fields: `description`, `input`, `expected`, `scenarioType`
- `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation)
- Create a helper function `test<Format>Scenario()` that switches on `scenarioType`
- Create main test function `Test<Format>FormatScenarios()` that iterates over scenarios
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
Test coverage must include:
- Basic data types (scalars, arrays, objects/maps)
- Nested structures
- Edge cases (empty inputs, special characters, escape sequences)
- Format-specific features or syntax
- Round-trip tests: decode → encode → decode should preserve data
See `hcl_test.go` for a complete example.
### Step 4: Register the Format in format.go
Edit `pkg/yqlib/format.go`:
1. Add a new format variable:
- `"<format>"` is the formal name (e.g., "json", "yaml")
- `[]string{...}` contains short aliases (can be empty)
- The first function creates an encoder (can be nil for encode-only formats)
- The second function creates a decoder (can be nil for decode-only formats)
2. Add the format to the `Formats` slice in the same file
See existing formats in `format.go` for the exact structure.
### Step 5: Handle Encoder Configuration (if needed)
If your format has preferences/configuration options:
1. Create a preferences struct with your configuration fields
2. Update the encoder to accept preferences in its factory function
3. Update `format.go` to pass the configured preferences
4. Update `operator_encoder_decoder.go` if special indent handling is needed (see existing formats like JSON and YAML for the pattern)
This pattern is optional and only needed if your format has user-configurable options.
## Build Tags
Use build tags to allow optional compilation of formats:
- Add `//go:build !yq_no<format>` at the top of your encoder and decoder files
- Create a no-build version in `pkg/yqlib/no_<format>.go` that returns nil for encoder/decoder factories
This allows users to compile yq without certain formats using: `go build -tags yq_no<format>`
## Working with CandidateNode
The `CandidateNode` struct represents a YAML node with:
- `Kind`: The node type (ScalarNode, SequenceNode, MappingNode)
- `Tag`: The YAML tag (e.g., "!!str", "!!int", "!!map")
- `Value`: The scalar value (for ScalarNode only)
- `Content`: Child nodes (for SequenceNode and MappingNode)
Key methods:
- `node.guessTagFromCustomType()` - Infer the tag from Go type
- `node.AsList()` - Convert to a list for processing
- `node.CreateReplacement()` - Create a new replacement node
- `NewCandidate()` - Create a new CandidateNode
## Key Points
✅ **DO:**
- Implement only the `Encoder` and `Decoder` interfaces
- Register your format in `format.go` only
- Keep format-specific logic in your encoder/decoder files
- Use the candidate_node style attribute to store style information for round-trip. Ask if this needs to be updated with new styles.
- Use build tags for optional compilation
- Add comprehensive tests
- Run the specific encoder/decoder test (e.g. <format>_test.go) whenever you make ay changes to the encoder_<format> or decoder_<format>
- Handle errors gracefully
- Add the no build directive, like the xml encoder and decoder, that enables a minimal yq builds. e.g. `//go:build !yq_<format>`. Be sure to also update the build_small-yq.sh and build-tinygo-yq.sh to not include the new format.
❌ **DON'T:**
- Modify `candidate_node.go` to add format-specific logic
- Add format-specific fields to `CandidateNode`
- Create special cases in core navigation or evaluation logic
- Bypass the encoder/decoder interfaces
- Use candidate_node tag attribute for anything other than indicate the data type
## Examples
Refer to existing format implementations for patterns:
- **Simple encoder/decoder**: `encoder_json.go`, `decoder_json.go`
- **Complex with preferences**: `encoder_yaml.go`, `decoder_yaml.go`
- **Encoder-only**: `encoder_sh.go` (ShFormat has nil decoder)
- **String-only operations**: `encoder_base64.go`, `decoder_base64.go`
## Testing Your Implementation (Mandatory)
Tests must be implemented in `<format>_test.go` following the `formatScenario` pattern:
1. **Create test scenarios** using the `formatScenario` struct with fields:
- `description`: Brief description of what's being tested
- `input`: Sample input in your format
- `expected`: Expected output (typically in YAML for decode tests)
- `scenarioType`: Either `"decode"` or `"roundtrip"`
2. **Test coverage must include:**
- Basic data types (scalars, arrays, objects/maps)
- Nested structures
- Edge cases (empty inputs, special characters, escape sequences)
- Format-specific features or syntax
- Round-trip tests: decode → encode → decode should preserve data
3. **Test function pattern:**
- `test<Format>Scenario()`: Helper function that switches on `scenarioType`
- `Test<Format>FormatScenarios()`: Main test function that iterates over scenarios
4. **Example from existing formats:**
- See `hcl_test.go` for a complete example
- See `yaml_test.go` for YAML-specific patterns
- See `json_test.go` for more complex scenarios
## Common Patterns
### Format with Indentation
Use preferences to control output formatting:
```go
type <format>Preferences struct {
Indent int
}
func (prefs *<format>Preferences) Copy() <format>Preferences {
return *prefs
}
```
### Multiple Documents
Decoders should support reading multiple documents:
```go
func (dec *<format>Decoder) Decode() (*CandidateNode, error) {
if dec.finished {
return nil, io.EOF
}
// ... decode next document ...
if noMoreDocuments {
dec.finished = true
}
return candidate, nil
}
```
---
# Adding a New Operator
This guide explains how to add a new operator to yq. Operators are the core of yq's expression language and process `CandidateNode` objects without requiring modifications to `candidate_node.go` itself.
## Overview
Operators transform data by implementing a handler function that processes a `Context` containing `CandidateNode` objects. Each operator is:
1. Defined as an `operationType` in `operation.go`
2. Registered in the lexer in `lexer_participle.go`
3. Implemented in its own `operator_<type>.go` file
4. Tested in `operator_<type>_test.go`
5. Documented in `pkg/yqlib/doc/operators/headers/<type>.md`
## Architecture
### Key Files
- `pkg/yqlib/operation.go` - Defines `operationType` and operator registry
- `pkg/yqlib/lexer_participle.go` - Registers operators with their syntax patterns
- `pkg/yqlib/operator_<type>.go` - Operator implementation
- `pkg/yqlib/operator_<type>_test.go` - Operator tests using `expressionScenario`
- `pkg/yqlib/doc/operators/headers/<type>.md` - Documentation header
### Core Types
**operationType:**
```go
type operationType struct {
Type string // Unique operator name (e.g., "REVERSE")
NumArgs uint // Number of arguments (0 for no args)
Precedence uint // Operator precedence (higher = higher precedence)
Handler operatorHandler // The function that executes the operator
CheckForPostTraverse bool // Whether to apply post-traversal logic
ToString func(*Operation) string // Custom string representation
}
```
**operatorHandler signature:**
```go
type operatorHandler func(*dataTreeNavigator, Context, *ExpressionNode) (Context, error)
```
**expressionScenario for tests:**
```go
type expressionScenario struct {
description string
subdescription string
document string
expression string
expected []string
skipDoc bool
expectedError string
}
```
## Step-by-Step: Adding a New Operator
### Step 1: Create the Operator Implementation File
Create `pkg/yqlib/operator_<type>.go` implementing the operator handler function:
- Implement the `operatorHandler` function signature
- Process nodes from `context.MatchingNodes`
- Return a new `Context` with results using `context.ChildContext()`
- Use `candidate.CreateReplacement()` or `candidate.CreateReplacementWithComments()` to create new nodes
- Handle errors gracefully with meaningful error messages
See `operator_reverse.go` or `operator_keys.go` for examples.
### Step 2: Register the Operator in operation.go
Add the operator type definition to `pkg/yqlib/operation.go`:
```go
var <type>OpType = &operationType{
Type: "<TYPE>", // All caps, matches pattern in lexer
NumArgs: 0, // 0 for no args, 1+ for args
Precedence: 50, // Typical range: 40-55
Handler: <type>Operator, // Reference to handler function
}
```
**Precedence guidelines:**
- 10-20: Logical operators (OR, AND, UNION)
- 30: Pipe operator
- 40: Assignment and comparison operators
- 42: Arithmetic operators (ADD, SUBTRACT, MULTIPLY, DIVIDE)
- 50-52: Most other operators
- 55: High precedence (e.g., GET_VARIABLE)
**Optional fields:**
- `CheckForPostTraverse: true` - If your operator can have another directly after it without the pipe character. Most of the time this is false.
- `ToString: customToString` - Custom string representation (rarely needed)
### Step 3: Register the Operator in lexer_participle.go
Edit `pkg/yqlib/lexer_participle.go` to add the operator to the lexer rules:
- Use `simpleOp()` for simple keyword patterns
- Use object syntax for regex patterns or complex syntax
- Support optional characters with `_?` and aliases with `|`
See existing operators in `lexer_participle.go` for pattern examples.
### Step 4: Create Tests (Mandatory)
Create `pkg/yqlib/operator_<type>_test.go` using the `expressionScenario` pattern:
- Define test scenarios with `description`, `document`, `expression`, and `expected` fields
- `expected` is a slice of strings showing output format: `"D<doc>, P[<path>], (<tag>)::<value>\n"`
- Set `skipDoc: true` for edge cases you don't want in generated documentation
- Include `subdescription` for longer test names
- Set `expectedError` if testing error cases
- Create main test function that iterates over scenarios
- The main test function should use `documentScenarios` to ensure testcase documentation is generated.
Test coverage must include:
- Basic data types and nested structures
- Edge cases (empty inputs, special characters, type errors)
- Multiple outputs if applicable
- Format-specific features
See `operator_reverse_test.go` for a simple example and `operator_keys_test.go` for complex cases.
### Step 5: Create Documentation Header
Create `pkg/yqlib/doc/operators/headers/<type>.md`:
- Use the exact operator name as the title
- Include a concise 1-2 sentence summary
- Add additional context or examples if the operator is complex
See existing headers in `doc/operators/headers/` for examples.
## Working with Context and CandidateNode
### Context Management
- `context.ChildContext(results)` - Create child context with results
- `context.GetVariable("varName")` - Get variables stored in context
- `context.SetVariable("varName", value)` - Set variables in context
### CandidateNode Operations
- `candidate.CreateReplacement(ScalarNode, "!!str", stringValue)` - Create a replacement node
- `candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style)` - With style preserved
- `candidate.Kind` - The node type (ScalarNode, SequenceNode, MappingNode)
- `candidate.Tag` - The YAML tag (!!str, !!int, etc.)
- `candidate.Value` - The scalar value (for ScalarNode only)
- `candidate.Content` - Child nodes (for SequenceNode and MappingNode)
- `candidate.guessTagFromCustomType()` - Infer the tag from Go type
- `candidate.AsList()` - Convert to a list representation
## Key Points
✅ **DO:**
- Implement the operator handler with the correct signature
- Register in `operation.go` with appropriate precedence
- Add the lexer pattern in `lexer_participle.go`
- Write comprehensive tests covering normal and edge cases
- Create a documentation header in `doc/operators/headers/`
- Use `Context.ChildContext()` for proper context threading
- Handle all node types gracefully
- Return meaningful error messages
❌ **DON'T:**
- Modify `candidate_node.go` (operators shouldn't need this)
- Modify core navigation or evaluation logic
- Bypass the handler function pattern
- Add format-specific or operator-specific fields to `CandidateNode`
- Skip tests or documentation
## Examples
Refer to existing operator implementations for patterns:
- **No-argument operator**: `operator_reverse.go` - Processes arrays/sequences
- **Single-argument operator**: `operator_map.go` - Takes an expression argument
- **Complex multi-output**: `operator_keys.go` - Produces multiple results
- **With preferences**: `operator_to_number.go` - Configuration options
- **Error handling**: `operator_error.go` - Control flow with errors
- **String operations**: `operator_strings.go` - Multiple related operators
## Testing Patterns
Refer to existing test files for specific patterns:
- Basic expression tests in `operator_reverse_test.go`
- Multi-output tests in `operator_keys_test.go`
- Error handling tests in `operator_error_test.go`
- Tests with `skipDoc` flag to exclude from generated documentation
## Common Patterns
Refer to existing operator implementations for these patterns:
- Simple transformation: see `operator_reverse.go`
- Type checking: see `operator_error.go`
- Working with arguments: see `operator_map.go`
- Post-traversal operators: see `operator_with.go`

View File

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

View File

@ -3,55 +3,227 @@ Not all new PRs will be merged in
It's recommended to check with the owner first (e.g. raise an issue) to discuss a new feature before developing, to ensure your hard efforts don't go to waste.
PRs to fix bugs and issues are almost always be welcome, just make sure you write tests as well.
PRs to fix bugs and issues are almost always welcome :pray: please ensure you write tests as well.
** PRs that significantly refactor code and release pipelines PRs will generally _NOT_ be accepted **
Significant refactors take a lot of time to understand and can have all sorts of unintended side effects.
Release pipeline PRs are a security risk - it's too easy for a serious vulnerability to sneak in (either intended or not). If there is a new cool way of releasing things, raise an issue for discussion first - it will need to be gone over with a fine tooth comb.
At this stage, yq is not going to maintain any other release platforms other than GitHub and Docker - that said, I'm more than happy to put in other community maintained methods in the README for visibility :heart:
The following types of PRs will _not_ be accepted:
- **Significant refactors** take a lot of time to understand and can have all sorts of unintended side effects. If you think there's a better way to do things (that requires significant changes) raise an issue for discussion first :)
- **Release pipeline PRs** are a security risk - it's too easy for a serious vulnerability to sneak in (either intended or not). If there is a new cool way of releasing things, raise an issue for discussion first - it will need to be gone over with a fine tooth comb.
- **Version bumps** are handled by dependabot, the bot will auto-raise PRs and they will be regularly merged in.
- **New release platforms** At this stage, yq is not going to maintain any other release platforms other than GitHub and Docker - that said, I'm more than happy to put in other community maintained methods in the README for visibility :heart:
# Development
1. Install (Golang)[https://golang.org/]
1. Run `scripts/devtools.sh` to install the required devtools
2. Run `make [local] vendor` to install the vendor dependencies
2. Run `make [local] test` to ensure you can run the existing tests
3. Write unit tests - (see existing examples). Changes will not be accepted without corresponding unit tests.
4. Make the code changes.
5. `make [local] test` to lint code and run tests
6. Profit! ok no profit, but raise a PR and get kudos :)
## Initial Setup
1. Install [Golang](https://golang.org/) (version 1.24.0 or later)
2. Run `scripts/devtools.sh` to install required development tools:
- golangci-lint for code linting
- gosec for security analysis
3. Run `make [local] vendor` to install vendor dependencies
4. Run `make [local] test` to ensure you can run the existing tests
## Development Workflow
1. **Write unit tests first** - Changes will not be accepted without corresponding unit tests (see Testing section below)
2. **Make your code changes**
3. **Run tests and linting**: `make [local] test` (this runs formatting, linting, security checks, and tests)
4. **Create your PR** and get kudos! :)
## Make Commands
- Use `make [local] <command>` for local development (runs in Docker container)
- Use `make <command>` for CI/CD environments
- Common commands:
- `make [local] vendor` - Install dependencies
- `make [local] test` - Run all checks and tests
- `make [local] build` - Build the yq binary
- `make [local] format` - Format code
- `make [local] check` - Run linting and security checks
# Code Quality
## Linting and Formatting
The project uses strict linting rules defined in `.golangci.yml`. All code must pass:
- **Code formatting**: gofmt, goimports, gci
- **Linting**: revive, errorlint, gosec, misspell, and others
- **Security checks**: gosec security analysis
- **Spelling checks**: misspell detection
Run `make [local] check` to verify your code meets all quality standards.
## Code Style Guidelines
- Follow standard Go conventions
- Use meaningful variable names
- Add comments for public functions and complex logic
- Keep functions focused and reasonably sized
- Use the project's existing patterns and conventions
# Testing
## Test Structure
Tests in yq use the `expressionScenario` pattern. Each test scenario includes:
- `expression`: The yq expression to test
- `document`: Input YAML/JSON (optional)
- `expected`: Expected output
- `skipDoc`: Whether to skip documentation generation
## Writing Tests
1. **Find the appropriate test file** (e.g., `operator_add_test.go` for addition operations)
2. **Add your test scenario** to the `*OperatorScenarios` slice
3. **Run the specific test**: `go test -run TestAddOperatorScenarios` (replace with appropriate test name)
4. **Verify documentation generation** (see Documentation section)
## Test Examples
```go
var addOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: `"foo" + "bar"`,
expected: []string{
"D0, P[], (!!str)::foobar\n",
},
},
{
document: "apples: 3",
expression: `.apples + 3`,
expected: []string{
"D0, P[apples], (!!int)::6\n",
},
},
}
```
## Running Tests
- **All tests**: `make [local] test`
- **Specific test**: `go test -run TestName`
- **With coverage**: `make [local] cover`
# Documentation
The various operator documentation (e.g. 'strings') are generated from the 'master' branch, and have a statically defined header (e.g. `pkg/yqlib/doc/operators/headers/add.md`) and the bulk of the docs are generated from the unit tests e.g. `pkg/yqlib/operator_add_test.go`.
## Documentation Generation
The pipeline will run the tests and automatically concatenate the files together, and put them under
`pkg/qylib/doc/add.md`. These files are checked in the master branch (and are copied to the gitbook branch as part of the release process).
The project uses a documentation system that combines static headers with dynamically generated content from tests.
### How It Works
Remaining static documentation is in the 'githook' branch (where the generated docs are copied across into)
1. **Static headers** are defined in `pkg/yqlib/doc/operators/headers/*.md`
2. **Dynamic content** is generated from test scenarios in `*_test.go` files
3. **Generated docs** are created in `pkg/yqlib/doc/*.md` by concatenating headers with test-generated content
4. **Documentation is synced** to the gitbook branch for the website
## How to contribute
### Updating Operator Documentation
The first step is to find if what you want is automatically generated or not - start by looking in the master branch.
#### For Test-Generated Documentation
Note that PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
Most operator documentation is generated from tests. To update:
### Updating dynamic documentation from master
- Search for the documentation you want to update. If you find matches in a `*_test.go` file - update that, as that will automatically update the matching `*.md` file
- Assuming you are updating a `*_test.go` file, once updated, run the test to regenerated the docs. E.g. for the 'Add' test generated docs, from the pkg/yqlib folder run:
`go test -run TestAddOperatorScenarios` which will run that test defined in the `operator_add_test.go` file.
- Ensure the tests still pass, and check the generated documentation have your update.
- Note: If the documentation is only in a `headers/*.md` file, then just update that directly
- Raise a PR to merge the changes into master!
1. **Find the test file** (e.g., `operator_add_test.go`)
2. **Update test scenarios** - each `expressionScenario` with `skipDoc: false` becomes documentation
3. **Run the test** to regenerate docs:
```bash
cd pkg/yqlib
go test -run TestAddOperatorScenarios
```
4. **Verify the generated documentation** in `pkg/yqlib/doc/add.md`
5. **Create a PR** with your changes
### Updating static documentation from the gitbook branch
If you haven't found what you want to update in the master branch, then check the gitbook branch directly as there are a few pages in there that are not in master.
#### For Header-Only Documentation
- Update the `*.md` files
- Raise a PR to merge the changes into gitbook.
If documentation exists only in `headers/*.md` files:
1. **Update the header file directly** (e.g., `pkg/yqlib/doc/operators/headers/add.md`)
2. **Create a PR** with your changes
### Updating Static Documentation
For documentation not in the master branch:
1. **Check the gitbook branch** for additional pages
2. **Update the `*.md` files** directly
3. **Create a PR** to the gitbook branch
### Documentation Best Practices
- **Write clear, concise examples** in test scenarios
- **Use meaningful variable names** in examples
- **Include edge cases** and error conditions
- **Test your documentation changes** by running the specific test
- **Verify generated output** matches expectations
Note: PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
# Troubleshooting
## Common Setup Issues
### Docker/Podman Issues
- **Problem**: `make` commands fail with Docker errors
- **Solution**: Ensure Docker or Podman is running and accessible
- **Alternative**: Use `make local <command>` to run in containers
### Go Version Issues
- **Problem**: Build fails with Go version errors
- **Solution**: Ensure you have Go 1.24.0 or later installed
- **Check**: Run `go version` to verify
### Vendor Dependencies
- **Problem**: `make vendor` fails or dependencies are outdated
- **Solution**:
```bash
go mod tidy
make [local] vendor
```
### Linting Failures
- **Problem**: `make check` fails with linting errors
- **Solution**:
```bash
make [local] format # Auto-fix formatting
# Manually fix remaining linting issues
make [local] check # Verify fixes
```
### Test Failures
- **Problem**: Tests fail locally but pass in CI
- **Solution**:
```bash
make [local] test # Run in Docker container
```
- **Problem**: Tests fail with a VCS error:
```bash
error obtaining VCS status: exit status 128
Use -buildvcs=false to disable VCS stamping.
```
- **Solution**:
Git security mechanisms prevent Golang from detecting the Git details inside
the container; either build with the `local` option, or pass GOFLAGS to
disable Golang buildvcs behaviour.
```bash
make local test
# OR
make test GOFLAGS='-buildvcs=true'
```
### Documentation Generation Issues
- **Problem**: Generated docs don't update after test changes
- **Solution**:
```bash
cd pkg/yqlib
go test -run TestSpecificOperatorScenarios
# Check if generated file updated in pkg/yqlib/doc/
```
## Getting Help
- **Check existing issues**: Search GitHub issues for similar problems
- **Create an issue**: If you can't find a solution, create a detailed issue
- **Ask questions**: Use GitHub Discussions for general questions
- **Join the community**: Check the project's community channels

View File

@ -1,4 +1,4 @@
FROM golang:1.25.2 AS builder
FROM golang:1.26.4@sha256:792443b89f65105abba56b9bd5e97f680a80074ac62fc844a584212f8c8102c3 AS builder
WORKDIR /go/src/mikefarah/yq
@ -10,7 +10,7 @@ RUN ./scripts/acceptance.sh
# Choose alpine as a base image to make this useful for CI, as many
# CI tools expect an interactive shell inside the container
FROM alpine:3 AS production
FROM alpine:3@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS production
LABEL maintainer="Mike Farah <mikefarah@users.noreply.github.com>"
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq

View File

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

View File

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

View File

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

211
README.md
View File

@ -3,44 +3,46 @@
![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq) ![CodeQL](https://github.com/mikefarah/yq/workflows/CodeQL/badge.svg)
a lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, kyaml, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
yq is written in go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below.
yq is written in Go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below.
## Quick Usage Guide
Read a value:
### Basic Operations
**Read a value:**
```bash
yq '.a.b[0].c' file.yaml
```
Pipe from STDIN:
**Pipe from STDIN:**
```bash
yq '.a.b[0].c' < file.yaml
```
Update a yaml file, in place
**Update a yaml file in place:**
```bash
yq -i '.a.b[0].c = "cool"' file.yaml
```
Update using environment variables
**Update using environment variables:**
```bash
NAME=mike yq -i '.a.b[0].c = strenv(NAME)' file.yaml
```
Merge multiple files
### Advanced Operations
**Merge multiple files:**
```bash
# merge two files
yq -n 'load("file1.yaml") * load("file2.yaml")'
# merge using globs:
# note the use of `ea` to evaluate all the files at once
# instead of in sequence
# merge using globs (note: `ea` evaluates all files at once instead of in sequence)
yq ea '. as $item ireduce ({}; . * $item )' path/to/*.yml
```
Multiple updates to a yaml file
**Multiple updates to a yaml file:**
```bash
yq -i '
.a.b[0].c = "cool" |
@ -49,14 +51,22 @@ yq -i '
' file.yaml
```
Find and update an item in an array:
**Find and update an item in an array:**
```bash
yq '(.[] | select(.name == "foo") | .address) = "12 cat st"'
# Note: requires input file - add your file at the end
yq -i '(.[] | select(.name == "foo") | .address) = "12 cat st"' data.yaml
```
Convert JSON to YAML
**Convert between formats:**
```bash
# Convert JSON to YAML (pretty print)
yq -Poy sample.json
# Convert YAML to JSON
yq -o json file.yaml
# Convert XML to YAML
yq -o yaml file.xml
```
See [recipes](https://mikefarah.gitbook.io/yq/recipes) for more examples and the [documentation](https://mikefarah.gitbook.io/yq/) for more information.
@ -68,31 +78,31 @@ Take a look at the discussions for [common questions](https://github.com/mikefar
### [Download the latest binary](https://github.com/mikefarah/yq/releases/latest)
### wget
Use wget to download, gzipped pre-compiled binaries:
Use wget to download pre-compiled binaries. Choose your platform and architecture:
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
#### Compressed via tar.gz
**For Linux (example):**
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
tar xz && mv ${BINARY} /usr/local/bin/yq
```
# Set your platform variables (adjust as needed)
VERSION=v4.2.0
PLATFORM=linux_amd64
#### Plain binary
# Download compressed binary
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM}.tar.gz -O - |\
tar xz && sudo mv yq_${PLATFORM} /usr/local/bin/yq
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/local/bin/yq &&\
# Or download plain binary
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM} -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
```
#### Latest version
**Latest version (Linux AMD64):**
```bash
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
```
**Available platforms:** `linux_amd64`, `linux_arm64`, `linux_arm`, `linux_386`, `darwin_amd64`, `darwin_arm64`, `windows_amd64`, `windows_386`, etc.
### MacOS / Linux via Homebrew:
Using [Homebrew](https://brew.sh/)
```
@ -123,28 +133,31 @@ rm /etc/myfile.tmp
```
### Run with Docker or Podman
#### Oneshot use:
#### One-time use:
```bash
docker run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
# Docker - process files in current directory
docker run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
# Podman - same usage as Docker
podman run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
```
Note that you can run `yq` in docker without network access and other privileges if you desire,
namely `--security-opt=no-new-privileges --cap-drop all --network none`.
**Security note:** You can run `yq` in Docker with restricted privileges:
```bash
podman run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
docker run --rm --security-opt=no-new-privileges --cap-drop all --network none \
-v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
```
#### Pipe in via STDIN:
#### Pipe data via STDIN:
You'll need to pass the `-i\--interactive` flag to docker:
You'll need to pass the `-i --interactive` flag to Docker/Podman:
```bash
# Process piped data
docker run -i --rm mikefarah/yq '.this.thing' < myfile.yml
```
```bash
# Same with Podman
podman run -i --rm mikefarah/yq '.this.thing' < myfile.yml
```
@ -340,7 +353,7 @@ gah install yq
- Supports yaml [front matter](https://mikefarah.gitbook.io/yq/usage/front-matter) blocks (e.g. jekyll/assemble)
- Colorized yaml output
- [Date/Time manipulation and formatting with TZ](https://mikefarah.gitbook.io/yq/operators/datetime)
- [Deeply data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
- [Deep data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
- [Sort keys](https://mikefarah.gitbook.io/yq/operators/sort-keys)
- Manipulate yaml [comments](https://mikefarah.gitbook.io/yq/operators/comment-operators), [styling](https://mikefarah.gitbook.io/yq/operators/style), [tags](https://mikefarah.gitbook.io/yq/operators/tag) and [anchors and aliases](https://mikefarah.gitbook.io/yq/operators/anchor-and-alias-operators).
- [Update in place](https://mikefarah.gitbook.io/yq/v/v4.x/commands/evaluate#flags)
@ -350,6 +363,8 @@ gah install yq
- [Load content from other files](https://mikefarah.gitbook.io/yq/operators/load)
- [Convert to/from json/ndjson](https://mikefarah.gitbook.io/yq/v/v4.x/usage/convert)
- [Convert to/from xml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/xml)
- [Convert to/from hcl (terraform)](https://mikefarah.gitbook.io/yq/v/v4.x/usage/hcl)
- [Convert to/from toml](https://mikefarah.gitbook.io/yq/v/v4.x/usage/toml)
- [Convert to/from properties](https://mikefarah.gitbook.io/yq/v/v4.x/usage/properties)
- [Convert to/from csv/tsv](https://mikefarah.gitbook.io/yq/usage/csv-tsv)
- [General shell completion scripts (bash/zsh/fish/powershell)](https://mikefarah.gitbook.io/yq/v/v4.x/commands/shell-completion)
@ -367,10 +382,18 @@ Usage:
Examples:
# yq defaults to 'eval' command if no command is specified. See "yq eval --help" for more examples.
yq '.stuff' < myfile.yml # outputs the data at the "stuff" node from "myfile.yml"
# yq tries to auto-detect the file format based off the extension, and defaults to YAML if it's unknown (or piping through STDIN)
# Use the '-p/--input-format' flag to specify a format type.
cat file.xml | yq -p xml
yq -i '.stuff = "foo"' myfile.yml # update myfile.yml in place
# read the "stuff" node from "myfile.yml"
yq '.stuff' < myfile.yml
# update myfile.yml in place
yq -i '.stuff = "foo"' myfile.yml
# print contents of sample.json as idiomatic YAML
yq -P -oy sample.json
Available Commands:
@ -380,49 +403,75 @@ Available Commands:
help Help about any command
Flags:
-C, --colors force print with colors
--csv-auto-parse parse CSV YAML/JSON values (default true)
--csv-separator char CSV Separator character (default ,)
-e, --exit-status set exit status if there are no matches or null or false is returned
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
--from-file string Load expression from specified file.
-f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact
--header-preprocess Slurp any header comments and separators before processing expression. (default true)
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the file in place of first file given.
-p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l|ini|i] parse format for input. (default "auto")
--lua-globals output keys as top-level global variables
--lua-prefix string prefix (default "return ")
--lua-suffix string suffix (default ";\n")
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
-o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l|ini|i] output format type. (default "auto")
-P, --prettyPrint pretty print, shorthand for '... style = ""'
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
--properties-separator string separator to use between keys and values (default " = ")
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
--split-exp-file string Use a file to specify the split-exp expression.
--string-interpolation Toggles strings interpolation of \(exp) (default true)
--tsv-auto-parse parse TSV YAML/JSON values (default true)
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
-v, --verbose verbose mode
-V, --version Print version information and quit
--xml-attribute-prefix string prefix for xml attributes (default "+@")
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
-C, --colors force print with colors
--csv-auto-parse parse CSV YAML/JSON values (default true)
--csv-separator char CSV Separator character (default ,)
--debug-node-info debug node info
-e, --exit-status set exit status if there are no matches or null or false is returned
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
--from-file string Load expression from specified file.
-f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact
--header-preprocess Slurp any header comments and separators before processing expression. (default true)
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the file in place of first file given.
-p, --input-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|lua|l|ini|i] parse format for input. (default "auto")
--lua-globals output keys as top-level global variables
--lua-prefix string prefix (default "return ")
--lua-suffix string suffix (default ";\n")
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
-o, --output-format string [auto|a|yaml|y|json|j|kyaml|ky|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|hcl|h|shell|s|lua|l|ini|i] output format type. (default "auto")
-P, --prettyPrint pretty print, shorthand for '... style = ""'
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
--properties-separator string separator to use between keys and values (default " = ")
--security-disable-env-ops Disable env related operations.
--security-disable-file-ops Disable file related operations (e.g. load)
--shell-key-separator string separator for shell variable key paths (default "_")
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
--split-exp-file string Use a file to specify the split-exp expression.
--string-interpolation Toggles strings interpolation of \(exp) (default true)
--tsv-auto-parse parse TSV YAML/JSON values (default true)
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
-v, --verbose verbose mode
-V, --version Print version information and quit
--xml-attribute-prefix string prefix for xml attributes (default "+@")
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
--yaml-fix-merge-anchor-to-spec Fix merge anchor to match YAML spec. Will default to true in late 2025
Use "yq [command] --help" for more information about a command.
```
## Troubleshooting
### Common Issues
**PowerShell quoting issues:**
```powershell
# Use single quotes for expressions
yq '.a.b[0].c' file.yaml
# Or escape double quotes
yq ".a.b[0].c = \"value\"" file.yaml
```
### Getting Help
- **Check existing issues**: [GitHub Issues](https://github.com/mikefarah/yq/issues)
- **Ask questions**: [GitHub Discussions](https://github.com/mikefarah/yq/discussions)
- **Documentation**: [Complete documentation](https://mikefarah.gitbook.io/yq/)
- **Examples**: [Recipes and examples](https://mikefarah.gitbook.io/yq/recipes)
## Known Issues / Missing Features
- `yq` attempts to preserve comment positions and whitespace as much as possible, but it does not handle all scenarios (see https://github.com/go-yaml/yaml/tree/v3 for details)
- Powershell has its own...[opinions on quoting yq](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell)

26
SECURITY.md Normal file
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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,329 @@
package cmd
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCreateEvaluateAllCommand(t *testing.T) {
cmd := createEvaluateAllCommand()
if cmd == nil {
t.Fatal("createEvaluateAllCommand returned nil")
return
}
// Test basic command properties
if cmd.Use != "eval-all [expression] [yaml_file1]..." {
t.Errorf("Expected Use to be 'eval-all [expression] [yaml_file1]...', got %q", cmd.Use)
}
if cmd.Short == "" {
t.Error("Expected Short description to be non-empty")
}
if cmd.Long == "" {
t.Error("Expected Long description to be non-empty")
}
// Test aliases
expectedAliases := []string{"ea"}
if len(cmd.Aliases) != len(expectedAliases) {
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
}
for i, expected := range expectedAliases {
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
}
}
}
func TestEvaluateAll_NoArgs(t *testing.T) {
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with no arguments and no null input
nullInput = false
defer func() { nullInput = false }()
err := evaluateAll(cmd, []string{})
// Should not error, but should print usage
if err != nil {
t.Errorf("evaluateAll with no args should not error, got: %v", err)
}
// Should have printed usage information
if output.Len() == 0 {
t.Error("Expected usage information to be printed")
}
}
func TestEvaluateAll_NullInput(t *testing.T) {
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with null input
nullInput = true
defer func() { nullInput = false }()
err := evaluateAll(cmd, []string{})
// Should not error when using null input
if err != nil {
t.Errorf("evaluateAll with null input should not error, got: %v", err)
}
}
func TestEvaluateAll_WithSingleFile(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with a single file
err = evaluateAll(cmd, []string{yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateAll with single file should not error, got: %v", err)
}
// Should have some output
if output.Len() == 0 {
t.Error("Expected output from evaluateAll with single file")
}
}
func TestEvaluateAll_WithMultipleFiles(t *testing.T) {
// Create temporary YAML files
tempDir := t.TempDir()
yamlFile1 := filepath.Join(tempDir, "test1.yaml")
yamlContent1 := []byte("name: test1\nage: 25\n")
err := os.WriteFile(yamlFile1, yamlContent1, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file 1: %v", err)
}
yamlFile2 := filepath.Join(tempDir, "test2.yaml")
yamlContent2 := []byte("name: test2\nage: 30\n")
err = os.WriteFile(yamlFile2, yamlContent2, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file 2: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with multiple files
err = evaluateAll(cmd, []string{yamlFile1, yamlFile2})
// Should not error
if err != nil {
t.Errorf("evaluateAll with multiple files should not error, got: %v", err)
}
// Should have output
if output.Len() == 0 {
t.Error("Expected output from evaluateAll with multiple files")
}
}
func TestEvaluateAll_WithExpression(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with expression
err = evaluateAll(cmd, []string{".name", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateAll with expression should not error, got: %v", err)
}
// Should have output
if output.Len() == 0 {
t.Error("Expected output from evaluateAll with expression")
}
}
func TestEvaluateAll_WriteInPlace(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Enable write in place
originalWriteInplace := writeInplace
writeInplace = true
defer func() { writeInplace = originalWriteInplace }()
// Test with write in place
err = evaluateAll(cmd, []string{".name = \"updated\"", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateAll with write in place should not error, got: %v", err)
}
// Verify the file was updated
updatedContent, err := os.ReadFile(yamlFile)
if err != nil {
t.Fatalf("Failed to read updated file: %v", err)
}
// Should contain the updated content
if !strings.Contains(string(updatedContent), "updated") {
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
}
}
func TestEvaluateAll_ExitStatus(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Enable exit status
originalExitStatus := exitStatus
exitStatus = true
defer func() { exitStatus = originalExitStatus }()
// Test with expression that should find no matches
err = evaluateAll(cmd, []string{".nonexistent", yamlFile})
// Should error when no matches found and exit status is enabled
if err == nil {
t.Error("Expected error when no matches found and exit status is enabled")
}
}
func TestEvaluateAll_WithMultipleDocuments(t *testing.T) {
// Create a temporary YAML file with multiple documents
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("---\nname: doc1\nage: 25\n---\nname: doc2\nage: 30\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with multiple documents
err = evaluateAll(cmd, []string{".", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateAll with multiple documents should not error, got: %v", err)
}
// Should have output
if output.Len() == 0 {
t.Error("Expected output from evaluateAll with multiple documents")
}
}
func TestEvaluateAll_NulSepOutput(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateAllCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Enable nul separator output
originalNulSepOutput := nulSepOutput
nulSepOutput = true
defer func() { nulSepOutput = originalNulSepOutput }()
// Test with nul separator output
err = evaluateAll(cmd, []string{".name", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateAll with nul separator output should not error, got: %v", err)
}
// Should have output
if output.Len() == 0 {
t.Error("Expected output from evaluateAll with nul separator output")
}
}

View File

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

View File

@ -0,0 +1,277 @@
package cmd
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCreateEvaluateSequenceCommand(t *testing.T) {
cmd := createEvaluateSequenceCommand()
if cmd == nil {
t.Fatal("createEvaluateSequenceCommand returned nil")
return
}
// Test basic command properties
if cmd.Use != "eval [expression] [yaml_file1]..." {
t.Errorf("Expected Use to be 'eval [expression] [yaml_file1]...', got %q", cmd.Use)
}
if cmd.Short == "" {
t.Error("Expected Short description to be non-empty")
}
if cmd.Long == "" {
t.Error("Expected Long description to be non-empty")
}
// Test aliases
expectedAliases := []string{"e"}
if len(cmd.Aliases) != len(expectedAliases) {
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
}
for i, expected := range expectedAliases {
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
}
}
}
func TestProcessExpression(t *testing.T) {
// Reset global variables
originalPrettyPrint := prettyPrint
defer func() { prettyPrint = originalPrettyPrint }()
tests := []struct {
name string
prettyPrint bool
expression string
expected string
}{
{
name: "empty expression without pretty print",
prettyPrint: false,
expression: "",
expected: "",
},
{
name: "empty expression with pretty print",
prettyPrint: true,
expression: "",
expected: `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
},
{
name: "simple expression without pretty print",
prettyPrint: false,
expression: ".a.b",
expected: ".a.b",
},
{
name: "simple expression with pretty print",
prettyPrint: true,
expression: ".a.b",
expected: `.a.b | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
},
{
name: "complex expression with pretty print",
prettyPrint: true,
expression: ".items[] | select(.active == true)",
expected: `.items[] | select(.active == true) | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prettyPrint = tt.prettyPrint
result := processExpression(tt.expression)
if result != tt.expected {
t.Errorf("processExpression(%q) = %q, want %q", tt.expression, result, tt.expected)
}
})
}
}
func TestEvaluateSequence_NoArgs(t *testing.T) {
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with no arguments and no null input
nullInput = false
defer func() { nullInput = false }()
err := evaluateSequence(cmd, []string{})
// Should not error, but should print usage
if err != nil {
t.Errorf("evaluateSequence with no args should not error, got: %v", err)
}
// Should have printed usage information
if output.Len() == 0 {
t.Error("Expected usage information to be printed")
}
}
func TestEvaluateSequence_NullInput(t *testing.T) {
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with null input
nullInput = true
defer func() { nullInput = false }()
err := evaluateSequence(cmd, []string{})
// Should not error when using null input
if err != nil {
t.Errorf("evaluateSequence with null input should not error, got: %v", err)
}
}
func TestEvaluateSequence_WithFile(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with a file
err = evaluateSequence(cmd, []string{yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateSequence with file should not error, got: %v", err)
}
// Should have some output
if output.Len() == 0 {
t.Error("Expected output from evaluateSequence with file")
}
}
func TestEvaluateSequence_WithExpressionAndFile(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Test with expression and file
err = evaluateSequence(cmd, []string{".name", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateSequence with expression and file should not error, got: %v", err)
}
// Should have output
if output.Len() == 0 {
t.Error("Expected output from evaluateSequence with expression and file")
}
}
func TestEvaluateSequence_WriteInPlace(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Enable write in place
originalWriteInplace := writeInplace
writeInplace = true
defer func() { writeInplace = originalWriteInplace }()
// Test with write in place
err = evaluateSequence(cmd, []string{".name = \"updated\"", yamlFile})
// Should not error
if err != nil {
t.Errorf("evaluateSequence with write in place should not error, got: %v", err)
}
// Verify the file was updated
updatedContent, err := os.ReadFile(yamlFile)
if err != nil {
t.Fatalf("Failed to read updated file: %v", err)
}
// Should contain the updated content
if !strings.Contains(string(updatedContent), "updated") {
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
}
}
func TestEvaluateSequence_ExitStatus(t *testing.T) {
// Create a temporary YAML file
tempDir := t.TempDir()
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := []byte("name: test\nage: 25\n")
err := os.WriteFile(yamlFile, yamlContent, 0600)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
// Create a temporary command
cmd := createEvaluateSequenceCommand()
// Set up command to capture output
var output bytes.Buffer
cmd.SetOut(&output)
// Enable exit status
originalExitStatus := exitStatus
exitStatus = true
defer func() { exitStatus = originalExitStatus }()
// Test with expression that should find no matches
err = evaluateSequence(cmd, []string{".nonexistent", yamlFile})
// Should error when no matches found and exit status is enabled
if err == nil {
t.Error("Expected error when no matches found and exit status is enabled")
}
}

View File

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

265
cmd/root_test.go Normal file
View File

@ -0,0 +1,265 @@
package cmd
import (
"strings"
"testing"
)
func TestNewRuneVar(t *testing.T) {
var r rune
runeVar := newRuneVar(&r)
if runeVar == nil {
t.Fatal("newRuneVar returned nil")
}
}
func TestRuneValue_String(t *testing.T) {
tests := []struct {
name string
runeVal rune
expected string
}{
{
name: "simple character",
runeVal: 'a',
expected: "a",
},
{
name: "special character",
runeVal: '\n',
expected: "\n",
},
{
name: "unicode character",
runeVal: 'ñ',
expected: "ñ",
},
{
name: "zero rune",
runeVal: 0,
expected: string(rune(0)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runeVal := runeValue(tt.runeVal)
result := runeVal.String()
if result != tt.expected {
t.Errorf("runeValue.String() = %q, want %q", result, tt.expected)
}
})
}
}
func TestRuneValue_Set(t *testing.T) {
tests := []struct {
name string
input string
expected rune
expectError bool
}{
{
name: "simple character",
input: "a",
expected: 'a',
expectError: false,
},
{
name: "newline escape",
input: "\\n",
expected: '\n',
expectError: false,
},
{
name: "tab escape",
input: "\\t",
expected: '\t',
expectError: false,
},
{
name: "carriage return escape",
input: "\\r",
expected: '\r',
expectError: false,
},
{
name: "form feed escape",
input: "\\f",
expected: '\f',
expectError: false,
},
{
name: "vertical tab escape",
input: "\\v",
expected: '\v',
expectError: false,
},
{
name: "empty string",
input: "",
expected: 0,
expectError: true,
},
{
name: "multiple characters",
input: "ab",
expected: 0,
expectError: true,
},
{
name: "special character",
input: "ñ",
expected: 'ñ',
expectError: true, // This will fail because the Set function checks len(val) != 1
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var r rune
runeVal := newRuneVar(&r)
err := runeVal.Set(tt.input)
if tt.expectError {
if err == nil {
t.Errorf("Expected error for input %q, but got none", tt.input)
}
} else {
if err != nil {
t.Errorf("Unexpected error for input %q: %v", tt.input, err)
}
if r != tt.expected {
t.Errorf("Expected rune %q (%d), got %q (%d)",
string(tt.expected), tt.expected, string(r), r)
}
}
})
}
}
func TestRuneValue_Set_ErrorMessages(t *testing.T) {
tests := []struct {
name string
input string
expectedError string
}{
{
name: "empty string error",
input: "",
expectedError: "[] is not a valid character. Must be length 1 was 0",
},
{
name: "multiple characters error",
input: "abc",
expectedError: "[abc] is not a valid character. Must be length 1 was 3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var r rune
runeVal := newRuneVar(&r)
err := runeVal.Set(tt.input)
if err == nil {
t.Errorf("Expected error for input %q, but got none", tt.input)
return
}
if !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("Expected error message to contain %q, got %q",
tt.expectedError, err.Error())
}
})
}
}
func TestRuneValue_Type(t *testing.T) {
var r rune
runeVal := newRuneVar(&r)
result := runeVal.Type()
expected := "char"
if result != expected {
t.Errorf("runeValue.Type() = %q, want %q", result, expected)
}
}
func TestNew(t *testing.T) {
rootCmd := New()
if rootCmd == nil {
t.Fatal("New() returned nil")
return
}
// Test basic command properties
if rootCmd.Use != "yq" {
t.Errorf("Expected Use to be 'yq', got %q", rootCmd.Use)
}
if rootCmd.Short == "" {
t.Error("Expected Short description to be non-empty")
}
if rootCmd.Long == "" {
t.Error("Expected Long description to be non-empty")
}
// Test that the command has the expected subcommands
expectedCommands := []string{"eval", "eval-all", "completion"}
actualCommands := make([]string, 0, len(rootCmd.Commands()))
for _, cmd := range rootCmd.Commands() {
actualCommands = append(actualCommands, cmd.Name())
}
for _, expected := range expectedCommands {
found := false
for _, actual := range actualCommands {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Expected command %q not found in actual commands: %v",
expected, actualCommands)
}
}
}
func TestNew_FlagCompletions(t *testing.T) {
rootCmd := New()
// Test that flag completion functions are registered
// This is a basic smoke test - we can't easily test the actual completion logic
// without more complex setup
flags := []string{
"output-format",
"input-format",
"xml-attribute-prefix",
"xml-content-name",
"xml-proc-inst-prefix",
"xml-directive-name",
"lua-prefix",
"lua-suffix",
"properties-separator",
"indent",
"front-matter",
"expression",
"split-exp",
}
for _, flagName := range flags {
flag := rootCmd.PersistentFlags().Lookup(flagName)
if flag == nil {
t.Errorf("Expected flag %q to exist", flagName)
}
}
}

View File

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

View File

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

View File

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

View File

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

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,8 +1 @@
# 001
---
abc: # 001
- 1 # one
- 2 # two
---
def # 002
a: apple

10
examples/kyaml.kyaml Normal file
View File

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

7
examples/kyaml.yml Normal file
View File

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

8
examples/sample.hcl Normal file
View File

@ -0,0 +1,8 @@
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)

27
examples/sample.tf Normal file
View File

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

8
examples/sample2.hcl Normal file
View File

@ -0,0 +1,8 @@
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)

View File

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

34
go.mod
View File

@ -6,30 +6,36 @@ require (
github.com/alecthomas/repr v0.5.2
github.com/dimchansky/utfbom v1.1.1
github.com/elliotchance/orderedmap v1.8.0
github.com/fatih/color v1.18.0
github.com/fatih/color v1.19.0
github.com/go-ini/ini v1.67.0
github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.18.0
github.com/goccy/go-json v0.10.6
github.com/goccy/go-yaml v1.19.2
github.com/hashicorp/hcl/v2 v2.24.0
github.com/jinzhu/copier v0.4.0
github.com/magiconair/properties v1.8.10
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pelletier/go-toml/v2 v2.4.2
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/net v0.46.0
golang.org/x/text v0.30.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
github.com/yuin/gopher-lua v1.1.2
github.com/zclconf/go-cty v1.18.1
go.yaml.in/yaml/v4 v4.0.0-rc.6
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/text v0.38.0
)
require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.37.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/tools v0.45.0 // indirect
)
go 1.24.0
toolchain go1.24.1
go 1.25.0

70
go.sum
View File

@ -1,26 +1,37 @@
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=
github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -33,36 +44,45 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

30
go_install_test.go Normal file
View File

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

View File

@ -1,8 +1,63 @@
# How it works
# Expression Syntax: A Visual Guide
In `yq`, expressions are made up of operators and pipes. A context of nodes is passed through the expression, and each operation takes the context as input and returns a new context as output. That output is piped in as input for the next operation in the expression.
In `yq` expressions are made up of operators and pipes. A context of nodes is passed through the expression and each operation takes the context as input and returns a new context as output. That output is piped in as input for the next operation in the expression. To begin with, the context is set to the first yaml document of the first yaml file (if processing in sequence using eval).
Let's break down the process step by step using a diagram. We'll start with a single YAML document, apply an expression, and observe how the context changes at each step.
Lets look at a couple of examples.
Given a document like:
```yaml
root:
items:
- name: apple
type: fruit
- name: carrot
type: vegetable
- name: banana
type: fruit
```
You can use dot notation to access nested structures. For example, to access the `name` of the first item, you would use the expression `.root.items[0].name`, which would return `apple`.
But lets see how we could find all the fruit under `items`
## Step 1: Initial Context
The context starts at the root of the YAML document. In this case, the entire document is the initial context.
```
root
└── items
├── name: apple
│ type: fruit
├── name: carrot
│ type: vegetable
└── name: banana
type: fruit
```
## Step 2: Splatting the Array
Using the expression `.root.items[]`, we "splat" the items array. This means each element of the array becomes its own node in the context:
```
Node 1: { name: apple, type: fruit }
Node 2: { name: carrot, type: vegetable }
Node 3: { name: banana, type: fruit }
```
## Step 3: Filtering the Nodes
Next, we apply a filter to select only the nodes where type is fruit. The expression `.root.items[] | select(.type == "fruit")` filters the nodes:
```
Filtered Node 1: { name: apple, type: fruit }
Filtered Node 2: { name: banana, type: fruit }
```
## Step 4: Extracting a Field
Finally, we extract the name field from the filtered nodes using `.root.items[] | select(.type == "fruit") | .name` This results in:
```
apple
banana
```
## Simple assignment example
@ -44,7 +99,6 @@ a: dog
b: dog
```
## Complex assignment, operator precedence rules
Just like math expressions - `yq` expressions have an order of precedence. The pipe `|` operator has a low order of precedence, so operators with higher precedence will get evaluated first.
@ -73,7 +127,7 @@ name: sally
fruit: mango
```
To properly update this yaml, you will need to use brackets (think BODMAS from maths) and wrap the entire LHS:
**Important**: To properly update this YAML, you must wrap the entire LHS in parentheses. Think of it like using brackets in math to ensure the correct order of operations.
`(.[] | select(.name == "sally") | .fruit) = "mango"`
@ -126,4 +180,4 @@ The assignment operator then copies across the value from the RHS to the value o
```yaml
a: 2
b: thing
```
```

View File

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

269
pkg/yqlib/base64_test.go Normal file
View File

@ -0,0 +1,269 @@
//go:build !yq_nobase64
package yqlib
import (
"bufio"
"fmt"
"testing"
"github.com/mikefarah/yq/v4/test"
)
const base64EncodedSimple = "YSBzcGVjaWFsIHN0cmluZw=="
const base64DecodedSimpleExtraSpaces = "\n " + base64EncodedSimple + " \n"
const base64DecodedSimple = "a special string"
const base64EncodedUTF8 = "V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig=="
const base64DecodedUTF8 = "Works with UTF-16 😊"
const base64EncodedYaml = "YTogYXBwbGUK"
const base64DecodedYaml = "a: apple\n"
const base64EncodedEmpty = ""
const base64DecodedEmpty = ""
const base64MissingPadding = "Y2F0cw"
const base64DecodedMissingPadding = "cats"
const base64EncodedCats = "Y2F0cw=="
const base64DecodedCats = "cats"
var base64Scenarios = []formatScenario{
{
skipDoc: true,
description: "empty decode",
input: base64EncodedEmpty,
expected: base64DecodedEmpty + "\n",
scenarioType: "decode",
},
{
skipDoc: true,
description: "simple decode",
input: base64EncodedSimple,
expected: base64DecodedSimple + "\n",
scenarioType: "decode",
},
{
description: "Decode base64: simple",
subdescription: "Decoded data is assumed to be a string.",
input: base64EncodedSimple,
expected: base64DecodedSimple + "\n",
scenarioType: "decode",
},
{
description: "Decode base64: UTF-8",
subdescription: "Base64 decoding supports UTF-8 encoded strings.",
input: base64EncodedUTF8,
expected: base64DecodedUTF8 + "\n",
scenarioType: "decode",
},
{
skipDoc: true,
description: "decode missing padding",
input: base64MissingPadding,
expected: base64DecodedMissingPadding + "\n",
scenarioType: "decode",
},
{
description: "Decode with extra spaces",
subdescription: "Extra leading/trailing whitespace is stripped",
input: base64DecodedSimpleExtraSpaces,
expected: base64DecodedSimple + "\n",
scenarioType: "decode",
},
{
skipDoc: true,
description: "decode with padding",
input: base64EncodedCats,
expected: base64DecodedCats + "\n",
scenarioType: "decode",
},
{
skipDoc: true,
description: "decode yaml document",
input: base64EncodedYaml,
expected: base64DecodedYaml + "\n",
scenarioType: "decode",
},
{
description: "Encode base64: string",
input: "\"" + base64DecodedSimple + "\"",
expected: base64EncodedSimple,
scenarioType: "encode",
},
{
description: "Encode base64: string from document",
subdescription: "Extract a string field and encode it to base64.",
input: "coolData: \"" + base64DecodedSimple + "\"",
expression: ".coolData",
expected: base64EncodedSimple,
scenarioType: "encode",
},
{
skipDoc: true,
description: "encode empty string",
input: "\"\"",
expected: "",
scenarioType: "encode",
},
{
skipDoc: true,
description: "encode UTF-8 string",
input: "\"" + base64DecodedUTF8 + "\"",
expected: base64EncodedUTF8,
scenarioType: "encode",
},
{
skipDoc: true,
description: "encode cats",
input: "\"" + base64DecodedCats + "\"",
expected: base64EncodedCats,
scenarioType: "encode",
},
{
description: "Roundtrip: simple",
skipDoc: true,
input: base64EncodedSimple,
expected: base64EncodedSimple,
scenarioType: "roundtrip",
},
{
description: "Roundtrip: UTF-8",
skipDoc: true,
input: base64EncodedUTF8,
expected: base64EncodedUTF8,
scenarioType: "roundtrip",
},
{
description: "Roundtrip: missing padding",
skipDoc: true,
input: base64MissingPadding,
expected: base64EncodedCats,
scenarioType: "roundtrip",
},
{
description: "Roundtrip: empty",
skipDoc: true,
input: base64EncodedEmpty,
expected: base64EncodedEmpty,
scenarioType: "roundtrip",
},
{
description: "Encode error: non-string",
skipDoc: true,
input: "123",
expectedError: "cannot encode !!int as base64, can only operate on strings",
scenarioType: "encode-error",
},
{
description: "Encode error: array",
skipDoc: true,
input: "[1, 2, 3]",
expectedError: "cannot encode !!seq as base64, can only operate on strings",
scenarioType: "encode-error",
},
{
description: "Encode error: map",
skipDoc: true,
input: "{b: c}",
expectedError: "cannot encode !!map as base64, can only operate on strings",
scenarioType: "encode-error",
},
}
func testBase64Scenario(t *testing.T, s formatScenario) {
switch s.scenarioType {
case "", "decode":
yamlPrefs := ConfiguredYamlPreferences.Copy()
yamlPrefs.Indent = 4
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(yamlPrefs)), s.description)
case "encode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder()), s.description)
case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewBase64Encoder()), s.description)
case "encode-error":
result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())
if err == nil {
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
} else {
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
}
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentBase64Scenario(_ *testing.T, w *bufio.Writer, i interface{}) {
s := i.(formatScenario)
if s.skipDoc {
return
}
switch s.scenarioType {
case "", "decode":
documentBase64DecodeScenario(w, s)
case "encode":
documentBase64EncodeScenario(w, s)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentBase64DecodeScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" {
writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
}
writeOrPanic(w, "Given a sample.txt file of:\n")
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if expression == "" {
expression = "."
}
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=base64 -oy '%v' sample.txt\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
}
func documentBase64EncodeScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" {
writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
}
writeOrPanic(w, "Given a sample.yml file of:\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if expression == "" {
expression = "."
}
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=base64 '%v' sample.yml\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())))
}
func TestBase64Scenarios(t *testing.T) {
for _, tt := range base64Scenarios {
testBase64Scenario(t, tt)
}
genericScenarios := make([]interface{}, len(base64Scenarios))
for i, s := range base64Scenarios {
genericScenarios[i] = s
}
documentScenarios(t, "usage", "base64", genericScenarios, documentBase64Scenario)
}

View File

@ -27,10 +27,30 @@ const (
FlowStyle
)
// EncodeHint controls how a mapping node is serialised by format-specific encoders
// that distinguish between inline and block/section representations (e.g. TOML, HCL).
type EncodeHint int
const (
// EncodeHintDefault lets the encoder choose the representation (e.g. TOML block
// mappings default to [section] headers).
EncodeHintDefault EncodeHint = iota
// EncodeHintSeparateBlock forces the node to be emitted as a separate block or
// table-section header (used by TOML [section] and HCL block decoders).
EncodeHintSeparateBlock
// EncodeHintInline forces the node to be emitted as an inline / flow table
// (used by TOML inline-table decoder and TOML encoder).
EncodeHintInline
)
func createStringScalarNode(stringValue string) *CandidateNode {
var node = &CandidateNode{Kind: ScalarNode}
node.Value = stringValue
node.Tag = "!!str"
if stringValue == "<<" {
node.Tag = "!!merge"
} else {
node.Tag = "!!str"
}
return node
}
@ -97,6 +117,9 @@ type CandidateNode struct {
// (e.g. top level cross document merge). This property does not propagate to child nodes.
EvaluateTogether bool
IsMapKey bool
// EncodeHint controls how a mapping node is serialised by format-specific encoders
// (e.g. TOML, HCL) that support both inline and block/section representations.
EncodeHint EncodeHint
}
func (n *CandidateNode) CreateChild() *CandidateNode {
@ -277,7 +300,7 @@ func (n *CandidateNode) AddChild(rawChild *CandidateNode) {
func (n *CandidateNode) AddChildren(children []*CandidateNode) {
if n.Kind == MappingNode {
for i := 0; i < len(children); i += 2 {
for i := 0; i < len(children)-1; i += 2 {
key := children[i]
value := children[i+1]
n.AddKeyValueChild(key, value)
@ -320,11 +343,11 @@ func (n *CandidateNode) guessTagFromCustomType() string {
dataBucket, errorReading := parseSnippet(n.Value)
if errorReading != nil {
log.Debug("guessTagFromCustomType: could not guess underlying tag type %v", errorReading)
log.Debugf("guessTagFromCustomType: could not guess underlying tag type %v", errorReading)
return n.Tag
}
guessedTag := dataBucket.Tag
log.Info("im guessing the tag %v is a %v", n.Tag, guessedTag)
log.Infof("im guessing the tag %v is a %v", n.Tag, guessedTag)
return guessedTag
}
@ -407,6 +430,8 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
EvaluateTogether: n.EvaluateTogether,
IsMapKey: n.IsMapKey,
EncodeHint: n.EncodeHint,
}
if cloneContent {
@ -440,7 +465,7 @@ func (n *CandidateNode) UpdateFrom(other *CandidateNode, prefs assignPreferences
}
func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignPreferences) {
log.Debug("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other))
log.Debugf("UpdateAttributesFrom: n: %v other: %v", NodeToString(n), NodeToString(other))
if n.Kind != other.Kind {
// clear out the contents when switching to a different type
// e.g. map to array
@ -460,6 +485,9 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
n.Anchor = other.Anchor
}
// Preserve EncodeHint for format-specific encoding hints
n.EncodeHint = other.EncodeHint
// merge will pickup the style of the new thing
// when autocreating nodes

View File

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

View File

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

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) {
key := createStringScalarNode("yourKey")
n := CandidateNode{Key: key, Value: "meow", document: 3}
@ -152,8 +177,6 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) {
key := CandidateNode{Value: "cool", IsMapKey: true}
node := CandidateNode{}
// if we use a key in a new node as a value, it should no longer be marked as a key
_, keyIsValueNow := node.AddKeyValueChild(&CandidateNode{Value: "newKey"}, &key)
test.AssertResult(t, keyIsValueNow.IsMapKey, false)
@ -204,3 +227,193 @@ func TestConvertToNodeInfo(t *testing.T) {
test.AssertResult(t, 2, childInfo.Line)
test.AssertResult(t, 3, childInfo.Column)
}
func TestCandidateNodeGetPath(t *testing.T) {
// Test root node with no parent
root := CandidateNode{Value: "root"}
path := root.GetPath()
test.AssertResult(t, 0, len(path))
// Test node with key
key := createStringScalarNode("myKey")
node := CandidateNode{Key: key, Value: "myValue"}
path = node.GetPath()
test.AssertResult(t, 1, len(path))
test.AssertResult(t, "myKey", path[0])
// Test nested path
parent := CandidateNode{}
parentKey := createStringScalarNode("parent")
parent.Key = parentKey
node.Parent = &parent
path = node.GetPath()
test.AssertResult(t, 2, len(path))
test.AssertResult(t, "parent", path[0])
test.AssertResult(t, "myKey", path[1])
}
func TestCandidateNodeGetNicePath(t *testing.T) {
// Test simple key
key := createStringScalarNode("simple")
node := CandidateNode{Key: key}
nicePath := node.GetNicePath()
test.AssertResult(t, "simple", nicePath)
// Test array index
arrayKey := createScalarNode(0, "0")
arrayNode := CandidateNode{Key: arrayKey}
nicePath = arrayNode.GetNicePath()
test.AssertResult(t, "[0]", nicePath)
dotKey := createStringScalarNode("key.with.dots")
dotNode := CandidateNode{Key: dotKey}
nicePath = dotNode.GetNicePath()
test.AssertResult(t, "key.with.dots", nicePath)
// Test nested path
parentKey := createStringScalarNode("parent")
parent := CandidateNode{Key: parentKey}
childKey := createStringScalarNode("child")
child := CandidateNode{Key: childKey, Parent: &parent}
nicePath = child.GetNicePath()
test.AssertResult(t, "parent.child", nicePath)
}
func TestCandidateNodeFilterMapContentByKey(t *testing.T) {
// Create a map with multiple key-value pairs
key1 := createStringScalarNode("key1")
value1 := createStringScalarNode("value1")
key2 := createStringScalarNode("key2")
value2 := createStringScalarNode("value2")
key3 := createStringScalarNode("key3")
value3 := createStringScalarNode("value3")
mapNode := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{key1, value1, key2, value2, key3, value3},
}
// Filter by key predicate that matches key1 and key3
filtered := mapNode.FilterMapContentByKey(func(key *CandidateNode) bool {
return key.Value == "key1" || key.Value == "key3"
})
// Should return key1, value1, key3, value3
test.AssertResult(t, 4, len(filtered))
test.AssertResult(t, "key1", filtered[0].Value)
test.AssertResult(t, "value1", filtered[1].Value)
test.AssertResult(t, "key3", filtered[2].Value)
test.AssertResult(t, "value3", filtered[3].Value)
}
func TestCandidateNodeVisitValues(t *testing.T) {
// Test mapping node
key1 := createStringScalarNode("key1")
value1 := createStringScalarNode("value1")
key2 := createStringScalarNode("key2")
value2 := createStringScalarNode("value2")
mapNode := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{key1, value1, key2, value2},
}
var visited []string
err := mapNode.VisitValues(func(node *CandidateNode) error {
visited = append(visited, node.Value)
return nil
})
test.AssertResult(t, nil, err)
test.AssertResult(t, 2, len(visited))
test.AssertResult(t, "value1", visited[0])
test.AssertResult(t, "value2", visited[1])
// Test sequence node
item1 := createStringScalarNode("item1")
item2 := createStringScalarNode("item2")
seqNode := &CandidateNode{
Kind: SequenceNode,
Content: []*CandidateNode{item1, item2},
}
visited = []string{}
err = seqNode.VisitValues(func(node *CandidateNode) error {
visited = append(visited, node.Value)
return nil
})
test.AssertResult(t, nil, err)
test.AssertResult(t, 2, len(visited))
test.AssertResult(t, "item1", visited[0])
test.AssertResult(t, "item2", visited[1])
// Test scalar node (should not visit anything)
scalarNode := &CandidateNode{
Kind: ScalarNode,
Value: "scalar",
}
visited = []string{}
err = scalarNode.VisitValues(func(node *CandidateNode) error {
visited = append(visited, node.Value)
return nil
})
test.AssertResult(t, nil, err)
test.AssertResult(t, 0, len(visited))
}
func TestCandidateNodeCanVisitValues(t *testing.T) {
mapNode := &CandidateNode{Kind: MappingNode}
seqNode := &CandidateNode{Kind: SequenceNode}
scalarNode := &CandidateNode{Kind: ScalarNode}
test.AssertResult(t, true, mapNode.CanVisitValues())
test.AssertResult(t, true, seqNode.CanVisitValues())
test.AssertResult(t, false, scalarNode.CanVisitValues())
}
func TestCandidateNodeAddChild(t *testing.T) {
parent := &CandidateNode{Kind: SequenceNode}
child := createStringScalarNode("child")
parent.AddChild(child)
test.AssertResult(t, 1, len(parent.Content))
test.AssertResult(t, false, parent.Content[0].IsMapKey)
test.AssertResult(t, "0", parent.Content[0].Key.Value)
// Check that parent is set correctly
if parent.Content[0].Parent != parent {
t.Errorf("Expected parent to be set correctly")
}
}
func TestCandidateNodeAddChildren(t *testing.T) {
// Test sequence node
parent := &CandidateNode{Kind: SequenceNode}
child1 := createStringScalarNode("child1")
child2 := createStringScalarNode("child2")
parent.AddChildren([]*CandidateNode{child1, child2})
test.AssertResult(t, 2, len(parent.Content))
test.AssertResult(t, "child1", parent.Content[0].Value)
test.AssertResult(t, "child2", parent.Content[1].Value)
// Test mapping node
mapParent := &CandidateNode{Kind: MappingNode}
key1 := createStringScalarNode("key1")
value1 := createStringScalarNode("value1")
key2 := createStringScalarNode("key2")
value2 := createStringScalarNode("value2")
mapParent.AddChildren([]*CandidateNode{key1, value1, key2, value2})
test.AssertResult(t, 4, len(mapParent.Content))
test.AssertResult(t, true, mapParent.Content[0].IsMapKey) // key1
test.AssertResult(t, false, mapParent.Content[1].IsMapKey) // value1
test.AssertResult(t, true, mapParent.Content[2].IsMapKey) // key2
test.AssertResult(t, false, mapParent.Content[3].IsMapKey) // value2
}

View File

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

View File

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

View File

@ -0,0 +1,139 @@
//go:build linux
package yqlib
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestChangeOwner(t *testing.T) {
// Create a temporary file for testing
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "testfile.txt")
// Create a test file
err := os.WriteFile(testFile, []byte("test content"), 0600)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Get file info
info, err := os.Stat(testFile)
if err != nil {
t.Fatalf("Failed to stat test file: %v", err)
}
// Create another temporary file to change ownership of
tempFile, err := os.CreateTemp(tempDir, "chown_test_*.txt")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Test changeOwner function
err = changeOwner(info, tempFile)
if err != nil {
t.Errorf("changeOwner failed: %v", err)
}
// Verify that the function doesn't panic with valid input
tempFile2, err := os.CreateTemp(tempDir, "chown_test2_*.txt")
if err != nil {
t.Fatalf("Failed to create second temp file: %v", err)
}
defer os.Remove(tempFile2.Name())
tempFile2.Close()
// Test with the second file
err = changeOwner(info, tempFile2)
if err != nil {
t.Errorf("changeOwner failed on second file: %v", err)
}
}
func TestChangeOwnerWithInvalidFileInfo(t *testing.T) {
// Create a mock file info that doesn't have syscall.Stat_t
mockInfo := &mockFileInfo{
name: "mock",
size: 0,
mode: 0600,
}
// Create a temporary file
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Test changeOwner with mock file info (should not panic)
err = changeOwner(mockInfo, tempFile)
if err != nil {
t.Errorf("changeOwner failed with mock file info: %v", err)
}
}
func TestChangeOwnerWithNonExistentFile(t *testing.T) {
// Create a temporary file
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Get file info
info, err := os.Stat(tempFile.Name())
if err != nil {
t.Fatalf("Failed to stat temp file: %v", err)
}
// Remove the file
os.Remove(tempFile.Name())
err = changeOwner(info, tempFile)
// The function should not panic even if the file doesn't exist
if err != nil {
t.Logf("Expected error when changing owner of non-existent file: %v", err)
}
}
// mockFileInfo implements fs.FileInfo but doesn't have syscall.Stat_t
type mockFileInfo struct {
name string
size int64
mode os.FileMode
}
func (m *mockFileInfo) Name() string { return m.name }
func (m *mockFileInfo) Size() int64 { return m.size }
func (m *mockFileInfo) Mode() os.FileMode { return m.mode }
func (m *mockFileInfo) ModTime() time.Time { return time.Time{} }
func (m *mockFileInfo) IsDir() bool { return false }
func (m *mockFileInfo) Sys() interface{} { return nil } // This will cause the type assertion to fail
func TestChangeOwnerWithSyscallStatT(t *testing.T) {
// Create a temporary file
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Get file info
info, err := os.Stat(tempFile.Name())
if err != nil {
t.Fatalf("Failed to stat temp file: %v", err)
}
err = changeOwner(info, tempFile)
if err != nil {
t.Logf("changeOwner returned error (this might be expected in some environments): %v", err)
}
}

View File

@ -0,0 +1,153 @@
package yqlib
import (
"bytes"
"strings"
"testing"
"github.com/fatih/color"
)
func TestFormat(t *testing.T) {
tests := []struct {
name string
attr color.Attribute
expected string
}{
{
name: "reset color",
attr: color.Reset,
expected: "\x1b[0m",
},
{
name: "red color",
attr: color.FgRed,
expected: "\x1b[31m",
},
{
name: "green color",
attr: color.FgGreen,
expected: "\x1b[32m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := format(tt.attr)
if result != tt.expected {
t.Errorf("format(%d) = %q, want %q", tt.attr, result, tt.expected)
}
})
}
}
func TestColorizeAndPrint(t *testing.T) {
tests := []struct {
name string
yamlBytes []byte
expectErr bool
}{
{
name: "simple yaml",
yamlBytes: []byte("name: test\nage: 25\n"),
expectErr: false,
},
{
name: "yaml with strings",
yamlBytes: []byte("name: \"hello world\"\nactive: true\ncount: 42\n"),
expectErr: false,
},
{
name: "yaml with anchors and aliases",
yamlBytes: []byte("default: &default\n name: test\nuser: *default\n"),
expectErr: false,
},
{
name: "yaml with comments",
yamlBytes: []byte("# This is a comment\nname: test\n"),
expectErr: false,
},
{
name: "empty yaml",
yamlBytes: []byte(""),
expectErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
err := colorizeAndPrint(tt.yamlBytes, &buf)
if tt.expectErr && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectErr && err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Check that output contains escape sequences (color codes)
if !tt.expectErr && len(tt.yamlBytes) > 0 {
output := buf.String()
if !strings.Contains(output, "\x1b[") {
t.Error("Expected output to contain color escape sequences")
}
}
})
}
}
func TestColorizeAndPrintWithDifferentYamlTypes(t *testing.T) {
testCases := []struct {
name string
yaml string
expectErr bool
}{
{
name: "boolean values",
yaml: "active: true\ninactive: false\n",
},
{
name: "numeric values",
yaml: "integer: 42\nfloat: 3.14\nnegative: -10\n",
},
{
name: "map keys",
yaml: "user:\n name: john\n age: 30\n",
},
{
name: "string values",
yaml: "message: \"hello world\"\ndescription: 'single quotes'\n",
},
{
name: "mixed types",
yaml: "config:\n debug: true\n port: 8080\n host: \"localhost\"\n",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
err := colorizeAndPrint([]byte(tc.yaml), &buf)
if tc.expectErr && err == nil {
t.Error("Expected error but got none")
}
if !tc.expectErr && err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Verify output contains color codes
if !tc.expectErr {
output := buf.String()
if !strings.Contains(output, "\x1b[") {
t.Error("Expected output to contain color escape sequences")
}
// Should end with newline
if !strings.HasSuffix(output, "\n") {
t.Error("Expected output to end with newline")
}
}
})
}
}

View File

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

View File

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

View File

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

View File

@ -312,7 +312,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
Value: "value",
}
// Try to assign to a path on a scalar (should fail)
path := []interface{}{"key"}
err := navigator.DeeplyAssign(context, path, assignNode)
@ -321,7 +320,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
t.Logf("Actual error: %v", err)
}
// This should fail because we can't assign to a scalar
test.AssertResult(t, nil, err)
}

View File

@ -9,28 +9,6 @@ import (
"strings"
)
type base64Padder struct {
count int
io.Reader
}
func (c *base64Padder) pad(buf []byte) (int, error) {
pad := strings.Repeat("=", (4 - c.count%4))
n, err := strings.NewReader(pad).Read(buf)
c.count += n
return n, err
}
func (c *base64Padder) Read(buf []byte) (int, error) {
n, err := c.Reader.Read(buf)
c.count += n
if err == io.EOF && c.count%4 != 0 {
return c.pad(buf)
}
return n, err
}
type base64Decoder struct {
reader io.Reader
finished bool
@ -43,7 +21,25 @@ func NewBase64Decoder() Decoder {
}
func (dec *base64Decoder) Init(reader io.Reader) error {
dec.reader = &base64Padder{Reader: reader}
// Read all data from the reader and strip leading/trailing whitespace
// This is necessary because base64 decoding needs to see the complete input
// to handle padding correctly, and we need to strip whitespace before decoding.
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(reader); err != nil {
return err
}
// Strip leading and trailing whitespace
stripped := strings.TrimSpace(buf.String())
// Add padding if needed (base64 strings should be a multiple of 4 characters)
padLen := len(stripped) % 4
if padLen > 0 {
stripped += strings.Repeat("=", 4-padLen)
}
// Create a new reader from the stripped and padded data
dec.reader = strings.NewReader(stripped)
dec.readAnything = false
dec.finished = false
return nil

471
pkg/yqlib/decoder_hcl.go Normal file
View File

@ -0,0 +1,471 @@
//go:build !yq_nohcl
package yqlib
import (
"fmt"
"io"
"math/big"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type hclDecoder struct {
file *hcl.File
fileBytes []byte
readAnything bool
documentIndex uint
}
func NewHclDecoder() Decoder {
return &hclDecoder{}
}
// sortedAttributes returns attributes in declaration order by source position
func sortedAttributes(attrs hclsyntax.Attributes) []*attributeWithName {
var sorted []*attributeWithName
for name, attr := range attrs {
sorted = append(sorted, &attributeWithName{Name: name, Attr: attr})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Attr.Range().Start.Byte < sorted[j].Attr.Range().Start.Byte
})
return sorted
}
type attributeWithName struct {
Name string
Attr *hclsyntax.Attribute
}
// extractLineComment extracts any inline comment after the given position
func extractLineComment(src []byte, endPos int) string {
// Look for # comment after the token
for i := endPos; i < len(src); i++ {
if src[i] == '#' {
// Found comment, extract until end of line
start := i
for i < len(src) && src[i] != '\n' {
i++
}
return strings.TrimSpace(string(src[start:i]))
}
if src[i] == '\n' {
// Hit newline before comment
break
}
// Skip whitespace and other characters
}
return ""
}
// extractHeadComment extracts comments before a given start position
func extractHeadComment(src []byte, startPos int) string {
var comments []string
// Start just before the token and skip trailing whitespace
i := startPos - 1
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i--
}
for i >= 0 {
// Find line boundaries
lineEnd := i
for i >= 0 && src[i] != '\n' {
i--
}
lineStart := i + 1
line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
trimmed := strings.TrimSpace(line)
if trimmed == "" {
break
}
if !strings.HasPrefix(trimmed, "#") {
break
}
comments = append([]string{trimmed}, comments...)
// Move to previous line (skip any whitespace/newlines)
i = lineStart - 1
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i--
}
}
if len(comments) > 0 {
return strings.Join(comments, "\n")
}
return ""
}
func (dec *hclDecoder) Init(reader io.Reader) error {
data, err := io.ReadAll(reader)
if err != nil {
return err
}
file, diags := hclsyntax.ParseConfig(data, "input.hcl", hcl.Pos{Line: 1, Column: 1})
if diags != nil && diags.HasErrors() {
return fmt.Errorf("hcl parse error: %w", diags)
}
dec.file = file
dec.fileBytes = data
dec.readAnything = false
dec.documentIndex = 0
return nil
}
func (dec *hclDecoder) Decode() (*CandidateNode, error) {
if dec.readAnything {
return nil, io.EOF
}
dec.readAnything = true
if dec.file == nil {
return nil, fmt.Errorf("no hcl file parsed")
}
root := &CandidateNode{Kind: MappingNode}
// process attributes in declaration order
body := dec.file.Body.(*hclsyntax.Body)
firstAttr := true
for _, attrWithName := range sortedAttributes(body.Attributes) {
keyNode := createStringScalarNode(attrWithName.Name)
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
// Attach comments if any
attrRange := attrWithName.Attr.Range()
headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte)
if firstAttr && headComment != "" {
// For the first attribute, apply its head comment to the root
root.HeadComment = headComment
firstAttr = false
} else if headComment != "" {
keyNode.HeadComment = headComment
}
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {
valNode.LineComment = lineComment
}
root.AddKeyValueChild(keyNode, valNode)
}
// process blocks
// Count blocks by type at THIS level to detect multiple separate blocks
blocksByType := make(map[string]int)
for _, block := range body.Blocks {
blocksByType[block.Type]++
}
for _, block := range body.Blocks {
addBlockToMapping(root, block, dec.fileBytes, blocksByType[block.Type] > 1)
}
dec.documentIndex++
root.document = dec.documentIndex - 1
return root, nil
}
func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode {
node := &CandidateNode{Kind: MappingNode}
for _, attrWithName := range sortedAttributes(body.Attributes) {
key := createStringScalarNode(attrWithName.Name)
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
// Attach comments if any
attrRange := attrWithName.Attr.Range()
if headComment := extractHeadComment(src, attrRange.Start.Byte); headComment != "" {
key.HeadComment = headComment
}
if lineComment := extractLineComment(src, attrRange.End.Byte); lineComment != "" {
val.LineComment = lineComment
}
node.AddKeyValueChild(key, val)
}
// Process nested blocks, counting blocks by type at THIS level
// to detect which block types appear multiple times
blocksByType := make(map[string]int)
for _, block := range body.Blocks {
blocksByType[block.Type]++
}
for _, block := range body.Blocks {
addBlockToMapping(node, block, src, blocksByType[block.Type] > 1)
}
return node
}
// addBlockToMapping nests block type and labels into the parent mapping, merging children.
// isMultipleBlocksOfType indicates if there are multiple blocks of this type at THIS level
func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte, isMultipleBlocksOfType bool) {
bodyNode := hclBodyToNode(block.Body, src)
current := parent
// ensure block type mapping exists
var typeNode *CandidateNode
for i := 0; i < len(current.Content); i += 2 {
if current.Content[i].Value == block.Type {
typeNode = current.Content[i+1]
break
}
}
if typeNode == nil {
_, typeNode = current.AddKeyValueChild(createStringScalarNode(block.Type), &CandidateNode{Kind: MappingNode})
// Mark the type node if there are multiple blocks of this type at this level
// This tells the encoder to emit them as separate blocks rather than consolidating them
if isMultipleBlocksOfType {
typeNode.EncodeHint = EncodeHintSeparateBlock
}
}
current = typeNode
// walk labels, creating/merging mappings
for _, label := range block.Labels {
var next *CandidateNode
for i := 0; i < len(current.Content); i += 2 {
if current.Content[i].Value == label {
next = current.Content[i+1]
break
}
}
if next == nil {
_, next = current.AddKeyValueChild(createStringScalarNode(label), &CandidateNode{Kind: MappingNode})
}
current = next
}
// merge body attributes/blocks into the final mapping
for i := 0; i < len(bodyNode.Content); i += 2 {
current.AddKeyValueChild(bodyNode.Content[i], bodyNode.Content[i+1])
}
}
func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode {
// handle literal values directly
switch e := expr.(type) {
case *hclsyntax.LiteralValueExpr:
v := e.Val
if v.IsNull() {
return createScalarNode(nil, "")
}
switch {
case v.Type().Equals(cty.String):
// prefer to extract exact source (to avoid extra quoting) when available
// Prefer the actual cty string value
s := v.AsString()
node := createScalarNode(s, s)
// Don't set style for regular quoted strings - let YAML handle naturally
return node
case v.Type().Equals(cty.Bool):
b := v.True()
return createScalarNode(b, strconv.FormatBool(b))
case v.Type() == cty.Number:
// prefer integers when the numeric value is integral
bf := v.AsBigFloat()
if bf == nil {
// fallback to string
return createStringScalarNode(v.GoString())
}
// check if bf represents an exact integer
if intVal, acc := bf.Int(nil); acc == big.Exact {
s := intVal.String()
return createScalarNode(intVal.Int64(), s)
}
s := bf.Text('g', -1)
return createScalarNode(0.0, s)
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
seq := &CandidateNode{Kind: SequenceNode}
it := v.ElementIterator()
for it.Next() {
_, val := it.Element()
// convert cty.Value to a node by wrapping in literal expr via string representation
child := convertCtyValueToNode(val)
seq.AddChild(child)
}
return seq
case v.Type().IsMapType() || v.Type().IsObjectType():
m := &CandidateNode{Kind: MappingNode}
it := v.ElementIterator()
for it.Next() {
key, val := it.Element()
keyStr := key.AsString()
keyNode := createStringScalarNode(keyStr)
valNode := convertCtyValueToNode(val)
m.AddKeyValueChild(keyNode, valNode)
}
return m
default:
// fallback to string
s := v.GoString()
return createStringScalarNode(s)
}
case *hclsyntax.TupleConsExpr:
// parse tuple/list into YAML sequence
seq := &CandidateNode{Kind: SequenceNode}
for _, exprVal := range e.Exprs {
child := convertHclExprToNode(exprVal, src)
seq.AddChild(child)
}
return seq
case *hclsyntax.ObjectConsExpr:
// parse object into YAML mapping
m := &CandidateNode{Kind: MappingNode}
m.Style = FlowStyle // Mark as inline object (flow style) for encoder
for _, item := range e.Items {
// evaluate key expression to get the key string
keyVal, keyDiags := item.KeyExpr.Value(nil)
if keyDiags != nil && keyDiags.HasErrors() {
// fallback: try to extract key from source
r := item.KeyExpr.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start:end])))
valNode := convertHclExprToNode(item.ValueExpr, src)
m.AddKeyValueChild(keyNode, valNode)
}
continue
}
keyStr := keyVal.AsString()
keyNode := createStringScalarNode(keyStr)
valNode := convertHclExprToNode(item.ValueExpr, src)
m.AddKeyValueChild(keyNode, valNode)
}
return m
case *hclsyntax.TemplateExpr:
// Reconstruct template string, preserving ${} syntax for interpolations
var parts []string
for _, p := range e.Parts {
switch lp := p.(type) {
case *hclsyntax.LiteralValueExpr:
if lp.Val.Type().Equals(cty.String) {
parts = append(parts, lp.Val.AsString())
} else {
parts = append(parts, lp.Val.GoString())
}
default:
// Non-literal expression - reconstruct with ${} wrapper
r := p.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
exprText := string(src[start:end])
parts = append(parts, "${"+exprText+"}")
} else {
parts = append(parts, fmt.Sprintf("${%v}", p))
}
}
}
combined := strings.Join(parts, "")
node := createScalarNode(combined, combined)
// Set DoubleQuotedStyle for all templates (which includes all quoted strings in HCL)
// This ensures HCL roundtrips preserve quotes, and YAML properly quotes strings with ${}
node.Style = DoubleQuotedStyle
return node
case *hclsyntax.ScopeTraversalExpr:
// Simple identifier/traversal (e.g. unquoted string literal in HCL)
r := e.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := strings.TrimSpace(string(src[start:end]))
return createStringScalarNode(text)
}
// Fallback to root name if source unavailable
if len(e.Traversal) > 0 {
if root, ok := e.Traversal[0].(hcl.TraverseRoot); ok {
return createStringScalarNode(root.Name)
}
}
return createStringScalarNode("")
case *hclsyntax.FunctionCallExpr:
// Preserve function calls as raw expressions for roundtrip
r := e.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := strings.TrimSpace(string(src[start:end]))
node := createStringScalarNode(text)
node.Style = 0
return node
}
node := createStringScalarNode(e.Name)
node.Style = 0
return node
default:
// try to evaluate the expression (handles unary, binary ops, etc.)
val, diags := expr.Value(nil)
if diags == nil || !diags.HasErrors() {
// successfully evaluated, convert cty.Value to node
return convertCtyValueToNode(val)
}
// fallback: extract source text for the expression
r := expr.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := string(src[start:end])
// Mark as unquoted expression so encoder emits without quoting
node := createStringScalarNode(text)
node.Style = 0
return node
}
return createStringScalarNode(fmt.Sprintf("%v", expr))
}
}
func convertCtyValueToNode(v cty.Value) *CandidateNode {
if v.IsNull() {
return createScalarNode(nil, "")
}
switch {
case v.Type().Equals(cty.String):
return createScalarNode("", v.AsString())
case v.Type().Equals(cty.Bool):
b := v.True()
return createScalarNode(b, strconv.FormatBool(b))
case v.Type() == cty.Number:
bf := v.AsBigFloat()
if bf == nil {
return createStringScalarNode(v.GoString())
}
if intVal, acc := bf.Int(nil); acc == big.Exact {
s := intVal.String()
return createScalarNode(intVal.Int64(), s)
}
s := bf.Text('g', -1)
return createScalarNode(0.0, s)
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
seq := &CandidateNode{Kind: SequenceNode}
it := v.ElementIterator()
for it.Next() {
_, val := it.Element()
seq.AddChild(convertCtyValueToNode(val))
}
return seq
case v.Type().IsMapType() || v.Type().IsObjectType():
m := &CandidateNode{Kind: MappingNode}
it := v.ElementIterator()
for it.Next() {
key, val := it.Element()
keyNode := createStringScalarNode(key.AsString())
valNode := convertCtyValueToNode(val)
m.AddKeyValueChild(keyNode, valNode)
}
return m
default:
return createStringScalarNode(v.GoString())
}
}

View File

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

View File

@ -16,10 +16,11 @@ type propertiesDecoder struct {
reader io.Reader
finished bool
d DataTreeNavigator
prefs PropertiesPreferences
}
func NewPropertiesDecoder() Decoder {
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false, prefs: ConfiguredPropertiesPreferences.Copy()}
}
func (dec *propertiesDecoder) Init(reader io.Reader) error {
@ -28,20 +29,56 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error {
return nil
}
func parsePropKey(key string) []interface{} {
func parsePropKey(key string, prefs PropertiesPreferences) []interface{} {
pathStrArray := strings.Split(key, ".")
path := make([]interface{}, len(pathStrArray))
for i, pathStr := range pathStrArray {
num, err := strconv.ParseInt(pathStr, 10, 32)
if err == nil {
path[i] = num
} else {
path[i] = pathStr
}
path := make([]interface{}, 0, len(pathStrArray))
for _, pathStr := range pathStrArray {
path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets)
}
return path
}
func appendPropKeySegment(path []interface{}, segment string, useArrayBrackets bool) []interface{} {
if useArrayBrackets && strings.Contains(segment, "[") {
bracketPath, ok := parsePropKeyArrayBracketSegment(segment)
if ok {
return append(path, bracketPath...)
}
}
num, err := strconv.ParseInt(segment, 10, 32)
if err == nil {
return append(path, num)
}
return append(path, segment)
}
func parsePropKeyArrayBracketSegment(segment string) ([]interface{}, bool) {
path := []interface{}{}
bracketIndex := strings.Index(segment, "[")
if bracketIndex > 0 {
path = append(path, segment[:bracketIndex])
}
remaining := segment[bracketIndex:]
for remaining != "" {
if !strings.HasPrefix(remaining, "[") {
return nil, false
}
closingBracket := strings.Index(remaining, "]")
if closingBracket < 0 {
return nil, false
}
arrayIndex, err := strconv.ParseInt(remaining[1:closingBracket], 10, 32)
if err != nil {
return nil, false
}
path = append(path, arrayIndex)
remaining = remaining[closingBracket+1:]
}
return path, true
}
func (dec *propertiesDecoder) processComment(c string) string {
if c == "" {
return ""
@ -75,7 +112,7 @@ func (dec *propertiesDecoder) applyPropertyComments(context Context, path []inte
func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error {
value, _ := properties.Get(key)
path := parsePropKey(key)
path := parsePropKey(key, dec.prefs)
propertyComments := properties.GetComments(key)
if len(propertyComments) > 0 {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,14 @@ import (
"regexp"
"strings"
yaml "go.yaml.in/yaml/v3"
yaml "go.yaml.in/yaml/v4"
)
var (
commentLineRe = regexp.MustCompile(`^\s*#`)
yamlDirectiveLineRe = regexp.MustCompile(`^\s*%YAML`)
separatorLineRe = regexp.MustCompile(`^\s*---\s*$`)
separatorPrefixRe = regexp.MustCompile(`^\s*---\s+`)
)
type yamlDecoder struct {
@ -33,51 +40,72 @@ func NewYamlDecoder(prefs YamlPreferences) Decoder {
}
func (dec *yamlDecoder) processReadStream(reader *bufio.Reader) (io.Reader, string, error) {
var commentLineRegEx = regexp.MustCompile(`^\s*#`)
var yamlDirectiveLineRegEx = regexp.MustCompile(`^\s*%YA`)
var sb strings.Builder
for {
peekBytes, err := reader.Peek(4)
if errors.Is(err, io.EOF) {
// EOF are handled else where..
return reader, sb.String(), nil
} else if err != nil {
return reader, sb.String(), err
} else if string(peekBytes[0]) == "\n" {
_, err := reader.ReadString('\n')
sb.WriteString("\n")
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
} else if err != nil {
return reader, sb.String(), err
}
} else if string(peekBytes) == "--- " {
_, err := reader.ReadString(' ')
sb.WriteString("$yqDocSeparator$\n")
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
} else if err != nil {
return reader, sb.String(), err
}
} else if string(peekBytes) == "---\n" {
_, err := reader.ReadString('\n')
sb.WriteString("$yqDocSeparator$\n")
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
} else if err != nil {
return reader, sb.String(), err
}
} else if commentLineRegEx.MatchString(string(peekBytes)) || yamlDirectiveLineRegEx.MatchString(string(peekBytes)) {
line, err := reader.ReadString('\n')
sb.WriteString(line)
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
} else if err != nil {
return reader, sb.String(), err
}
} else {
line, err := reader.ReadString('\n')
if errors.Is(err, io.EOF) && line == "" {
// no more data
return reader, sb.String(), nil
}
if err != nil && !errors.Is(err, io.EOF) {
return reader, sb.String(), err
}
// Determine newline style and strip it for inspection
newline := ""
if strings.HasSuffix(line, "\r\n") {
newline = "\r\n"
line = strings.TrimSuffix(line, "\r\n")
} else if strings.HasSuffix(line, "\n") {
newline = "\n"
line = strings.TrimSuffix(line, "\n")
}
trimmed := strings.TrimSpace(line)
// Document separator: exact line '---' or a '--- ' prefix followed by content
if separatorLineRe.MatchString(trimmed) {
sb.WriteString("$yqDocSeparator$")
sb.WriteString(newline)
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
}
continue
}
// Handle lines that start with '--- ' followed by more content (e.g. '--- cat')
if separatorPrefixRe.MatchString(line) {
match := separatorPrefixRe.FindString(line)
remainder := line[len(match):]
// normalise separator newline: if original had none, default to LF
sepNewline := newline
if sepNewline == "" {
sepNewline = "\n"
}
sb.WriteString("$yqDocSeparator$")
sb.WriteString(sepNewline)
// push the remainder back onto the reader and continue processing
reader = bufio.NewReader(io.MultiReader(strings.NewReader(remainder), reader))
if errors.Is(err, io.EOF) && remainder == "" {
return reader, sb.String(), nil
}
continue
}
// Comments, YAML directives, and blank lines are leading content
if commentLineRe.MatchString(line) || yamlDirectiveLineRe.MatchString(line) || trimmed == "" {
sb.WriteString(line)
sb.WriteString(newline)
if errors.Is(err, io.EOF) {
return reader, sb.String(), nil
}
continue
}
// First non-leading line: push it back onto a reader and return
originalLine := line + newline
return io.MultiReader(strings.NewReader(originalLine), reader), sb.String(), nil
}
}

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

View File

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

View File

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

View File

@ -29,6 +29,9 @@ as follows:
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
```
## Disabling env operators
If required, you can use the `--security-disable-env-ops` to disable env operations.
## Read string environment variable
Running
@ -254,3 +257,39 @@ will output
Error: variable ${notThere} not set
```
## env() operation fails when security is enabled
Use `--security-disable-env-ops` to disable env operations for security.
Running
```bash
yq --null-input 'env("MYENV")'
```
will output
```bash
Error: env operations have been disabled
```
## strenv() operation fails when security is enabled
Use `--security-disable-env-ops` to disable env operations for security.
Running
```bash
yq --null-input 'strenv("MYENV")'
```
will output
```bash
Error: env operations have been disabled
```
## envsubst() operation fails when security is enabled
Use `--security-disable-env-ops` to disable env operations for security.
Running
```bash
yq --null-input '"value: ${MYENV}" | envsubst'
```
will output
```bash
Error: env operations have been disabled
```

View File

@ -1,3 +1,8 @@
# First
Returns the first matching element in an array, or first matching value in a map.
Can be given an expression to match with, otherwise will just return the first.
## First matching element from array
Given a sample.yml file of:
@ -20,8 +25,10 @@ Given a sample.yml file of:
```yaml
- a: banana
- a: cat
b: firstCat
- a: apple
- a: cat
b: secondCat
```
then
```bash
@ -30,6 +37,7 @@ yq 'first(.a == "cat")' sample.yml
will output
```yaml
a: cat
b: firstCat
```
## First matching element from array with numeric condition
@ -38,6 +46,7 @@ Given a sample.yml file of:
- a: 10
- a: 100
- a: 1
- a: 101
```
then
```bash
@ -53,7 +62,10 @@ Given a sample.yml file of:
```yaml
- a: false
- a: true
b: firstTrue
- a: false
- a: true
b: secondTrue
```
then
```bash
@ -62,6 +74,7 @@ yq 'first(.a == true)' sample.yml
will output
```yaml
a: true
b: firstTrue
```
## First matching element from array with null values
@ -84,19 +97,19 @@ a: cat
Given a sample.yml file of:
```yaml
- a: dog
b: 5
b: 7
- a: cat
b: 3
- a: apple
b: 7
b: 5
```
then
```bash
yq 'first(.b > 4)' sample.yml
yq 'first(.b > 4 and .b < 6)' sample.yml
```
will output
```yaml
a: dog
a: apple
b: 5
```
@ -127,7 +140,7 @@ x:
y:
a: 100
z:
a: 1
a: 101
```
then
```bash

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).
`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

View File

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

View File

@ -29,3 +29,6 @@ as follows:
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
```
## Disabling env operators
If required, you can use the `--security-disable-env-ops` to disable env operations.

View File

@ -0,0 +1,5 @@
# First
Returns the first matching element in an array, or first matching value in a map.
Can be given an expression to match with, otherwise will just return the first.

View File

@ -46,3 +46,7 @@ this.is = a properties file
```
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
```
## Disabling file operators
If required, you can use the `--security-disable-file-ops` to disable file operations.

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

@ -47,6 +47,10 @@ this.is = a properties file
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
```
## Disabling file operators
If required, you can use the `--security-disable-file-ops` to disable file operations.
## Simple example
Given a sample.yml file of:
```yaml
@ -194,3 +198,63 @@ cool: things
more_stuff: my secret chilli recipe is....
```
## load() operation fails when security is enabled
Use `--security-disable-file-ops` to disable file operations for security.
Running
```bash
yq --null-input 'load("../../examples/thing.yml")'
```
will output
```bash
Error: file operations have been disabled
```
## load_str() operation fails when security is enabled
Use `--security-disable-file-ops` to disable file operations for security.
Running
```bash
yq --null-input 'load_str("../../examples/thing.yml")'
```
will output
```bash
Error: file operations have been disabled
```
## load_xml() operation fails when security is enabled
Use `--security-disable-file-ops` to disable file operations for security.
Running
```bash
yq --null-input 'load_xml("../../examples/small.xml")'
```
will output
```bash
Error: file operations have been disabled
```
## load_props() operation fails when security is enabled
Use `--security-disable-file-ops` to disable file operations for security.
Running
```bash
yq --null-input 'load_props("../../examples/small.properties")'
```
will output
```bash
Error: file operations have been disabled
```
## load_base64() operation fails when security is enabled
Use `--security-disable-file-ops` to disable file operations for security.
Running
```bash
yq --null-input 'load_base64("../../examples/base64.txt")'
```
will output
```bash
Error: file operations have been disabled
```

View File

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

View File

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

View File

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

View File

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

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
Given a sample.yml file of:
@ -103,3 +103,81 @@ will output
- cow
```
## Slicing strings
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[0:5]' sample.yml
```
will output
```yaml
Austr
```
## Slicing strings - without the second number
Finishes at the end of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[5:]' sample.yml
```
will output
```yaml
alia
```
## Slicing strings - without the first number
Starts from the start of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[:5]' sample.yml
```
will output
```yaml
Austr
```
## Slicing strings - use negative numbers to count backwards from the end
Negative indices count from the end of the string
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[-5:]' sample.yml
```
will output
```yaml
ralia
```
## Slicing strings - Unicode
Indices are rune-based, so multi-byte characters are handled correctly
Given a sample.yml file of:
```yaml
greeting: héllo
```
then
```bash
yq '.greeting[1:3]' sample.yml
```
will output
```yaml
él
```

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

View File

@ -0,0 +1,88 @@
# Base64
Encode and decode to and from Base64.
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
See below for examples
## Decode base64: simple
Decoded data is assumed to be a string.
Given a sample.txt file of:
```
YSBzcGVjaWFsIHN0cmluZw==
```
then
```bash
yq -p=base64 -oy '.' sample.txt
```
will output
```yaml
a special string
```
## Decode base64: UTF-8
Base64 decoding supports UTF-8 encoded strings.
Given a sample.txt file of:
```
V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig==
```
then
```bash
yq -p=base64 -oy '.' sample.txt
```
will output
```yaml
Works with UTF-16 😊
```
## Decode with extra spaces
Extra leading/trailing whitespace is stripped
Given a sample.txt file of:
```
YSBzcGVjaWFsIHN0cmluZw==
```
then
```bash
yq -p=base64 -oy '.' sample.txt
```
will output
```yaml
a special string
```
## Encode base64: string
Given a sample.yml file of:
```yaml
"a special string"
```
then
```bash
yq -o=base64 '.' sample.yml
```
will output
```
YSBzcGVjaWFsIHN0cmluZw==```
## Encode base64: string from document
Extract a string field and encode it to base64.
Given a sample.yml file of:
```yaml
coolData: "a special string"
```
then
```bash
yq -o=base64 '.coolData' sample.yml
```
will output
```
YSBzcGVjaWFsIHN0cmluZw==```

View File

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

201
pkg/yqlib/doc/usage/hcl.md Normal file
View File

@ -0,0 +1,201 @@
# HCL
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
- Blocks and attributes
- String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments)
- Nested structures (maps and lists)
- Syntax colorisation when enabled
## Parse HCL
Given a sample.hcl file of:
```hcl
io_mode = "async"
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
io_mode: "async"
```
## Roundtrip: Sample Doc
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq sample.hcl
```
will output
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
## Roundtrip: With an update
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq '.service.cat.process.main.command += "meow"' sample.hcl
```
will output
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server", "meow"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
## Parse HCL: Sample Doc
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
service:
cat:
process:
main:
command:
- "/usr/local/bin/awesome-app"
- "server"
management:
command:
- "/usr/local/bin/awesome-app"
- "management"
```
## Parse HCL: with comments
Given a sample.hcl file of:
```hcl
# Configuration
port = 8080 # server port
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
# Configuration
port: 8080 # server port
```
## Roundtrip: with comments
Given a sample.hcl file of:
```hcl
# Configuration
port = 8080
```
then
```bash
yq sample.hcl
```
will output
```hcl
# Configuration
port = 8080
```
## Roundtrip: With templates, functions and arithmetic
Given a sample.hcl file of:
```hcl
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
```
then
```bash
yq sample.hcl
```
will output
```hcl
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
```
## Roundtrip: Separate blocks with same name.
Given a sample.hcl file of:
```hcl
resource "aws_instance" "web" {
ami = "ami-12345"
}
resource "aws_instance" "db" {
ami = "ami-67890"
}
```
then
```bash
yq sample.hcl
```
will output
```hcl
resource "aws_instance" "web" {
ami = "ami-12345"
}
resource "aws_instance" "db" {
ami = "ami-67890"
}
```

View File

@ -0,0 +1,9 @@
# Base64
Encode and decode to and from Base64.
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
See below for examples

View File

@ -0,0 +1,11 @@
# HCL
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
- Blocks and attributes
- String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments)
- Nested structures (maps and lists)
- Syntax colorisation when enabled

View File

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

View File

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

View File

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

View File

@ -84,3 +84,23 @@ will output
name='Miles O'"'"'Brien'
```
## Encode shell variables: custom separator
Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.
Given a sample.yml file of:
```yaml
my_app:
db_config:
host: localhost
port: 5432
```
then
```bash
yq -o=shell --shell-key-separator="__" sample.yml
```
will output
```sh
my_app__db_config__host=localhost
my_app__db_config__port=5432
```

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