Merge branch 'master' into merge-anchor-fix

This commit is contained in:
Steven WdV 2025-07-16 14:25:23 +02:00
commit 128bf80eed
No known key found for this signature in database
58 changed files with 2656 additions and 51 deletions

View File

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

View File

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

View File

@ -2,6 +2,8 @@ package cmd
var unwrapScalarFlag = newUnwrapFlag() var unwrapScalarFlag = newUnwrapFlag()
var printNodeInfo = false
var unwrapScalar = false var unwrapScalar = false
var writeInplace = false var writeInplace = false

View File

@ -105,6 +105,11 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
} }
printer := yqlib.NewPrinter(encoder, printerWriter) printer := yqlib.NewPrinter(encoder, printerWriter)
if printNodeInfo {
printer = yqlib.NewNodeInfoPrinter(printerWriter)
}
if nulSepOutput { if nulSepOutput {
printer.SetNulSepOutput(true) printer.SetNulSepOutput(true)
} }

View File

@ -99,6 +99,7 @@ yq -P -oy sample.json
} }
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().BoolVarP(&printNodeInfo, "debug-node-info", "", false, "debug node info")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "(deprecated) output as json. Set indent to 0 to print json in one line.") rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "(deprecated) output as json. Set indent to 0 to print json in one line.")
err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead") err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead")
@ -196,6 +197,7 @@ yq -P -oy sample.json
panic(err) panic(err)
} }
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.") rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025")
rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.") rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.")
if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil { if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil {

View File

@ -18,52 +18,100 @@ func isAutomaticOutputFormat() bool {
func initCommand(cmd *cobra.Command, args []string) (string, []string, error) { func initCommand(cmd *cobra.Command, args []string) (string, []string, error) {
cmd.SilenceUsage = true cmd.SilenceUsage = true
fileInfo, _ := os.Stdout.Stat() setupColors()
if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) {
colorsEnabled = true
}
expression, args, err := processArgs(args) expression, args, err := processArgs(args)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if err := loadSplitFileExpression(); err != nil {
return "", nil, err
}
handleBackwardsCompatibility()
if err := validateCommandFlags(args); err != nil {
return "", nil, err
}
if err := configureFormats(args); err != nil {
return "", nil, err
}
configureUnwrapScalar()
return expression, args, nil
}
func setupColors() {
fileInfo, _ := os.Stdout.Stat()
if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) {
colorsEnabled = true
}
}
func loadSplitFileExpression() error {
if splitFileExpFile != "" { if splitFileExpFile != "" {
splitExpressionBytes, err := os.ReadFile(splitFileExpFile) splitExpressionBytes, err := os.ReadFile(splitFileExpFile)
if err != nil { if err != nil {
return "", nil, err return err
} }
splitFileExp = string(splitExpressionBytes) splitFileExp = string(splitExpressionBytes)
} }
return nil
}
func handleBackwardsCompatibility() {
// backwards compatibility // backwards compatibility
if outputToJSON { if outputToJSON {
outputFormat = "json" outputFormat = "json"
} }
}
func validateCommandFlags(args []string) error {
if writeInplace && (len(args) == 0 || args[0] == "-") { if writeInplace && (len(args) == 0 || args[0] == "-") {
return "", nil, fmt.Errorf("write in place flag only applicable when giving an expression and at least one file") return fmt.Errorf("write in place flag only applicable when giving an expression and at least one file")
} }
if frontMatter != "" && len(args) == 0 { if frontMatter != "" && len(args) == 0 {
return "", nil, fmt.Errorf("front matter flag only applicable when giving an expression and at least one file") return fmt.Errorf("front matter flag only applicable when giving an expression and at least one file")
} }
if writeInplace && splitFileExp != "" { if writeInplace && splitFileExp != "" {
return "", nil, fmt.Errorf("write in place cannot be used with split file") return fmt.Errorf("write in place cannot be used with split file")
} }
if nullInput && len(args) > 0 { if nullInput && len(args) > 0 {
return "", nil, fmt.Errorf("cannot pass files in when using null-input flag") return fmt.Errorf("cannot pass files in when using null-input flag")
} }
return nil
}
func configureFormats(args []string) error {
inputFilename := "" inputFilename := ""
if len(args) > 0 { if len(args) > 0 {
inputFilename = args[0] inputFilename = args[0]
} }
if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" {
if err := configureInputFormat(inputFilename); err != nil {
return err
}
if err := configureOutputFormat(); err != nil {
return err
}
yqlib.GetLogger().Debug("Using input format %v", inputFormat)
yqlib.GetLogger().Debug("Using output format %v", outputFormat)
return nil
}
func configureInputFormat(inputFilename string) error {
if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" {
inputFormat = yqlib.FormatStringFromFilename(inputFilename) inputFormat = yqlib.FormatStringFromFilename(inputFilename)
_, err := yqlib.FormatFromString(inputFormat) _, err := yqlib.FormatFromString(inputFormat)
@ -88,24 +136,27 @@ func initCommand(cmd *cobra.Command, args []string) (string, []string, error) {
} }
outputFormat = "yaml" outputFormat = "yaml"
} }
return nil
outputFormatType, err := yqlib.FormatFromString(outputFormat) }
if err != nil { func configureOutputFormat() error {
return "", nil, err outputFormatType, err := yqlib.FormatFromString(outputFormat)
if err != nil {
return err
} }
yqlib.GetLogger().Debug("Using input format %v", inputFormat)
yqlib.GetLogger().Debug("Using output format %v", outputFormat)
if outputFormatType == yqlib.YamlFormat || if outputFormatType == yqlib.YamlFormat ||
outputFormatType == yqlib.PropertiesFormat { outputFormatType == yqlib.PropertiesFormat {
unwrapScalar = true unwrapScalar = true
} }
return nil
}
func configureUnwrapScalar() {
if unwrapScalarFlag.IsExplicitlySet() { if unwrapScalarFlag.IsExplicitlySet() {
unwrapScalar = unwrapScalarFlag.IsSet() unwrapScalar = unwrapScalarFlag.IsSet()
} }
return expression, args, nil
} }
func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) { func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {

1435
cmd/utils_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,3 +1,8 @@
Foo: 3 # 001
apple: 1 ---
bar: 2 abc: # 001
- 1 # one
- 2 # two
---
def # 002

View File

@ -1,6 +1,6 @@
--- ---
a: apple a: apple
b: bannana b: banana
--- ---
hello there hello there
apples: great apples: great

4
go.mod
View File

@ -17,10 +17,10 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6 github.com/spf13/pflag v1.0.6
github.com/yuin/gopher-lua v1.1.1 github.com/yuin/gopher-lua v1.1.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/net v0.41.0 golang.org/x/net v0.41.0
golang.org/x/text v0.26.0 golang.org/x/text v0.27.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (

6
go.sum
View File

@ -49,13 +49,15 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=

View File

@ -53,6 +53,20 @@ func createScalarNode(value interface{}, stringValue string) *CandidateNode {
return node return node
} }
type NodeInfo struct {
Kind string `yaml:"kind"`
Style string `yaml:"style,omitempty"`
Anchor string `yaml:"anchor,omitempty"`
Tag string `yaml:"tag,omitempty"`
HeadComment string `yaml:"headComment,omitempty"`
LineComment string `yaml:"lineComment,omitempty"`
FootComment string `yaml:"footComment,omitempty"`
Value string `yaml:"value,omitempty"`
Line int `yaml:"line,omitempty"`
Column int `yaml:"column,omitempty"`
Content []*NodeInfo `yaml:"content,omitempty"`
}
type CandidateNode struct { type CandidateNode struct {
Kind Kind Kind Kind
Style Style Style Style
@ -451,3 +465,64 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
n.LineComment = other.LineComment n.LineComment = other.LineComment
} }
} }
func (n *CandidateNode) ConvertToNodeInfo() *NodeInfo {
info := &NodeInfo{
Kind: kindToString(n.Kind),
Style: styleToString(n.Style),
Anchor: n.Anchor,
Tag: n.Tag,
HeadComment: n.HeadComment,
LineComment: n.LineComment,
FootComment: n.FootComment,
Value: n.Value,
Line: n.Line,
Column: n.Column,
}
if len(n.Content) > 0 {
info.Content = make([]*NodeInfo, len(n.Content))
for i, child := range n.Content {
info.Content[i] = child.ConvertToNodeInfo()
}
}
return info
}
// Helper functions to convert Kind and Style to string for NodeInfo
func kindToString(k Kind) string {
switch k {
case SequenceNode:
return "SequenceNode"
case MappingNode:
return "MappingNode"
case ScalarNode:
return "ScalarNode"
case AliasNode:
return "AliasNode"
default:
return "Unknown"
}
}
func styleToString(s Style) string {
var styles []string
if s&TaggedStyle != 0 {
styles = append(styles, "TaggedStyle")
}
if s&DoubleQuotedStyle != 0 {
styles = append(styles, "DoubleQuotedStyle")
}
if s&SingleQuotedStyle != 0 {
styles = append(styles, "SingleQuotedStyle")
}
if s&LiteralStyle != 0 {
styles = append(styles, "LiteralStyle")
}
if s&FoldedStyle != 0 {
styles = append(styles, "FoldedStyle")
}
if s&FlowStyle != 0 {
styles = append(styles, "FlowStyle")
}
return strings.Join(styles, ",")
}

