mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-04 19:35:38 +00:00
Refining
This commit is contained in:
parent
bd3a647650
commit
7923d04bdd
8
examples/sample.hcl
Normal file
8
examples/sample.hcl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Arithmetic with literals and application-provided variables
|
||||||
|
sum = 1 + addend
|
||||||
|
|
||||||
|
# String interpolation and templates
|
||||||
|
message = "Hello, ${name}!"
|
||||||
|
|
||||||
|
# Application-provided functions
|
||||||
|
shouty_message = upper(message)
|
||||||
@ -128,7 +128,9 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
|||||||
// prefer to extract exact source (to avoid extra quoting) when available
|
// prefer to extract exact source (to avoid extra quoting) when available
|
||||||
// Prefer the actual cty string value
|
// Prefer the actual cty string value
|
||||||
s := v.AsString()
|
s := v.AsString()
|
||||||
return createScalarNode(s, s)
|
node := createScalarNode(s, s)
|
||||||
|
// Don't set style for regular quoted strings - let YAML handle naturally
|
||||||
|
return node
|
||||||
case v.Type().Equals(cty.Bool):
|
case v.Type().Equals(cty.Bool):
|
||||||
b := v.True()
|
b := v.True()
|
||||||
return createScalarNode(b, strconv.FormatBool(b))
|
return createScalarNode(b, strconv.FormatBool(b))
|
||||||
@ -192,8 +194,8 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
|||||||
r := item.KeyExpr.Range()
|
r := item.KeyExpr.Range()
|
||||||
start := r.Start.Byte
|
start := r.Start.Byte
|
||||||
end := r.End.Byte
|
end := r.End.Byte
|
||||||
if start > 0 && end >= start && end <= len(src) {
|
if start >= 0 && end >= start && end <= len(src) {
|
||||||
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start-1 : end])))
|
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start:end])))
|
||||||
valNode := convertHclExprToNode(item.ValueExpr, src)
|
valNode := convertHclExprToNode(item.ValueExpr, src)
|
||||||
m.AddKeyValueChild(keyNode, valNode)
|
m.AddKeyValueChild(keyNode, valNode)
|
||||||
}
|
}
|
||||||
@ -206,7 +208,7 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
|||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
case *hclsyntax.TemplateExpr:
|
case *hclsyntax.TemplateExpr:
|
||||||
// join parts; if single literal, return that string
|
// Reconstruct template string, preserving ${} syntax for interpolations
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, p := range e.Parts {
|
for _, p := range e.Parts {
|
||||||
switch lp := p.(type) {
|
switch lp := p.(type) {
|
||||||
@ -217,18 +219,24 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
|||||||
parts = append(parts, lp.Val.GoString())
|
parts = append(parts, lp.Val.GoString())
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
// Non-literal expression - reconstruct with ${} wrapper
|
||||||
r := p.Range()
|
r := p.Range()
|
||||||
start := r.Start.Byte
|
start := r.Start.Byte
|
||||||
end := r.End.Byte
|
end := r.End.Byte
|
||||||
if start > 0 && end >= start && end <= len(src) {
|
if start >= 0 && end >= start && end <= len(src) {
|
||||||
parts = append(parts, strings.TrimSpace(string(src[start-1:end])))
|
exprText := string(src[start:end])
|
||||||
|
parts = append(parts, "${"+exprText+"}")
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, fmt.Sprintf("%v", p))
|
parts = append(parts, fmt.Sprintf("${%v}", p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
combined := strings.Join(parts, "")
|
combined := strings.Join(parts, "")
|
||||||
return createScalarNode(combined, combined)
|
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
|
||||||
default:
|
default:
|
||||||
// try to evaluate the expression (handles unary, binary ops, etc.)
|
// try to evaluate the expression (handles unary, binary ops, etc.)
|
||||||
val, diags := expr.Value(nil)
|
val, diags := expr.Value(nil)
|
||||||
@ -240,8 +248,9 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
|||||||
r := expr.Range()
|
r := expr.Range()
|
||||||
start := r.Start.Byte
|
start := r.Start.Byte
|
||||||
end := r.End.Byte
|
end := r.End.Byte
|
||||||
if start > 0 && end >= start && end <= len(src) {
|
if start >= 0 && end >= start && end <= len(src) {
|
||||||
text := string(src[start-1 : end])
|
text := string(src[start:end])
|
||||||
|
// Unquoted identifier - no style
|
||||||
return createStringScalarNode(text)
|
return createStringScalarNode(text)
|
||||||
}
|
}
|
||||||
return createStringScalarNode(fmt.Sprintf("%v", expr))
|
return createStringScalarNode(fmt.Sprintf("%v", expr))
|
||||||
|
|||||||
@ -6,7 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
@ -55,6 +58,36 @@ func (he *hclEncoder) compactSpacing(input []byte) []byte {
|
|||||||
return re.ReplaceAll(input, []byte("$1 ="))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// encodeNode encodes a CandidateNode directly to HCL, preserving style information
|
// encodeNode encodes a CandidateNode directly to HCL, preserving style information
|
||||||
func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error {
|
func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error {
|
||||||
if node.Kind != MappingNode {
|
if node.Kind != MappingNode {
|
||||||
@ -75,11 +108,79 @@ func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Render as attribute: key = value
|
// Render as attribute: key = value
|
||||||
ctyValue, err := nodeToCtyValue(valueNode)
|
// Check the style to determine how to encode strings
|
||||||
if err != nil {
|
if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" {
|
||||||
return err
|
// 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)
|
||||||
}
|
}
|
||||||
body.SetAttributeValue(key, ctyValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -96,11 +197,21 @@ func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateN
|
|||||||
valueNode := node.Content[i+1]
|
valueNode := node.Content[i+1]
|
||||||
key := keyNode.Value
|
key := keyNode.Value
|
||||||
|
|
||||||
ctyValue, err := nodeToCtyValue(valueNode)
|
// Check if this is an unquoted identifier (no DoubleQuotedStyle)
|
||||||
if err != nil {
|
if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" && valueNode.Style&DoubleQuotedStyle == 0 {
|
||||||
return err
|
// 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)
|
||||||
}
|
}
|
||||||
body.SetAttributeValue(key, ctyValue)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,9 +10,33 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
{
|
{
|
||||||
description: "Simple decode",
|
description: "Simple decode",
|
||||||
input: `io_mode = "async"`,
|
input: `io_mode = "async"`,
|
||||||
|
expected: "io_mode: \"async\"\n",
|
||||||
|
scenarioType: "decode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Simple decode, no quotes",
|
||||||
|
input: `io_mode = async`,
|
||||||
expected: "io_mode: async\n",
|
expected: "io_mode: async\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Simple roundtrip, no quotes",
|
||||||
|
input: `io_mode = async`,
|
||||||
|
expected: "io_mode = async\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Template decode",
|
||||||
|
input: `message = "Hello, ${name}!"`,
|
||||||
|
expected: "message: \"Hello, ${name}!\"\n",
|
||||||
|
scenarioType: "decode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Template roundtrip",
|
||||||
|
input: `message = "Hello, ${name}!"`,
|
||||||
|
expected: "message = \"Hello, ${name}!\"\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "number attribute",
|
description: "number attribute",
|
||||||
input: `port = 8080`,
|
input: `port = 8080`,
|
||||||
@ -34,13 +58,13 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
{
|
{
|
||||||
description: "list of strings",
|
description: "list of strings",
|
||||||
input: `tags = ["a", "b"]`,
|
input: `tags = ["a", "b"]`,
|
||||||
expected: "tags:\n - a\n - b\n",
|
expected: "tags:\n - \"a\"\n - \"b\"\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "object/map attribute",
|
description: "object/map attribute",
|
||||||
input: `obj = { a = 1, b = "two" }`,
|
input: `obj = { a = 1, b = "two" }`,
|
||||||
expected: "obj: {a: 1, b: two}\n",
|
expected: "obj: {a: 1, b: \"two\"}\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,7 +76,7 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
{
|
{
|
||||||
description: "multiple attributes",
|
description: "multiple attributes",
|
||||||
input: "name = \"app\"\nversion = 1\nenabled = true",
|
input: "name = \"app\"\nversion = 1\nenabled = true",
|
||||||
expected: "name: app\nversion: 1\nenabled: true\n",
|
expected: "name: \"app\"\nversion: 1\nenabled: true\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -76,19 +100,19 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
{
|
{
|
||||||
description: "nested object",
|
description: "nested object",
|
||||||
input: `config = { db = { host = "localhost", port = 5432 } }`,
|
input: `config = { db = { host = "localhost", port = 5432 } }`,
|
||||||
expected: "config: {db: {host: localhost, port: 5432}}\n",
|
expected: "config: {db: {host: \"localhost\", port: 5432}}\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "mixed list",
|
description: "mixed list",
|
||||||
input: `values = [1, "two", true]`,
|
input: `values = [1, "two", true]`,
|
||||||
expected: "values:\n - 1\n - two\n - true\n",
|
expected: "values:\n - 1\n - \"two\"\n - true\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "block with labels",
|
description: "block with labels",
|
||||||
input: `resource "aws_instance" "example" { ami = "ami-12345" }`,
|
input: `resource "aws_instance" "example" { ami = "ami-12345" }`,
|
||||||
expected: "resource aws_instance example:\n ami: ami-12345\n",
|
expected: "resource aws_instance example:\n ami: \"ami-12345\"\n",
|
||||||
scenarioType: "decode",
|
scenarioType: "decode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -189,8 +189,11 @@ risentveber
|
|||||||
rmescandon
|
rmescandon
|
||||||
Rosey
|
Rosey
|
||||||
roundtrip
|
roundtrip
|
||||||
|
roundtrips
|
||||||
Roundtrip
|
Roundtrip
|
||||||
roundtripping
|
roundtripping
|
||||||
|
Interp
|
||||||
|
interp
|
||||||
runningvms
|
runningvms
|
||||||
sadface
|
sadface
|
||||||
selfupdate
|
selfupdate
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user