mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e4df2304 | ||
|
|
79a92d0478 | ||
|
|
88a31ae8c6 | ||
|
|
5a7e72a743 | ||
|
|
562531d936 | ||
|
|
2c471b6498 | ||
|
|
f4ef6ef3cf | ||
|
|
f49f2bd2d8 | ||
|
|
6ccc7b7797 | ||
|
|
b3e1fbb7d1 | ||
|
|
288ca2d114 | ||
|
|
eb04fa87af | ||
|
|
2be0094729 | ||
|
|
3c18d5b035 | ||
|
|
2dcc2293da | ||
|
|
eb4fde4ef8 | ||
|
|
06ea4cf62e | ||
|
|
37089d24af | ||
|
|
7cf88a0291 | ||
|
|
41adc1ad18 | ||
|
|
b4b96f2a68 | ||
|
|
2824d66a65 | ||
|
|
4bbffa9022 | ||
|
|
bdeedbd275 |
6
.github/workflows/docker-release.yml
vendored
6
.github/workflows/docker-release.yml
vendored
@ -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 }}
|
||||
|
||||
@ -39,7 +39,6 @@ builds:
|
||||
- openbsd_amd64
|
||||
- windows_386
|
||||
- windows_amd64
|
||||
- windows_arm
|
||||
- windows_arm64
|
||||
|
||||
no_unique_dist_dir: true
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.6 AS builder
|
||||
FROM golang:1.26.0 AS builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.6
|
||||
FROM golang:1.26.0
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y npm && \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
10
go.mod
@ -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
20
go.sum
@ -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
24
go_install_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
160
pkg/yqlib/decoder_uri_test.go
Normal file
160
pkg/yqlib/decoder_uri_test.go
Normal 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)
|
||||
}
|
||||
@ -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"
|
||||
```
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -293,3 +293,8 @@ buildvcs
|
||||
behaviour
|
||||
GOFLAGS
|
||||
gocache
|
||||
subsubarray
|
||||
Ffile
|
||||
Fquery
|
||||
coverpkg
|
||||
gsub
|
||||
@ -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
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user