Compare commits

...

24 Commits

Author SHA1 Message Date
dependabot[bot]
16e4df2304
Bump docker/login-action from 3 to 4 (#2620)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 13:47:56 +11:00
Mike Farah
88a31ae8c6 updating release notes 2026-02-14 18:43:51 +11:00
Mike Farah
5a7e72a743 Bumping version 2026-02-14 18:43:09 +11:00
Mike Farah
562531d936 Dropping windows/arm 2026-02-14 18:42:31 +11:00
Mike Farah
2c471b6498 Bumping version 2026-02-14 11:51:00 +11:00
Mike Farah
f4ef6ef3cf Release notes 2026-02-14 11:50:51 +11:00
dependabot[bot]
f49f2bd2d8
Bump golang.org/x/mod from 0.31.0 to 0.33.0 (#2606)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.31.0 to 0.33.0.
- [Commits](https://github.com/golang/mod/compare/v0.31.0...v0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 11:42:40 +11:00
dependabot[bot]
6ccc7b7797
Bump golang.org/x/net from 0.49.0 to 0.50.0 (#2604)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 11:32:58 +11:00
dependabot[bot]
b3e1fbb7d1
Bump golang from 1.25.6 to 1.26.0 (#2603)
Bumps golang from 1.25.6 to 1.26.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 11:32:47 +11:00
Mike Farah
288ca2d114
Fixing comments in TOML arrays #2592 (#2595) 2026-02-03 19:42:49 +11:00
Mike Farah
eb04fa87af More tests 2026-02-01 10:27:18 +11:00
Mike Farah
2be0094729 Bumping version 2026-02-01 09:19:54 +11:00
Mike Farah
3c18d5b035 Preparing release 2026-02-01 09:19:45 +11:00
Mike Farah
2dcc2293da Merge branch 'tomers-fix/toml-comments-table-scope-2588' 2026-02-01 09:14:32 +11:00
Mike Farah
eb4fde4ef8 Pulling out common code 2026-02-01 09:14:18 +11:00
Mike Farah
06ea4cf62e Fixing spelling 2026-02-01 09:10:48 +11:00
Mike Farah
37089d24af Merge branch 'fix/toml-comments-table-scope-2588' of github.com:tomers/yq into tomers-fix/toml-comments-table-scope-2588 2026-02-01 09:08:20 +11:00
Slava Ezhkin
7cf88a0291
Add regression test for go install compatibility #2587 (#2591) 2026-02-01 09:01:53 +11:00
Mike Farah
41adc1ad18 Fixing wrongly named instructions file 2026-02-01 08:53:12 +11:00
Tomer Shalev
b4b96f2a68 Fix TOML table parsing after standalone comments
Standalone TOML comments immediately inside a table/array-table no longer end the table scope, preventing subsequent keys from being flattened to the document root.
2026-01-31 14:41:30 +02:00
Mike Farah
2824d66a65 Multiply uses a readonly context #2558 2026-01-31 16:47:58 +11:00
Mike Farah
4bbffa9022 Fixed merge globbing wildcards in keys #2564 2026-01-31 15:44:50 +11:00
Mike Farah
bdeedbd275 Fixing TOML subarray parsing issue #2581 2026-01-31 15:25:11 +11:00
25 changed files with 1026 additions and 97 deletions

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: all
@ -31,13 +31,13 @@ jobs:
run: echo ${{ steps.buildx.outputs.platforms }} && docker version
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

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

View File

@ -1,4 +1,4 @@
FROM golang:1.25.6 AS builder
FROM golang:1.26.0 AS builder
WORKDIR /go/src/mikefarah/yq

View File

@ -1,4 +1,4 @@
FROM golang:1.25.6
FROM golang:1.26.0
RUN apt-get update && \
apt-get install -y npm && \

View File

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

View File

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

10
go.mod
View File

@ -20,8 +20,9 @@ require (
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.49.0
golang.org/x/text v0.33.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.50.0
golang.org/x/text v0.34.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
)
@ -33,10 +34,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.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/tools v0.41.0 // indirect
)
go 1.24.0

20
go.sum
View File

@ -70,19 +70,19 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmB
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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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=

24
go_install_test.go Normal file
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)
}
}

View File

@ -47,6 +47,18 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
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 {
@ -145,13 +157,30 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
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)
}
@ -329,20 +358,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)
@ -405,19 +453,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 +511,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
}

View File

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

View File