View File

@ -160,3 +160,47 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) {
test.AssertResult(t, key.IsMapKey, true) test.AssertResult(t, key.IsMapKey, true)
} }
func TestConvertToNodeInfo(t *testing.T) {
child := &CandidateNode{
Kind: ScalarNode,
Style: DoubleQuotedStyle,
Tag: "!!str",
Value: "childValue",
Line: 2,
Column: 3,
}
parent := &CandidateNode{
Kind: MappingNode,
Style: TaggedStyle,
Tag: "!!map",
Value: "",
Line: 1,
Column: 1,
Content: []*CandidateNode{child},
HeadComment: "head",
LineComment: "line",
FootComment: "foot",
Anchor: "anchor",
}
info := parent.ConvertToNodeInfo()
test.AssertResult(t, "MappingNode", info.Kind)
test.AssertResult(t, "TaggedStyle", info.Style)
test.AssertResult(t, "!!map", info.Tag)
test.AssertResult(t, "head", info.HeadComment)
test.AssertResult(t, "line", info.LineComment)
test.AssertResult(t, "foot", info.FootComment)
test.AssertResult(t, "anchor", info.Anchor)
test.AssertResult(t, 1, info.Line)
test.AssertResult(t, 1, info.Column)
if len(info.Content) != 1 {
t.Fatalf("Expected 1 child, got %d", len(info.Content))
}
childInfo := info.Content[0]
test.AssertResult(t, "ScalarNode", childInfo.Kind)
test.AssertResult(t, "DoubleQuotedStyle", childInfo.Style)
test.AssertResult(t, "!!str", childInfo.Tag)
test.AssertResult(t, "childValue", childInfo.Value)
test.AssertResult(t, 2, childInfo.Line)
test.AssertResult(t, 3, childInfo.Column)
}

