From 8e731ac13cd5b536aed0d0686d64f8a76528e4d0 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Tue, 15 Jul 2025 21:35:54 +1000 Subject: [PATCH] Added "debug-node-info" flag for inspecting yq AST --- cmd/constant.go | 2 + cmd/evaluate_sequence_command.go | 5 ++ cmd/root.go | 1 + examples/data1.yaml | 11 +++-- pkg/yqlib/candidate_node.go | 75 ++++++++++++++++++++++++++++ pkg/yqlib/candidate_node_test.go | 44 +++++++++++++++++ pkg/yqlib/printer_node_info.go | 76 +++++++++++++++++++++++++++++ pkg/yqlib/printer_node_info_test.go | 51 +++++++++++++++++++ 8 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 pkg/yqlib/printer_node_info.go create mode 100644 pkg/yqlib/printer_node_info_test.go diff --git a/cmd/constant.go b/cmd/constant.go index 762897d0..bf15c4fb 100644 --- a/cmd/constant.go +++ b/cmd/constant.go @@ -2,6 +2,8 @@ package cmd var unwrapScalarFlag = newUnwrapFlag() +var printNodeInfo = false + var unwrapScalar = false var writeInplace = false diff --git a/cmd/evaluate_sequence_command.go b/cmd/evaluate_sequence_command.go index e722415e..8bba7677 100644 --- a/cmd/evaluate_sequence_command.go +++ b/cmd/evaluate_sequence_command.go @@ -105,6 +105,11 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) { } printer := yqlib.NewPrinter(encoder, printerWriter) + + if printNodeInfo { + printer = yqlib.NewNodeInfoPrinter(printerWriter) + } + if nulSepOutput { printer.SetNulSepOutput(true) } diff --git a/cmd/root.go b/cmd/root.go index 2071e278..268ff169 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -99,6 +99,7 @@ yq -P -oy sample.json } 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.") err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead") diff --git a/examples/data1.yaml b/examples/data1.yaml index 26c3161c..470fab3d 100644 --- a/examples/data1.yaml +++ b/examples/data1.yaml @@ -1,3 +1,8 @@ -Foo: 3 -apple: 1 -bar: 2 \ No newline at end of file +# 001 +--- +abc: # 001 +- 1 # one +- 2 # two + +--- +def # 002 \ No newline at end of file diff --git a/pkg/yqlib/candidate_node.go b/pkg/yqlib/candidate_node.go index 5156ac4b..51485f11 100644 --- a/pkg/yqlib/candidate_node.go +++ b/pkg/yqlib/candidate_node.go @@ -53,6 +53,20 @@ func createScalarNode(value interface{}, stringValue string) *CandidateNode { 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 { Kind Kind Style Style @@ -451,3 +465,64 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP 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, ",") +} diff --git a/pkg/yqlib/candidate_node_test.go b/pkg/yqlib/candidate_node_test.go index 2a09b48a..ceaa4052 100644 --- a/pkg/yqlib/candidate_node_test.go +++ b/pkg/yqlib/candidate_node_test.go @@ -160,3 +160,47 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) { 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) +} diff --git a/pkg/yqlib/printer_node_info.go b/pkg/yqlib/printer_node_info.go new file mode 100644 index 00000000..cd047aee --- /dev/null +++ b/pkg/yqlib/printer_node_info.go @@ -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 +} diff --git a/pkg/yqlib/printer_node_info_test.go b/pkg/yqlib/printer_node_info_test.go new file mode 100644 index 00000000..91b29ccc --- /dev/null +++ b/pkg/yqlib/printer_node_info_test.go @@ -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")) +}