@ -329,3 +329,58 @@ 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"
```

View File

@ -222,6 +222,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(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 {

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)

View File

@ -451,6 +451,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
}

View File

@ -30,7 +30,7 @@ func multiplyAssignOperator(d *dataTreeNavigator, context Context, expressionNod
func multiplyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("MultiplyOperator")
return crossFunction(d, context, expressionNode, multiply(expressionNode.Operation.Preferences.(multiplyPreferences)), false)
return crossFunction(d, context.ReadOnlyClone(), expressionNode, multiply(expressionNode.Operation.Preferences.(multiplyPreferences)), false)
}
func getComments(lhs *CandidateNode, rhs *CandidateNode) (leadingContent string, headComment string, footComment string) {
@ -168,7 +168,7 @@ func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs
// only need to recurse the array if we are doing a deep merge
prefs := recursiveDescentPreferences{RecurseArray: preferences.DeepMergeArrays,
TraversePreferences: traversePreferences{DontFollowAlias: true, IncludeMapKeys: true}}
TraversePreferences: traversePreferences{DontFollowAlias: true, IncludeMapKeys: true, ExactKeyMatch: true}}
log.Debugf("merge - preferences.DeepMergeArrays %v", preferences.DeepMergeArrays)
log.Debugf("merge - preferences.AppendArrays %v", preferences.AppendArrays)
err := recursiveDecent(results, context.SingleChildContext(rhs), prefs)

View File

@ -86,7 +86,35 @@ c:
<<: *cat
`
var mergeWithGlobA = `
"**cat": things,
"meow**cat": stuff
`
var mergeWithGlobB = `
"**cat": newThings,
`
var multiplyOperatorScenarios = []expressionScenario{
{
description: "multiple should be readonly",
skipDoc: true,
document: "",
expression: ".x |= (root | (.a * .b))",
expected: []string{
"D0, P[], ()::x: null\n",
},
},
{
description: "glob keys are treated as literals when merging",
skipDoc: true,
document: mergeWithGlobA,
document2: mergeWithGlobB,
expression: `select(fi == 0) * select(fi == 1)`,
expected: []string{
"D0, P[], (!!map)::\n\"**cat\": newThings,\n\"meow**cat\": stuff\n",
},
},
{
skipDoc: true,
document: mergeArrayWithAnchors,

View File

@ -14,6 +14,7 @@ type traversePreferences struct {
DontAutoCreate bool // by default, we automatically create entries on the fly.
DontIncludeMapValues bool
OptionalTraverse bool // e.g. .adf?
ExactKeyMatch bool // by default we let wild/glob patterns. Don't do that for merge though.
}
func splat(context Context, prefs traversePreferences) (Context, error) {
@ -216,7 +217,11 @@ func traverseArrayWithIndices(node *CandidateNode, indices []*CandidateNode, pre
return newMatches, nil
}
func keyMatches(key *CandidateNode, wantedKey string) bool {
func keyMatches(key *CandidateNode, wantedKey string, exactKeyMatch bool) bool {
if exactKeyMatch {
// this is used for merge
return key.Value == wantedKey
}
return matchKey(key.Value, wantedKey)
}
@ -303,7 +308,7 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, node *CandidateNode, wante
return err
}
}
} else if splat || keyMatches(key, wantedKey) {
} else if splat || keyMatches(key, wantedKey, prefs.ExactKeyMatch) {
log.Debug("MATCHED")
if prefs.IncludeMapKeys {
log.Debug("including key")

View File

@ -49,3 +49,179 @@ func TestNodeInfoPrinter_PrintResults(t *testing.T) {
test.AssertResult(t, true, strings.Contains(outStr, "footComment: foot"))
test.AssertResult(t, true, strings.Contains(outStr, "anchor: anchor"))
}
func TestNodeInfoPrinter_PrintedAnything_True(t *testing.T) {
node := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "test",
}
listNodes := list.New()
listNodes.PushBack(node)
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
// Before printing, should be false
test.AssertResult(t, false, printer.PrintedAnything())
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
// After printing, should be true
test.AssertResult(t, true, printer.PrintedAnything())
}
func TestNodeInfoPrinter_PrintedAnything_False(t *testing.T) {
listNodes := list.New()
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
// No nodes printed, should still be false
test.AssertResult(t, false, printer.PrintedAnything())
}
func TestNodeInfoPrinter_SetNulSepOutput(_ *testing.T) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
// Should not panic or error
printer.SetNulSepOutput(true)
printer.SetNulSepOutput(false)
}
func TestNodeInfoPrinter_SetAppendix(t *testing.T) {
node := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "test",
}
listNodes := list.New()
listNodes.PushBack(node)
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
appendixText := "This is appendix text\n"
appendixReader := strings.NewReader(appendixText)
printer.SetAppendix(appendixReader)
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
outStr := output.String()
test.AssertResult(t, true, strings.Contains(outStr, "test"))
test.AssertResult(t, true, strings.Contains(outStr, appendixText))
}
func TestNodeInfoPrinter_MultipleNodes(t *testing.T) {
node1 := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "first",
}
node2 := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "second",
}
listNodes := list.New()
listNodes.PushBack(node1)
listNodes.PushBack(node2)
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
outStr := output.String()
test.AssertResult(t, true, strings.Contains(outStr, "value: first"))
test.AssertResult(t, true, strings.Contains(outStr, "value: second"))
}
func TestNodeInfoPrinter_SequenceNode(t *testing.T) {
node := &CandidateNode{
Kind: SequenceNode,
Tag: "!!seq",
Style: FlowStyle,
}
listNodes := list.New()
listNodes.PushBack(node)
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
outStr := output.String()
test.AssertResult(t, true, strings.Contains(outStr, "kind: SequenceNode"))
test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!seq'"))
test.AssertResult(t, true, strings.Contains(outStr, "style: FlowStyle"))
}
func TestNodeInfoPrinter_MappingNode(t *testing.T) {
node := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
}
listNodes := list.New()
listNodes.PushBack(node)
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
outStr := output.String()
test.AssertResult(t, true, strings.Contains(outStr, "kind: MappingNode"))
test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!map'"))
}
func TestNodeInfoPrinter_EmptyList(t *testing.T) {
listNodes := list.New()
var output bytes.Buffer
writer := bufio.NewWriter(&output)
printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer))
err := printer.PrintResults(listNodes)
writer.Flush()
if err != nil {
t.Fatalf("PrintResults error: %v", err)
}
test.AssertResult(t, "", output.String())
test.AssertResult(t, false, printer.PrintedAnything())
}