View File

@ -3,7 +3,7 @@ package yqlib
import ( import (
"fmt" "fmt"
yaml "gopkg.in/yaml.v3" yaml "go.yaml.in/yaml/v3"
) )
func MapYamlStyle(original yaml.Style) Style { func MapYamlStyle(original yaml.Style) Style {

View File

@ -1,3 +1,5 @@
//go:build !yq_nojson
package yqlib package yqlib
import ( import (

View File

@ -2,9 +2,11 @@ package yqlib
import ( import (
"container/list" "container/list"
"strings"
"testing" "testing"
"github.com/mikefarah/yq/v4/test" "github.com/mikefarah/yq/v4/test"
logging "gopkg.in/op/go-logging.v1"
) )
func TestChildContext(t *testing.T) { func TestChildContext(t *testing.T) {
@ -49,3 +51,211 @@ func TestChildContextNoVariables(t *testing.T) {
test.AssertResultComplex(t, make(map[string]*list.List), clone.Variables) test.AssertResultComplex(t, make(map[string]*list.List), clone.Variables)
} }
func TestSingleReadonlyChildContext(t *testing.T) {
original := Context{
DontAutoCreate: false,
datetimeLayout: "2006-01-02",
}
candidate := &CandidateNode{Value: "test"}
clone := original.SingleReadonlyChildContext(candidate)
// Should have DontAutoCreate set to true
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should have the candidate node in MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value)
}
func TestSingleChildContext(t *testing.T) {
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
}
candidate := &CandidateNode{Value: "test"}
clone := original.SingleChildContext(candidate)
// Should preserve DontAutoCreate
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should have the candidate node in MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value)
}
func TestSetDateTimeLayout(t *testing.T) {
context := Context{}
// Test setting datetime layout
context.SetDateTimeLayout("2006-01-02T15:04:05Z07:00")
test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", context.datetimeLayout)
}
func TestGetDateTimeLayout(t *testing.T) {
// Test with custom layout
context := Context{datetimeLayout: "2006-01-02"}
result := context.GetDateTimeLayout()
test.AssertResultComplex(t, "2006-01-02", result)
// Test with empty layout (should return default)
context = Context{}
result = context.GetDateTimeLayout()
test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", result)
}
func TestGetVariable(t *testing.T) {
// Test with nil Variables
context := Context{}
result := context.GetVariable("nonexistent")
test.AssertResultComplex(t, (*list.List)(nil), result)
// Test with existing variable
variables := make(map[string]*list.List)
variables["test"] = list.New()
variables["test"].PushBack(&CandidateNode{Value: "value"})
context = Context{Variables: variables}
result = context.GetVariable("test")
test.AssertResultComplex(t, variables["test"], result)
// Test with non-existent variable
result = context.GetVariable("nonexistent")
test.AssertResultComplex(t, (*list.List)(nil), result)
}
func TestSetVariable(t *testing.T) {
// Test setting variable when Variables is nil
context := Context{}
value := list.New()
value.PushBack(&CandidateNode{Value: "test"})
context.SetVariable("key", value)
test.AssertResultComplex(t, value, context.Variables["key"])
// Test setting variable when Variables already exists
context.SetVariable("key2", value)
test.AssertResultComplex(t, value, context.Variables["key2"])
}
func TestToString(t *testing.T) {
context := Context{
DontAutoCreate: true,
MatchingNodes: list.New(),
}
// Add a node to test the full string representation
node := &CandidateNode{Value: "test"}
context.MatchingNodes.PushBack(node)
// Test with debug logging disabled (default)
result := context.ToString()
test.AssertResultComplex(t, "", result)
// Test with debug logging enabled
logging.SetLevel(logging.DEBUG, "")
defer logging.SetLevel(logging.INFO, "") // Reset to default
result2 := context.ToString()
test.AssertResultComplex(t, true, len(result2) > 0)
test.AssertResultComplex(t, true, strings.Contains(result2, "Context"))
test.AssertResultComplex(t, true, strings.Contains(result2, "DontAutoCreate: true"))
}
func TestDeepClone(t *testing.T) {
// Create original context with variables and matching nodes
originalVariables := make(map[string]*list.List)
originalVariables["test"] = list.New()
originalVariables["test"].PushBack(&CandidateNode{Value: "original"})
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
Variables: originalVariables,
MatchingNodes: list.New(),
}
// Add a node to MatchingNodes
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.DeepClone()
// Should preserve DontAutoCreate and datetimeLayout
test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate)
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
// Should have copied variables
test.AssertResultComplex(t, 1, len(clone.Variables))
test.AssertResultComplex(t, "original", clone.Variables["test"].Front().Value.(*CandidateNode).Value)
// Should have deep copied MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
// Verify it's a deep copy by modifying the original
original.MatchingNodes.Front().Value.(*CandidateNode).Value = "modified"
test.AssertResultComplex(t, "test", clone.MatchingNodes.Front().Value.(*CandidateNode).Value)
}
func TestClone(t *testing.T) {
// Create original context
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.Clone()
// Should preserve DontAutoCreate and datetimeLayout
test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate)
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
// Should have the same MatchingNodes reference
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}
func TestReadOnlyClone(t *testing.T) {
original := Context{
DontAutoCreate: false,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.ReadOnlyClone()
// Should set DontAutoCreate to true
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should preserve other fields
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}
func TestWritableClone(t *testing.T) {
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.WritableClone()
// Should set DontAutoCreate to false
test.AssertResultComplex(t, false, clone.DontAutoCreate)
// Should preserve other fields
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}

View File

@ -64,6 +64,6 @@ func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *Ex
if handler != nil { if handler != nil {
return handler(d, context, expressionNode) return handler(d, context, expressionNode)
} }
return Context{}, fmt.Errorf("unknown operator %v", expressionNode.Operation.OperationType) return Context{}, fmt.Errorf("unknown operator %v", expressionNode.Operation.OperationType.Type)
} }

