mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-07 06:10:36 +00:00
Merge branch 'master' into merge-anchor-fix
This commit is contained in:
commit
128bf80eed
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 && \
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
85
cmd/utils.go
85
cmd/utils.go
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureOutputFormat() error {
|
||||||
outputFormatType, err := yqlib.FormatFromString(outputFormat)
|
outputFormatType, err := yqlib.FormatFromString(outputFormat)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
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
1435
cmd/utils_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
Foo: 3
|
# 001
|
||||||
apple: 1
|
---
|
||||||
bar: 2
|
abc: # 001
|
||||||
|
- 1 # one
|
||||||
|
- 2 # two
|
||||||
|
|
||||||
|
---
|
||||||
|
def # 002
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
a: apple
|
a: apple
|
||||||
b: bannana
|
b: banana
|
||||||
---
|
---
|
||||||
hello there
|
hello there
|
||||||
apples: great
|
apples: great
|
||||||
4
go.mod
4
go.mod
@ -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
6
go.sum
@ -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=
|
||||||
|
|||||||
@ -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, ",")
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nojson
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
437
pkg/yqlib/data_tree_navigator_test.go
Normal file
437
pkg/yqlib/data_tree_navigator_test.go
Normal 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)
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nobase64
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nocsv
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_noprops
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nouri
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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`.
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nobase64
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nocsv
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_noprops
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nosh
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_noshell
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//go:build !yq_nouri
|
||||||
|
|
||||||
package yqlib
|
package yqlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
11
pkg/yqlib/no_base64.go
Normal 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
11
pkg/yqlib/no_csv.go
Normal 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
|
||||||
|
}
|
||||||
@ -6,6 +6,6 @@ func NewINIDecoder() Decoder {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewINIEncoder(indent int) Encoder {
|
func NewINIEncoder() Encoder {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
11
pkg/yqlib/no_props.go
Normal 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
7
pkg/yqlib/no_sh.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build yq_nosh
|
||||||
|
|
||||||
|
package yqlib
|
||||||
|
|
||||||
|
func NewShEncoder() Encoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
pkg/yqlib/no_shellvariables.go
Normal file
7
pkg/yqlib/no_shellvariables.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build yq_noshell
|
||||||
|
|
||||||
|
package yqlib
|
||||||
|
|
||||||
|
func NewShellVariablesEncoder() Encoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
11
pkg/yqlib/no_uri.go
Normal file
11
pkg/yqlib/no_uri.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//go:build yq_nouri
|
||||||
|
|
||||||
|
package yqlib
|
||||||
|
|
||||||
|
func NewUriDecoder() Decoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUriEncoder() Encoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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]))
|
||||||
|
|||||||
@ -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]`,
|
||||||
|
|||||||
76
pkg/yqlib/printer_node_info.go
Normal file
76
pkg/yqlib/printer_node_info.go
Normal 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
|
||||||
|
}
|
||||||
51
pkg/yqlib/printer_node_info_test.go
Normal file
51
pkg/yqlib/printer_node_info_test.go
Normal 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"))
|
||||||
|
}
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -258,3 +258,11 @@ nolint
|
|||||||
shortfile
|
shortfile
|
||||||
Unmarshalling
|
Unmarshalling
|
||||||
noini
|
noini
|
||||||
|
nocsv
|
||||||
|
nobase64
|
||||||
|
nouri
|
||||||
|
noprops
|
||||||
|
nosh
|
||||||
|
noshell
|
||||||
|
tinygo
|
||||||
|
nonexistent
|
||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
4
scripts/build-tinygo-yq.sh
Executable 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" .
|
||||||
@ -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
|
||||||
@ -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
36
test/format_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user