View File

@ -228,31 +228,64 @@ B = 12
name = "Tom" # name comment
`
// var sampleFromWeb = `
// # This is a TOML document
// Reproduce bug for https://github.com/mikefarah/yq/issues/2588
// Bug: standalone comments inside a table cause subsequent key-values to be assigned at root.
var issue2588RustToolchainWithComments = `[owner]
# comment
name = "Tomer"
`
// title = "TOML Example"
var tableWithComment = `[owner]
# comment
[things]
`
// [owner]
// name = "Tom Preston-Werner"
// dob = 1979-05-27T07:32:00-08:00
var sampleFromWeb = `# This is a TOML document
title = "TOML Example"
// [database]
// enabled = true
// ports = [8000, 8001, 8002]
// data = [["delta", "phi"], [3.14]]
// temp_targets = { cpu = 79.5, case = 72.0 }
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
// [servers]
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
// [servers.alpha]
// ip = "10.0.0.1"
// role = "frontend"
# [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"
// `
[servers.beta]
ip = "10.0.0.2"
role = "backend"
`
var subArrays = `
[[array]]
[[array.subarray]]
[[array.subarray.subsubarray]]
`
var tomlTableWithComments = `[section]
the_array = [
# comment
"value 1",
# comment
"value 2",
]
`
var expectedSubArrays = `array:
- subarray:
- subsubarray:
- {}
`
var tomlScenarios = []formatScenario{
{
@ -461,6 +494,13 @@ var tomlScenarios = []formatScenario{
expected: expectedMultipleEmptyTables,
scenarioType: "decode",
},
{
description: "subArrays",
skipDoc: true,
input: subArrays,
expected: expectedSubArrays,
scenarioType: "decode",
},
// Roundtrip scenarios
{
description: "Roundtrip: inline table attribute",
@ -532,13 +572,48 @@ var tomlScenarios = []formatScenario{
expected: rtComments,
scenarioType: "roundtrip",
},
// {
// description: "Roundtrip: sample from web",
// input: sampleFromWeb,
// expression: ".",
// expected: sampleFromWeb,
// scenarioType: "roundtrip",
// },
{
skipDoc: true,
description: "Issue #2588: comments inside table must not flatten (.owner.name)",
input: issue2588RustToolchainWithComments,
expression: ".owner.name",
expected: "Tomer\n",
scenarioType: "decode",
},
{
skipDoc: true,
description: "Issue #2588: comments inside table must not flatten (.name)",
input: issue2588RustToolchainWithComments,
expression: ".name",
expected: "null\n",
scenarioType: "decode",
},
{
skipDoc: true,
input: issue2588RustToolchainWithComments,
expected: issue2588RustToolchainWithComments,
scenarioType: "roundtrip",
},
{
skipDoc: true,
input: tableWithComment,
expression: ".owner | headComment",
expected: "comment\n",
scenarioType: "roundtrip",
},
{
description: "Roundtrip: sample from web",
input: sampleFromWeb,
expression: ".",
expected: sampleFromWeb,
scenarioType: "roundtrip",
},
{
skipDoc: true,
input: tomlTableWithComments,
expected: tomlTableWithComments,
scenarioType: "roundtrip",
},
}
func testTomlScenario(t *testing.T, s formatScenario) {
@ -891,3 +966,32 @@ func TestTomlStringEscapeColourization(t *testing.T) {
})
}
}
func TestTomlEncoderPrintDocumentSeparator(t *testing.T) {
encoder := NewTomlEncoder()
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 TestTomlEncoderPrintLeadingContent(t *testing.T) {
encoder := NewTomlEncoder()
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 TestTomlEncoderCanHandleAliases(t *testing.T) {
encoder := NewTomlEncoder()
test.AssertResult(t, false, encoder.CanHandleAliases())
}

View File

@ -293,3 +293,8 @@ buildvcs
behaviour
GOFLAGS
gocache
subsubarray
Ffile
Fquery
coverpkg
gsub

View File

@ -1,3 +1,18 @@
4.52.4:
- Dropping windows/arm - no longer supported in cross-compile
4.52.3:
- Fixing comments in TOML arrays (#2592)
- Bumped dependencies
4.52.2:
- Fixed bad instructions file breaking go-install (#2587) Thanks @theyoprst
- Fixed TOML table scope after comments (#2588) Thanks @tomers
- Multiply uses a readonly context (#2558)
- Fixed merge globbing wildcards in keys (#2564)
- Fixing TOML subarray parsing issue (#2581)
4.52.1:
- TOML encoder support - you can now roundtrip! #1364
- Parent now supports negative indices, and added a 'root' command for referencing the top level document

View File

@ -3,7 +3,9 @@
set -e
echo "Running tests and generating coverage..."
go test -coverprofile=coverage.out -v $(go list ./... | grep -v -E 'examples' | grep -v -E 'test')
packages=$(go list ./... | grep -v -E 'examples' | grep -v -E 'test' | tr '\n' ',' | sed 's/,$//')
test_packages=$(go list ./... | grep -v -E 'examples' | grep -v -E 'test' | grep -v '^github.com/mikefarah/yq/v4$')
go test -coverprofile=coverage.out -coverpkg="$packages" -v $test_packages
echo "Generating HTML coverage report..."
go tool cover -html=coverage.out -o coverage.html
@ -58,11 +60,31 @@ tail -n +1 coverage_sorted.txt | while read percent file; do
done
echo ""
echo "Top 10 files needing attention (lowest coverage):"
echo "Top 10 files by uncovered statements:"
echo "================================================="
grep -v "TOTAL:" coverage_sorted.txt | tail -10 | while read percent file; do
# Calculate uncovered statements for each file and sort by that
go tool cover -func=coverage.out | grep -E "\.go:[0-9]+:" | \
awk '{
# Extract filename and percentage
split($1, parts, ":")
file = parts[1]
pct = $NF
gsub(/%/, "", pct)
# Track stats per file
total[file]++
covered[file] += pct
}
END {
for (file in total) {
avg_pct = covered[file] / total[file]
uncovered = total[file] * (100 - avg_pct) / 100
covered_count = total[file] - uncovered
printf "%.0f %d %.0f %.1f %s\n", uncovered, total[file], covered_count, avg_pct, file
}
}' | sort -rn | head -10 | while read uncovered total covered pct file; do
filename=$(basename "$file")
printf "%-60s %8.1f%%\n" "$filename" "$percent"
printf "%-60s %4d uncovered (%4d/%4d, %5.1f%%)\n" "$filename" "$uncovered" "$covered" "$total" "$pct"
done
echo ""

View File

@ -1,5 +1,5 @@
name: yq
version: 'v4.52.1'
version: 'v4.52.4'
summary: A lightweight and portable command-line data file processor
description: |
`yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files.
@ -32,6 +32,6 @@ parts:
build-environment:
- CGO_ENABLED: 0
source: https://github.com/mikefarah/yq.git
source-tag: v4.52.1
source-tag: v4.52.4
build-snaps:
- go/latest/stable