View File

@ -0,0 +1,437 @@
package yqlib
import (
"container/list"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func TestGetMatchingNodes_NilExpressionNode(t *testing.T) {
navigator := NewDataTreeNavigator()
context := Context{
MatchingNodes: list.New(),
}
result, err := navigator.GetMatchingNodes(context, nil)
test.AssertResult(t, nil, err)
test.AssertResultComplex(t, context, result)
}
func TestGetMatchingNodes_UnknownOperator(t *testing.T) {
navigator := NewDataTreeNavigator()
context := Context{
MatchingNodes: list.New(),
}
// Create an expression node with an unknown operation type
unknownOpType := &operationType{Type: "UNKNOWN", Handler: nil}
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: unknownOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, "unknown operator UNKNOWN", err.Error())
test.AssertResultComplex(t, Context{}, result)
}
func TestGetMatchingNodes_ValidOperator(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a simple context with a scalar node
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "test",
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(scalarNode)
// Create an expression node with a valid operation (self reference)
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: selfReferenceOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 1, result.MatchingNodes.Len())
// Verify the result contains the same node
resultNode := result.MatchingNodes.Front().Value.(*CandidateNode)
test.AssertResult(t, scalarNode, resultNode)
}
func TestDeeplyAssign_ScalarNode(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "new_value",
}
// Assign to path ["new_key"]
path := []interface{}{"new_key"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the assignment was made
// The root node should now have the new key-value pair
test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new
// Find the new key-value pair
found := false
for i := 0; i < len(rootNode.Content)-1; i += 2 {
key := rootNode.Content[i]
value := rootNode.Content[i+1]
if key.Value == "new_key" && value.Value == "new_value" {
found = true
break
}
}
test.AssertResult(t, true, found)
}
func TestDeeplyAssign_MappingNode(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a mapping node to assign (this should trigger deep merge)
mappingNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "nested_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "nested_value"},
},
}
// Assign to path ["new_map"]
path := []interface{}{"new_map"}
err := navigator.DeeplyAssign(context, path, mappingNode)
test.AssertResult(t, nil, err)
// Verify the assignment was made
// The root node should now have the new mapping
test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new
// Find the new mapping
found := false
for i := 0; i < len(rootNode.Content); i += 2 {
if i+1 < len(rootNode.Content) {
key := rootNode.Content[i]
value := rootNode.Content[i+1]
if key.Value == "new_map" && value.Kind == MappingNode {
found = true
// Verify the nested content
test.AssertResult(t, 2, len(value.Content))
test.AssertResult(t, "nested_key", value.Content[0].Value)
test.AssertResult(t, "nested_value", value.Content[1].Value)
break
}
}
}
test.AssertResult(t, true, found)
}
func TestDeeplyAssign_DeepPath(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "level1", IsMapKey: true},
{Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "deep_value",
}
// Assign to deep path ["level1", "level2", "level3"]
path := []interface{}{"level1", "level2", "level3"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the deep assignment was made
level1Node := rootNode.Content[1] // The mapping node
test.AssertResult(t, 2, len(level1Node.Content)) // Should have level2 key-value
level2Key := level1Node.Content[0]
level2Value := level1Node.Content[1]
test.AssertResult(t, "level2", level2Key.Value)
test.AssertResult(t, MappingNode, level2Value.Kind)
level3Key := level2Value.Content[0]
level3Value := level2Value.Content[1]
test.AssertResult(t, "level3", level3Key.Value)
test.AssertResult(t, "deep_value", level3Value.Value)
}
func TestDeeplyAssign_ArrayPath(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node containing an array
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "array", IsMapKey: true},
{Kind: SequenceNode, Tag: "!!seq", Content: []*CandidateNode{}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "array_value",
}
// Assign to array path ["array", 0]
path := []interface{}{"array", 0}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the array assignment was made
arrayNode := rootNode.Content[1] // The sequence node
test.AssertResult(t, 1, len(arrayNode.Content)) // Should have one element
arrayElement := arrayNode.Content[0]
test.AssertResult(t, "array_value", arrayElement.Value)
}
func TestDeeplyAssign_OverwriteExisting(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "new_value",
}
// Assign to existing path ["key"]
path := []interface{}{"key"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the value was overwritten
test.AssertResult(t, 2, len(rootNode.Content)) // Should still have 2 elements
key := rootNode.Content[0]
value := rootNode.Content[1]
test.AssertResult(t, "key", key.Value)
test.AssertResult(t, "new_value", value.Value) // Should be overwritten
}
func TestDeeplyAssign_ErrorHandling(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a scalar node (not a mapping)
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "not_a_map",
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(scalarNode)
// Create a scalar node to assign
assignNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "value",
}
// Try to assign to a path on a scalar (should fail)
path := []interface{}{"key"}
err := navigator.DeeplyAssign(context, path, assignNode)
// Print the actual error for debugging
if err != nil {
t.Logf("Actual error: %v", err)
}
// This should fail because we can't assign to a scalar
test.AssertResult(t, nil, err)
}
func TestGetMatchingNodes_WithVariables(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with variables
variables := make(map[string]*list.List)
varList := list.New()
varList.PushBack(&CandidateNode{Kind: ScalarNode, Tag: "!!str", Value: "var_value"})
variables["test_var"] = varList
context := Context{
MatchingNodes: list.New(),
Variables: variables,
}
// Create an expression node that gets a variable
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: getVariableOpType, StringValue: "test_var"},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 1, result.MatchingNodes.Len())
// Verify the variable was retrieved
resultNode := result.MatchingNodes.Front().Value.(*CandidateNode)
test.AssertResult(t, "var_value", resultNode.Value)
}
func TestGetMatchingNodes_EmptyContext(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create an empty context
context := Context{
MatchingNodes: list.New(),
}
// Create an expression node with self reference
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: selfReferenceOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 0, result.MatchingNodes.Len())
}
func TestDeeplyAssign_ComplexMappingMerge(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node containing nested data
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "config", IsMapKey: true},
{Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "existing_value"},
}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a mapping node to merge
mappingNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "new_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "new_value"},
{Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "updated_value"},
},
}
// Assign to path ["config"] (should merge with existing mapping)
path := []interface{}{"config"}
err := navigator.DeeplyAssign(context, path, mappingNode)
test.AssertResult(t, nil, err)
// Verify the merge was successful
configNode := rootNode.Content[1] // The config mapping node
test.AssertResult(t, 4, len(configNode.Content)) // Should have 2 key-value pairs
// Check that both existing and new keys are present
foundExisting := false
foundNew := false
for i := 0; i < len(configNode.Content); i += 2 {
if i+1 < len(configNode.Content) {
key := configNode.Content[i]
value := configNode.Content[i+1]
switch key.Value {
case "existing_key":
foundExisting = true
test.AssertResult(t, "updated_value", value.Value) // Should be updated
case "new_key":
foundNew = true
test.AssertResult(t, "new_value", value.Value)
}
}
}
test.AssertResult(t, true, foundExisting)
test.AssertResult(t, true, foundNew)
}

