Compare commits

..
86 Commits
Author SHA1 Message Date
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 FarahandGitHub 602586d8fd Create scorecard.yml
Signed-off-by: Mike Farah <mikefarah@gmail.com>
2026-04-14 18:43:11 +10:00
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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]GitHubmikefarahdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubmikefarahCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Mike Farah
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
CopilotGitHubmikefarahCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Mike Farah
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
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
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
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
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
CopilotGitHubmikefarahcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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 ChaiandGitHub 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
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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 RedkoandGitHub 4c148178e2 Fix typo in filename (#2611) 2026-03-21 09:29:07 +11:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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 FarahandGitHub 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 EzhkinandGitHub 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
sydarnandGitHub e35d32a0b6 buildfix: which -> command -v (#2582) 2026-01-31 14:51:56 +11:00
jfenalandGitHub 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
jfenalandGitHub 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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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-elastisysandGitHub c6fa371d8d Add symlink check to file rename util (#2576) 2026-01-22 13:43:32 +11:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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 MillerandGitHub 414a085563 Fix default command used for __completeNoDesc alias (#2568) 2026-01-22 13:41:42 +11:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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. JohnsonandGitHub 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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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 WintersandGitHub 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
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 FarahandGitHub 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
151 changed files with 4301 additions and 603 deletions
+2 -2
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
+6 -4
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@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
# ️ 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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
+10 -5
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@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+2 -2
View File
@@ -11,13 +11,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '^1.20'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get dependencies
run: |
+16 -3
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@v6
- uses: actions/setup-go@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '^1.20'
check-latest: true
@@ -37,14 +42,22 @@ jobs:
--output=yq.1
man.md
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Cross compile
run: |
sudo apt-get install rhash -y
go install github.com/goreleaser/goreleaser/v2@latest
./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@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: build/*
draft: true
+78
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.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@v3
with:
sarif_file: results.sarif
+7 -3
View File
@@ -7,19 +7,23 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions: {}
jobs:
buildSnap:
environment: snap
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: snapcore/action-build@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
id: build
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snapcraft-args: "remote-build --launchpad-accept-public-upload"
- uses: snapcore/action-publish@v1
- uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get test
id: get_value
uses: mikefarah/yq@master
+5
View File
@@ -47,6 +47,7 @@ test*.tf
test*.xml
test*.toml
test*.yaml
*.kyaml
test_dir1/
test_dir2/
0.yml
@@ -69,3 +70,7 @@ debian/files
.vscode
yq3
# Golang
.gomodcache/
.gocache/
+5
View File
@@ -14,6 +14,11 @@ linters:
- unconvert
- unparam
settings:
misspell:
locale: UK
ignore-rules:
- color
- colors
depguard:
rules:
prevent_unmaintained_packages:
-1
View File
@@ -39,7 +39,6 @@ builds:
- openbsd_amd64
- windows_386
- windows_amd64
- windows_arm
- windows_arm64
no_unique_dist_dir: true
+6 -6
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
+15
View File
@@ -197,6 +197,21 @@ Note: PRs with small changes (e.g. minor typos) may not be merged (see https://j
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**:
+2 -2
View File
@@ -1,4 +1,4 @@
FROM golang:1.25.5 AS builder
FROM golang:1.26.2@sha256:2a2b4b5791cea8ae09caecba7bad0bd9631def96e5fe362e4a5e67009fe4ae61 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:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 AS production
LABEL maintainer="Mike Farah <mikefarah@users.noreply.github.com>"
COPY --from=builder /go/src/mikefarah/yq/yq /usr/bin/yq
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.25.5
FROM golang:1.26.2@sha256:2a2b4b5791cea8ae09caecba7bad0bd9631def96e5fe362e4a5e67009fe4ae61
RUN apt-get update && \
apt-get install -y npm && \
+1
View File
@@ -35,6 +35,7 @@ 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`'
+4 -1
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} \
+3 -3
View File
@@ -3,7 +3,7 @@
![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq) ![CodeQL](https://github.com/mikefarah/yq/workflows/CodeQL/badge.svg)
A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
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.
@@ -415,7 +415,7 @@ Flags:
-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|hcl|h|lua|l|ini|i] parse format for input. (default "auto")
-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")
@@ -424,7 +424,7 @@ Flags:
-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|hcl|h|shell|s|lua|l|ini|i] output format type. (default "auto")
-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 " = ")
+26
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.
+32 -1
View File
@@ -154,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"
}
@@ -313,4 +344,4 @@ EOM
assertEquals "$expected" "$X"
}
source ./scripts/shunit2
source ./scripts/shunit2
+49
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}}
+5 -10
View File
@@ -1,13 +1,14 @@
# 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 then ./scripts/check.sh to format, then validate linting and spelling
- 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 (e.g. Colorisation not Colorization)
- Use UK english spelling
**DON'T:**
- Git add or commit
- Add comments to functions that are self-explanatory
@@ -82,6 +83,7 @@ Create a test file `pkg/yqlib/<format>_test.go` using the `formatScenario` patte
- `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation)
- 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)
@@ -196,14 +198,6 @@ Tests must be implemented in `<format>_test.go` following the `formatScenario` p
## Common Patterns
### Scalar-Only Formats
Some formats only work with scalars (like base64, uri):
```go
if node.guessTagFromCustomType() != "!!str" {
return fmt.Errorf("cannot encode %v as <format>, can only operate on strings", node.Tag)
}
```
### Format with Indentation
Use preferences to control output formatting:
```go
@@ -345,6 +339,7 @@ Create `pkg/yqlib/operator_<type>_test.go` using the `expressionScenario` patter
- 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
+4 -1
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()
+1
View File
@@ -13,6 +13,7 @@ func TestCreateEvaluateAllCommand(t *testing.T) {
if cmd == nil {
t.Fatal("createEvaluateAllCommand returned nil")
return
}
// Test basic command properties
+4 -1
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()
+1
View File
@@ -13,6 +13,7 @@ func TestCreateEvaluateSequenceCommand(t *testing.T) {
if cmd == nil {
t.Fatal("createEvaluateSequenceCommand returned nil")
return
}
// Test basic command properties
+11 -18
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
@@ -184,7 +175,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.")
@@ -203,6 +194,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 {
@@ -220,6 +212,7 @@ yq -P -oy sample.json
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.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(),
+1
View File
@@ -195,6 +195,7 @@ func TestNew(t *testing.T) {
if rootCmd == nil {
t.Fatal("New() returned nil")
return
}
// Test basic command properties
+16 -8
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,18 +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()
@@ -227,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 {
@@ -272,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:]
}
@@ -288,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:]
}
+3 -3
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,
+1 -1
View File
@@ -11,7 +11,7 @@ var (
GitDescribe string
// Version is main version number that is being run at the moment.
Version = "v4.50.1"
Version = "v4.53.2"
// 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
+131 -1
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)
}
}
+10
View File
@@ -0,0 +1,10 @@
# leading
{
a: 1, # a line
# head b
b: 2,
c: [
# head d
"d", # d line
],
}
+7
View File
@@ -0,0 +1,7 @@
# leading
a: 1 # a line
# head b
b: 2
c:
# head d
- d # d line
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mikefarah/yq:4
FROM mikefarah/yq:4@sha256:603ebff15eb308a05f1c5b8b7613179cad859aed3ec9fdd04f2ef5d32345950e
COPY entrypoint.sh /entrypoint.sh
+14 -17
View File
@@ -6,23 +6,23 @@ 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.19.1
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.3.0
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1
github.com/zclconf/go-cty v1.17.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/net v0.48.0
golang.org/x/text v0.32.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.0
go.yaml.in/yaml/v4 v4.0.0-rc.4
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
golang.org/x/text v0.35.0
)
require (
@@ -33,12 +33,9 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.42.0 // indirect
)
go 1.24.0
toolchain go1.24.1
go 1.25.0
+26 -28
View File
@@ -18,16 +18,16 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/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/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.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.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
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=
@@ -46,8 +46,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/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=
@@ -61,30 +61,28 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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=
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
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.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA=
github.com/zclconf/go-cty v1.18.0/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=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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=
+24
View File
@@ -0,0 +1,24 @@
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.
// 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)
}
}
+22
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)
}
+26 -10
View File
@@ -27,6 +27,22 @@ 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
@@ -97,9 +113,9 @@ type CandidateNode struct {
// (e.g. top level cross document merge). This property does not propagate to child nodes.
EvaluateTogether bool
IsMapKey bool
// For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables
// rather than consolidated into nested mappings (default behaviour)
EncodeSeparate 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 {
@@ -280,7 +296,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)
@@ -323,11 +339,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
}
@@ -411,7 +427,7 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
EvaluateTogether: n.EvaluateTogether,
IsMapKey: n.IsMapKey,
EncodeSeparate: n.EncodeSeparate,
EncodeHint: n.EncodeHint,
}
if cloneContent {
@@ -445,7 +461,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
@@ -465,8 +481,8 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
n.Anchor = other.Anchor
}
// Preserve EncodeSeparate flag for format-specific encoding hints
n.EncodeSeparate = other.EncodeSeparate
// Preserve EncodeHint for format-specific encoding hints
n.EncodeHint = other.EncodeHint
// merge will pickup the style of the new thing
// when autocreating nodes
+7 -7
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
}
@@ -120,7 +120,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)
+6 -6
View File
@@ -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
+1 -1
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
+2 -3
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)
+3 -3
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)
+2 -3
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)))
}
+1 -1
View File
@@ -226,7 +226,7 @@ func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte
// Mark the type node if there are multiple blocks of this type at this level
// This tells the encoder to emit them as separate blocks rather than consolidating them
if isMultipleBlocksOfType {
typeNode.EncodeSeparate = true
typeNode.EncodeHint = EncodeHintSeparateBlock
}
}
current = typeNode
+1 -1
View File
@@ -16,7 +16,7 @@ type iniDecoder struct {
func NewINIDecoder() Decoder {
return &iniDecoder{
finished: false, // Initialize the flag as false
finished: false, // Initialise the flag as false
}
}
+1 -1
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
+158 -56
View File
@@ -44,9 +44,22 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
}
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 {
@@ -93,7 +106,7 @@ 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)
switch nextItem.Kind {
case toml.KeyValue:
@@ -105,7 +118,7 @@ func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode
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
}
}
@@ -137,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)
}
@@ -241,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
@@ -268,7 +299,7 @@ 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
@@ -296,7 +327,7 @@ func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error
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
}
@@ -304,7 +335,7 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
log.Debug("Enter processTable")
child := currentNode.Child()
fullPath := dec.getFullPath(child)
log.Debug("fullpath: %v", fullPath)
log.Debugf("fullpath: %v", fullPath)
c := Context{}
c = c.SingleChildContext(dec.rootMap)
@@ -315,10 +346,10 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
}
tableNodeValue := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: make([]*CandidateNode, 0),
EncodeSeparate: true,
Kind: MappingNode,
Tag: "!!map",
Content: make([]*CandidateNode, 0),
EncodeHint: EncodeHintSeparateBlock,
}
// Attach pending head comments to the table
@@ -329,20 +360,39 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
var tableValue *toml.Node
runAgainstCurrentExp := false
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)
}
err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue)
@@ -353,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",
@@ -378,7 +428,7 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
log.Debug("Enter processArrayTable")
child := currentNode.Child()
fullPath := dec.getFullPath(child)
log.Debug("Fullpath: %v", fullPath)
log.Debugf("Fullpath: %v", fullPath)
c := Context{}
c = c.SingleChildContext(dec.rootMap)
@@ -393,9 +443,9 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
hasValue := dec.parser.NextExpression()
tableNodeValue := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
EncodeSeparate: true,
Kind: MappingNode,
Tag: "!!map",
EncodeHint: EncodeHintSeparateBlock,
}
// Attach pending head comments to the array table
@@ -405,19 +455,52 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
}
runAgainstCurrentExp := false
// 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 hasValue && (dec.parser.Expression().Kind == toml.ArrayTable || dec.parser.Expression().Kind == toml.Table) {
runAgainstCurrentExp = true
} else if hasValue {
// otherwise, if there is a value, it must be some key value pairs of the
// first object in the array!
tableValue := dec.parser.Expression()
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
if err != nil && !errors.Is(err, io.EOF) {
return false, err
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)
@@ -430,23 +513,42 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error)
// Because TOML. So we'll inject the last index into the path.
func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) {
pathToCheck := fullPath
if len(fullPath) >= 1 {
pathToCheck = fullPath[:len(fullPath)-1]
}
readOp := createTraversalTree(pathToCheck, traversePreferences{DontAutoCreate: true}, false)
// 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.
resultContext, err := dec.d.GetMatchingNodes(c, readOp)
if err != nil {
return nil, err
if len(fullPath) == 0 {
return fullPath, nil
}
if resultContext.MatchingNodes.Len() >= 1 {
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
// path refers to an array, we need to add this to the last element in the array
if match.Kind == SequenceNode {
fullPath = append(pathToCheck, len(match.Content)-1, fullPath[len(fullPath)-1])
log.Debugf("Adding to end of %v array, using path: %v", pathToCheck, fullPath)
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)
}
}
}
return fullPath, err
// Add the last segment
resultPath = append(resultPath, fullPath[len(fullPath)-1])
log.Debugf("getPathToUse: original path %v -> result path %v", fullPath, resultPath)
return resultPath, nil
}
+160
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)
}
+15 -15
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}, " ")
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ func (dec *yamlDecoder) processReadStream(reader *bufio.Reader) (io.Reader, stri
if separatorPrefixRe.MatchString(line) {
match := separatorPrefixRe.FindString(line)
remainder := line[len(match):]
// normalize separator newline: if original had none, default to LF
// normalise separator newline: if original had none, default to LF
sepNewline := newline
if sepNewline == "" {
sepNewline = "\n"
@@ -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
- !!merge <<: *CENTRE
r: 10
```
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
@@ -299,7 +299,7 @@ Given a sample.yml file of:
- &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
@@ -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
@@ -412,7 +412,7 @@ Given a sample.yml file of:
- &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
+1 -1
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.
+1 -1
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.
@@ -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.
@@ -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.
+20 -1
View File
@@ -80,7 +80,7 @@ will output
```
## Get the top (root) parent
Use negative numbers to get the top parents
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
@@ -156,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
+81 -3
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
```
@@ -0,0 +1,76 @@
# System Operators
The `system` operator allows you to run an external command and use its output as a value in your expression.
**Security warning**: The system operator is disabled by default. You must explicitly pass `--security-enable-system-operator` to use it.
**Note:** When enabled, the system operator can replicate the functionality of `env` and `load`
operators via external commands. Enabling it effectively overrides `--security-disable-env-ops`
and `--security-disable-file-ops`.
## Usage
```bash
yq --security-enable-system-operator --null-input '.field = system("command"; "arg1")'
```
The operator takes:
- A command string (required)
- An argument (or an array of arguments), separated from the command by `;` (optional)
The current matched node's value is serialised and piped to the command via stdin. The command's stdout (with trailing newline stripped) is returned as a string.
## Disabling the system operator
The system operator is disabled by default. When disabled, an error is returned instead of running the command, consistent with `--security-disable-env-ops` and `--security-disable-file-ops`.
Use `--security-enable-system-operator` flag to enable it.
## system operator returns error when disabled
Use `--security-enable-system-operator` to enable the system operator.
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country = system("/usr/bin/echo"; "test")' sample.yml
```
will output
```bash
Error: system operations are disabled, use --security-enable-system-operator to enable
```
## Run a command with an argument
Use `--security-enable-system-operator` to enable the system operator.
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq --security-enable-system-operator '.country = system("/usr/bin/echo"; "test")' sample.yml
```
will output
```yaml
country: test
```
## Run a command without arguments
Omit the semicolon and args to run the command with no extra arguments.
Given a sample.yml file of:
```yaml
a: hello
```
then
```bash
yq --security-enable-system-operator '.a = system("/usr/bin/echo")' sample.yml
```
will output
```yaml
a: ""
```
+9
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).
+253
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",
},
],
},
],
},
]
```
+72
View File
@@ -329,3 +329,75 @@ B = 12
name = "Tom" # name comment
```
## Roundtrip: sample from web
Given a sample.toml file of:
```toml
# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
# [servers] yq can't do this one yet
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"
```
then
```bash
yq '.' sample.toml
```
will output
```yaml
# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
# [servers] yq can't do this one yet
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"
```
## Encode: Simple mapping produces table section
Given a sample.yml file of:
```yaml
arg:
hello: foo
```
then
```bash
yq -o toml '.' sample.yml
```
will output
```toml
[arg]
hello = "foo"
```
+65
View File
@@ -1,7 +1,12 @@
package yqlib
import (
"bufio"
"errors"
"io"
"strings"
"github.com/fatih/color"
)
type Encoder interface {
@@ -25,3 +30,63 @@ func mapKeysToStrings(node *CandidateNode) {
mapKeysToStrings(child)
}
}
// Some funcs are shared between encoder_yaml and encoder_kyaml
func PrintYAMLDocumentSeparator(writer io.Writer, PrintDocSeparators bool) error {
if PrintDocSeparators {
log.Debug("writing doc sep")
if err := writeString(writer, "---\n"); err != nil {
return err
}
}
return nil
}
func PrintYAMLLeadingContent(writer io.Writer, content string, PrintDocSeparators bool, ColorsEnabled bool) error {
reader := bufio.NewReader(strings.NewReader(content))
// reuse precompiled package-level regex
// (declared in decoder_yaml.go)
for {
readline, errReading := reader.ReadString('\n')
if errReading != nil && !errors.Is(errReading, io.EOF) {
return errReading
}
if strings.Contains(readline, "$yqDocSeparator$") {
// Preserve the original line ending (CRLF or LF)
lineEnding := "\n"
if strings.HasSuffix(readline, "\r\n") {
lineEnding = "\r\n"
}
if PrintDocSeparators {
if err := writeString(writer, "---"+lineEnding); err != nil {
return err
}
}
} else {
if len(readline) > 0 && readline != "\n" && readline[0] != '%' && !commentLineRe.MatchString(readline) {
readline = "# " + readline
}
if ColorsEnabled && strings.TrimSpace(readline) != "" {
readline = format(color.FgHiBlack) + readline + format(color.Reset)
}
if err := writeString(writer, readline); err != nil {
return err
}
}
if errors.Is(errReading, io.EOF) {
if readline != "" {
// the last comment we read didn't have a newline, put one in
if err := writeString(writer, "\n"); err != nil {
return err
}
}
break
}
}
return nil
}
+10 -11
View File
@@ -66,8 +66,8 @@ func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
finalOutput := he.injectComments(compactOutput, commentMap)
if he.prefs.ColorsEnabled {
colorized := he.colorizeHcl(finalOutput)
_, err := writer.Write(colorized)
colourized := he.colorizeHcl(finalOutput)
_, err := writer.Write(colourized)
return err
}
@@ -173,19 +173,18 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string
return []byte(result)
}
// colorizeHcl applies syntax highlighting to HCL output using fatih/color
func (he *hclEncoder) colorizeHcl(input []byte) []byte {
hcl := string(input)
result := strings.Builder{}
// Create color functions for different token types
// Create colour functions for different token types
commentColor := color.New(color.FgHiBlack).SprintFunc()
stringColor := color.New(color.FgGreen).SprintFunc()
numberColor := color.New(color.FgHiMagenta).SprintFunc()
keyColor := color.New(color.FgCyan).SprintFunc()
boolColor := color.New(color.FgHiMagenta).SprintFunc()
// Simple tokenization for HCL coloring
// Simple tokenization for HCL colouring
i := 0
for i < len(hcl) {
ch := hcl[i]
@@ -450,8 +449,8 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
return false
}
// If EncodeSeparate is set, emit children as separate blocks regardless of label extraction
if valueNode.EncodeSeparate {
// If EncodeHintSeparateBlock is set, emit children as separate blocks regardless of label extraction
if valueNode.EncodeHint == EncodeHintSeparateBlock {
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true
}
@@ -476,12 +475,12 @@ func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valu
}
}
// If all child values are mappings, treat each child key as a labeled instance of this block type
// If all child values are mappings, treat each child key as a labelled instance of this block type
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true
}
// No labels detected, render as unlabeled block
// No labels detected, render as unlabelled block
block := body.AppendNewBlock(key, nil)
if err := he.encodeNodeAttributes(block.Body(), valueNode); err == nil {
return true
@@ -538,9 +537,9 @@ func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockTy
return false, nil
}
// Only emit as separate blocks if EncodeSeparate is true
// Only emit as separate blocks if EncodeHintSeparateBlock is set
// This allows the encoder to respect the original block structure preserved by the decoder
if !valueNode.EncodeSeparate {
if valueNode.EncodeHint != EncodeHintSeparateBlock {
return false, nil
}
+318
View File
@@ -0,0 +1,318 @@
//go:build !yq_nokyaml
package yqlib
import (
"bytes"
"io"
"regexp"
"strconv"
"strings"
)
type kyamlEncoder struct {
prefs KYamlPreferences
}
func NewKYamlEncoder(prefs KYamlPreferences) Encoder {
return &kyamlEncoder{prefs: prefs}
}
func (ke *kyamlEncoder) CanHandleAliases() bool {
// KYAML is a restricted subset; avoid emitting anchors/aliases.
return false
}
func (ke *kyamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
return PrintYAMLDocumentSeparator(writer, ke.prefs.PrintDocSeparators)
}
func (ke *kyamlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
return PrintYAMLLeadingContent(writer, content, ke.prefs.PrintDocSeparators, ke.prefs.ColorsEnabled)
}
func (ke *kyamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("encoderKYaml - going to print %v", NodeToString(node))
if node.Kind == ScalarNode && ke.prefs.UnwrapScalar {
return writeString(writer, node.Value+"\n")
}
destination := writer
tempBuffer := bytes.NewBuffer(nil)
if ke.prefs.ColorsEnabled {
destination = tempBuffer
}
// Mirror the YAML encoder behaviour: trailing comments on the document root
// are stored in FootComment and need to be printed after the document.
trailingContent := node.FootComment
if err := ke.writeCommentBlock(destination, node.HeadComment, 0); err != nil {
return err
}
if err := ke.writeNode(destination, node, 0); err != nil {
return err
}
if err := ke.writeInlineComment(destination, node.LineComment); err != nil {
return err
}
if err := writeString(destination, "\n"); err != nil {
return err
}
if err := ke.PrintLeadingContent(destination, trailingContent); err != nil {
return err
}
if ke.prefs.ColorsEnabled {
return colorizeAndPrint(tempBuffer.Bytes(), writer)
}
return nil
}
func (ke *kyamlEncoder) writeNode(writer io.Writer, node *CandidateNode, indent int) error {
switch node.Kind {
case MappingNode:
return ke.writeMapping(writer, node, indent)
case SequenceNode:
return ke.writeSequence(writer, node, indent)
case ScalarNode:
return writeString(writer, ke.formatScalar(node))
case AliasNode:
// Should have been exploded by the printer, but handle defensively.
if node.Alias == nil {
return writeString(writer, "null")
}
return ke.writeNode(writer, node.Alias, indent)
default:
return writeString(writer, "null")
}
}
func (ke *kyamlEncoder) writeMapping(writer io.Writer, node *CandidateNode, indent int) error {
if len(node.Content) == 0 {
return writeString(writer, "{}")
}
if err := writeString(writer, "{\n"); err != nil {
return err
}
for i := 0; i+1 < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
entryIndent := indent + ke.prefs.Indent
if err := ke.writeCommentBlock(writer, keyNode.HeadComment, entryIndent); err != nil {
return err
}
if valueNode.HeadComment != "" && valueNode.HeadComment != keyNode.HeadComment {
if err := ke.writeCommentBlock(writer, valueNode.HeadComment, entryIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, entryIndent); err != nil {
return err
}
if err := writeString(writer, ke.formatKey(keyNode)); err != nil {
return err
}
if err := writeString(writer, ": "); err != nil {
return err
}
if err := ke.writeNode(writer, valueNode, entryIndent); err != nil {
return err
}
// Always emit a trailing comma; KYAML encourages explicit separators,
// and this ensures all quoted strings have a trailing `",` as requested.
if err := writeString(writer, ","); err != nil {
return err
}
inline := valueNode.LineComment
if inline == "" {
inline = keyNode.LineComment
}
if err := ke.writeInlineComment(writer, inline); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
foot := valueNode.FootComment
if foot == "" {
foot = keyNode.FootComment
}
if err := ke.writeCommentBlock(writer, foot, entryIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
return writeString(writer, "}")
}
func (ke *kyamlEncoder) writeSequence(writer io.Writer, node *CandidateNode, indent int) error {
if len(node.Content) == 0 {
return writeString(writer, "[]")
}
if err := writeString(writer, "[\n"); err != nil {
return err
}
for _, child := range node.Content {
itemIndent := indent + ke.prefs.Indent
if err := ke.writeCommentBlock(writer, child.HeadComment, itemIndent); err != nil {
return err
}
if err := ke.writeIndent(writer, itemIndent); err != nil {
return err
}
if err := ke.writeNode(writer, child, itemIndent); err != nil {
return err
}
if err := writeString(writer, ","); err != nil {
return err
}
if err := ke.writeInlineComment(writer, child.LineComment); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
if err := ke.writeCommentBlock(writer, child.FootComment, itemIndent); err != nil {
return err
}
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
return writeString(writer, "]")
}
func (ke *kyamlEncoder) writeIndent(writer io.Writer, indent int) error {
if indent <= 0 {
return nil
}
return writeString(writer, strings.Repeat(" ", indent))
}
func (ke *kyamlEncoder) formatKey(keyNode *CandidateNode) string {
// KYAML examples use bare keys. Quote keys only when needed.
key := keyNode.Value
if isValidKYamlBareKey(key) {
return key
}
return `"` + escapeDoubleQuotedString(key) + `"`
}
func (ke *kyamlEncoder) formatScalar(node *CandidateNode) string {
switch node.Tag {
case "!!null":
return "null"
case "!!bool":
return strings.ToLower(node.Value)
case "!!int", "!!float":
return node.Value
case "!!str":
return `"` + escapeDoubleQuotedString(node.Value) + `"`
default:
// Fall back to a string representation to avoid implicit typing surprises.
return `"` + escapeDoubleQuotedString(node.Value) + `"`
}
}
var kyamlBareKeyRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]*$`)
func isValidKYamlBareKey(s string) bool {
// Conservative: require an identifier-like key; otherwise quote.
if s == "" {
return false
}
return kyamlBareKeyRe.MatchString(s)
}
func escapeDoubleQuotedString(s string) string {
var b strings.Builder
b.Grow(len(s) + 2)
for _, r := range s {
switch r {
case '\\':
b.WriteString(`\\`)
case '"':
b.WriteString(`\"`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
if r < 0x20 {
// YAML double-quoted strings support \uXXXX escapes.
b.WriteString(`\u`)
hex := "0000" + strings.ToUpper(strconv.FormatInt(int64(r), 16))
b.WriteString(hex[len(hex)-4:])
} else {
b.WriteRune(r)
}
}
}
return b.String()
}
func (ke *kyamlEncoder) writeCommentBlock(writer io.Writer, comment string, indent int) error {
if strings.TrimSpace(comment) == "" {
return nil
}
lines := strings.Split(strings.ReplaceAll(comment, "\r\n", "\n"), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if err := ke.writeIndent(writer, indent); err != nil {
return err
}
toWrite := line
if !commentLineRe.MatchString(toWrite) {
toWrite = "# " + toWrite
}
if err := writeString(writer, toWrite); err != nil {
return err
}
if err := writeString(writer, "\n"); err != nil {
return err
}
}
return nil
}
func (ke *kyamlEncoder) writeInlineComment(writer io.Writer, comment string) error {
comment = strings.TrimSpace(strings.ReplaceAll(comment, "\r\n", "\n"))
if comment == "" {
return nil
}
lines := strings.Split(comment, "\n")
first := strings.TrimSpace(lines[0])
if first == "" {
return nil
}
if !strings.HasPrefix(first, "#") {
first = "# " + first
}
if err := writeString(writer, " "); err != nil {
return err
}
return writeString(writer, first)
}
+7 -1
View File
@@ -57,7 +57,13 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
// let's just pick a fallback key to use if we are encoding a single scalar
nonemptyPath = "value"
}
_, err := io.WriteString(*w, nonemptyPath+"="+quoteValue(node.Value)+"\n")
var valueString string
if pe.prefs.UnwrapScalar {
valueString = node.Value
} else {
valueString = quoteValue(node.Value)
}
_, err := io.WriteString(*w, nonemptyPath+"="+valueString+"\n")
return err
case SequenceNode:
for index, child := range node.Content {
+33
View File
@@ -135,3 +135,36 @@ func TestShellVariablesEncoderCustomSeparatorArray(t *testing.T) {
func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) {
assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X")
}
func assertEncodesToUnwrapped(t *testing.T, yaml string, shellvars string) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
originalUnwrapScalar := ConfiguredShellVariablesPreferences.UnwrapScalar
defer func() {
ConfiguredShellVariablesPreferences.UnwrapScalar = originalUnwrapScalar
}()
ConfiguredShellVariablesPreferences.UnwrapScalar = true
var encoder = NewShellVariablesEncoder()
inputs, err := readDocuments(strings.NewReader(yaml), "test.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
if err != nil {
panic(err)
}
node := inputs.Front().Value.(*CandidateNode)
err = encoder.Encode(writer, node)
if err != nil {
panic(err)
}
writer.Flush()
test.AssertResult(t, shellvars, strings.TrimSuffix(output.String(), "\n"))
}
func TestShellVariablesEncoderUnwrapScalar(t *testing.T) {
assertEncodesToUnwrapped(t, "a: Lewis Carroll", "a=Lewis Carroll")
assertEncodesToUnwrapped(t, "b: 123", "b=123")
assertEncodesToUnwrapped(t, "c: true", "c=true")
assertEncodesToUnwrapped(t, "d: value with spaces", "d=value with spaces")
}
+160 -70
View File
@@ -1,3 +1,5 @@
//go:build !yq_notoml
package yqlib
import (
@@ -45,8 +47,8 @@ func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
}
if te.prefs.ColorsEnabled {
colorized := te.colorizeToml(buf.Bytes())
_, err := writer.Write(colorized)
colourised := te.colorizeToml(buf.Bytes())
_, err := writer.Write(colourised)
return err
}
@@ -67,6 +69,27 @@ func (te *tomlEncoder) CanHandleAliases() bool {
// ---- helpers ----
// tomlKey returns the key quoted if it contains characters that are not valid
// in a TOML bare key. TOML bare keys may only contain ASCII letters, ASCII
// digits, underscores, and dashes.
func tomlKey(key string) string {
for _, r := range key {
if (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '_' && r != '-' {
return fmt.Sprintf("%q", key)
}
}
return key
}
// tomlDottedKey joins path components, quoting any that require it.
func tomlDottedKey(path []string) string {
parts := make([]string, len(path))
for i, p := range path {
parts[i] = tomlKey(p)
}
return strings.Join(parts, ".")
}
func (te *tomlEncoder) writeComment(w io.Writer, comment string) error {
if comment == "" {
return nil
@@ -122,6 +145,10 @@ func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error
// encodeTopLevelEntry encodes a key/value at the root, dispatching to attribute, table, or array-of-tables
func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *CandidateNode) error {
if len(path) == 0 {
return fmt.Errorf("cannot encode TOML entry with empty path")
}
switch node.Kind {
case ScalarNode:
// key = value
@@ -142,9 +169,10 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
}
if allMaps {
key := path[len(path)-1]
quotedKey := tomlKey(key)
for _, it := range node.Content {
// [[key]] then body
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
@@ -156,12 +184,9 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
// Regular array attribute
return te.writeArrayAttribute(w, path[len(path)-1], node)
case MappingNode:
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
if !node.EncodeSeparate {
// If children contain mappings or arrays of mappings, prefer separate sections
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
return te.encodeSeparateMapping(w, path, node)
}
// Use inline table syntax for nodes explicitly marked as TOML inline tables
// or YAML flow mappings. All other mappings become readable TOML table sections.
if node.EncodeHint == EncodeHintInline || node.Style&FlowStyle != 0 {
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
}
return te.encodeSeparateMapping(w, path, node)
@@ -179,7 +204,7 @@ func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateN
}
// Write the attribute
line := key + " = " + te.formatScalar(value)
line := tomlKey(key) + " = " + te.formatScalar(value)
// Add line comment if present
if value.LineComment != "" {
@@ -204,7 +229,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
// Handle empty arrays
if len(seq.Content) == 0 {
line := key + " = []"
line := tomlKey(key) + " = []"
if seq.LineComment != "" {
lineComment := strings.TrimSpace(seq.LineComment)
if !strings.HasPrefix(lineComment, "#") {
@@ -216,6 +241,81 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
return err
}
// Check if any array elements have head comments - if so, use multiline format
hasElementComments := false
for _, it := range seq.Content {
if it.HeadComment != "" {
hasElementComments = true
break
}
}
if hasElementComments {
// Write multiline array format with comments
if _, err := w.Write([]byte(tomlKey(key) + " = [\n")); err != nil {
return err
}
for i, it := range seq.Content {
// Write head comment for this element
if it.HeadComment != "" {
commentLines := strings.Split(it.HeadComment, "\n")
for _, commentLine := range commentLines {
if strings.TrimSpace(commentLine) != "" {
if !strings.HasPrefix(strings.TrimSpace(commentLine), "#") {
commentLine = "# " + commentLine
}
if _, err := w.Write([]byte(" " + commentLine + "\n")); err != nil {
return err
}
}
}
}
// Write the element value
var itemStr string
switch it.Kind {
case ScalarNode:
itemStr = te.formatScalar(it)
case SequenceNode:
nested, err := te.sequenceToInlineArray(it)
if err != nil {
return err
}
itemStr = nested
case MappingNode:
inline, err := te.mappingToInlineTable(it)
if err != nil {
return err
}
itemStr = inline
case AliasNode:
return fmt.Errorf("aliases are not supported in TOML")
default:
return fmt.Errorf("unsupported array item kind: %v", it.Kind)
}
// Always add trailing comma in multiline arrays
itemStr += ","
if _, err := w.Write([]byte(" " + itemStr + "\n")); err != nil {
return err
}
// Add blank line between elements (except after the last one)
if i < len(seq.Content)-1 {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
}
}
if _, err := w.Write([]byte("]\n")); err != nil {
return err
}
return nil
}
// Join scalars or nested arrays recursively into TOML array syntax
items := make([]string, 0, len(seq.Content))
for _, it := range seq.Content {
@@ -243,7 +343,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
}
}
line := key + " = [" + strings.Join(items, ", ") + "]"
line := tomlKey(key) + " = [" + strings.Join(items, ", ") + "]"
// Add line comment if present
if seq.LineComment != "" {
@@ -291,21 +391,21 @@ func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
v := m.Content[i+1]
switch v.Kind {
case ScalarNode:
parts = append(parts, fmt.Sprintf("%s = %s", k, te.formatScalar(v)))
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), te.formatScalar(v)))
case SequenceNode:
// inline array in inline table
arr, err := te.sequenceToInlineArray(v)
if err != nil {
return "", err
}
parts = append(parts, fmt.Sprintf("%s = %s", k, arr))
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), arr))
case MappingNode:
// nested inline table
inline, err := te.mappingToInlineTable(v)
if err != nil {
return "", err
}
parts = append(parts, fmt.Sprintf("%s = %s", k, inline))
parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), inline))
default:
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
}
@@ -318,7 +418,7 @@ func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *Can
if err != nil {
return err
}
_, err = w.Write([]byte(key + " = " + inline + "\n"))
_, err = w.Write([]byte(tomlKey(key) + " = " + inline + "\n"))
return err
}
@@ -340,7 +440,7 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
}
// Write table header [a.b.c]
header := "[" + strings.Join(path, ".") + "]\n"
header := "[" + tomlDottedKey(path) + "]\n"
_, err := w.Write([]byte(header))
return err
}
@@ -348,7 +448,8 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
// It emits the table header for this mapping if it has any content, then processes children.
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes)
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
// Inline mapping children also count as attributes since they render as key = { ... }.
hasAttrs := false
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
@@ -356,6 +457,10 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
hasAttrs = true
break
}
if v.Kind == MappingNode && (v.EncodeHint == EncodeHintInline || v.Style&FlowStyle != 0) {
hasAttrs = true
break
}
if v.Kind == SequenceNode {
// Check if it's NOT an array of tables
allMaps := true
@@ -383,18 +488,14 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
return nil
}
// No attributes, just nested structures - process children
// No attributes, just nested table structures - process children recursively
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
switch v.Kind {
case MappingNode:
// Emit [path.k]
newPath := append(append([]string{}, path...), k)
if err := te.writeTableHeader(w, newPath, v); err != nil {
return err
}
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
return err
}
case SequenceNode:
@@ -407,7 +508,7 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
}
}
if allMaps {
key := strings.Join(append(append([]string{}, path...), k), ".")
key := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err
@@ -432,39 +533,6 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
return nil
}
func (te *tomlEncoder) hasEncodeSeparateChild(m *CandidateNode) bool {
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
if v.Kind == MappingNode && v.EncodeSeparate {
return true
}
}
return false
}
func (te *tomlEncoder) hasStructuralChildren(m *CandidateNode) bool {
for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1]
// Only consider it structural if mapping has EncodeSeparate or is non-empty
if v.Kind == MappingNode && v.EncodeSeparate {
return true
}
if v.Kind == SequenceNode {
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
return true
}
}
}
return false
}
// encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context
func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error {
// First, attributes (scalars and non-map arrays)
@@ -505,7 +573,7 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
}
}
if allMaps {
dotted := strings.Join(append(append([]string{}, path...), k), ".")
dotted := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
return err
@@ -518,13 +586,21 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
}
}
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes
// Finally, child mappings: inline-hint or flow-style ones become inline table attributes,
// while all others are emitted as separate sub-table sections.
for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value
v := m.Content[i+1]
if v.Kind == MappingNode && !v.EncodeSeparate {
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
return err
if v.Kind == MappingNode {
if v.EncodeHint == EncodeHintInline || v.Style&FlowStyle != 0 {
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
return err
}
} else {
subPath := append(append([]string{}, path...), k)
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
return err
}
}
}
}
@@ -540,14 +616,28 @@ func (te *tomlEncoder) colorizeToml(input []byte) []byte {
color.NoColor = false
// Create color functions for different token types
commentColor := color.New(color.FgHiBlack).SprintFunc()
stringColor := color.New(color.FgGreen).SprintFunc()
numberColor := color.New(color.FgHiMagenta).SprintFunc()
keyColor := color.New(color.FgCyan).SprintFunc()
boolColor := color.New(color.FgHiMagenta).SprintFunc()
sectionColor := color.New(color.FgYellow, color.Bold).SprintFunc()
// Use EnableColor() to ensure colors work even when NO_COLOR env is set
commentColorObj := color.New(color.FgHiBlack)
commentColorObj.EnableColor()
stringColorObj := color.New(color.FgGreen)
stringColorObj.EnableColor()
numberColorObj := color.New(color.FgHiMagenta)
numberColorObj.EnableColor()
keyColorObj := color.New(color.FgCyan)
keyColorObj.EnableColor()
boolColorObj := color.New(color.FgHiMagenta)
boolColorObj.EnableColor()
sectionColorObj := color.New(color.FgYellow, color.Bold)
sectionColorObj.EnableColor()
// Simple tokenization for TOML coloring
commentColor := commentColorObj.SprintFunc()
stringColor := stringColorObj.SprintFunc()
numberColor := numberColorObj.SprintFunc()
keyColor := keyColorObj.SprintFunc()
boolColor := boolColorObj.SprintFunc()
sectionColor := sectionColorObj.SprintFunc()
// Simple tokenization for TOML colouring
i := 0
for i < len(toml) {
ch := toml[i]
+3 -3
View File
@@ -59,7 +59,7 @@ func (e *xmlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
return err
}
if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warning("Unable to write newline, skipping: %w", err)
log.Warningf("Unable to write newline, skipping: %v", err)
}
}
}
@@ -131,7 +131,7 @@ func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *CandidateNode
return err
}
if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warning("Unable to write newline, skipping: %w", err)
log.Warningf("Unable to write newline, skipping: %v", err)
}
} else if key.Value == e.prefs.DirectiveName {
var directive xml.Directive = []byte(value.Value)
@@ -139,7 +139,7 @@ func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *CandidateNode
return err
}
if _, err := e.writer.Write([]byte("\n")); err != nil {
log.Warning("Unable to write newline, skipping: %w", err)
log.Warningf("Unable to write newline, skipping: %v", err)
}
} else {
+6 -58
View File
@@ -1,13 +1,10 @@
package yqlib
import (
"bufio"
"bytes"
"errors"
"io"
"strings"
"github.com/fatih/color"
"go.yaml.in/yaml/v4"
)
@@ -24,67 +21,15 @@ func (ye *yamlEncoder) CanHandleAliases() bool {
}
func (ye *yamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
if ye.prefs.PrintDocSeparators {
log.Debug("writing doc sep")
if err := writeString(writer, "---\n"); err != nil {
return err
}
}
return nil
return PrintYAMLDocumentSeparator(writer, ye.prefs.PrintDocSeparators)
}
func (ye *yamlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
reader := bufio.NewReader(strings.NewReader(content))
// reuse precompiled package-level regex
// (declared in decoder_yaml.go)
for {
readline, errReading := reader.ReadString('\n')
if errReading != nil && !errors.Is(errReading, io.EOF) {
return errReading
}
if strings.Contains(readline, "$yqDocSeparator$") {
// Preserve the original line ending (CRLF or LF)
lineEnding := "\n"
if strings.HasSuffix(readline, "\r\n") {
lineEnding = "\r\n"
}
if ye.prefs.PrintDocSeparators {
if err := writeString(writer, "---"+lineEnding); err != nil {
return err
}
}
} else {
if len(readline) > 0 && readline != "\n" && readline[0] != '%' && !commentLineRe.MatchString(readline) {
readline = "# " + readline
}
if ye.prefs.ColorsEnabled && strings.TrimSpace(readline) != "" {
readline = format(color.FgHiBlack) + readline + format(color.Reset)
}
if err := writeString(writer, readline); err != nil {
return err
}
}
if errors.Is(errReading, io.EOF) {
if readline != "" {
// the last comment we read didn't have a newline, put one in
if err := writeString(writer, "\n"); err != nil {
return err
}
}
break
}
}
return nil
return PrintYAMLLeadingContent(writer, content, ye.prefs.PrintDocSeparators, ye.prefs.ColorsEnabled)
}
func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debug("encoderYaml - going to print %v", NodeToString(node))
log.Debugf("encoderYaml - going to print %v", NodeToString(node))
// Detect line ending style from LeadingContent
lineEnding := "\n"
if strings.Contains(node.LeadingContent, "\r\n") {
@@ -107,6 +52,9 @@ func (ye *yamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
var encoder = yaml.NewEncoder(destination)
encoder.SetIndent(ye.prefs.Indent)
if ye.prefs.CompactSequenceIndent {
encoder.CompactSeqIndent()
}
target, err := node.MarshalYAML()
+1 -1
View File
@@ -26,7 +26,7 @@ func newExpressionParser() ExpressionParserInterface {
}
func (p *expressionParserImpl) ParseExpression(expression string) (*ExpressionNode, error) {
log.Debug("Parsing expression: [%v]", expression)
log.Debugf("Parsing expression: [%v]", expression)
tokens, err := p.pathTokeniser.Tokenise(expression)
if err != nil {
return nil, err
+1
View File
@@ -95,6 +95,7 @@ func TestParserSingleOperation(t *testing.T) {
test.AssertResultComplex(t, nil, err)
if result == nil {
t.Fatal("Expected non-nil result for single operation")
return
}
if result.Operation == nil {
t.Fatal("Expected operation to be set")
+2 -3
View File
@@ -3,8 +3,7 @@ package yqlib
import (
"errors"
"fmt"
logging "gopkg.in/op/go-logging.v1"
"log/slog"
)
type expressionPostFixer interface {
@@ -134,7 +133,7 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
return nil, fmt.Errorf("bad expression - probably missing close bracket on %v", opStack[len(opStack)-1].toString(false))
}
if log.IsEnabledFor(logging.DEBUG) {
if log.IsEnabledFor(slog.LevelDebug) {
log.Debugf("PostFix Result:")
for _, currentToken := range result {
log.Debugf("> %v", currentToken.toString())
+11 -4
View File
@@ -7,7 +7,15 @@ import (
)
func tryRenameFile(from string, to string) error {
if renameError := os.Rename(from, to); renameError != nil {
if info, err := os.Lstat(to); err == nil && info.Mode()&os.ModeSymlink != 0 {
log.Debug("Target file is symlink, skipping rename and attempting to copy contents")
if copyError := copyFileContents(from, to); copyError != nil {
return fmt.Errorf("failed copying from %v to %v: %w", from, to, copyError)
}
tryRemoveTempFile(from)
return nil
} else if renameError := os.Rename(from, to); renameError != nil {
log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to)
log.Debug(renameError.Error())
log.Debug("going to try copying instead")
@@ -22,7 +30,7 @@ func tryRenameFile(from string, to string) error {
}
func tryRemoveTempFile(filename string) {
log.Debug("Removing temp file: %v", filename)
log.Debugf("Removing temp file: %v", filename)
removeErr := os.Remove(filename)
if removeErr != nil {
log.Errorf("Failed to remove temp file: %v", filename)
@@ -60,8 +68,7 @@ func SafelyCloseReader(reader io.Reader) {
func safelyCloseFile(file *os.File) {
err := file.Close()
if err != nil {
log.Error("Error closing file!")
log.Error(err.Error())
log.Errorf("Error closing file %v: %v", file.Name(), err)
}
}
+7
View File
@@ -22,6 +22,12 @@ var YamlFormat = &Format{"yaml", []string{"y", "yml"},
func() Decoder { return NewYamlDecoder(ConfiguredYamlPreferences) },
}
var KYamlFormat = &Format{"kyaml", []string{"ky"},
func() Encoder { return NewKYamlEncoder(ConfiguredKYamlPreferences) },
// KYaml is stricter YAML
func() Decoder { return NewYamlDecoder(ConfiguredYamlPreferences) },
}
var JSONFormat = &Format{"json", []string{"j"},
func() Encoder { return NewJSONEncoder(ConfiguredJSONPreferences) },
func() Decoder { return NewJSONDecoder() },
@@ -89,6 +95,7 @@ var INIFormat = &Format{"ini", []string{"i"},
var Formats = []*Format{
YamlFormat,
KYamlFormat,
JSONFormat,
PropertiesFormat,
CSVFormat,
+1 -1
View File
@@ -58,7 +58,7 @@ func (f *frontMatterHandlerImpl) Split() error {
return err
}
f.yamlFrontMatterFilename = yamlTempFile.Name()
log.Debug("yamlTempFile: %v", yamlTempFile.Name())
log.Debugf("yamlTempFile: %v", yamlTempFile.Name())
lineCount := 0
+49
View File
@@ -108,6 +108,55 @@ yaml: doc
fmHandler.CleanUp()
}
func TestFrontMatterFilenamePreserved(t *testing.T) {
// Regression test for https://github.com/mikefarah/yq/issues/2538
// When using --front-matter, the filename operator should return
// the original filename, not the path to the temporary file.
file := createTestFile(`---
name: john
---
Some content
`)
originalFilename := "/path/to/original/file.md"
fmHandler := NewFrontMatterHandler(file)
err := fmHandler.Split()
if err != nil {
panic(err)
}
tempFilename := fmHandler.GetYamlFrontMatterFilename()
// Register the alias (as the command code does)
SetFilenameAlias(tempFilename, originalFilename)
defer ClearFilenameAliases()
// Verify resolveFilename returns the original name
resolved := resolveFilename(tempFilename)
test.AssertResult(t, originalFilename, resolved)
// Read documents using the temp file, verify they get the original filename
reader, err := readStream(tempFilename)
if err != nil {
panic(err)
}
decoder := NewYamlDecoder(ConfiguredYamlPreferences)
docs, err := readDocuments(reader, tempFilename, 0, decoder)
if err != nil {
panic(err)
}
if docs.Len() == 0 {
t.Fatal("expected at least one document")
}
firstDoc := docs.Front().Value.(*CandidateNode)
test.AssertResult(t, originalFilename, firstDoc.filename)
tryRemoveTempFile(file)
fmHandler.CleanUp()
}
func TestFrontMatterSplitWithArray(t *testing.T) {
file := createTestFile(`[1,2,3]
---
+30
View File
@@ -4,6 +4,7 @@ package yqlib
import (
"bufio"
"bytes"
"fmt"
"testing"
@@ -543,6 +544,35 @@ func documentHclRoundTripScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("```hcl\n%v```\n\n", mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder(ConfiguredHclPreferences))))
}
func TestHclEncoderPrintDocumentSeparator(t *testing.T) {
encoder := NewHclEncoder(ConfiguredHclPreferences)
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
err := encoder.PrintDocumentSeparator(writer)
writer.Flush()
test.AssertResult(t, nil, err)
test.AssertResult(t, "", buf.String())
}
func TestHclEncoderPrintLeadingContent(t *testing.T) {
encoder := NewHclEncoder(ConfiguredHclPreferences)
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
err := encoder.PrintLeadingContent(writer, "some content")
writer.Flush()
test.AssertResult(t, nil, err)
test.AssertResult(t, "", buf.String())
}
func TestHclEncoderCanHandleAliases(t *testing.T) {
encoder := NewHclEncoder(ConfiguredHclPreferences)
test.AssertResult(t, false, encoder.CanHandleAliases())
}
func TestHclFormatScenarios(t *testing.T) {
for _, tt := range hclFormatScenarios {
testHclScenario(t, tt)
+30
View File
@@ -0,0 +1,30 @@
//go:build !yq_nokyaml
package yqlib
type KYamlPreferences struct {
Indent int
ColorsEnabled bool
PrintDocSeparators bool
UnwrapScalar bool
}
func NewDefaultKYamlPreferences() KYamlPreferences {
return KYamlPreferences{
Indent: 2,
ColorsEnabled: false,
PrintDocSeparators: true,
UnwrapScalar: true,
}
}
func (p *KYamlPreferences) Copy() KYamlPreferences {
return KYamlPreferences{
Indent: p.Indent,
ColorsEnabled: p.ColorsEnabled,
PrintDocSeparators: p.PrintDocSeparators,
UnwrapScalar: p.UnwrapScalar,
}
}
var ConfiguredKYamlPreferences = NewDefaultKYamlPreferences()
+542
View File
@@ -0,0 +1,542 @@
//go:build !yq_nokyaml
package yqlib
import (
"bufio"
"bytes"
"fmt"
"regexp"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func stripANSI(s string) string {
return ansiRe.ReplaceAllString(s, "")
}
var kyamlFormatScenarios = []formatScenario{
{
description: "Encode kyaml: plain string scalar",
subdescription: "Strings are always double-quoted in KYaml output.",
scenarioType: "encode",
indent: 2,
input: "cat\n",
expected: "\"cat\"\n",
},
{
description: "encode plain int scalar",
scenarioType: "encode",
indent: 2,
input: "12\n",
expected: "12\n",
skipDoc: true,
},
{
description: "encode plain bool scalar",
scenarioType: "encode",
indent: 2,
input: "true\n",
expected: "true\n",
skipDoc: true,
},
{
description: "encode plain null scalar",
scenarioType: "encode",
indent: 2,
input: "null\n",
expected: "null\n",
skipDoc: true,
},
{
description: "encode flow mapping and sequence",
scenarioType: "encode",
indent: 2,
input: "a: b\nc:\n - d\n",
expected: "{\n" +
" a: \"b\",\n" +
" c: [\n" +
" \"d\",\n" +
" ],\n" +
"}\n",
},
{
description: "encode non-string scalars",
scenarioType: "encode",
indent: 2,
input: "a: 12\n" +
"b: true\n" +
"c: null\n" +
"d: \"true\"\n",
expected: "{\n" +
" a: 12,\n" +
" b: true,\n" +
" c: null,\n" +
" d: \"true\",\n" +
"}\n",
},
{
description: "quote non-identifier keys",
scenarioType: "encode",
indent: 2,
input: "\"1a\": b\n\"has space\": c\n",
expected: "{\n" +
" \"1a\": \"b\",\n" +
" \"has space\": \"c\",\n" +
"}\n",
},
{
description: "escape quoted strings",
scenarioType: "encode",
indent: 2,
input: "a: \"line1\\nline2\\t\\\"q\\\"\"\n",
expected: "{\n" +
" a: \"line1\\nline2\\t\\\"q\\\"\",\n" +
"}\n",
},
{
description: "preserve comments when encoding",
scenarioType: "encode",
indent: 2,
input: "# leading\n" +
"a: 1 # a line\n" +
"# head b\n" +
"b: 2\n" +
"c:\n" +
" # head d\n" +
" - d # d line\n" +
" - e\n" +
"# trailing\n",
expected: "# leading\n" +
"{\n" +
" a: 1, # a line\n" +
" # head b\n" +
" b: 2,\n" +
" c: [\n" +
" # head d\n" +
" \"d\", # d line\n" +
" \"e\",\n" +
" ],\n" +
" # trailing\n" +
"}\n",
},
{
description: "Encode kyaml: anchors and aliases",
subdescription: "KYaml output does not support anchors/aliases; they are expanded to concrete values.",
scenarioType: "encode",
indent: 2,
input: "base: &base\n" +
" a: b\n" +
"copy: *base\n",
expected: "{\n" +
" base: {\n" +
" a: \"b\",\n" +
" },\n" +
" copy: {\n" +
" a: \"b\",\n" +
" },\n" +
"}\n",
},
{
description: "Encode kyaml: yaml to kyaml shows formatting differences",
subdescription: "KYaml uses flow-style collections (braces/brackets) and explicit commas.",
scenarioType: "encode",
indent: 2,
input: "person:\n" +
" name: John\n" +
" pets:\n" +
" - cat\n" +
" - dog\n",
expected: "{\n" +
" person: {\n" +
" name: \"John\",\n" +
" pets: [\n" +
" \"cat\",\n" +
" \"dog\",\n" +
" ],\n" +
" },\n" +
"}\n",
},
{
description: "Encode kyaml: nested lists of objects",
subdescription: "Lists and objects can be nested arbitrarily; KYaml always uses flow-style collections.",
scenarioType: "encode",
indent: 2,
input: "- name: a\n" +
" items:\n" +
" - id: 1\n" +
" tags:\n" +
" - k: x\n" +
" v: y\n" +
" - k: x2\n" +
" v: y2\n" +
" - id: 2\n" +
" tags:\n" +
" - k: z\n" +
" v: w\n",
expected: "[\n" +
" {\n" +
" name: \"a\",\n" +
" items: [\n" +
" {\n" +
" id: 1,\n" +
" tags: [\n" +
" {\n" +
" k: \"x\",\n" +
" v: \"y\",\n" +
" },\n" +
" {\n" +
" k: \"x2\",\n" +
" v: \"y2\",\n" +
" },\n" +
" ],\n" +
" },\n" +
" {\n" +
" id: 2,\n" +
" tags: [\n" +
" {\n" +
" k: \"z\",\n" +
" v: \"w\",\n" +
" },\n" +
" ],\n" +
" },\n" +
" ],\n" +
" },\n" +
"]\n",
},
}
func testKYamlScenario(t *testing.T, s formatScenario) {
prefs := ConfiguredKYamlPreferences.Copy()
prefs.Indent = s.indent
prefs.UnwrapScalar = false
switch s.scenarioType {
case "encode":
test.AssertResultWithContext(
t,
s.expected,
mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewKYamlEncoder(prefs)),
s.description,
)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentKYamlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
s := i.(formatScenario)
if s.skipDoc {
return
}
switch s.scenarioType {
case "encode":
documentKYamlEncodeScenario(w, s)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentKYamlEncodeScenario(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 = "."
}
if s.indent == 2 {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=kyaml '%v' sample.yml\n```\n", expression))
} else {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=kyaml -I=%v '%v' sample.yml\n```\n", s.indent, expression))
}
writeOrPanic(w, "will output\n")
prefs := ConfiguredKYamlPreferences.Copy()
prefs.Indent = s.indent
prefs.UnwrapScalar = false
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewKYamlEncoder(prefs))))
}
func TestKYamlFormatScenarios(t *testing.T) {
for _, s := range kyamlFormatScenarios {
testKYamlScenario(t, s)
}
genericScenarios := make([]interface{}, len(kyamlFormatScenarios))
for i, s := range kyamlFormatScenarios {
genericScenarios[i] = s
}
documentScenarios(t, "usage", "kyaml", genericScenarios, documentKYamlScenario)
}
func TestKYamlEncoderPrintDocumentSeparator(t *testing.T) {
t.Run("enabled", func(t *testing.T) {
prefs := NewDefaultKYamlPreferences()
prefs.PrintDocSeparators = true
var buf bytes.Buffer
err := NewKYamlEncoder(prefs).PrintDocumentSeparator(&buf)
if err != nil {
t.Fatal(err)
}
if buf.String() != "---\n" {
t.Fatalf("expected doc separator, got %q", buf.String())
}
})
t.Run("disabled", func(t *testing.T) {
prefs := NewDefaultKYamlPreferences()
prefs.PrintDocSeparators = false
var buf bytes.Buffer
err := NewKYamlEncoder(prefs).PrintDocumentSeparator(&buf)
if err != nil {
t.Fatal(err)
}
if buf.String() != "" {
t.Fatalf("expected no output, got %q", buf.String())
}
})
}
func TestKYamlEncoderEncodeUnwrapScalar(t *testing.T) {
prefs := NewDefaultKYamlPreferences()
prefs.UnwrapScalar = true
var buf bytes.Buffer
err := NewKYamlEncoder(prefs).Encode(&buf, &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "cat",
})
if err != nil {
t.Fatal(err)
}
if buf.String() != "cat\n" {
t.Fatalf("expected unwrapped scalar, got %q", buf.String())
}
}
func TestKYamlEncoderEncodeColorsEnabled(t *testing.T) {
prefs := NewDefaultKYamlPreferences()
prefs.UnwrapScalar = false
prefs.ColorsEnabled = true
var buf bytes.Buffer
err := NewKYamlEncoder(prefs).Encode(&buf, &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "a"},
{Kind: ScalarNode, Tag: "!!str", Value: "b"},
},
})
if err != nil {
t.Fatal(err)
}
out := stripANSI(buf.String())
if !strings.Contains(out, "a:") || !strings.Contains(out, "\"b\"") {
t.Fatalf("expected colourised output to contain rendered tokens, got %q", out)
}
}
func TestKYamlEncoderWriteNodeAliasAndUnknown(t *testing.T) {
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
t.Run("alias_nil", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{Kind: AliasNode}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "null" {
t.Fatalf("expected null for nil alias, got %q", buf.String())
}
})
t.Run("alias_value", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{
Kind: AliasNode,
Alias: &CandidateNode{Kind: ScalarNode, Tag: "!!int", Value: "12"},
}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "12" {
t.Fatalf("expected dereferenced alias value, got %q", buf.String())
}
})
t.Run("unknown_kind", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{Kind: Kind(12345)}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "null" {
t.Fatalf("expected null for unknown kind, got %q", buf.String())
}
})
}
func TestKYamlEncoderEmptyCollections(t *testing.T) {
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
t.Run("empty_mapping", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{Kind: MappingNode}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "{}" {
t.Fatalf("expected empty mapping, got %q", buf.String())
}
})
t.Run("empty_sequence", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{Kind: SequenceNode}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "[]" {
t.Fatalf("expected empty sequence, got %q", buf.String())
}
})
}
func TestKYamlEncoderScalarFallbackAndEscaping(t *testing.T) {
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
t.Run("unknown_tag_falls_back_to_string", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{Kind: ScalarNode, Tag: "!!timestamp", Value: "2020-01-01T00:00:00Z"}, 0)
if err != nil {
t.Fatal(err)
}
if buf.String() != "\"2020-01-01T00:00:00Z\"" {
t.Fatalf("expected quoted fallback, got %q", buf.String())
}
})
t.Run("escape_double_quoted", func(t *testing.T) {
got := escapeDoubleQuotedString("a\\b\"c\n\r\t" + string(rune(0x01)))
want := "a\\\\b\\\"c\\n\\r\\t\\u0001"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
})
t.Run("valid_bare_key", func(t *testing.T) {
if isValidKYamlBareKey("") {
t.Fatalf("expected empty string to be invalid")
}
if isValidKYamlBareKey("1a") {
t.Fatalf("expected leading digit to be invalid")
}
if !isValidKYamlBareKey("a_b-2") {
t.Fatalf("expected identifier-like key to be valid")
}
})
}
func TestKYamlEncoderCommentsInMapping(t *testing.T) {
prefs := NewDefaultKYamlPreferences()
prefs.UnwrapScalar = false
ke := NewKYamlEncoder(prefs).(*kyamlEncoder)
var buf bytes.Buffer
err := ke.writeNode(&buf, &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{
{
Kind: ScalarNode,
Tag: "!!str",
Value: "a",
HeadComment: "key head",
LineComment: "key line",
FootComment: "key foot",
},
{
Kind: ScalarNode,
Tag: "!!str",
Value: "b",
HeadComment: "value head",
},
},
}, 0)
if err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "# key head\n") {
t.Fatalf("expected key head comment, got %q", out)
}
if !strings.Contains(out, "# value head\n") {
t.Fatalf("expected value head comment, got %q", out)
}
if !strings.Contains(out, ", # key line\n") {
t.Fatalf("expected inline key comment fallback, got %q", out)
}
if !strings.Contains(out, "# key foot\n") {
t.Fatalf("expected foot comment fallback, got %q", out)
}
}
func TestKYamlEncoderCommentBlockAndInlineComment(t *testing.T) {
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
t.Run("comment_block_prefixing_and_crlf", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeCommentBlock(&buf, "line1\r\n\r\n# already\r\nline2", 2)
if err != nil {
t.Fatal(err)
}
want := " # line1\n # already\n # line2\n"
if buf.String() != want {
t.Fatalf("expected %q, got %q", want, buf.String())
}
})
t.Run("inline_comment_prefix_and_first_line_only", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeInlineComment(&buf, "hello\r\nsecond line")
if err != nil {
t.Fatal(err)
}
if buf.String() != " # hello" {
t.Fatalf("expected %q, got %q", " # hello", buf.String())
}
})
t.Run("inline_comment_already_prefixed", func(t *testing.T) {
var buf bytes.Buffer
err := ke.writeInlineComment(&buf, "# hello")
if err != nil {
t.Fatal(err)
}
if buf.String() != " # hello" {
t.Fatalf("expected %q, got %q", " # hello", buf.String())
}
})
}
+6 -1
View File
@@ -105,7 +105,7 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
skipNextToken = false
currentToken := tokens[index]
log.Debug("processing %v", currentToken.toString(true))
log.Debugf("processing %v", currentToken.toString(true))
if currentToken.TokenType == traverseArrayCollect {
// `.[exp]`` works by creating a traversal array of [self, exp] and piping that into the traverse array operator
@@ -131,6 +131,11 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
log.Debugf("previous token is : traverseArrayOpType")
// need to put the number 0 before this token, as that is implied
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
} else if index >= 2 && tokens[index-1].TokenType == openCollect &&
(tokens[index-2].TokenType == operationToken || tokens[index-2].TokenType == closeCollect || tokens[index-2].TokenType == closeCollectObject) {
log.Debugf("previous token is : openCollect following a traversal, implying 0 start")
// need to put the number 0 before this token, as that is implied
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
}
}
+8 -5
View File
@@ -96,6 +96,8 @@ var participleYqRules = []*participleYqRule{
simpleOp("load_?str|str_?load", loadStringOpType),
{"LoadYaml", `load`, loadOp(NewYamlDecoder(LoadYamlPreferences)), 0},
simpleOp("system", systemOpType),
{"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0},
simpleOp("select", selectOpType),
@@ -283,7 +285,7 @@ func pathToken(wrapped bool) yqAction {
if wrapped {
value = unwrap(value)
}
log.Debug("PathToken %v", value)
log.Debugf("PathToken %v", value)
op := &Operation{OperationType: traversePathOpType, Value: value, StringValue: value, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op, CheckForPostTraverse: true}, nil
}
@@ -336,7 +338,7 @@ func flattenWithDepth() yqAction {
func assignAllCommentsOp(updateAssign bool) yqAction {
return func(rawToken lexer.Token) (*token, error) {
log.Debug("assignAllCommentsOp %v", rawToken.Value)
log.Debugf("assignAllCommentsOp %v", rawToken.Value)
value := rawToken.Value
op := &Operation{
OperationType: assignCommentOpType,
@@ -351,7 +353,7 @@ func assignAllCommentsOp(updateAssign bool) yqAction {
func assignOpToken(updateAssign bool) yqAction {
return func(rawToken lexer.Token) (*token, error) {
log.Debug("assignOpToken %v", rawToken.Value)
log.Debugf("assignOpToken %v", rawToken.Value)
value := rawToken.Value
prefs := assignPreferences{DontOverWriteAnchor: true}
if strings.Contains(value, "c") {
@@ -376,9 +378,9 @@ func nullValue() yqAction {
func stringValue() yqAction {
return func(rawToken lexer.Token) (*token, error) {
log.Debug("rawTokenvalue: %v", rawToken.Value)
log.Debugf("rawTokenvalue: %v", rawToken.Value)
value := unwrap(rawToken.Value)
log.Debug("unwrapped: %v", value)
log.Debugf("unwrapped: %v", value)
value = processEscapeCharacters(value)
return &token{TokenType: operationToken, Operation: &Operation{
OperationType: stringInterpolationOpType,
@@ -451,6 +453,7 @@ func multiplyWithPrefs(op *operationType) yqAction {
prefs.AssignPrefs.ClobberCustomTags = true
}
prefs.TraversePrefs.DontFollowAlias = true
prefs.TraversePrefs.ExactKeyMatch = true
op := &Operation{OperationType: op, Value: multiplyOpType.Type, StringValue: options, Preferences: prefs}
return &token{TokenType: operationToken, Operation: op}, nil
}
+9 -10
View File
@@ -4,11 +4,10 @@ package yqlib
import (
"container/list"
"fmt"
"log/slog"
"math"
"strconv"
"strings"
logging "gopkg.in/op/go-logging.v1"
)
var ExpressionParser ExpressionParserInterface
@@ -19,17 +18,17 @@ func InitExpressionParser() {
}
}
var log = logging.MustGetLogger("yq-lib")
var log = newLogger()
var PrettyPrintExp = `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`
// GetLogger returns the yq logger instance.
func GetLogger() *logging.Logger {
func GetLogger() *Logger {
return log
}
func getContentValueByKey(content []*CandidateNode, key string) *CandidateNode {
for index := 0; index < len(content); index = index + 2 {
for index := 0; index < len(content)-1; index = index + 2 {
keyNode := content[index]
valueNode := content[index+1]
if keyNode.Value == key {
@@ -81,7 +80,7 @@ func recurseNodeObjectEqual(lhs *CandidateNode, rhs *CandidateNode) bool {
key := lhs.Content[index]
value := lhs.Content[index+1]
indexInRHS := findInArray(rhs, key)
indexInRHS := findKeyInMap(rhs, key)
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
return false
@@ -251,7 +250,7 @@ func processEscapeCharacters(original string) string {
value := result.String()
if value != original {
log.Debug("processEscapeCharacters from [%v] to [%v]", original, value)
log.Debugf("processEscapeCharacters from [%v] to [%v]", original, value)
}
return value
}
@@ -274,7 +273,7 @@ func footComment(node *CandidateNode) string {
// use for debugging only
func NodesToString(collection *list.List) string {
if !log.IsEnabledFor(logging.DEBUG) {
if !log.IsEnabledFor(slog.LevelDebug) {
return ""
}
@@ -286,7 +285,7 @@ func NodesToString(collection *list.List) string {
}
func NodeToString(node *CandidateNode) string {
if !log.IsEnabledFor(logging.DEBUG) {
if !log.IsEnabledFor(slog.LevelDebug) {
return ""
}
if node == nil {
@@ -304,7 +303,7 @@ func NodeToString(node *CandidateNode) string {
}
func NodeContentToString(node *CandidateNode, depth int) string {
if !log.IsEnabledFor(logging.DEBUG) {
if !log.IsEnabledFor(slog.LevelDebug) {
return ""
}
var sb strings.Builder
+19 -1
View File
@@ -24,7 +24,7 @@ type parseSnippetScenario struct {
var parseSnippetScenarios = []parseSnippetScenario{
{
snippet: ":",
expectedError: "yaml: did not find expected key",
expectedError: "yaml: while parsing a block mapping at <unknown position>: did not find expected key",
},
{
snippet: "",
@@ -300,6 +300,24 @@ func TestRecurseNodeObjectEqual(t *testing.T) {
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
// A null key must not match a null value in the other map.
// Regression test for https://issues.oss-fuzz.com/issues/383860504
nullKey := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
nullVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
intKey := createScalarNode(2, "2")
intKey.Tag = "!!int"
intVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
mapWithNullKey := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{nullKey, nullVal},
}
mapWithIntKey := &CandidateNode{
Kind: MappingNode,
Content: []*CandidateNode{intKey, intVal},
}
test.AssertResult(t, false, recurseNodeObjectEqual(mapWithNullKey, mapWithIntKey))
}
func TestParseInt(t *testing.T) {
+77
View File
@@ -0,0 +1,77 @@
package yqlib
import (
"fmt"
"log/slog"
"os"
)
// Logger wraps log/slog providing a printf-style interface used throughout yq.
type Logger struct {
levelVar slog.LevelVar
slogger *slog.Logger
}
func newLogger() *Logger {
l := &Logger{}
l.levelVar.Set(slog.LevelWarn)
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: &l.levelVar})
l.slogger = slog.New(handler)
return l
}
// SetLevel sets the minimum log level.
func (l *Logger) SetLevel(level slog.Level) {
l.levelVar.Set(level)
}
// GetLevel returns the current log level.
func (l *Logger) GetLevel() slog.Level {
return l.levelVar.Level()
}
// IsEnabledFor returns true if the given level is enabled.
func (l *Logger) IsEnabledFor(level slog.Level) bool {
return l.levelVar.Level() <= level
}
// SetSlogger replaces the underlying slog.Logger (e.g. to configure output format).
func (l *Logger) SetSlogger(logger *slog.Logger) {
l.slogger = logger
}
func (l *Logger) Debug(msg string) {
if l.IsEnabledFor(slog.LevelDebug) {
l.slogger.Debug(msg)
}
}
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.IsEnabledFor(slog.LevelDebug) {
l.slogger.Debug(fmt.Sprintf(format, args...))
}
}
func (l *Logger) Info(msg string) {
l.slogger.Info(msg)
}
func (l *Logger) Infof(format string, args ...interface{}) {
l.slogger.Info(fmt.Sprintf(format, args...))
}
func (l *Logger) Warning(msg string) {
l.slogger.Warn(msg)
}
func (l *Logger) Warningf(format string, args ...interface{}) {
l.slogger.Warn(fmt.Sprintf(format, args...))
}
func (l *Logger) Error(msg string) {
l.slogger.Error(msg)
}
func (l *Logger) Errorf(format string, args ...interface{}) {
l.slogger.Error(fmt.Sprintf(format, args...))
}
+7
View File
@@ -0,0 +1,7 @@
//go:build yq_nokyaml
package yqlib
func NewKYamlEncoder(_ KYamlPreferences) Encoder {
return nil
}
+8
View File
@@ -5,3 +5,11 @@ package yqlib
func NewTomlDecoder() Decoder {
return nil
}
func NewTomlEncoder() Encoder {
return nil
}
func NewTomlEncoderWithPrefs(prefs TomlPreferences) Encoder {
return nil
}
+3 -1
View File
@@ -25,7 +25,7 @@ var valueToStringFunc = func(p *Operation) string {
}
func createValueOperation(value interface{}, stringValue string) *Operation {
log.Debug("creating value op for string %v", stringValue)
log.Debugf("creating value op for string %v", stringValue)
var node = createScalarNode(value, stringValue)
return &Operation{
@@ -164,6 +164,8 @@ var stringInterpolationOpType = &operationType{Type: "STRING_INT", NumArgs: 0, P
var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 52, Handler: loadOperator, CheckForPostTraverse: true}
var loadStringOpType = &operationType{Type: "LOAD_STRING", NumArgs: 1, Precedence: 52, Handler: loadStringOperator}
var systemOpType = &operationType{Type: "SYSTEM", NumArgs: 1, Precedence: 50, Handler: systemOperator}
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 52, Handler: keysOperator, CheckForPostTraverse: true}
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}
+2 -2
View File
@@ -195,9 +195,9 @@ func addMaps(target *CandidateNode, lhsC *CandidateNode, rhsC *CandidateNode) {
for index := 0; index < len(rhs.Content); index = index + 2 {
key := rhs.Content[index]
value := rhs.Content[index+1]
log.Debug("finding %v", key.Value)
log.Debugf("finding %v", key.Value)
indexInLHS := findKeyInMap(target, key)
log.Debug("indexInLhs %v", indexInLHS)
log.Debugf("indexInLhs %v", indexInLHS)
if indexInLHS < 0 {
// not in there, append it
target.AddKeyValueChild(key, value)
+12
View File
@@ -527,6 +527,18 @@ var addOperatorScenarios = []expressionScenario{
expression: `.a += [2]`,
expectedError: "!!seq () cannot be added to a !!str (a)",
},
{
// Regression test for https://issues.oss-fuzz.com/issues/383860504
// Adding a map to itself must not panic when sequence keys contain
// single-entry mappings with a null key in one and a non-null key
// in the other.
skipDoc: true,
document: "? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2",
expression: `. += .`,
expected: []string{
"D0, P[], (!!map)::? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2\n",
},
},
}
func TestAddOperatorScenarios(t *testing.T) {
+2 -2
View File
@@ -256,7 +256,7 @@ func explodeNode(node *CandidateNode, context Context) error {
node.Value = node.Alias.Value
node.Alias = nil
}
log.Debug("now I'm %v", NodeToString(node))
log.Debugf("now I'm %v", NodeToString(node))
return nil
case MappingNode:
// //check the map has an alias in it
@@ -304,7 +304,7 @@ func applyAlias(node *CandidateNode, alias *CandidateNode, aliasIndex int, newCo
if alias == nil {
return nil
}
log.Debug("alias: %v", NodeToString(alias))
log.Debugf("alias: %v", NodeToString(alias))
if alias.Kind != MappingNode {
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", alias.Tag)
}
+6 -6
View File
@@ -4,7 +4,7 @@ import (
"testing"
)
var specDocument = `- &CENTER { x: 1, y: 2 }
var specDocument = `- &CENTRE { x: 1, y: 2 }
- &LEFT { x: 0, y: 2 }
- &BIG { r: 10 }
- &SMALL { r: 1 }
@@ -139,7 +139,7 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
{
description: "FIXED: Merge multiple maps",
subdescription: "Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.",
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
document: specDocument + "- << : [ *CENTRE, *BIG ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"},
},
@@ -171,7 +171,7 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
},
{
skipDoc: true,
description: "Exploding merge anchor should not explode neighbors",
description: "Exploding merge anchor should not explode neighbours",
subdescription: "b must not be exploded, as `r: *a` will become invalid",
document: `{b: &b {a: &a 42}, r: *a, c: {<<: *b}}`,
expression: `explode(.c)`,
@@ -181,7 +181,7 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
},
{
skipDoc: true,
description: "Exploding sequence merge anchor should not explode neighbors",
description: "Exploding sequence merge anchor should not explode neighbours",
subdescription: "b must not be exploded, as `r: *a` will become invalid",
document: `{b: &b {a: &a 42}, r: *a, c: {<<: [*b]}}`,
expression: `explode(.c)`,
@@ -265,7 +265,7 @@ var badAnchorOperatorScenarios = []expressionScenario{
{
description: "LEGACY: Merge multiple maps",
subdescription: "see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.",
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
document: specDocument + "- << : [ *CENTRE, *BIG ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
@@ -297,7 +297,7 @@ var anchorOperatorScenarios = []expressionScenario{
{
description: "Merge one map",
subdescription: "see https://yaml.org/type/merge.html",
document: specDocument + "- << : *CENTER\n r: 10\n",
document: specDocument + "- << : *CENTRE\n r: 10\n",
expression: ".[4] | explode(.)",
expected: []string{expectedSpecResult},
},
+1 -1
View File
@@ -37,7 +37,7 @@ func assignUpdateOperator(d *dataTreeNavigator, context Context, expressionNode
prefs := getAssignPreferences(expressionNode.Operation.Preferences)
log.Debug("assignUpdateOperator prefs: %v", prefs)
log.Debugf("assignUpdateOperator prefs: %v", prefs)
if !expressionNode.Operation.UpdateAssign {
// this works because we already ran against LHS with an editable context.
+1 -1
View File
@@ -142,7 +142,7 @@ func notOperator(_ *dataTreeNavigator, context Context, _ *ExpressionNode) (Cont
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debug("notOperation checking %v", candidate)
log.Debugf("notOperation checking %v", candidate)
truthy := isTruthyNode(candidate)
result := createBooleanCandidate(candidate, !truthy)
results.PushBack(result)
+2 -2
View File
@@ -46,9 +46,9 @@ func containsObject(lhs *CandidateNode, rhs *CandidateNode) (bool, error) {
rhsKey := rhs.Content[index]
rhsValue := rhs.Content[index+1]
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
lhsKeyIndex := findInArray(lhs, rhsKey)
lhsKeyIndex := findKeyInMap(lhs, rhsKey)
log.Debugf("index is %v", lhsKeyIndex)
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 {
if lhsKeyIndex < 0 {
return false, nil
}
lhsValue := lhs.Content[lhsKeyIndex+1]
+10
View File
@@ -65,6 +65,16 @@ var containsOperatorScenarios = []expressionScenario{
"D0, P[], (!!bool)::false\n",
},
},
{
// Regression: findInArray could match a null key against a null
// value at an earlier odd index, producing a false negative.
skipDoc: true,
document: "? 1\n: ~\n? ~\n: x",
expression: `contains({~: "x"})`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "String contains substring",
document: `"foobar"`,

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