mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
342 lines
10 KiB
Go
342 lines
10 KiB
Go
//go:build !yq_nohcl
|
|
|
|
package yqlib
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type hclEncoder struct {
|
|
}
|
|
|
|
// NewHclEncoder creates a new HCL encoder
|
|
func NewHclEncoder() Encoder {
|
|
return &hclEncoder{}
|
|
}
|
|
|
|
func (he *hclEncoder) CanHandleAliases() bool {
|
|
return false
|
|
}
|
|
|
|
func (he *hclEncoder) PrintDocumentSeparator(_ io.Writer) error {
|
|
return nil
|
|
}
|
|
|
|
func (he *hclEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
|
log.Debugf("I need to encode %v", NodeToString(node))
|
|
|
|
f := hclwrite.NewEmptyFile()
|
|
body := f.Body()
|
|
if err := he.encodeNode(body, node); err != nil {
|
|
return fmt.Errorf("failed to encode HCL: %w", err)
|
|
}
|
|
|
|
// Get the formatted output and remove extra spacing before '='
|
|
output := f.Bytes()
|
|
compactOutput := he.compactSpacing(output)
|
|
|
|
_, err := writer.Write(compactOutput)
|
|
return err
|
|
}
|
|
|
|
// compactSpacing removes extra whitespace before '=' in attribute assignments
|
|
func (he *hclEncoder) compactSpacing(input []byte) []byte {
|
|
// Use regex to replace multiple spaces before = with single space
|
|
re := regexp.MustCompile(`(\S)\s{2,}=`)
|
|
return re.ReplaceAll(input, []byte("$1 ="))
|
|
}
|
|
|
|
// Helper runes for unquoted identifiers
|
|
func isHCLIdentifierStart(r rune) bool {
|
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_'
|
|
}
|
|
|
|
func isHCLIdentifierPart(r rune) bool {
|
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-'
|
|
}
|
|
|
|
// isValidHCLIdentifier checks if a string is a valid HCL identifier (unquoted)
|
|
func isValidHCLIdentifier(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
// HCL identifiers must start with a letter or underscore
|
|
// and contain only letters, digits, underscores, and hyphens
|
|
for i, r := range s {
|
|
if i == 0 {
|
|
if !isHCLIdentifierStart(r) {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
if !isHCLIdentifierPart(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// tokensForRawHCLExpr produces a minimal token stream for a simple HCL expression so we can
|
|
// write it without introducing quotes (e.g. function calls like upper(message)).
|
|
func tokensForRawHCLExpr(expr string) (hclwrite.Tokens, error) {
|
|
var tokens hclwrite.Tokens
|
|
for i := 0; i < len(expr); {
|
|
ch := expr[i]
|
|
switch {
|
|
case ch == ' ' || ch == '\t':
|
|
i++
|
|
continue
|
|
case isHCLIdentifierStart(rune(ch)):
|
|
start := i
|
|
i++
|
|
for i < len(expr) && isHCLIdentifierPart(rune(expr[i])) {
|
|
i++
|
|
}
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenIdent, Bytes: []byte(expr[start:i])})
|
|
continue
|
|
case ch >= '0' && ch <= '9':
|
|
start := i
|
|
i++
|
|
for i < len(expr) && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] == '.') {
|
|
i++
|
|
}
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNumberLit, Bytes: []byte(expr[start:i])})
|
|
continue
|
|
case ch == '(':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenOParen, Bytes: []byte{'('}})
|
|
case ch == ')':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCParen, Bytes: []byte{')'}})
|
|
case ch == ',':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte{','}})
|
|
case ch == '.':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenDot, Bytes: []byte{'.'}})
|
|
case ch == '+':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenPlus, Bytes: []byte{'+'}})
|
|
case ch == '-':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenMinus, Bytes: []byte{'-'}})
|
|
case ch == '*':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenStar, Bytes: []byte{'*'}})
|
|
case ch == '/':
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenSlash, Bytes: []byte{'/'}})
|
|
default:
|
|
return nil, fmt.Errorf("unsupported character %q in raw HCL expression", ch)
|
|
}
|
|
i++
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
// encodeNode encodes a CandidateNode directly to HCL, preserving style information
|
|
func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error {
|
|
if node.Kind != MappingNode {
|
|
return fmt.Errorf("HCL encoder expects a mapping at the root level")
|
|
}
|
|
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
keyNode := node.Content[i]
|
|
valueNode := node.Content[i+1]
|
|
key := keyNode.Value
|
|
|
|
// Check if value is a mapping without FlowStyle -> render as block
|
|
if valueNode.Kind == MappingNode && valueNode.Style != FlowStyle {
|
|
// Render as block: key { ... }
|
|
block := body.AppendNewBlock(key, nil)
|
|
if err := he.encodeNodeAttributes(block.Body(), valueNode); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Render as attribute: key = value
|
|
// Check the style to determine how to encode strings
|
|
if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" {
|
|
if valueNode.Style&LiteralStyle != 0 {
|
|
tokens, err := tokensForRawHCLExpr(valueNode.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body.SetAttributeRaw(key, tokens)
|
|
continue
|
|
}
|
|
// Check style: DoubleQuotedStyle means template, no style could be unquoted or regular
|
|
// To distinguish unquoted from regular, we check if the value is a valid identifier
|
|
if valueNode.Style&DoubleQuotedStyle != 0 && strings.Contains(valueNode.Value, "${") {
|
|
// Template string - use raw tokens to preserve ${} syntax
|
|
tokens := hclwrite.Tokens{
|
|
{Type: hclsyntax.TokenOQuote, Bytes: []byte{'"'}},
|
|
}
|
|
// Parse the string and add tokens
|
|
for i := 0; i < len(valueNode.Value); i++ {
|
|
if i < len(valueNode.Value)-1 && valueNode.Value[i] == '$' && valueNode.Value[i+1] == '{' {
|
|
// Start of template interpolation
|
|
tokens = append(tokens, &hclwrite.Token{
|
|
Type: hclsyntax.TokenTemplateInterp,
|
|
Bytes: []byte("${"),
|
|
})
|
|
i++ // skip the '{'
|
|
// Find the matching '}'
|
|
start := i + 1
|
|
depth := 1
|
|
for i++; i < len(valueNode.Value) && depth > 0; i++ {
|
|
switch valueNode.Value[i] {
|
|
case '{':
|
|
depth++
|
|
case '}':
|
|
depth--
|
|
}
|
|
}
|
|
i-- // back up to the '}'
|
|
interpExpr := valueNode.Value[start:i]
|
|
tokens = append(tokens, &hclwrite.Token{
|
|
Type: hclsyntax.TokenIdent,
|
|
Bytes: []byte(interpExpr),
|
|
})
|
|
tokens = append(tokens, &hclwrite.Token{
|
|
Type: hclsyntax.TokenTemplateSeqEnd,
|
|
Bytes: []byte("}"),
|
|
})
|
|
} else {
|
|
// Regular character
|
|
tokens = append(tokens, &hclwrite.Token{
|
|
Type: hclsyntax.TokenQuotedLit,
|
|
Bytes: []byte{valueNode.Value[i]},
|
|
})
|
|
}
|
|
}
|
|
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCQuote, Bytes: []byte{'"'}})
|
|
body.SetAttributeRaw(key, tokens)
|
|
} else if isValidHCLIdentifier(valueNode.Value) && valueNode.Style == 0 {
|
|
// Could be unquoted identifier - but only if it came from HCL originally
|
|
// For safety, only use traversal if style is explicitly 0 (not set)
|
|
// This avoids treating strings from YAML as unquoted
|
|
traversal := hcl.Traversal{
|
|
hcl.TraverseRoot{Name: valueNode.Value},
|
|
}
|
|
body.SetAttributeTraversal(key, traversal)
|
|
} else {
|
|
// Regular quoted string - use cty.Value
|
|
ctyValue, err := nodeToCtyValue(valueNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body.SetAttributeValue(key, ctyValue)
|
|
}
|
|
} else {
|
|
// Non-string value - use cty.Value
|
|
ctyValue, err := nodeToCtyValue(valueNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body.SetAttributeValue(key, ctyValue)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// encodeNodeAttributes encodes the attributes of a mapping node (used for blocks)
|
|
func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateNode) error {
|
|
if node.Kind != MappingNode {
|
|
return fmt.Errorf("expected mapping node for block body")
|
|
}
|
|
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
keyNode := node.Content[i]
|
|
valueNode := node.Content[i+1]
|
|
key := keyNode.Value
|
|
|
|
// Check if this is an unquoted identifier (no DoubleQuotedStyle)
|
|
if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" && valueNode.Style&DoubleQuotedStyle == 0 {
|
|
if valueNode.Style&LiteralStyle != 0 {
|
|
tokens, err := tokensForRawHCLExpr(valueNode.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body.SetAttributeRaw(key, tokens)
|
|
continue
|
|
}
|
|
// Unquoted identifier - use traversal
|
|
traversal := hcl.Traversal{
|
|
hcl.TraverseRoot{Name: valueNode.Value},
|
|
}
|
|
body.SetAttributeTraversal(key, traversal)
|
|
} else {
|
|
// Quoted value or non-string - use cty.Value
|
|
ctyValue, err := nodeToCtyValue(valueNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body.SetAttributeValue(key, ctyValue)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nodeToCtyValue converts a CandidateNode directly to cty.Value, preserving order
|
|
func nodeToCtyValue(node *CandidateNode) (cty.Value, error) {
|
|
switch node.Kind {
|
|
case ScalarNode:
|
|
// Parse scalar value based on its tag
|
|
switch node.Tag {
|
|
case "!!bool":
|
|
return cty.BoolVal(node.Value == "true"), nil
|
|
case "!!int":
|
|
var i int64
|
|
_, err := fmt.Sscanf(node.Value, "%d", &i)
|
|
if err != nil {
|
|
return cty.NilVal, err
|
|
}
|
|
return cty.NumberIntVal(i), nil
|
|
case "!!float":
|
|
var f float64
|
|
_, err := fmt.Sscanf(node.Value, "%f", &f)
|
|
if err != nil {
|
|
return cty.NilVal, err
|
|
}
|
|
return cty.NumberFloatVal(f), nil
|
|
case "!!null":
|
|
return cty.NullVal(cty.DynamicPseudoType), nil
|
|
default:
|
|
// Default to string
|
|
return cty.StringVal(node.Value), nil
|
|
}
|
|
case MappingNode:
|
|
// Preserve order by iterating Content directly
|
|
m := make(map[string]cty.Value)
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
keyNode := node.Content[i]
|
|
valueNode := node.Content[i+1]
|
|
v, err := nodeToCtyValue(valueNode)
|
|
if err != nil {
|
|
return cty.NilVal, err
|
|
}
|
|
m[keyNode.Value] = v
|
|
}
|
|
return cty.ObjectVal(m), nil
|
|
case SequenceNode:
|
|
vals := make([]cty.Value, len(node.Content))
|
|
for i, item := range node.Content {
|
|
v, err := nodeToCtyValue(item)
|
|
if err != nil {
|
|
return cty.NilVal, err
|
|
}
|
|
vals[i] = v
|
|
}
|
|
return cty.TupleVal(vals), nil
|
|
case AliasNode:
|
|
return cty.NilVal, fmt.Errorf("HCL encoder does not support aliases")
|
|
default:
|
|
return cty.NilVal, fmt.Errorf("unsupported node kind: %v", node.Kind)
|
|
}
|
|
}
|