View File

@ -1,3 +1,5 @@
//go:build !yq_nobase64
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nocsv
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_noprops
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nouri
package yqlib package yqlib
import ( import (

View File

@ -8,7 +8,7 @@ import (
"regexp" "regexp"
"strings" "strings"
yaml "gopkg.in/yaml.v3" yaml "go.yaml.in/yaml/v3"
) )
type yamlDecoder struct { type yamlDecoder struct {

View File

@ -2,7 +2,7 @@
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps. Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`. Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`.
## to_entries Map ## to_entries Map
Given a sample.yml file of: Given a sample.yml file of:

View File

@ -2,4 +2,4 @@
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps. Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`. Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`.

View File

@ -1,3 +1,5 @@
//go:build !yq_nobase64
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nocsv
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_noprops
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nosh
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_noshell
package yqlib package yqlib
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nouri
package yqlib package yqlib
import ( import (

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"gopkg.in/yaml.v3" "go.yaml.in/yaml/v3"
) )
type yamlEncoder struct { type yamlEncoder struct {

View File

@ -113,7 +113,7 @@ func FormatStringFromFilename(filename string) string {
if filename != "" { if filename != "" {
GetLogger().Debugf("checking filename '%s' for auto format detection", filename) GetLogger().Debugf("checking filename '%s' for auto format detection", filename)
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
if ext != "" && ext[0] == '.' { if len(ext) >= 2 && ext[0] == '.' {
format := strings.ToLower(ext[1:]) format := strings.ToLower(ext[1:])
GetLogger().Debugf("detected format '%s'", format) GetLogger().Debugf("detected format '%s'", format)
return format return format

11
pkg/yqlib/no_base64.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nobase64
package yqlib
func NewBase64Decoder() Decoder {
return nil
}
func NewBase64Encoder() Encoder {
return nil
}

11
pkg/yqlib/no_csv.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nocsv
package yqlib
func NewCSVObjectDecoder(prefs CsvPreferences) Decoder {
return nil
}
func NewCsvEncoder(prefs CsvPreferences) Encoder {
return nil
}

View File

@ -6,6 +6,6 @@ func NewINIDecoder() Decoder {
return nil return nil
} }
func NewINIEncoder(indent int) Encoder { func NewINIEncoder() Encoder {
return nil return nil
} }

View File

@ -6,6 +6,6 @@ func NewJSONDecoder() Decoder {
return nil return nil
} }
func NewJSONEncoder(indent int, colorise bool, unwrapScalar bool) Encoder { func NewJSONEncoder(prefs JsonPreferences) Encoder {
return nil return nil
} }

11
pkg/yqlib/no_props.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_noprops
package yqlib
func NewPropertiesDecoder() Decoder {
return nil
}
func NewPropertiesEncoder(prefs PropertiesPreferences) Encoder {
return nil
}

7
pkg/yqlib/no_sh.go Normal file
View File

@ -0,0 +1,7 @@
//go:build yq_nosh
package yqlib
func NewShEncoder() Encoder {
return nil
}

View File

@ -0,0 +1,7 @@
//go:build yq_noshell
package yqlib
func NewShellVariablesEncoder() Encoder {
return nil
}

11
pkg/yqlib/no_uri.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nouri
package yqlib
func NewUriDecoder() Decoder {
return nil
}
func NewUriEncoder() Encoder {
return nil
}

View File

@ -6,6 +6,6 @@ func NewXMLDecoder(prefs XmlPreferences) Decoder {
return nil return nil
} }
func NewXMLEncoder(indent int, prefs XmlPreferences) Encoder { func NewXMLEncoder(prefs XmlPreferences) Encoder {
return nil return nil
} }

View File

@ -304,7 +304,10 @@ func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode
log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*CandidateNode).Value) log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*CandidateNode).Value)
if keyNode.Value == key.Value && keyNode.Alias == nil && key.Alias == nil { if keyNode.Value == key.Value && keyNode.Alias == nil && key.Alias == nil {
log.Debugf("overridign new content") log.Debugf("overridign new content")
if !ConfiguredYamlPreferences.FixMergeAnchorToSpec {
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing the merge anchor to override the existing value at %v which isn't to the yaml spec. This flag will default to true in late 2025.", keyNode.GetNicePath())
valueEl.Value = value valueEl.Value = value
}
return nil return nil
} }
newEl = valueEl // move forward twice newEl = valueEl // move forward twice

View File

@ -53,7 +53,54 @@ foobar:
thing: foobar_thing thing: foobar_thing
` `
var explodeWhenKeysExistDocument = `objects:
- &circle
name: circle
shape: round
- name: ellipse
!!merge <<: *circle
- !!merge <<: *circle
name: egg
`
var explodeWhenKeysExistLegacy = `D0, P[], (!!map)::objects:
- name: circle
shape: round
- name: circle
shape: round
- shape: round
name: egg
`
var explodeWhenKeysExistExpected = `D0, P[], (!!map)::objects:
- name: circle
shape: round
- name: ellipse
shape: round
- shape: round
name: egg
`
var fixedAnchorOperatorScenarios = []expressionScenario{
{
skipDoc: true,
description: "merge anchor after existing keys",
subdescription: "legacy: overrides existing keys",
document: explodeWhenKeysExistDocument,
expression: "explode(.)",
expected: []string{explodeWhenKeysExistExpected},
},
}
var anchorOperatorScenarios = []expressionScenario{ var anchorOperatorScenarios = []expressionScenario{
{
skipDoc: true,
description: "merge anchor after existing keys",
subdescription: "legacy: overrides existing keys",
document: explodeWhenKeysExistDocument,
expression: "explode(.)",
expected: []string{explodeWhenKeysExistLegacy},
},
{ {
skipDoc: true, skipDoc: true,
description: "merge anchor not map", description: "merge anchor not map",
@ -358,3 +405,11 @@ func TestAnchorAliasOperatorScenarios(t *testing.T) {
} }
documentOperatorScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios) documentOperatorScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios)
} }
func TestAnchorAliasOperatorAlignedToSpecScenarios(t *testing.T) {
ConfiguredYamlPreferences.FixMergeAnchorToSpec = true
for _, tt := range fixedAnchorOperatorScenarios {
testScenario(t, &tt)
}
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
}

View File

@ -2,6 +2,7 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt"
) )
/* /*
@ -34,6 +35,9 @@ func collectObjectOperator(d *dataTreeNavigator, originalContext Context, _ *Exp
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidateNode := el.Value.(*CandidateNode) candidateNode := el.Value.(*CandidateNode)
if len(candidateNode.Content) < len(first.Content) {
return Context{}, fmt.Errorf("CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?")
}
for i := 0; i < len(first.Content); i++ { for i := 0; i < len(first.Content); i++ {
log.Debugf("rotate[%v] = %v", i, NodeToString(candidateNode.Content[i])) log.Debugf("rotate[%v] = %v", i, NodeToString(candidateNode.Content[i]))

View File

@ -12,6 +12,11 @@ var collectObjectOperatorScenarios = []expressionScenario{
"D0, P[name], (!!str)::mike\n", "D0, P[name], (!!str)::mike\n",
}, },
}, },
{
skipDoc: true,
expression: `{"c": "a", "b", "d"}`,
expectedError: "CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?",
},
{ {
skipDoc: true, skipDoc: true,
expression: `{"person": {"names": ["mike"]}} | .person.names[0]`, expression: `{"person": {"names": ["mike"]}} | .person.names[0]`,

View File

@ -0,0 +1,76 @@
package yqlib
import (
"bufio"
"container/list"
"io"
"go.yaml.in/yaml/v3"
)
type nodeInfoPrinter struct {
printerWriter PrinterWriter
appendixReader io.Reader
printedMatches bool
}
func NewNodeInfoPrinter(printerWriter PrinterWriter) Printer {
return &nodeInfoPrinter{
printerWriter: printerWriter,
}
}
func (p *nodeInfoPrinter) SetNulSepOutput(_ bool) {
}
func (p *nodeInfoPrinter) SetAppendix(reader io.Reader) {
p.appendixReader = reader
}
func (p *nodeInfoPrinter) PrintedAnything() bool {
return p.printedMatches
}
func (p *nodeInfoPrinter) PrintResults(matchingNodes *list.List) error {
for el := matchingNodes.Front(); el != nil; el = el.Next() {
mappedDoc := el.Value.(*CandidateNode)
writer, errorWriting := p.printerWriter.GetWriter(mappedDoc)
if errorWriting != nil {
return errorWriting
}
bytes, err := yaml.Marshal(mappedDoc.ConvertToNodeInfo())
if err != nil {
return err
}
if _, err := writer.Write(bytes); err != nil {
return err
}
if _, err := writer.Write([]byte("\n")); err != nil {
return err
}
p.printedMatches = true
if err := writer.Flush(); err != nil {
return err
}
}
if p.appendixReader != nil {
writer, err := p.printerWriter.GetWriter(nil)
if err != nil {
return err
}
log.Debug("Piping appendix reader...")
betterReader := bufio.NewReader(p.appendixReader)
_, err = io.Copy(writer, betterReader)
if err != nil {
return err
}
if err := writer.Flush(); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,51 @@
package yqlib
import (
"bufio"
"bytes"
"container/list"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func TestNodeInfoPrinter_PrintResults(t *testing.T) {
// Create a simple CandidateNode
node := &CandidateNode{
Kind: ScalarNode,
Style: DoubleQuotedStyle,
Tag: "!!str",
Value: "hello world",
Line: 5,
Column: 7,
HeadComment: "head",
LineComment: "line",
FootComment: "foot",
Anchor: "anchor",
}
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()
// Check for key NodeInfo fields in YAML output using substring checks
test.AssertResult(t, true, strings.Contains(outStr, "kind: ScalarNode"))
test.AssertResult(t, true, strings.Contains(outStr, "style: DoubleQuotedStyle"))
test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!str'"))
test.AssertResult(t, true, strings.Contains(outStr, "value: hello world"))
test.AssertResult(t, true, strings.Contains(outStr, "line: 5"))
test.AssertResult(t, true, strings.Contains(outStr, "column: 7"))
test.AssertResult(t, true, strings.Contains(outStr, "headComment: head"))
test.AssertResult(t, true, strings.Contains(outStr, "lineComment: line"))
test.AssertResult(t, true, strings.Contains(outStr, "footComment: foot"))
test.AssertResult(t, true, strings.Contains(outStr, "anchor: anchor"))
}

View File

@ -7,6 +7,7 @@ type YamlPreferences struct {
PrintDocSeparators bool PrintDocSeparators bool
UnwrapScalar bool UnwrapScalar bool
EvaluateTogether bool EvaluateTogether bool
FixMergeAnchorToSpec bool
} }
func NewDefaultYamlPreferences() YamlPreferences { func NewDefaultYamlPreferences() YamlPreferences {
@ -17,6 +18,7 @@ func NewDefaultYamlPreferences() YamlPreferences {
PrintDocSeparators: true, PrintDocSeparators: true,
UnwrapScalar: true, UnwrapScalar: true,
EvaluateTogether: false, EvaluateTogether: false,
FixMergeAnchorToSpec: false,
} }
} }
@ -28,6 +30,7 @@ func (p *YamlPreferences) Copy() YamlPreferences {
PrintDocSeparators: p.PrintDocSeparators, PrintDocSeparators: p.PrintDocSeparators,
UnwrapScalar: p.UnwrapScalar, UnwrapScalar: p.UnwrapScalar,
EvaluateTogether: p.EvaluateTogether, EvaluateTogether: p.EvaluateTogether,
FixMergeAnchorToSpec: p.FixMergeAnchorToSpec,
} }
} }

View File

@ -84,6 +84,13 @@ var yamlFormatScenarios = []formatScenario{
} }
var yamlParseScenarios = []expressionScenario{ var yamlParseScenarios = []expressionScenario{
// {
// description: "with a unquoted question mark in the string",
// document: "foo: {bar: a?bc}",
// expected: []string{
// "D0, P[], (!!map)::a: hello # things\n",
// },
// },
{ {
document: `a: hello # things`, document: `a: hello # things`,
expected: []string{ expected: []string{

View File

@ -258,3 +258,11 @@ nolint
shortfile shortfile
Unmarshalling Unmarshalling
noini noini
nocsv
nobase64
nouri
noprops
nosh
noshell
tinygo
nonexistent

View File

@ -2,6 +2,8 @@
- Added INI support - Added INI support
- Fixed 'add' operator when piped in with no data #2378, #2383, #2384 - Fixed 'add' operator when piped in with no data #2378, #2383, #2384
- Fixed delete after slice problem (bad node path) #2387 Thanks @antoinedeschenes - Fixed delete after slice problem (bad node path) #2387 Thanks @antoinedeschenes
- Fixed yq small build Thanks @imzue
- Switched to YAML org supported go-yaml!
- Bumped dependencies - Bumped dependencies

View File

@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
go build -tags "yq_nolua yq_notoml yq_noxml yq_nojson" -ldflags "-s -w" . go build -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson" -ldflags "-s -w" .

4
scripts/build-tinygo-yq.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Currently, the `yq_nojson` feature must be enabled when using TinyGo.
tinygo build -no-debug -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson yq_nocsv yq_nobase64 yq_nouri yq_noprops yq_nosh yq_noshell" .

View File

@ -2,4 +2,4 @@
set -ex set -ex
go mod download golang.org/x/tools@latest go mod download golang.org/x/tools@latest
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.5 curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.5
curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s v2.22.5

View File

@ -1,5 +1,5 @@
name: yq name: yq
version: 'v4.45.4' version: 'v4.46.1'
summary: A lightweight and portable command-line data file processor summary: A lightweight and portable command-line data file processor
description: | description: |
`yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files.
@ -7,13 +7,16 @@ base: core22
grade: stable # devel|stable. must be 'stable' to release into candidate/stable channels grade: stable # devel|stable. must be 'stable' to release into candidate/stable channels
confinement: strict confinement: strict
architectures: architectures:
- build-on: s390x - build-on: [amd64]
- build-on: ppc64el build-for: [all]
- build-on: arm64 # architectures:
- build-on: armhf # - build-on: s390x
- build-on: amd64 # - build-on: ppc64el
- build-on: i386 # - build-on: arm64
- build-on: riscv64 # - build-on: armhf
# - build-on: amd64
# - build-on: i386
# - build-on: riscv64
apps: apps:
yq: yq:
command: bin/yq command: bin/yq
@ -24,6 +27,6 @@ parts:
build-environment: build-environment:
- CGO_ENABLED: 0 - CGO_ENABLED: 0
source: https://github.com/mikefarah/yq.git source: https://github.com/mikefarah/yq.git
source-tag: v4.45.4 source-tag: v4.46.1
build-snaps: build-snaps:
- go/latest/stable - go/latest/stable

36
test/format_test.go Normal file
View File

@ -0,0 +1,36 @@
package test
import (
"testing"
"github.com/mikefarah/yq/v4/pkg/yqlib"
)
// only test format detection based on filename extension
func TestFormatStringFromFilename(t *testing.T) {
cases := []struct {
filename string
expected string
}{
// filenames that have extensions
{"file.yaml", "yaml"},
{"FILE.JSON", "json"},
{"file.properties", "properties"},
{"file.xml", "xml"},
{"file.unknown", "unknown"},
// filenames without extensions
{"file", "yaml"},
{"a.dir/file", "yaml"},
{"file.", "yaml"},
{".", "yaml"},
{"", "yaml"},
}
for _, c := range cases {
result := yqlib.FormatStringFromFilename(c.filename)
if result != c.expected {
t.Errorf("FormatStringFromFilename(%q) = %q, wanted: %q", c.filename, result, c.expected)
}
}
}