mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
* feat: K8S KYAML output format support Reference: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/5295-kyaml/README.md Co-authored-by: Codex <codex@openai.com> Generated-with: OpenAI Codex CLI (partial) Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * build: gomodcache/gocache should not be committed Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * chore: fix spelling of behaviour Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> * build: pass GOFLAGS to docker to support buildvcs=false In trying to develop the KYAML support, various tests gave false positive results because they made assumptions about Git functionality Make it possible to avoid that by passing GOFLAGS='-buildvcs=false' to to Makefile. Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> * doc: cover documentScenarios for tests Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * build: exclude go caches from gosec Without tuning, gosec scans all of the vendor/gocache/gomodcache, taking several minutes (3m35 here), whereas the core of the yq takes only 15 seconds to scan. If we intend to remediate upstream issues in future; add a seperate target to scan those. Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> --------- Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> Co-authored-by: Codex <codex@openai.com>
319 lines
7.8 KiB
Go
319 lines
7.8 KiB
Go
//go:build !yq_nokyaml
|
|
|
|
package yqlib
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type kyamlEncoder struct {
|
|
prefs KYamlPreferences
|
|
}
|
|
|
|
func NewKYamlEncoder(prefs KYamlPreferences) Encoder {
|
|
return &kyamlEncoder{prefs: prefs}
|
|
}
|
|
|
|
func (ke *kyamlEncoder) CanHandleAliases() bool {
|
|
// KYAML is a restricted subset; avoid emitting anchors/aliases.
|
|
return false
|
|
}
|
|
|
|
func (ke *kyamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
|
|
return PrintYAMLDocumentSeparator(writer, ke.prefs.PrintDocSeparators)
|
|
}
|
|
|
|
func (ke *kyamlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
|
|
return PrintYAMLLeadingContent(writer, content, ke.prefs.PrintDocSeparators, ke.prefs.ColorsEnabled)
|
|
}
|
|
|
|
func (ke *kyamlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
|
log.Debug("encoderKYaml - going to print %v", NodeToString(node))
|
|
if node.Kind == ScalarNode && ke.prefs.UnwrapScalar {
|
|
return writeString(writer, node.Value+"\n")
|
|
}
|
|
|
|
destination := writer
|
|
tempBuffer := bytes.NewBuffer(nil)
|
|
if ke.prefs.ColorsEnabled {
|
|
destination = tempBuffer
|
|
}
|
|
|
|
// Mirror the YAML encoder behaviour: trailing comments on the document root
|
|
// are stored in FootComment and need to be printed after the document.
|
|
trailingContent := node.FootComment
|
|
|
|
if err := ke.writeCommentBlock(destination, node.HeadComment, 0); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeNode(destination, node, 0); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeInlineComment(destination, node.LineComment); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(destination, "\n"); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.PrintLeadingContent(destination, trailingContent); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ke.prefs.ColorsEnabled {
|
|
return colorizeAndPrint(tempBuffer.Bytes(), writer)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeNode(writer io.Writer, node *CandidateNode, indent int) error {
|
|
switch node.Kind {
|
|
case MappingNode:
|
|
return ke.writeMapping(writer, node, indent)
|
|
case SequenceNode:
|
|
return ke.writeSequence(writer, node, indent)
|
|
case ScalarNode:
|
|
return writeString(writer, ke.formatScalar(node))
|
|
case AliasNode:
|
|
// Should have been exploded by the printer, but handle defensively.
|
|
if node.Alias == nil {
|
|
return writeString(writer, "null")
|
|
}
|
|
return ke.writeNode(writer, node.Alias, indent)
|
|
default:
|
|
return writeString(writer, "null")
|
|
}
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeMapping(writer io.Writer, node *CandidateNode, indent int) error {
|
|
if len(node.Content) == 0 {
|
|
return writeString(writer, "{}")
|
|
}
|
|
if err := writeString(writer, "{\n"); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
keyNode := node.Content[i]
|
|
valueNode := node.Content[i+1]
|
|
|
|
entryIndent := indent + ke.prefs.Indent
|
|
if err := ke.writeCommentBlock(writer, keyNode.HeadComment, entryIndent); err != nil {
|
|
return err
|
|
}
|
|
if valueNode.HeadComment != "" && valueNode.HeadComment != keyNode.HeadComment {
|
|
if err := ke.writeCommentBlock(writer, valueNode.HeadComment, entryIndent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := ke.writeIndent(writer, entryIndent); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, ke.formatKey(keyNode)); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, ": "); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeNode(writer, valueNode, entryIndent); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Always emit a trailing comma; KYAML encourages explicit separators,
|
|
// and this ensures all quoted strings have a trailing `",` as requested.
|
|
if err := writeString(writer, ","); err != nil {
|
|
return err
|
|
}
|
|
inline := valueNode.LineComment
|
|
if inline == "" {
|
|
inline = keyNode.LineComment
|
|
}
|
|
if err := ke.writeInlineComment(writer, inline); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, "\n"); err != nil {
|
|
return err
|
|
}
|
|
|
|
foot := valueNode.FootComment
|
|
if foot == "" {
|
|
foot = keyNode.FootComment
|
|
}
|
|
if err := ke.writeCommentBlock(writer, foot, entryIndent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := ke.writeIndent(writer, indent); err != nil {
|
|
return err
|
|
}
|
|
return writeString(writer, "}")
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeSequence(writer io.Writer, node *CandidateNode, indent int) error {
|
|
if len(node.Content) == 0 {
|
|
return writeString(writer, "[]")
|
|
}
|
|
if err := writeString(writer, "[\n"); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, child := range node.Content {
|
|
itemIndent := indent + ke.prefs.Indent
|
|
if err := ke.writeCommentBlock(writer, child.HeadComment, itemIndent); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeIndent(writer, itemIndent); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeNode(writer, child, itemIndent); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, ","); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeInlineComment(writer, child.LineComment); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, "\n"); err != nil {
|
|
return err
|
|
}
|
|
if err := ke.writeCommentBlock(writer, child.FootComment, itemIndent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := ke.writeIndent(writer, indent); err != nil {
|
|
return err
|
|
}
|
|
return writeString(writer, "]")
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeIndent(writer io.Writer, indent int) error {
|
|
if indent <= 0 {
|
|
return nil
|
|
}
|
|
return writeString(writer, strings.Repeat(" ", indent))
|
|
}
|
|
|
|
func (ke *kyamlEncoder) formatKey(keyNode *CandidateNode) string {
|
|
// KYAML examples use bare keys. Quote keys only when needed.
|
|
key := keyNode.Value
|
|
if isValidKYamlBareKey(key) {
|
|
return key
|
|
}
|
|
return `"` + escapeDoubleQuotedString(key) + `"`
|
|
}
|
|
|
|
func (ke *kyamlEncoder) formatScalar(node *CandidateNode) string {
|
|
switch node.Tag {
|
|
case "!!null":
|
|
return "null"
|
|
case "!!bool":
|
|
return strings.ToLower(node.Value)
|
|
case "!!int", "!!float":
|
|
return node.Value
|
|
case "!!str":
|
|
return `"` + escapeDoubleQuotedString(node.Value) + `"`
|
|
default:
|
|
// Fall back to a string representation to avoid implicit typing surprises.
|
|
return `"` + escapeDoubleQuotedString(node.Value) + `"`
|
|
}
|
|
}
|
|
|
|
var kyamlBareKeyRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]*$`)
|
|
|
|
func isValidKYamlBareKey(s string) bool {
|
|
// Conservative: require an identifier-like key; otherwise quote.
|
|
if s == "" {
|
|
return false
|
|
}
|
|
return kyamlBareKeyRe.MatchString(s)
|
|
}
|
|
|
|
func escapeDoubleQuotedString(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s) + 2)
|
|
|
|
for _, r := range s {
|
|
switch r {
|
|
case '\\':
|
|
b.WriteString(`\\`)
|
|
case '"':
|
|
b.WriteString(`\"`)
|
|
case '\n':
|
|
b.WriteString(`\n`)
|
|
case '\r':
|
|
b.WriteString(`\r`)
|
|
case '\t':
|
|
b.WriteString(`\t`)
|
|
default:
|
|
if r < 0x20 {
|
|
// YAML double-quoted strings support \uXXXX escapes.
|
|
b.WriteString(`\u`)
|
|
hex := "0000" + strings.ToUpper(strconv.FormatInt(int64(r), 16))
|
|
b.WriteString(hex[len(hex)-4:])
|
|
} else {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeCommentBlock(writer io.Writer, comment string, indent int) error {
|
|
if strings.TrimSpace(comment) == "" {
|
|
return nil
|
|
}
|
|
|
|
lines := strings.Split(strings.ReplaceAll(comment, "\r\n", "\n"), "\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
|
|
if err := ke.writeIndent(writer, indent); err != nil {
|
|
return err
|
|
}
|
|
|
|
toWrite := line
|
|
if !commentLineRe.MatchString(toWrite) {
|
|
toWrite = "# " + toWrite
|
|
}
|
|
if err := writeString(writer, toWrite); err != nil {
|
|
return err
|
|
}
|
|
if err := writeString(writer, "\n"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ke *kyamlEncoder) writeInlineComment(writer io.Writer, comment string) error {
|
|
comment = strings.TrimSpace(strings.ReplaceAll(comment, "\r\n", "\n"))
|
|
if comment == "" {
|
|
return nil
|
|
}
|
|
|
|
lines := strings.Split(comment, "\n")
|
|
first := strings.TrimSpace(lines[0])
|
|
if first == "" {
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasPrefix(first, "#") {
|
|
first = "# " + first
|
|
}
|
|
|
|
if err := writeString(writer, " "); err != nil {
|
|
return err
|
|
}
|
|
return writeString(writer, first)
|
|
}
|