mirror of
https://github.com/outbackdingo/kubernetes.git
synced 2026-01-27 10:19:35 +00:00
Add KYAML support to kubectl
KYAML is a strict subset of YAML, which is sort of halfway between YAML
and JSON. It has the following properties:
* Does not depend on whitespace (easier to text-patch and template).
* Always quotes value strings (no ambiguity aroud things like "no").
* Allows quoted keys, but does not require them, and only quotes them if
they are not obviously safe (e.g. "no" would always be quoted).
* Always uses {} for structs and maps (no more obscure errors about
mapping values).
* Always uses [] for lists (no more trying to figure out if a dash
changes the meaning).
* When printing, it includes a header which makes it clear this is YAML
and not ill-formed JSON.
* Allows trailing commas
* Allows comments,
* Tries to economize on vertical space by "cuddling" some kinds of
brackets together.
* Retains comments.
Examples:
A struct:
```yaml
metadata: {
creationTimestamp: "2024-12-11T00:10:11Z",
labels: {
app: "hostnames",
},
name: "hostnames",
namespace: "default",
resourceVersion: "15231643",
uid: "f64dbcba-9c58-40b0-bbe7-70495efb5202",
}
```
A list of primitves:
```yaml
ipFamilies: [
"IPv4",
"IPv6",
]
```
A list of structs:
```yaml
ports: [{
port: 80,
protocol: "TCP",
targetPort: 80,
}, {
port: 443,
protocol: "TCP",
targetPort: 443,
}]
```
A multi-document stream:
```yaml
---
{
foo: "bar",
}
---
{
qux: "zrb",
}
```
This commit is contained in:
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
package genericclioptions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -29,7 +31,12 @@ func (f *JSONYamlPrintFlags) AllowedFormats() []string {
|
||||
if f == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string{"json", "yaml"}
|
||||
formats := []string{"json", "yaml"}
|
||||
// We can't use the cmdutil pkg directly because of import cycle.
|
||||
if strings.ToLower(os.Getenv("KUBECTL_KYAML")) == "true" {
|
||||
formats = append(formats, "kyaml")
|
||||
}
|
||||
return formats
|
||||
}
|
||||
|
||||
// JSONYamlPrintFlags provides default flags necessary for json/yaml printing.
|
||||
@@ -47,11 +54,19 @@ func (f *JSONYamlPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePr
|
||||
var printer printers.ResourcePrinter
|
||||
|
||||
outputFormat = strings.ToLower(outputFormat)
|
||||
|
||||
valid := f.AllowedFormats()
|
||||
if !slices.Contains(valid, outputFormat) {
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: valid}
|
||||
}
|
||||
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
printer = &printers.JSONPrinter{}
|
||||
case "yaml":
|
||||
printer = &printers.YAMLPrinter{}
|
||||
case "kyaml":
|
||||
printer = &printers.KYAMLPrinter{}
|
||||
default:
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
@@ -105,8 +105,9 @@ func TestPrintersSuccess(t *testing.T) {
|
||||
om := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{Name: name} }
|
||||
|
||||
genericPrinters := map[string]ResourcePrinter{
|
||||
"json": NewTypeSetter(scheme.Scheme).ToPrinter(&JSONPrinter{}),
|
||||
"yaml": NewTypeSetter(scheme.Scheme).ToPrinter(&YAMLPrinter{}),
|
||||
"json": NewTypeSetter(scheme.Scheme).ToPrinter(&JSONPrinter{}),
|
||||
"yaml": NewTypeSetter(scheme.Scheme).ToPrinter(&YAMLPrinter{}),
|
||||
"kyaml": NewTypeSetter(scheme.Scheme).ToPrinter(&KYAMLPrinter{}),
|
||||
}
|
||||
objects := map[string]runtime.Object{
|
||||
"pod": &v1.Pod{ObjectMeta: om("pod")},
|
||||
|
||||
66
staging/src/k8s.io/cli-runtime/pkg/printers/kyaml.go
Normal file
66
staging/src/k8s.io/cli-runtime/pkg/printers/kyaml.go
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// package kyaml provides a printer for Kubernetes objects that formats them
|
||||
// as KYAML, a strict subset of YAML that is designed to be explicit and
|
||||
// unambiguous. KYAML is YAML.
|
||||
package printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml/kyaml"
|
||||
)
|
||||
|
||||
// KYAMLPrinter is an implementation of ResourcePrinter which formats data into
|
||||
// a specific dialect of YAML, known as KYAML. KYAML is halfway between YAML
|
||||
// and JSON, but is a strict subset of YAML, and so it should should be
|
||||
// readable by any YAML parser. It is designed to be explicit and unambiguous,
|
||||
// and eschews significant whitespace.
|
||||
type KYAMLPrinter struct {
|
||||
encoder kyaml.Encoder
|
||||
}
|
||||
|
||||
// PrintObj prints the data as KYAML to the specified writer.
|
||||
func (p *KYAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
// We use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// We need an actual value in order to retrieve the package path for an object.
|
||||
// Using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
case *runtime.Unknown:
|
||||
return p.encoder.FromYAML(bytes.NewReader(obj.Raw), w)
|
||||
}
|
||||
|
||||
if obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
|
||||
}
|
||||
|
||||
return p.encoder.FromObject(obj, w)
|
||||
}
|
||||
33
staging/src/k8s.io/cli-runtime/pkg/printers/kyaml_test.go
Normal file
33
staging/src/k8s.io/cli-runtime/pkg/printers/kyaml_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package printers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func TestKYAMLPrinter(t *testing.T) {
|
||||
testPrinter(t, NewTypeSetter(scheme.Scheme).ToPrinter(&KYAMLPrinter{}), kyamlUnmarshal)
|
||||
}
|
||||
|
||||
func kyamlUnmarshal(data []byte, v interface{}) error {
|
||||
return yaml.Unmarshal(data, v)
|
||||
}
|
||||
@@ -455,6 +455,12 @@ const (
|
||||
// Transition to WebSockets.
|
||||
RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS"
|
||||
PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS"
|
||||
|
||||
// owner: @thockin
|
||||
// kep: https://kep.k8s.io/5296
|
||||
//
|
||||
// Support KYAML output.
|
||||
KYAMLOutput FeatureGate = "KUBECTL_KYAML"
|
||||
)
|
||||
|
||||
// IsEnabled returns true iff environment variable is set to true.
|
||||
|
||||
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@@ -1288,3 +1288,4 @@ sigs.k8s.io/structured-merge-diff/v6/value
|
||||
# sigs.k8s.io/yaml v1.6.0
|
||||
## explicit; go 1.22
|
||||
sigs.k8s.io/yaml
|
||||
sigs.k8s.io/yaml/kyaml
|
||||
|
||||
828
vendor/sigs.k8s.io/yaml/kyaml/kyaml.go
generated
vendored
Normal file
828
vendor/sigs.k8s.io/yaml/kyaml/kyaml.go
generated
vendored
Normal file
@@ -0,0 +1,828 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package kyaml provides an encoder for KYAML, a strict subset of YAML that is
|
||||
// designed to be explicit and unambiguous. KYAML is YAML, so any YAML parser
|
||||
// should be able to read it.
|
||||
//
|
||||
// KYAML is designed to be halfway between YAML and JSON, with the following
|
||||
// properties:
|
||||
// - Not whitespace-sensitive
|
||||
// - Allows comments
|
||||
// - Allows trailing commas
|
||||
// - Does not require quoted keys
|
||||
//
|
||||
// KYAML is an output format, and will follow these conventions:
|
||||
// - Always double-quote strings, even if they are not ambiguous.
|
||||
// - Only quote keys that might be ambiguously interpreted (e.g. "no" is
|
||||
// always quoted).
|
||||
// - Always use `{}` for structs and maps, and `[]` for lists.
|
||||
// - Economize on vertical space by cuddling some kinds of brackets together.
|
||||
// - Render multi-line strings with YAML's line folding, which is close to
|
||||
// the Go string literal format.
|
||||
//
|
||||
// KYAML also includes a document-separator "header" (still valid YAML), which
|
||||
// helps to disambiguate a KYAML document from an ill-formed JSON document.
|
||||
//
|
||||
// Because KYAML is YAML, a KYAML multi-document is a YAML multi-document.
|
||||
package kyaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// Encoder formats objects or YAML data (JSON is valid YAML) into KYAML. KYAML
|
||||
// is halfway between YAML and JSON, but is a strict subset of YAML, so it
|
||||
// should should be readable by any YAML parser. It is designed to be explicit
|
||||
// and unambiguous, and eschews significant whitespace.
|
||||
type Encoder struct {
|
||||
// Compact tells the encoder to use compact formatting. This puts all the
|
||||
// data on one line, with no extra newlines, no comments, and no multi-line
|
||||
// formatting.
|
||||
Compact bool
|
||||
}
|
||||
|
||||
// FromYAML renders a KYAML (multi-)document from YAML bytes (JSON is YAML),
|
||||
// including the KYAML header. The result always has a trailing newline.
|
||||
func (ky *Encoder) FromYAML(in io.Reader, out io.Writer) error {
|
||||
// We need a YAML decoder to handle multi-document streams.
|
||||
dec := yaml.NewDecoder(in)
|
||||
|
||||
// Process each document in the stream.
|
||||
for {
|
||||
var doc yaml.Node
|
||||
err := dec.Decode(&doc)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding: %v", err)
|
||||
}
|
||||
if doc.Kind != yaml.DocumentNode {
|
||||
return fmt.Errorf("kyaml internal error: line %d: expected a document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
|
||||
}
|
||||
|
||||
// Always emit a document separator, which helps disambiguate between YAML
|
||||
// and JSON.
|
||||
if _, err := fmt.Fprintln(out, "---"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ky.renderDocument(&doc, 0, ky.flags(), out); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromObject renders a KYAML document from a Go object, including the KYAML
|
||||
// header. The result always has a trailing newline.
|
||||
func (ky *Encoder) FromObject(obj any, out io.Writer) error {
|
||||
jb, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling to JSON: %v", err)
|
||||
}
|
||||
// JSON is YAML.
|
||||
return ky.FromYAML(bytes.NewReader(jb), out)
|
||||
}
|
||||
|
||||
// Marshal renders a single Go object as KYAML, without the header or trailing
|
||||
// newline.
|
||||
func (ky *Encoder) Marshal(obj any) ([]byte, error) {
|
||||
// Convert the object to JSON bytes to take advantage of all the JSON tag
|
||||
// handling and things like that.
|
||||
jb, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling to JSON: %v", err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
// JSON is YAML.
|
||||
if err := ky.fromObjectYAML(bytes.NewReader(jb), buf); err != nil {
|
||||
return nil, fmt.Errorf("error rendering object: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (ky *Encoder) fromObjectYAML(in io.Reader, out io.Writer) error {
|
||||
yb, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(yb, &doc); err != nil {
|
||||
return fmt.Errorf("error decoding: %v", err)
|
||||
}
|
||||
if doc.Kind != yaml.DocumentNode {
|
||||
return fmt.Errorf("kyaml internal error: line %d: expected document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
|
||||
}
|
||||
|
||||
if err := ky.renderNode(&doc, 0, ky.flags(), out); err != nil {
|
||||
return fmt.Errorf("error rendering document: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// From the YAML spec.
|
||||
const (
|
||||
intTag = "!!int"
|
||||
floatTag = "!!float"
|
||||
boolTag = "!!bool"
|
||||
strTag = "!!str"
|
||||
timestampTag = "!!timestamp"
|
||||
seqTag = "!!seq"
|
||||
mapTag = "!!map"
|
||||
nullTag = "!!null"
|
||||
binaryTag = "!!binary"
|
||||
mergeTag = "!!merge"
|
||||
)
|
||||
|
||||
type flagMask uint64
|
||||
|
||||
const (
|
||||
flagsNone flagMask = 0
|
||||
flagLazyQuote flagMask = 0x01
|
||||
flagCompact flagMask = 0x02
|
||||
)
|
||||
|
||||
// flags returns a flagMask representing the current encoding options. It can
|
||||
// be used directly or OR'ed with another mask.
|
||||
func (ky *Encoder) flags() flagMask {
|
||||
flags := flagsNone
|
||||
if ky.Compact {
|
||||
flags |= flagCompact
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// renderNode processes a YAML node, calling the appropriate render function
|
||||
// for its type. Each render function should assume that the output "cursor"
|
||||
// is positioned at the start of the node and should not emit a final newline.
|
||||
// If a render function needs to linewrap or indent (e.g. a struct), it should
|
||||
// assume the indent level is currently correct for the node type itself, and
|
||||
// may need to indent more.
|
||||
func (ky *Encoder) renderNode(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch node.Kind {
|
||||
case yaml.DocumentNode:
|
||||
return ky.renderDocument(node, indent, flags, out)
|
||||
case yaml.ScalarNode:
|
||||
return ky.renderScalar(node, indent, flags, out)
|
||||
case yaml.SequenceNode:
|
||||
return ky.renderSequence(node, indent, flags, out)
|
||||
case yaml.MappingNode:
|
||||
return ky.renderMapping(node, indent, flags, out)
|
||||
case yaml.AliasNode:
|
||||
return ky.renderAlias(node, indent, flags, out)
|
||||
}
|
||||
return fmt.Errorf("kyaml internal error: line %d: unknown node kind %v", node.Line, node.Kind)
|
||||
}
|
||||
|
||||
// renderDocument processes a YAML document node, rendering it to the output.
|
||||
// This function assumes that the output "cursor" is positioned at the start of
|
||||
// the document. This does not emit a final newline.
|
||||
func (ky *Encoder) renderDocument(doc *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
if len(doc.Content) == 0 {
|
||||
return fmt.Errorf("kyaml internal error: line %d: document has no content node (%d)", doc.Line, len(doc.Content))
|
||||
}
|
||||
if len(doc.Content) > 1 {
|
||||
return fmt.Errorf("kyaml internal error: line %d: document has more than one content node (%d)", doc.Line, len(doc.Content))
|
||||
}
|
||||
if indent != 0 {
|
||||
return fmt.Errorf("kyaml internal error: line %d: document non-zero indent (%d)", doc.Line, indent)
|
||||
}
|
||||
|
||||
compact := flags&flagCompact != 0
|
||||
|
||||
// For document nodes, the cursor is assumed to be ready to render.
|
||||
child := doc.Content[0]
|
||||
if !compact {
|
||||
if len(doc.HeadComment) > 0 {
|
||||
ky.renderComments(doc.HeadComment, indent, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
}
|
||||
if len(child.HeadComment) > 0 {
|
||||
ky.renderComments(child.HeadComment, indent, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
}
|
||||
}
|
||||
if err := ky.renderNode(child, indent, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
if !compact {
|
||||
if len(child.LineComment) > 0 {
|
||||
ky.renderComments(" "+child.LineComment, 0, out)
|
||||
}
|
||||
if len(child.FootComment) > 0 {
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.renderComments(child.FootComment, indent, out)
|
||||
}
|
||||
if len(doc.LineComment) > 0 {
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.renderComments(" "+doc.LineComment, 0, out)
|
||||
}
|
||||
if len(doc.FootComment) > 0 {
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.renderComments(doc.FootComment, indent, out)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderScalar processes a YAML scalar node, rendering it to the output. This
|
||||
// DOES NOT render a trailing newline or head/line/foot comments, as those
|
||||
// require the parent context.
|
||||
func (ky *Encoder) renderScalar(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
switch node.Tag {
|
||||
case intTag, floatTag, boolTag, nullTag:
|
||||
fmt.Fprint(out, node.Value)
|
||||
case strTag, timestampTag:
|
||||
return ky.renderString(node.Value, indent+1, flags, out)
|
||||
default:
|
||||
return fmt.Errorf("kyaml internal error: line %d: unknown tag %q on scalar node %q", node.Line, node.Tag, node.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const kyamlFoldStr = "\\\n"
|
||||
|
||||
var regularEscapeMap = map[rune]string{
|
||||
'\n': "\\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable
|
||||
'\t': "\t", // literal tab
|
||||
}
|
||||
var compactEscapeMap = map[rune]string{
|
||||
'\n': "\\n",
|
||||
'\t': "\\t",
|
||||
}
|
||||
|
||||
// renderString processes a string (either single-line or multi-line),
|
||||
// rendering it to the output. This DOES NOT render a trailing newline.
|
||||
func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.Writer) error {
|
||||
lazyQuote := flags&flagLazyQuote != 0
|
||||
compact := flags&flagCompact != 0
|
||||
multi := strings.Contains(val, "\n")
|
||||
|
||||
if !multi && lazyQuote && !needsQuotes(val) {
|
||||
fmt.Fprint(out, val)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Special cases for certain input.
|
||||
escapeOverrides := regularEscapeMap
|
||||
if compact {
|
||||
escapeOverrides = compactEscapeMap
|
||||
}
|
||||
|
||||
//
|
||||
// The rest of this is borrowed from Go's strconv.Quote implementation.
|
||||
//
|
||||
|
||||
// accumulate into a buffer
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// opening quote
|
||||
fmt.Fprint(buf, `"`)
|
||||
if multi && !compact {
|
||||
fmt.Fprint(buf, kyamlFoldStr)
|
||||
}
|
||||
|
||||
// Iterating a string with invalid UTF8 returns RuneError rather than the
|
||||
// bytes, so we iterate the string and decode the runes. This is a bit
|
||||
// slower, but gives us a better result.
|
||||
s := val
|
||||
for width := 0; len(s) > 0; s = s[width:] {
|
||||
r := rune(s[0])
|
||||
width = 1
|
||||
if r >= utf8.RuneSelf {
|
||||
r, width = utf8.DecodeRuneInString(s)
|
||||
}
|
||||
if width == 1 && r == utf8.RuneError {
|
||||
fmt.Fprint(buf, `\x`)
|
||||
fmt.Fprintf(buf, "%02x", s[0])
|
||||
continue
|
||||
}
|
||||
ky.appendEscapedRune(r, indent, escapeOverrides, buf)
|
||||
}
|
||||
|
||||
// closing quote
|
||||
afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n'
|
||||
if multi && !compact {
|
||||
if !afterNewline {
|
||||
fmt.Fprint(buf, kyamlFoldStr)
|
||||
}
|
||||
ky.writeIndent(indent, buf)
|
||||
}
|
||||
fmt.Fprint(buf, `"`)
|
||||
|
||||
fmt.Fprint(out, buf.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var allowedUnquotedAnywhere = map[rune]bool{
|
||||
'_': true,
|
||||
}
|
||||
|
||||
var allowedUnquotedInterior = map[rune]bool{
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
}
|
||||
|
||||
func needsQuotes(s string) bool {
|
||||
if s == "" {
|
||||
return true
|
||||
}
|
||||
if isTypeAmbiguous(s) {
|
||||
return true
|
||||
}
|
||||
runes := []rune(s)
|
||||
for i, r := range runes {
|
||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || allowedUnquotedAnywhere[r] {
|
||||
continue
|
||||
}
|
||||
if i > 0 && i < len(runes)-1 && allowedUnquotedInterior[r] {
|
||||
continue
|
||||
}
|
||||
// it's something we don't explicitly allow
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// From https://yaml.org/type/int.html and https://yaml.org/type/float.html
|
||||
var sexagesimalRE = regexp.MustCompile(`^[+-]?[1-9][0-9_]*(:[0-5]?[0-9])+(\.[0-9_]*)?$`)
|
||||
|
||||
// isTypeAmbiguous returns true if a YAML parser might interpret the unquoted
|
||||
// form of the string argument as a YAML type other than string (e.g. `true`
|
||||
// would be interpreted as a boolean).
|
||||
func isTypeAmbiguous(s string) bool {
|
||||
// Null-like strings: https://yaml.org/type/null.html
|
||||
if len(s) <= 5 {
|
||||
switch strings.ToLower(s) {
|
||||
case "null", "~", "":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean-like strings: https://yaml.org/type/bool.html
|
||||
if _, err := strconv.ParseBool(s); err == nil {
|
||||
return true
|
||||
}
|
||||
if len(s) <= 5 {
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "y", "yes", "on", "false", "n", "no", "off":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Number-like strings: https://yaml.org/type/int.html and
|
||||
// https://yaml.org/type/float.html
|
||||
//
|
||||
// NOTE: the stripping of underscores is gross.
|
||||
sWithoutUnderscores := strings.ReplaceAll(s, "_", "")
|
||||
// Handles binary ("0b"), octal ("0" or "0o"), decimal, and hex ("0x")
|
||||
if _, err := strconv.ParseInt(sWithoutUnderscores, 0, 64); err == nil && !isSyntaxError(err) {
|
||||
return true
|
||||
}
|
||||
// Handles standard and scientific notation.
|
||||
if _, err := strconv.ParseFloat(sWithoutUnderscores, 64); err == nil && !isSyntaxError(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sexagesimal strings like "11:00" (in YAML 1.1, removed in 1.2):
|
||||
// https://yaml.org/type/int.html and https://yaml.org/type/float.html
|
||||
if sexagesimalRE.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Infinity and NaN: https://yaml.org/type/float.html
|
||||
if len(s) <= 5 {
|
||||
switch strings.ToLower(s) {
|
||||
case ".inf", "-.inf", "+.inf", ".nan":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Time-like strings
|
||||
if _, matches := parseTimestamp(s); matches {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isSyntaxError(err error) bool {
|
||||
var numerr *strconv.NumError
|
||||
if ok := errors.As(err, &numerr); ok {
|
||||
return errors.Is(numerr.Err, strconv.ErrSyntax)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This is a subset of the formats allowed by the regular expression
|
||||
// defined at http://yaml.org/type/timestamp.html.
|
||||
//
|
||||
// NOTE: This was copied from go.yaml.in/yaml/v2
|
||||
var allowedTimestampFormats = []string{
|
||||
"2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields.
|
||||
"2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t".
|
||||
"2006-1-2 15:4:5.999999999", // space separated with no time zone
|
||||
"2006-1-2", // date only
|
||||
// Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
|
||||
// from the set of examples.
|
||||
}
|
||||
|
||||
// parseTimestamp parses s as a timestamp string and
|
||||
// returns the timestamp and reports whether it succeeded.
|
||||
// Timestamp formats are defined at http://yaml.org/type/timestamp.html
|
||||
//
|
||||
// NOTE: This was copied from go.yaml.in/yaml/v2
|
||||
func parseTimestamp(s string) (time.Time, bool) {
|
||||
// TODO write code to check all the formats supported by
|
||||
// http://yaml.org/type/timestamp.html instead of using time.Parse.
|
||||
|
||||
// Quick check: all date formats start with YYYY-.
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if c := s[i]; c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i != 4 || i == len(s) || s[i] != '-' {
|
||||
return time.Time{}, false
|
||||
}
|
||||
for _, format := range allowedTimestampFormats {
|
||||
if t, err := time.Parse(format, s); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// We use a buffer here so we can peek backwards.
|
||||
func (ky *Encoder) appendEscapedRune(r rune, indent int, escapeOverrides map[rune]string, buf *bytes.Buffer) {
|
||||
afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n'
|
||||
|
||||
if afterNewline {
|
||||
ky.writeIndent(indent, buf)
|
||||
// We want to preserve leading whitespace in the source string, so if
|
||||
// we find whitespace, we need to escape it. We don't want to
|
||||
// escape lines without leading whitespace, but we DO want to render
|
||||
// the result with fidelity to vertical alignment, so we write an extra
|
||||
// space. This is OK, because all whitespace before the first
|
||||
// non-whitespace character is dropped, as per YAML spec. If there are
|
||||
// no lines with leading whitespace it looks like the indent is one too
|
||||
// many, which seems OK.
|
||||
if unicode.IsSpace(r) && r != '\n' {
|
||||
buf.WriteRune('\\')
|
||||
} else {
|
||||
buf.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
if s, found := escapeOverrides[r]; found {
|
||||
buf.WriteString(s)
|
||||
return
|
||||
}
|
||||
if r == '"' || r == '\\' { // always escaped
|
||||
buf.WriteRune('\\')
|
||||
buf.WriteRune(r)
|
||||
return
|
||||
}
|
||||
if unicode.IsPrint(r) {
|
||||
buf.WriteRune(r)
|
||||
return
|
||||
}
|
||||
switch r {
|
||||
case '\a':
|
||||
buf.WriteString(`\a`)
|
||||
case '\b':
|
||||
buf.WriteString(`\b`)
|
||||
case '\f':
|
||||
buf.WriteString(`\f`)
|
||||
case '\n':
|
||||
buf.WriteString(`\n`)
|
||||
case '\r':
|
||||
buf.WriteString(`\r`)
|
||||
case '\t':
|
||||
buf.WriteString(`\t`)
|
||||
case '\v':
|
||||
buf.WriteString(`\v`)
|
||||
case '\x00':
|
||||
buf.WriteString(`\0`)
|
||||
case '\x1b':
|
||||
buf.WriteString(`\e`)
|
||||
case '\x85':
|
||||
buf.WriteString(`\N`)
|
||||
case '\xa0':
|
||||
buf.WriteString(`\_`)
|
||||
case '\u2028':
|
||||
buf.WriteString(`\L`)
|
||||
case '\u2029':
|
||||
buf.WriteString(`\P`)
|
||||
default:
|
||||
const hexits = "0123456789abcdef"
|
||||
switch {
|
||||
case r < ' ' || r == 0x7f:
|
||||
buf.WriteString(`\x`)
|
||||
buf.WriteByte(hexits[byte(r)>>4])
|
||||
buf.WriteByte(hexits[byte(r)&0xF])
|
||||
case !utf8.ValidRune(r):
|
||||
r = utf8.RuneError
|
||||
fallthrough
|
||||
case r < 0x10000:
|
||||
buf.WriteString(`\u`)
|
||||
for s := 12; s >= 0; s -= 4 {
|
||||
buf.WriteByte(hexits[r>>uint(s)&0xF])
|
||||
}
|
||||
default:
|
||||
buf.WriteString(`\U`)
|
||||
for s := 28; s >= 0; s -= 4 {
|
||||
buf.WriteByte(hexits[r>>uint(s)&0xF])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderSequence processes a YAML sequence node, rendering it to the output. This
|
||||
// DOES NOT render a trailing newline or head/line/foot comments of the sequence
|
||||
// itself, but DOES render comments of the child nodes.
|
||||
func (ky *Encoder) renderSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
if len(node.Content) == 0 {
|
||||
fmt.Fprint(out, "[]")
|
||||
return nil
|
||||
}
|
||||
if flags&flagCompact != 0 {
|
||||
return ky.renderCompactSequence(node, flags, out)
|
||||
}
|
||||
|
||||
// See if this list can use cuddled formatting.
|
||||
cuddle := true
|
||||
for _, child := range node.Content {
|
||||
if !isCuddledKind(child) {
|
||||
cuddle = false
|
||||
break
|
||||
}
|
||||
if len(child.HeadComment)+len(child.LineComment)+len(child.FootComment) > 0 {
|
||||
cuddle = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cuddle {
|
||||
return ky.renderCuddledSequence(node, indent, flags, out)
|
||||
}
|
||||
return ky.renderUncuddledSequence(node, indent, flags, out)
|
||||
}
|
||||
|
||||
// renderCompactSequence renders a YAML sequence node in compact form.
|
||||
func (ky *Encoder) renderCompactSequence(node *yaml.Node, flags flagMask, out io.Writer) error {
|
||||
fmt.Fprint(out, "[")
|
||||
for i, child := range node.Content {
|
||||
if i > 0 {
|
||||
fmt.Fprint(out, ", ")
|
||||
}
|
||||
if err := ky.renderNode(child, 0, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprint(out, "]")
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCuddledSequence processes a YAML sequence node which has already been
|
||||
// determined to be cuddled. We only cuddle sequences of structs or lists
|
||||
// which have no comments.
|
||||
func (ky *Encoder) renderCuddledSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
fmt.Fprint(out, "[")
|
||||
for i, child := range node.Content {
|
||||
// Each iteration should leave us cuddled for the next item.
|
||||
if i > 0 {
|
||||
fmt.Fprint(out, ", ")
|
||||
}
|
||||
if err := ky.renderNode(child, indent, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprint(out, "]")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ky *Encoder) renderUncuddledSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
// Get into the right state for the first item.
|
||||
fmt.Fprint(out, "[\n")
|
||||
ky.writeIndent(indent, out)
|
||||
for _, child := range node.Content {
|
||||
// Each iteration should leave us ready to close the list. Since we
|
||||
// have an item to render, we need 1 more indent.
|
||||
ky.writeIndent(1, out)
|
||||
|
||||
if len(child.HeadComment) > 0 {
|
||||
ky.renderComments(child.HeadComment, indent+1, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.writeIndent(indent+1, out)
|
||||
}
|
||||
|
||||
if err := ky.renderNode(child, indent+1, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(out, ",")
|
||||
if len(child.LineComment) > 0 {
|
||||
ky.renderComments(" "+child.LineComment, 0, out)
|
||||
}
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.writeIndent(indent, out)
|
||||
if len(child.FootComment) > 0 {
|
||||
ky.writeIndent(1, out)
|
||||
ky.renderComments(child.FootComment, indent+1, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.writeIndent(indent, out)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(out, "]")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ky *Encoder) nodeKindString(kind yaml.Kind) string {
|
||||
switch kind {
|
||||
case yaml.DocumentNode:
|
||||
return "document"
|
||||
case yaml.ScalarNode:
|
||||
return "scalar"
|
||||
case yaml.MappingNode:
|
||||
return "mapping"
|
||||
case yaml.SequenceNode:
|
||||
return "sequence"
|
||||
case yaml.AliasNode:
|
||||
return "alias"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func isCuddledKind(node *yaml.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
switch node.Kind {
|
||||
case yaml.SequenceNode, yaml.MappingNode:
|
||||
return true
|
||||
case yaml.AliasNode:
|
||||
return isCuddledKind(node.Alias)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// renderMapping processes a YAML mapping node, rendering it to the output. This
|
||||
// DOES NOT render a trailing newline or head/line/foot comments of the mapping
|
||||
// itself, but DOES render comments of the child nodes.
|
||||
func (ky *Encoder) renderMapping(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
if len(node.Content) == 0 {
|
||||
fmt.Fprint(out, "{}")
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags&flagCompact != 0 {
|
||||
return ky.renderCompactMapping(node, flags, out)
|
||||
}
|
||||
|
||||
joinComments := func(a, b string) string {
|
||||
if len(a) > 0 && len(b) > 0 {
|
||||
return a + "\n" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
fmt.Fprint(out, "{\n")
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
key := node.Content[i]
|
||||
val := node.Content[i+1]
|
||||
|
||||
ky.writeIndent(indent+1, out)
|
||||
|
||||
// Only one of these should be set.
|
||||
if comments := joinComments(key.HeadComment, val.HeadComment); len(comments) > 0 {
|
||||
ky.renderComments(comments, indent+1, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.writeIndent(indent+1, out)
|
||||
}
|
||||
|
||||
// Mapping keys are always strings in KYAML, even if the YAML node says
|
||||
// otherwise.
|
||||
if err := ky.renderString(key.Value, indent+1, flagLazyQuote|flagCompact, out); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(out, ": ")
|
||||
if err := ky.renderNode(val, indent+1, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(out, ",")
|
||||
if len(key.LineComment) > 0 && len(val.LineComment) > 0 {
|
||||
return fmt.Errorf("kyaml internal error: line %d: both key and value have line comments", key.Line)
|
||||
}
|
||||
if len(key.LineComment) > 0 {
|
||||
ky.renderComments(" "+key.LineComment, 0, out)
|
||||
} else if len(val.LineComment) > 0 {
|
||||
ky.renderComments(" "+val.LineComment, 0, out)
|
||||
}
|
||||
fmt.Fprint(out, "\n")
|
||||
// Only one of these should be set.
|
||||
if comments := joinComments(key.FootComment, val.FootComment); len(comments) > 0 {
|
||||
ky.writeIndent(indent+1, out)
|
||||
ky.renderComments(comments, indent+1, out)
|
||||
fmt.Fprint(out, "\n")
|
||||
}
|
||||
}
|
||||
ky.writeIndent(indent, out)
|
||||
fmt.Fprint(out, "}")
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCompactMapping renders a YAML mapping node in compact form.
|
||||
func (ky *Encoder) renderCompactMapping(node *yaml.Node, flags flagMask, out io.Writer) error {
|
||||
fmt.Fprint(out, "{")
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
key := node.Content[i]
|
||||
val := node.Content[i+1]
|
||||
|
||||
if i > 0 {
|
||||
fmt.Fprint(out, ", ")
|
||||
}
|
||||
// Mapping keys are always strings in KYAML, even if the YAML node says
|
||||
// otherwise.
|
||||
if err := ky.renderString(key.Value, 0, flags|flagLazyQuote|flagCompact, out); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(out, ": ")
|
||||
if err := ky.renderNode(val, 0, flags, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprint(out, "}")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ky *Encoder) writeIndent(level int, out io.Writer) {
|
||||
const indentString = " "
|
||||
for range level {
|
||||
fmt.Fprint(out, indentString)
|
||||
}
|
||||
}
|
||||
|
||||
// renderCommentBlock writes the comments node to the output. This assumes the
|
||||
// cursor is at the right place to start writing and DOES NOT render a trailing
|
||||
// newline.
|
||||
func (ky *Encoder) renderComments(comments string, indent int, out io.Writer) {
|
||||
if len(comments) == 0 {
|
||||
return
|
||||
}
|
||||
lines := strings.Split(comments, "\n")
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
fmt.Fprint(out, "\n")
|
||||
ky.writeIndent(indent, out)
|
||||
}
|
||||
fmt.Fprint(out, line)
|
||||
}
|
||||
}
|
||||
|
||||
func (ky *Encoder) renderAlias(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
|
||||
if node.Alias != nil {
|
||||
return ky.renderNode(node.Alias, indent+1, flags, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user