mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
472 lines
14 KiB
Go
472 lines
14 KiB
Go
//go:build !yq_nohcl
|
|
|
|
package yqlib
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type hclDecoder struct {
|
|
file *hcl.File
|
|
fileBytes []byte
|
|
readAnything bool
|
|
documentIndex uint
|
|
}
|
|
|
|
func NewHclDecoder() Decoder {
|
|
return &hclDecoder{}
|
|
}
|
|
|
|
// sortedAttributes returns attributes in declaration order by source position
|
|
func sortedAttributes(attrs hclsyntax.Attributes) []*attributeWithName {
|
|
var sorted []*attributeWithName
|
|
for name, attr := range attrs {
|
|
sorted = append(sorted, &attributeWithName{Name: name, Attr: attr})
|
|
}
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return sorted[i].Attr.Range().Start.Byte < sorted[j].Attr.Range().Start.Byte
|
|
})
|
|
return sorted
|
|
}
|
|
|
|
type attributeWithName struct {
|
|
Name string
|
|
Attr *hclsyntax.Attribute
|
|
}
|
|
|
|
// extractLineComment extracts any inline comment after the given position
|
|
func extractLineComment(src []byte, endPos int) string {
|
|
// Look for # comment after the token
|
|
for i := endPos; i < len(src); i++ {
|
|
if src[i] == '#' {
|
|
// Found comment, extract until end of line
|
|
start := i
|
|
for i < len(src) && src[i] != '\n' {
|
|
i++
|
|
}
|
|
return strings.TrimSpace(string(src[start:i]))
|
|
}
|
|
if src[i] == '\n' {
|
|
// Hit newline before comment
|
|
break
|
|
}
|
|
// Skip whitespace and other characters
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractHeadComment extracts comments before a given start position
|
|
func extractHeadComment(src []byte, startPos int) string {
|
|
var comments []string
|
|
|
|
// Start just before the token and skip trailing whitespace
|
|
i := startPos - 1
|
|
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
|
i--
|
|
}
|
|
|
|
for i >= 0 {
|
|
// Find line boundaries
|
|
lineEnd := i
|
|
for i >= 0 && src[i] != '\n' {
|
|
i--
|
|
}
|
|
lineStart := i + 1
|
|
|
|
line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
if trimmed == "" {
|
|
break
|
|
}
|
|
|
|
if !strings.HasPrefix(trimmed, "#") {
|
|
break
|
|
}
|
|
|
|
comments = append([]string{trimmed}, comments...)
|
|
|
|
// Move to previous line (skip any whitespace/newlines)
|
|
i = lineStart - 1
|
|
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
|
i--
|
|
}
|
|
}
|
|
|
|
if len(comments) > 0 {
|
|
return strings.Join(comments, "\n")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (dec *hclDecoder) Init(reader io.Reader) error {
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file, diags := hclsyntax.ParseConfig(data, "input.hcl", hcl.Pos{Line: 1, Column: 1})
|
|
if diags != nil && diags.HasErrors() {
|
|
return fmt.Errorf("hcl parse error: %w", diags)
|
|
}
|
|
dec.file = file
|
|
dec.fileBytes = data
|
|
dec.readAnything = false
|
|
dec.documentIndex = 0
|
|
return nil
|
|
}
|
|
|
|
func (dec *hclDecoder) Decode() (*CandidateNode, error) {
|
|
if dec.readAnything {
|
|
return nil, io.EOF
|
|
}
|
|
dec.readAnything = true
|
|
|
|
if dec.file == nil {
|
|
return nil, fmt.Errorf("no hcl file parsed")
|
|
}
|
|
|
|
root := &CandidateNode{Kind: MappingNode}
|
|
|
|
// process attributes in declaration order
|
|
body := dec.file.Body.(*hclsyntax.Body)
|
|
firstAttr := true
|
|
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
|
keyNode := createStringScalarNode(attrWithName.Name)
|
|
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
|
|
|
|
// Attach comments if any
|
|
attrRange := attrWithName.Attr.Range()
|
|
headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte)
|
|
if firstAttr && headComment != "" {
|
|
// For the first attribute, apply its head comment to the root
|
|
root.HeadComment = headComment
|
|
firstAttr = false
|
|
} else if headComment != "" {
|
|
keyNode.HeadComment = headComment
|
|
}
|
|
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {
|
|
valNode.LineComment = lineComment
|
|
}
|
|
|
|
root.AddKeyValueChild(keyNode, valNode)
|
|
}
|
|
|
|
// process blocks
|
|
// Count blocks by type at THIS level to detect multiple separate blocks
|
|
blocksByType := make(map[string]int)
|
|
for _, block := range body.Blocks {
|
|
blocksByType[block.Type]++
|
|
}
|
|
|
|
for _, block := range body.Blocks {
|
|
addBlockToMapping(root, block, dec.fileBytes, blocksByType[block.Type] > 1)
|
|
}
|
|
|
|
dec.documentIndex++
|
|
root.document = dec.documentIndex - 1
|
|
return root, nil
|
|
}
|
|
|
|
func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode {
|
|
node := &CandidateNode{Kind: MappingNode}
|
|
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
|
key := createStringScalarNode(attrWithName.Name)
|
|
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
|
|
|
|
// Attach comments if any
|
|
attrRange := attrWithName.Attr.Range()
|
|
if headComment := extractHeadComment(src, attrRange.Start.Byte); headComment != "" {
|
|
key.HeadComment = headComment
|
|
}
|
|
if lineComment := extractLineComment(src, attrRange.End.Byte); lineComment != "" {
|
|
val.LineComment = lineComment
|
|
}
|
|
|
|
node.AddKeyValueChild(key, val)
|
|
}
|
|
|
|
// Process nested blocks, counting blocks by type at THIS level
|
|
// to detect which block types appear multiple times
|
|
blocksByType := make(map[string]int)
|
|
for _, block := range body.Blocks {
|
|
blocksByType[block.Type]++
|
|
}
|
|
|
|
for _, block := range body.Blocks {
|
|
addBlockToMapping(node, block, src, blocksByType[block.Type] > 1)
|
|
}
|
|
return node
|
|
}
|
|
|
|
// addBlockToMapping nests block type and labels into the parent mapping, merging children.
|
|
// isMultipleBlocksOfType indicates if there are multiple blocks of this type at THIS level
|
|
func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte, isMultipleBlocksOfType bool) {
|
|
bodyNode := hclBodyToNode(block.Body, src)
|
|
current := parent
|
|
|
|
// ensure block type mapping exists
|
|
var typeNode *CandidateNode
|
|
for i := 0; i < len(current.Content); i += 2 {
|
|
if current.Content[i].Value == block.Type {
|
|
typeNode = current.Content[i+1]
|
|
break
|
|
}
|
|
}
|
|
if typeNode == nil {
|
|
_, typeNode = current.AddKeyValueChild(createStringScalarNode(block.Type), &CandidateNode{Kind: MappingNode})
|
|
// Mark the type node if there are multiple blocks of this type at this level
|
|
// This tells the encoder to emit them as separate blocks rather than consolidating them
|
|
if isMultipleBlocksOfType {
|
|
typeNode.EncodeSeparate = true
|
|
}
|
|
}
|
|
current = typeNode
|
|
|
|
// walk labels, creating/merging mappings
|
|
for _, label := range block.Labels {
|
|
var next *CandidateNode
|
|
for i := 0; i < len(current.Content); i += 2 {
|
|
if current.Content[i].Value == label {
|
|
next = current.Content[i+1]
|
|
break
|
|
}
|
|
}
|
|
if next == nil {
|
|
_, next = current.AddKeyValueChild(createStringScalarNode(label), &CandidateNode{Kind: MappingNode})
|
|
}
|
|
current = next
|
|
}
|
|
|
|
// merge body attributes/blocks into the final mapping
|
|
for i := 0; i < len(bodyNode.Content); i += 2 {
|
|
current.AddKeyValueChild(bodyNode.Content[i], bodyNode.Content[i+1])
|
|
}
|
|
}
|
|
|
|
func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode {
|
|
// handle literal values directly
|
|
switch e := expr.(type) {
|
|
case *hclsyntax.LiteralValueExpr:
|
|
v := e.Val
|
|
if v.IsNull() {
|
|
return createScalarNode(nil, "")
|
|
}
|
|
switch {
|
|
case v.Type().Equals(cty.String):
|
|
// prefer to extract exact source (to avoid extra quoting) when available
|
|
// Prefer the actual cty string value
|
|
s := v.AsString()
|
|
node := createScalarNode(s, s)
|
|
// Don't set style for regular quoted strings - let YAML handle naturally
|
|
return node
|
|
case v.Type().Equals(cty.Bool):
|
|
b := v.True()
|
|
return createScalarNode(b, strconv.FormatBool(b))
|
|
case v.Type() == cty.Number:
|
|
// prefer integers when the numeric value is integral
|
|
bf := v.AsBigFloat()
|
|
if bf == nil {
|
|
// fallback to string
|
|
return createStringScalarNode(v.GoString())
|
|
}
|
|
// check if bf represents an exact integer
|
|
if intVal, acc := bf.Int(nil); acc == big.Exact {
|
|
s := intVal.String()
|
|
return createScalarNode(intVal.Int64(), s)
|
|
}
|
|
s := bf.Text('g', -1)
|
|
return createScalarNode(0.0, s)
|
|
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
|
|
seq := &CandidateNode{Kind: SequenceNode}
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
_, val := it.Element()
|
|
// convert cty.Value to a node by wrapping in literal expr via string representation
|
|
child := convertCtyValueToNode(val)
|
|
seq.AddChild(child)
|
|
}
|
|
return seq
|
|
case v.Type().IsMapType() || v.Type().IsObjectType():
|
|
m := &CandidateNode{Kind: MappingNode}
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
key, val := it.Element()
|
|
keyStr := key.AsString()
|
|
keyNode := createStringScalarNode(keyStr)
|
|
valNode := convertCtyValueToNode(val)
|
|
m.AddKeyValueChild(keyNode, valNode)
|
|
}
|
|
return m
|
|
default:
|
|
// fallback to string
|
|
s := v.GoString()
|
|
return createStringScalarNode(s)
|
|
}
|
|
case *hclsyntax.TupleConsExpr:
|
|
// parse tuple/list into YAML sequence
|
|
seq := &CandidateNode{Kind: SequenceNode}
|
|
for _, exprVal := range e.Exprs {
|
|
child := convertHclExprToNode(exprVal, src)
|
|
seq.AddChild(child)
|
|
}
|
|
return seq
|
|
case *hclsyntax.ObjectConsExpr:
|
|
// parse object into YAML mapping
|
|
m := &CandidateNode{Kind: MappingNode}
|
|
m.Style = FlowStyle // Mark as inline object (flow style) for encoder
|
|
for _, item := range e.Items {
|
|
// evaluate key expression to get the key string
|
|
keyVal, keyDiags := item.KeyExpr.Value(nil)
|
|
if keyDiags != nil && keyDiags.HasErrors() {
|
|
// fallback: try to extract key from source
|
|
r := item.KeyExpr.Range()
|
|
start := r.Start.Byte
|
|
end := r.End.Byte
|
|
if start >= 0 && end >= start && end <= len(src) {
|
|
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start:end])))
|
|
valNode := convertHclExprToNode(item.ValueExpr, src)
|
|
m.AddKeyValueChild(keyNode, valNode)
|
|
}
|
|
continue
|
|
}
|
|
keyStr := keyVal.AsString()
|
|
keyNode := createStringScalarNode(keyStr)
|
|
valNode := convertHclExprToNode(item.ValueExpr, src)
|
|
m.AddKeyValueChild(keyNode, valNode)
|
|
}
|
|
return m
|
|
case *hclsyntax.TemplateExpr:
|
|
// Reconstruct template string, preserving ${} syntax for interpolations
|
|
var parts []string
|
|
for _, p := range e.Parts {
|
|
switch lp := p.(type) {
|
|
case *hclsyntax.LiteralValueExpr:
|
|
if lp.Val.Type().Equals(cty.String) {
|
|
parts = append(parts, lp.Val.AsString())
|
|
} else {
|
|
parts = append(parts, lp.Val.GoString())
|
|
}
|
|
default:
|
|
// Non-literal expression - reconstruct with ${} wrapper
|
|
r := p.Range()
|
|
start := r.Start.Byte
|
|
end := r.End.Byte
|
|
if start >= 0 && end >= start && end <= len(src) {
|
|
exprText := string(src[start:end])
|
|
parts = append(parts, "${"+exprText+"}")
|
|
} else {
|
|
parts = append(parts, fmt.Sprintf("${%v}", p))
|
|
}
|
|
}
|
|
}
|
|
combined := strings.Join(parts, "")
|
|
node := createScalarNode(combined, combined)
|
|
// Set DoubleQuotedStyle for all templates (which includes all quoted strings in HCL)
|
|
// This ensures HCL roundtrips preserve quotes, and YAML properly quotes strings with ${}
|
|
node.Style = DoubleQuotedStyle
|
|
return node
|
|
case *hclsyntax.ScopeTraversalExpr:
|
|
// Simple identifier/traversal (e.g. unquoted string literal in HCL)
|
|
r := e.Range()
|
|
start := r.Start.Byte
|
|
end := r.End.Byte
|
|
if start >= 0 && end >= start && end <= len(src) {
|
|
text := strings.TrimSpace(string(src[start:end]))
|
|
return createStringScalarNode(text)
|
|
}
|
|
// Fallback to root name if source unavailable
|
|
if len(e.Traversal) > 0 {
|
|
if root, ok := e.Traversal[0].(hcl.TraverseRoot); ok {
|
|
return createStringScalarNode(root.Name)
|
|
}
|
|
}
|
|
return createStringScalarNode("")
|
|
case *hclsyntax.FunctionCallExpr:
|
|
// Preserve function calls as raw expressions for roundtrip
|
|
r := e.Range()
|
|
start := r.Start.Byte
|
|
end := r.End.Byte
|
|
if start >= 0 && end >= start && end <= len(src) {
|
|
text := strings.TrimSpace(string(src[start:end]))
|
|
node := createStringScalarNode(text)
|
|
node.Style = 0
|
|
return node
|
|
}
|
|
node := createStringScalarNode(e.Name)
|
|
node.Style = 0
|
|
return node
|
|
default:
|
|
// try to evaluate the expression (handles unary, binary ops, etc.)
|
|
val, diags := expr.Value(nil)
|
|
if diags == nil || !diags.HasErrors() {
|
|
// successfully evaluated, convert cty.Value to node
|
|
return convertCtyValueToNode(val)
|
|
}
|
|
// fallback: extract source text for the expression
|
|
r := expr.Range()
|
|
start := r.Start.Byte
|
|
end := r.End.Byte
|
|
if start >= 0 && end >= start && end <= len(src) {
|
|
text := string(src[start:end])
|
|
// Mark as unquoted expression so encoder emits without quoting
|
|
node := createStringScalarNode(text)
|
|
node.Style = 0
|
|
return node
|
|
}
|
|
return createStringScalarNode(fmt.Sprintf("%v", expr))
|
|
}
|
|
}
|
|
|
|
func convertCtyValueToNode(v cty.Value) *CandidateNode {
|
|
if v.IsNull() {
|
|
return createScalarNode(nil, "")
|
|
}
|
|
switch {
|
|
case v.Type().Equals(cty.String):
|
|
return createScalarNode("", v.AsString())
|
|
case v.Type().Equals(cty.Bool):
|
|
b := v.True()
|
|
return createScalarNode(b, strconv.FormatBool(b))
|
|
case v.Type() == cty.Number:
|
|
bf := v.AsBigFloat()
|
|
if bf == nil {
|
|
return createStringScalarNode(v.GoString())
|
|
}
|
|
if intVal, acc := bf.Int(nil); acc == big.Exact {
|
|
s := intVal.String()
|
|
return createScalarNode(intVal.Int64(), s)
|
|
}
|
|
s := bf.Text('g', -1)
|
|
return createScalarNode(0.0, s)
|
|
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
|
|
seq := &CandidateNode{Kind: SequenceNode}
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
_, val := it.Element()
|
|
seq.AddChild(convertCtyValueToNode(val))
|
|
}
|
|
return seq
|
|
case v.Type().IsMapType() || v.Type().IsObjectType():
|
|
m := &CandidateNode{Kind: MappingNode}
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
key, val := it.Element()
|
|
keyNode := createStringScalarNode(key.AsString())
|
|
valNode := convertCtyValueToNode(val)
|
|
m.AddKeyValueChild(keyNode, valNode)
|
|
}
|
|
return m
|
|
default:
|
|
return createStringScalarNode(v.GoString())
|
|
}
|
|
}
|