mirror of
https://github.com/mikefarah/yq.git
synced 2026-08-24 16:39:58 +08:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9701ca3a | ||
|
|
83ef7ee5cf | ||
|
|
817287ec90 | ||
|
|
c5994a8b28 | ||
|
|
3435fee1f9 | ||
|
|
ececd00fbd | ||
|
|
46dbd0c859 | ||
|
|
cad809515c | ||
|
|
1d35134310 | ||
|
|
fae1dbf0be | ||
|
|
04847502bf | ||
|
|
af7e36bd47 | ||
|
|
fdad478684 | ||
|
|
22f376bbfd | ||
|
|
688fe55bb9 | ||
|
|
91b3fb2af3 | ||
|
|
bd5e5dc965 | ||
|
|
c915113120 | ||
|
|
cf02b90624 | ||
|
|
7f5dda93c6 | ||
|
|
a0be871a9d | ||
|
|
5af062a86f | ||
|
|
ed551bf339 | ||
|
|
5e490527de | ||
|
|
c887042a1b | ||
|
|
0cc5e75432 | ||
|
|
6d6cd43255 | ||
|
|
d99614f55a | ||
|
|
880397d549 | ||
|
|
46c32f4c79 | ||
|
|
b25114c1fc |
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19.2 as builder
|
||||
FROM golang:1.19.3 as builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19.2
|
||||
FROM golang:1.19.3
|
||||
|
||||
COPY scripts/devtools.sh /opt/devtools.sh
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ Take a look at the discussions for [common questions](https://github.com/mikefar
|
||||
### wget
|
||||
Use wget to download, gzipped pre-compiled binaries:
|
||||
|
||||
|
||||
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
|
||||
|
||||
#### Compressed via tar.gz
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
|
||||
@@ -74,9 +77,7 @@ wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /
|
||||
chmod +x /usr/bin/yq
|
||||
```
|
||||
|
||||
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
|
||||
|
||||
Use VERSION=latest to always get the latest version:
|
||||
#### Latest version
|
||||
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
|
||||
|
||||
@@ -9,7 +9,7 @@ EOL
|
||||
|
||||
testEmptyEval() {
|
||||
X=$(./yq e test.yml)
|
||||
expected=$(cat test.yml)
|
||||
expected="# comment"
|
||||
assertEquals 0 $?
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ EOL
|
||||
read -r -d '' expected << EOM
|
||||
cat:
|
||||
+content: BiBi
|
||||
+legs: "4"
|
||||
+@legs: "4"
|
||||
EOM
|
||||
|
||||
X=$(./yq e -p=xml test.yml)
|
||||
@@ -129,9 +129,9 @@ EOL
|
||||
read -r -d '' expected << EOM
|
||||
+p_xml: version="1.0"
|
||||
map:
|
||||
+xmlns: some-namespace
|
||||
+xmlns:xsi: some-instance
|
||||
+xsi:schemaLocation: some-url
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@xsi:schemaLocation: some-url
|
||||
EOM
|
||||
|
||||
X=$(./yq e -p=xml test.yml)
|
||||
@@ -190,7 +190,7 @@ EOL
|
||||
read -r -d '' expected << EOM
|
||||
cat:
|
||||
+content: BiBi
|
||||
+legs: "4"
|
||||
+@legs: "4"
|
||||
EOM
|
||||
|
||||
X=$(cat /dev/null | ./yq e -p=xml test.yml)
|
||||
|
||||
@@ -48,6 +48,55 @@ EOM
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testOutputYamlRawDefault() {
|
||||
cat >test.yml <<EOL
|
||||
a: "cat"
|
||||
EOL
|
||||
|
||||
X=$(./yq e '.a' test.yml)
|
||||
assertEquals "cat" "$X"
|
||||
|
||||
X=$(./yq ea '.a' test.yml)
|
||||
assertEquals "cat" "$X"
|
||||
}
|
||||
|
||||
testOutputYamlRawOff() {
|
||||
cat >test.yml <<EOL
|
||||
a: "cat"
|
||||
EOL
|
||||
|
||||
X=$(./yq e -r=false '.a' test.yml)
|
||||
assertEquals "\"cat\"" "$X"
|
||||
|
||||
X=$(./yq ea -r=false '.a' test.yml)
|
||||
assertEquals "\"cat\"" "$X"
|
||||
}
|
||||
|
||||
testOutputJsonRaw() {
|
||||
cat >test.yml <<EOL
|
||||
a: cat
|
||||
EOL
|
||||
|
||||
X=$(./yq e -r --output-format=json '.a' test.yml)
|
||||
assertEquals "cat" "$X"
|
||||
|
||||
X=$(./yq ea -r --output-format=json '.a' test.yml)
|
||||
assertEquals "cat" "$X"
|
||||
}
|
||||
|
||||
testOutputJsonDefault() {
|
||||
cat >test.yml <<EOL
|
||||
a: cat
|
||||
EOL
|
||||
|
||||
X=$(./yq e --output-format=json '.a' test.yml)
|
||||
assertEquals "\"cat\"" "$X"
|
||||
|
||||
X=$(./yq ea --output-format=json '.a' test.yml)
|
||||
assertEquals "\"cat\"" "$X"
|
||||
}
|
||||
|
||||
|
||||
testOutputJsonShort() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: ["cat"]}
|
||||
@@ -72,11 +121,11 @@ EOM
|
||||
|
||||
testOutputProperties() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: {c: ["cat"]}}
|
||||
a: {b: {c: ["cat cat"]}}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
a.b.c.0 = cat
|
||||
a.b.c.0 = cat cat
|
||||
EOM
|
||||
|
||||
X=$(./yq e --output-format=props test.yml)
|
||||
@@ -86,13 +135,30 @@ EOM
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
testOutputPropertiesShort() {
|
||||
testOutputPropertiesDontUnwrap() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: {c: ["cat"]}}
|
||||
a: {b: {c: ["cat cat"]}}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
a.b.c.0 = cat
|
||||
a.b.c.0 = "cat cat"
|
||||
EOM
|
||||
|
||||
X=$(./yq e -r=false --output-format=props test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
|
||||
X=$(./yq ea -r=false --output-format=props test.yml)
|
||||
assertEquals "$expected" "$X"
|
||||
}
|
||||
|
||||
|
||||
testOutputPropertiesShort() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: {c: ["cat cat"]}}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
a.b.c.0 = cat cat
|
||||
EOM
|
||||
|
||||
X=$(./yq e -o=p test.yml)
|
||||
@@ -186,7 +252,7 @@ EOM
|
||||
|
||||
testOutputXmComplex() {
|
||||
cat >test.yml <<EOL
|
||||
a: {b: {c: ["cat", "dog"], +f: meow}}
|
||||
a: {b: {c: ["cat", "dog"], +@f: meow}}
|
||||
EOL
|
||||
|
||||
read -r -d '' expected << EOM
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
package cmd
|
||||
|
||||
var leadingContentPreProcessing = true
|
||||
var unwrapScalar = true
|
||||
var unwrapScalarFlag = newUnwrapFlag()
|
||||
|
||||
var unwrapScalar = false
|
||||
|
||||
var writeInplace = false
|
||||
var outputToJSON = false
|
||||
|
||||
@@ -75,7 +75,7 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
return err
|
||||
}
|
||||
|
||||
decoder, err := configureDecoder()
|
||||
decoder, err := configureDecoder(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,13 +109,13 @@ func evaluateAll(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
if nullInput {
|
||||
err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(expression), printer, "")
|
||||
err = yqlib.NewStreamEvaluator().EvaluateNew(processExpression(expression), printer)
|
||||
} else {
|
||||
cmd.Println(cmd.UsageString())
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
err = allAtOnceEvaluator.EvaluateFiles(processExpression(expression), args, printer, leadingContentPreProcessing, decoder)
|
||||
err = allAtOnceEvaluator.EvaluateFiles(processExpression(expression), args, printer, decoder)
|
||||
}
|
||||
|
||||
completedSuccessfully = err == nil
|
||||
|
||||
@@ -97,7 +97,7 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
|
||||
printer := yqlib.NewPrinter(encoder, printerWriter)
|
||||
|
||||
decoder, err := configureDecoder()
|
||||
decoder, err := configureDecoder(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,13 +123,13 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
if nullInput {
|
||||
err = streamEvaluator.EvaluateNew(processExpression(expression), printer, "")
|
||||
err = streamEvaluator.EvaluateNew(processExpression(expression), printer)
|
||||
} else {
|
||||
cmd.Println(cmd.UsageString())
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
err = streamEvaluator.EvaluateFiles(processExpression(expression), args, printer, leadingContentPreProcessing, decoder)
|
||||
err = streamEvaluator.EvaluateFiles(processExpression(expression), args, printer, decoder)
|
||||
}
|
||||
completedSuccessfully = err == nil
|
||||
|
||||
|
||||
+48
-16
@@ -35,7 +35,7 @@ yq -P sample.json
|
||||
return evaluateSequence(cmd, args)
|
||||
|
||||
},
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmd.SetOut(cmd.OutOrStdout())
|
||||
|
||||
var format = logging.MustStringFormatter(
|
||||
@@ -52,13 +52,43 @@ yq -P sample.json
|
||||
|
||||
logging.SetBackend(backend)
|
||||
yqlib.InitExpressionParser()
|
||||
if (inputFormat == "x" || inputFormat == "xml") &&
|
||||
outputFormat != "x" && outputFormat != "xml" &&
|
||||
yqlib.ConfiguredXMLPreferences.AttributePrefix == "+" {
|
||||
yqlib.GetLogger().Warning("The default xml-attribute-prefix will change in the v4.30 to `+@` to avoid " +
|
||||
|
||||
outputFormatType, err := yqlib.OutputFormatFromString(outputFormat)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inputFormatType, err := yqlib.InputFormatFromString(inputFormat)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (inputFormatType == yqlib.XMLInputFormat &&
|
||||
outputFormatType != yqlib.XMLOutputFormat ||
|
||||
inputFormatType != yqlib.XMLInputFormat &&
|
||||
outputFormatType == yqlib.XMLOutputFormat) &&
|
||||
yqlib.ConfiguredXMLPreferences.AttributePrefix == "+@" {
|
||||
yqlib.GetLogger().Warning("The default xml-attribute-prefix has changed in the v4.30 to `+@` to avoid " +
|
||||
"naming conflicts with the default content name, directive name and proc inst prefix. If you need to keep " +
|
||||
"`+` please set that value explicityly with --xml-attribute-prefix.")
|
||||
}
|
||||
|
||||
if outputFormatType == yqlib.YamlOutputFormat ||
|
||||
outputFormatType == yqlib.PropsOutputFormat {
|
||||
unwrapScalar = true
|
||||
}
|
||||
if unwrapScalarFlag.IsExplicitySet() {
|
||||
unwrapScalar = unwrapScalarFlag.IsSet()
|
||||
}
|
||||
|
||||
//copy preference form global setting
|
||||
yqlib.ConfiguredYamlPreferences.UnwrapScalar = unwrapScalar
|
||||
|
||||
yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,15 +103,15 @@ yq -P sample.json
|
||||
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "yaml", "[yaml|y|json|j|props|p|xml|x] output format type.")
|
||||
rootCmd.PersistentFlags().StringVarP(&inputFormat, "input-format", "p", "yaml", "[yaml|y|props|p|xml|x] parse format for input. Note that json is a subset of yaml.")
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.AttributePrefix, "xml-attribute-prefix", "+", "prefix for xml attributes")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.ContentName, "xml-content-name", "+content", "name for xml content (if no attribute name is present).")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.StrictMode, "xml-strict-mode", false, "enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.KeepNamespace, "xml-keep-namespace", true, "enables keeping namespace after parsing attributes")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.UseRawToken, "xml-raw-token", true, "enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details.")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.ProcInstPrefix, "xml-proc-inst-prefix", "+p_", "prefix for xml processing instructions (e.g. <?xml version=\"1\"?>)")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.DirectiveName, "xml-directive-name", "+directive", "name for xml directives (e.g. <!DOCTYPE thing cat>)")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.SkipProcInst, "xml-skip-proc-inst", false, "skip over process instructions (e.g. <?xml version=\"1\"?>)")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.SkipDirectives, "xml-skip-directives", false, "skip over directives (e.g. <!DOCTYPE thing cat>)")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.AttributePrefix, "xml-attribute-prefix", yqlib.ConfiguredXMLPreferences.AttributePrefix, "prefix for xml attributes")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.ContentName, "xml-content-name", yqlib.ConfiguredXMLPreferences.ContentName, "name for xml content (if no attribute name is present).")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.StrictMode, "xml-strict-mode", yqlib.ConfiguredXMLPreferences.StrictMode, "enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.KeepNamespace, "xml-keep-namespace", yqlib.ConfiguredXMLPreferences.KeepNamespace, "enables keeping namespace after parsing attributes")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.UseRawToken, "xml-raw-token", yqlib.ConfiguredXMLPreferences.UseRawToken, "enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details.")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.ProcInstPrefix, "xml-proc-inst-prefix", yqlib.ConfiguredXMLPreferences.ProcInstPrefix, "prefix for xml processing instructions (e.g. <?xml version=\"1\"?>)")
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredXMLPreferences.DirectiveName, "xml-directive-name", yqlib.ConfiguredXMLPreferences.DirectiveName, "name for xml directives (e.g. <!DOCTYPE thing cat>)")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.SkipProcInst, "xml-skip-proc-inst", yqlib.ConfiguredXMLPreferences.SkipProcInst, "skip over process instructions (e.g. <?xml version=\"1\"?>)")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredXMLPreferences.SkipDirectives, "xml-skip-directives", yqlib.ConfiguredXMLPreferences.SkipDirectives, "skip over directives (e.g. <!DOCTYPE thing cat>)")
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&noDocSeparators, "no-doc", "N", false, "Don't print document separators (---)")
|
||||
@@ -89,7 +119,9 @@ yq -P sample.json
|
||||
rootCmd.PersistentFlags().IntVarP(&indent, "indent", "I", 2, "sets indent level for output")
|
||||
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
|
||||
rootCmd.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the file inplace of first file given.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&unwrapScalar, "unwrapScalar", "r", true, "unwrap scalar, print the value with no quotes, colors or comments")
|
||||
rootCmd.PersistentFlags().VarP(unwrapScalarFlag, "unwrapScalar", "r", "unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml")
|
||||
rootCmd.PersistentFlags().Lookup("unwrapScalar").NoOptDefVal = "true"
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&prettyPrint, "prettyPrint", "P", false, "pretty print, shorthand for '... style = \"\"'")
|
||||
rootCmd.PersistentFlags().BoolVarP(&exitStatus, "exit-status", "e", false, "set exit status if there are no matches or null or false is returned")
|
||||
|
||||
@@ -97,7 +129,7 @@ yq -P sample.json
|
||||
rootCmd.PersistentFlags().BoolVarP(&forceNoColor, "no-colors", "M", false, "force print with no colors")
|
||||
rootCmd.PersistentFlags().StringVarP(&frontMatter, "front-matter", "f", "", "(extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact")
|
||||
rootCmd.PersistentFlags().StringVarP(&forceExpression, "expression", "", "", "forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&leadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter.")
|
||||
rootCmd.PersistentFlags().StringVarP(&splitFileExpFile, "split-exp-file", "", "", "Use a file to specify the split-exp expression.")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type boolFlag interface {
|
||||
pflag.Value
|
||||
IsExplicitySet() bool
|
||||
IsSet() bool
|
||||
}
|
||||
|
||||
type unwrapScalarFlagStrc struct {
|
||||
explicitySet bool
|
||||
value bool
|
||||
}
|
||||
|
||||
func newUnwrapFlag() boolFlag {
|
||||
return &unwrapScalarFlagStrc{value: true}
|
||||
}
|
||||
|
||||
func (f *unwrapScalarFlagStrc) IsExplicitySet() bool {
|
||||
return f.explicitySet
|
||||
}
|
||||
|
||||
func (f *unwrapScalarFlagStrc) IsSet() bool {
|
||||
return f.value
|
||||
}
|
||||
|
||||
func (f *unwrapScalarFlagStrc) String() string {
|
||||
return strconv.FormatBool(f.value)
|
||||
}
|
||||
|
||||
func (f *unwrapScalarFlagStrc) Set(value string) error {
|
||||
|
||||
v, err := strconv.ParseBool(value)
|
||||
f.value = v
|
||||
f.explicitySet = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (*unwrapScalarFlagStrc) Type() string {
|
||||
return "bool"
|
||||
}
|
||||
+6
-5
@@ -56,7 +56,7 @@ func initCommand(cmd *cobra.Command, args []string) (string, []string, error) {
|
||||
return expression, args, nil
|
||||
}
|
||||
|
||||
func configureDecoder() (yqlib.Decoder, error) {
|
||||
func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {
|
||||
yqlibInputFormat, err := yqlib.InputFormatFromString(inputFormat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -73,8 +73,9 @@ func configureDecoder() (yqlib.Decoder, error) {
|
||||
case yqlib.TSVObjectInputFormat:
|
||||
return yqlib.NewCSVObjectDecoder('\t'), nil
|
||||
}
|
||||
|
||||
return yqlib.NewYamlDecoder(), nil
|
||||
prefs := yqlib.ConfiguredYamlPreferences
|
||||
prefs.EvaluateTogether = evaluateTogether
|
||||
return yqlib.NewYamlDecoder(prefs), nil
|
||||
}
|
||||
|
||||
func configurePrinterWriter(format yqlib.PrinterOutputFormat, out io.Writer) (yqlib.PrinterWriter, error) {
|
||||
@@ -97,7 +98,7 @@ func configurePrinterWriter(format yqlib.PrinterOutputFormat, out io.Writer) (yq
|
||||
func configureEncoder(format yqlib.PrinterOutputFormat) yqlib.Encoder {
|
||||
switch format {
|
||||
case yqlib.JSONOutputFormat:
|
||||
return yqlib.NewJSONEncoder(indent, colorsEnabled)
|
||||
return yqlib.NewJSONEncoder(indent, colorsEnabled, unwrapScalar)
|
||||
case yqlib.PropsOutputFormat:
|
||||
return yqlib.NewPropertiesEncoder(unwrapScalar)
|
||||
case yqlib.CSVOutputFormat:
|
||||
@@ -105,7 +106,7 @@ func configureEncoder(format yqlib.PrinterOutputFormat) yqlib.Encoder {
|
||||
case yqlib.TSVOutputFormat:
|
||||
return yqlib.NewCsvEncoder('\t')
|
||||
case yqlib.YamlOutputFormat:
|
||||
return yqlib.NewYamlEncoder(indent, colorsEnabled, !noDocSeparators, unwrapScalar)
|
||||
return yqlib.NewYamlEncoder(indent, colorsEnabled, yqlib.ConfiguredYamlPreferences)
|
||||
case yqlib.XMLOutputFormat:
|
||||
return yqlib.NewXMLEncoder(indent, yqlib.ConfiguredXMLPreferences)
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ var (
|
||||
GitDescribe string
|
||||
|
||||
// Version is main version number that is being run at the moment.
|
||||
Version = "4.28.2"
|
||||
Version = "4.30.1"
|
||||
|
||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||
|
||||
+2
-11
@@ -1,11 +1,2 @@
|
||||
---
|
||||
- become: true
|
||||
gather_facts: false
|
||||
hosts: lalaland
|
||||
name: "Apply smth"
|
||||
roles:
|
||||
- lala
|
||||
- land
|
||||
serial: 1
|
||||
- become: false
|
||||
gather_facts: true
|
||||
- [cat, dog, frog, cow]
|
||||
- [apple, banana, grape, mango]
|
||||
+1
-5
@@ -1,5 +1 @@
|
||||
volumes:
|
||||
beehive-conf:
|
||||
driver: local
|
||||
services:
|
||||
- beehive
|
||||
["foobar", "foobaz", "blarp"]
|
||||
@@ -1,3 +1,3 @@
|
||||
name,numberOfCats,likesApples,height
|
||||
Gary,1,true,168.8
|
||||
,1,true,168.8
|
||||
Samantha's Rabbit,2,false,-188.8
|
||||
|
@@ -8,11 +8,12 @@ require (
|
||||
github.com/elliotchance/orderedmap v1.5.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/goccy/go-yaml v1.9.5
|
||||
github.com/goccy/go-yaml v1.9.6
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/magiconair/properties v1.8.6
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
||||
github.com/spf13/cobra v1.6.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -22,7 +23,6 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
|
||||
@@ -21,8 +21,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
|
||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
github.com/goccy/go-yaml v1.9.6 h1:KhAu1zf9JXnm3vbG49aDE0E5uEBUsM4uwD31/58ZWyI=
|
||||
github.com/goccy/go-yaml v1.9.6/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
@@ -43,8 +43,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
|
||||
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -62,6 +62,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// A yaml expression evaluator that runs the expression once against all files/nodes in memory.
|
||||
type Evaluator interface {
|
||||
EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error
|
||||
EvaluateFiles(expression string, filenames []string, printer Printer, decoder Decoder) error
|
||||
|
||||
// EvaluateNodes takes an expression and one or more yaml nodes, returning a list of matching candidate nodes
|
||||
EvaluateNodes(expression string, nodes ...*yaml.Node) (*list.List, error)
|
||||
@@ -46,21 +46,16 @@ func (e *allAtOnceEvaluator) EvaluateCandidateNodes(expression string, inputCand
|
||||
return context.MatchingNodes, nil
|
||||
}
|
||||
|
||||
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error {
|
||||
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, decoder Decoder) error {
|
||||
fileIndex := 0
|
||||
firstFileLeadingContent := ""
|
||||
|
||||
var allDocuments = list.New()
|
||||
for _, filename := range filenames {
|
||||
reader, leadingContent, err := readStream(filename, fileIndex == 0 && leadingContentPreProcessing)
|
||||
reader, err := readStream(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileIndex == 0 {
|
||||
firstFileLeadingContent = leadingContent
|
||||
}
|
||||
|
||||
fileDocuments, err := readDocuments(reader, filename, fileIndex, decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -75,11 +70,9 @@ func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string
|
||||
Filename: "",
|
||||
Node: &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Tag: "!!null", Kind: yaml.ScalarNode}}},
|
||||
FileIndex: 0,
|
||||
LeadingContent: firstFileLeadingContent,
|
||||
LeadingContent: "",
|
||||
}
|
||||
allDocuments.PushBack(candidateNode)
|
||||
} else {
|
||||
allDocuments.Front().Value.(*CandidateNode).LeadingContent = firstFileLeadingContent
|
||||
}
|
||||
|
||||
matches, err := e.EvaluateCandidateNodes(expression, allDocuments)
|
||||
|
||||
@@ -54,12 +54,12 @@ func compare(prefs compareTypePref) func(d *dataTreeNavigator, context Context,
|
||||
}
|
||||
|
||||
func compareDateTime(layout string, prefs compareTypePref, lhs *yaml.Node, rhs *yaml.Node) (bool, error) {
|
||||
lhsTime, err := time.Parse(layout, lhs.Value)
|
||||
lhsTime, err := parseDateTime(layout, lhs.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rhsTime, err := time.Parse(layout, rhs.Value)
|
||||
rhsTime, err := parseDateTime(layout, rhs.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func compareScalars(context Context, prefs compareTypePref, lhs *yaml.Node, rhs
|
||||
isDateTime := lhs.Tag == "!!timestamp"
|
||||
// if the lhs is a string, it might be a timestamp in a custom format.
|
||||
if lhsTag == "!!str" && context.GetDateTimeLayout() != time.RFC3339 {
|
||||
_, err := time.Parse(context.GetDateTimeLayout(), lhs.Value)
|
||||
_, err := parseDateTime(context.GetDateTimeLayout(), lhs.Value)
|
||||
isDateTime = err == nil
|
||||
}
|
||||
if isDateTime {
|
||||
|
||||
+18
-9
@@ -12,7 +12,9 @@ const csvSimple = `name,numberOfCats,likesApples,height
|
||||
Gary,1,true,168.8
|
||||
Samantha's Rabbit,2,false,-188.8
|
||||
`
|
||||
|
||||
const csvMissing = `name,numberOfCats,likesApples,height
|
||||
,null,,168.8
|
||||
`
|
||||
const expectedUpdatedSimpleCsv = `name,numberOfCats,likesApples,height
|
||||
Gary,3,true,168.8
|
||||
Samantha's Rabbit,2,false,-188.8
|
||||
@@ -110,6 +112,13 @@ var csvScenarios = []formatScenario{
|
||||
expected: csvSimpleMissingData,
|
||||
scenarioType: "encode-csv",
|
||||
},
|
||||
{
|
||||
description: "decode csv missing",
|
||||
skipDoc: true,
|
||||
input: csvMissing,
|
||||
expected: csvMissing,
|
||||
scenarioType: "roundtrip-csv",
|
||||
},
|
||||
{
|
||||
description: "Parse CSV into an array of objects",
|
||||
subdescription: "First row is assumed to be the header row.",
|
||||
@@ -136,15 +145,15 @@ var csvScenarios = []formatScenario{
|
||||
func testCSVScenario(t *testing.T, s formatScenario) {
|
||||
switch s.scenarioType {
|
||||
case "encode-csv":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewCsvEncoder(',')), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewCsvEncoder(',')), s.description)
|
||||
case "encode-tsv":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewCsvEncoder('\t')), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewCsvEncoder('\t')), s.description)
|
||||
case "decode-csv-object":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewCSVObjectDecoder(','), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewCSVObjectDecoder(','), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "decode-tsv-object":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewCSVObjectDecoder('\t'), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewCSVObjectDecoder('\t'), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "roundtrip-csv":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewCSVObjectDecoder(','), NewCsvEncoder(',')), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewCSVObjectDecoder(','), NewCsvEncoder(',')), s.description)
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
@@ -171,7 +180,7 @@ func documentCSVDecodeObjectScenario(w *bufio.Writer, s formatScenario, formatTy
|
||||
}
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n",
|
||||
processFormatScenario(s, NewCSVObjectDecoder(separator), NewYamlEncoder(s.indent, false, true, true))),
|
||||
mustProcessFormatScenario(s, NewCSVObjectDecoder(separator), NewYamlEncoder(s.indent, false, ConfiguredYamlPreferences))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,7 +212,7 @@ func documentCSVEncodeScenario(w *bufio.Writer, s formatScenario, formatType str
|
||||
}
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```%v\n%v```\n\n", formatType,
|
||||
processFormatScenario(s, NewYamlDecoder(), NewCsvEncoder(separator))),
|
||||
mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewCsvEncoder(separator))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -235,7 +244,7 @@ func documentCSVRoundTripScenario(w *bufio.Writer, s formatScenario, formatType
|
||||
}
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```%v\n%v```\n\n", formatType,
|
||||
processFormatScenario(s, NewCSVObjectDecoder(separator), NewCsvEncoder(separator))),
|
||||
mustProcessFormatScenario(s, NewCSVObjectDecoder(separator), NewCsvEncoder(separator))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ package yqlib
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type InputFormat uint
|
||||
@@ -20,8 +18,8 @@ const (
|
||||
)
|
||||
|
||||
type Decoder interface {
|
||||
Init(reader io.Reader)
|
||||
Decode(node *yaml.Node) error
|
||||
Init(reader io.Reader) error
|
||||
Decode() (*CandidateNode, error)
|
||||
}
|
||||
|
||||
func InputFormatFromString(format string) (InputFormat, error) {
|
||||
|
||||
@@ -19,21 +19,22 @@ func NewBase64Decoder() Decoder {
|
||||
return &base64Decoder{finished: false, encoding: *base64.StdEncoding}
|
||||
}
|
||||
|
||||
func (dec *base64Decoder) Init(reader io.Reader) {
|
||||
func (dec *base64Decoder) Init(reader io.Reader) error {
|
||||
dec.reader = reader
|
||||
dec.readAnything = false
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *base64Decoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
func (dec *base64Decoder) Decode() (*CandidateNode, error) {
|
||||
if dec.finished {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
base64Reader := base64.NewDecoder(&dec.encoding, dec.reader)
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if _, err := buf.ReadFrom(base64Reader); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
dec.finished = true
|
||||
@@ -42,12 +43,15 @@ func (dec *base64Decoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
// otherwise if we've already read some bytes, and now we get
|
||||
// an empty string, then we are done.
|
||||
if dec.readAnything {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
}
|
||||
dec.readAnything = true
|
||||
rootYamlNode.Kind = yaml.ScalarNode
|
||||
rootYamlNode.Tag = "!!str"
|
||||
rootYamlNode.Value = buf.String()
|
||||
return nil
|
||||
return &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: buf.String(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ func NewCSVObjectDecoder(separator rune) Decoder {
|
||||
return &csvObjectDecoder{separator: separator}
|
||||
}
|
||||
|
||||
func (dec *csvObjectDecoder) Init(reader io.Reader) {
|
||||
func (dec *csvObjectDecoder) Init(reader io.Reader) error {
|
||||
cleanReader, enc := utfbom.Skip(reader)
|
||||
log.Debugf("Detected encoding: %s\n", enc)
|
||||
dec.reader = *csv.NewReader(cleanReader)
|
||||
dec.reader.Comma = dec.separator
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *csvObjectDecoder) convertToYamlNode(content string) *yaml.Node {
|
||||
@@ -47,14 +48,14 @@ func (dec *csvObjectDecoder) createObject(headerRow []string, contentRow []strin
|
||||
return objectNode
|
||||
}
|
||||
|
||||
func (dec *csvObjectDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
func (dec *csvObjectDecoder) Decode() (*CandidateNode, error) {
|
||||
if dec.finished {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
headerRow, err := dec.reader.Read()
|
||||
log.Debugf(": headerRow%v", headerRow)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootArray := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
||||
@@ -68,13 +69,13 @@ func (dec *csvObjectDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
log.Debugf("Read next contentRow: %v, %v", contentRow, err)
|
||||
}
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("finished, contentRow%v", contentRow)
|
||||
log.Debugf("err: %v", err)
|
||||
|
||||
rootYamlNode.Kind = yaml.DocumentNode
|
||||
rootYamlNode.Content = []*yaml.Node{rootArray}
|
||||
return nil
|
||||
return &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{rootArray},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -16,26 +16,31 @@ func NewJSONDecoder() Decoder {
|
||||
return &jsonDecoder{}
|
||||
}
|
||||
|
||||
func (dec *jsonDecoder) Init(reader io.Reader) {
|
||||
func (dec *jsonDecoder) Init(reader io.Reader) error {
|
||||
dec.decoder = *json.NewDecoder(reader)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *jsonDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
func (dec *jsonDecoder) Decode() (*CandidateNode, error) {
|
||||
|
||||
var dataBucket orderedMap
|
||||
log.Debug("going to decode")
|
||||
err := dec.decoder.Decode(&dataBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
node, err := dec.convertToYamlNode(&dataBucket)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
rootYamlNode.Kind = yaml.DocumentNode
|
||||
rootYamlNode.Content = []*yaml.Node{node}
|
||||
return nil
|
||||
|
||||
return &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{node},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dec *jsonDecoder) convertToYamlNode(data *orderedMap) (*yaml.Node, error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package yqlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -20,9 +21,10 @@ func NewPropertiesDecoder() Decoder {
|
||||
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) Init(reader io.Reader) {
|
||||
func (dec *propertiesDecoder) Init(reader io.Reader) error {
|
||||
dec.reader = reader
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePropKey(key string) []interface{} {
|
||||
@@ -46,15 +48,49 @@ func (dec *propertiesDecoder) processComment(c string) string {
|
||||
return "# " + c
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) applyProperty(properties *properties.Properties, context Context, key string) error {
|
||||
func (dec *propertiesDecoder) applyPropertyComments(context Context, path []interface{}, comments []string) error {
|
||||
assignmentOp := &Operation{OperationType: assignOpType, Preferences: assignPreferences{}}
|
||||
|
||||
rhsCandidateNode := &CandidateNode{
|
||||
Path: path,
|
||||
Node: &yaml.Node{
|
||||
Tag: "!!str",
|
||||
Value: fmt.Sprintf("%v", path[len(path)-1]),
|
||||
HeadComment: dec.processComment(strings.Join(comments, "\n")),
|
||||
Kind: yaml.ScalarNode,
|
||||
},
|
||||
}
|
||||
|
||||
rhsCandidateNode.Node.Tag = guessTagFromCustomType(rhsCandidateNode.Node)
|
||||
|
||||
rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhsCandidateNode}
|
||||
|
||||
assignmentOpNode := &ExpressionNode{
|
||||
Operation: assignmentOp,
|
||||
LHS: createTraversalTree(path, traversePreferences{}, true),
|
||||
RHS: &ExpressionNode{Operation: rhsOp},
|
||||
}
|
||||
|
||||
_, err := dec.d.GetMatchingNodes(context, assignmentOpNode)
|
||||
return err
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error {
|
||||
value, _ := properties.Get(key)
|
||||
path := parsePropKey(key)
|
||||
|
||||
propertyComments := properties.GetComments(key)
|
||||
if len(propertyComments) > 0 {
|
||||
err := dec.applyPropertyComments(context, path, propertyComments)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rhsNode := &yaml.Node{
|
||||
Value: value,
|
||||
Tag: "!!str",
|
||||
Kind: yaml.ScalarNode,
|
||||
LineComment: dec.processComment(properties.GetComment(key)),
|
||||
Value: value,
|
||||
Tag: "!!str",
|
||||
Kind: yaml.ScalarNode,
|
||||
}
|
||||
|
||||
rhsNode.Tag = guessTagFromCustomType(rhsNode)
|
||||
@@ -78,22 +114,22 @@ func (dec *propertiesDecoder) applyProperty(properties *properties.Properties, c
|
||||
return err
|
||||
}
|
||||
|
||||
func (dec *propertiesDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
func (dec *propertiesDecoder) Decode() (*CandidateNode, error) {
|
||||
if dec.finished {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if _, err := buf.ReadFrom(dec.reader); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
dec.finished = true
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
properties, err := properties.LoadString(buf.String())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
properties.DisableExpansion = true
|
||||
|
||||
@@ -108,15 +144,18 @@ func (dec *propertiesDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
context = context.SingleChildContext(rootMap)
|
||||
|
||||
for _, key := range properties.Keys() {
|
||||
if err := dec.applyProperty(properties, context, key); err != nil {
|
||||
return err
|
||||
if err := dec.applyProperty(context, properties, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rootYamlNode.Kind = yaml.DocumentNode
|
||||
rootYamlNode.Content = []*yaml.Node{rootMap.Node}
|
||||
dec.finished = true
|
||||
return nil
|
||||
|
||||
return &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{rootMap.Node},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ type formatScenario struct {
|
||||
subdescription string
|
||||
skipDoc bool
|
||||
scenarioType string
|
||||
expectedError string
|
||||
}
|
||||
|
||||
func processFormatScenario(s formatScenario, decoder Decoder, encoder Encoder) string {
|
||||
|
||||
func processFormatScenario(s formatScenario, decoder Decoder, encoder Encoder) (string, error) {
|
||||
var output bytes.Buffer
|
||||
writer := bufio.NewWriter(&output)
|
||||
|
||||
if decoder == nil {
|
||||
decoder = NewYamlDecoder()
|
||||
decoder = NewYamlDecoder(ConfiguredYamlPreferences)
|
||||
}
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
expression := s.expression
|
||||
@@ -39,22 +39,31 @@ func processFormatScenario(s formatScenario, decoder Decoder, encoder Encoder) s
|
||||
exp, err := getExpressionParser().ParseExpression(expression)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
context, err := NewDataTreeNavigator().GetMatchingNodes(Context{MatchingNodes: inputs}, exp)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
printer := NewPrinter(encoder, NewSinglePrinterWriter(writer))
|
||||
err = printer.PrintResults(context.MatchingNodes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return "", err
|
||||
}
|
||||
writer.Flush()
|
||||
|
||||
return output.String()
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
func mustProcessFormatScenario(s formatScenario, decoder Decoder, encoder Encoder) string {
|
||||
|
||||
result, err := processFormatScenario(s, decoder, encoder)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ func NewXMLDecoder(prefs XmlPreferences) Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *xmlDecoder) Init(reader io.Reader) {
|
||||
func (dec *xmlDecoder) Init(reader io.Reader) error {
|
||||
dec.reader = reader
|
||||
dec.readAnything = false
|
||||
dec.finished = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *xmlDecoder) createSequence(nodes []*xmlNode) (*yaml.Node, error) {
|
||||
@@ -118,32 +119,36 @@ func (dec *xmlDecoder) convertToYamlNode(n *xmlNode) (*yaml.Node, error) {
|
||||
return scalar, nil
|
||||
}
|
||||
|
||||
func (dec *xmlDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
func (dec *xmlDecoder) Decode() (*CandidateNode, error) {
|
||||
if dec.finished {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
root := &xmlNode{}
|
||||
// cant use xj - it doesn't keep map order.
|
||||
err := dec.decodeXML(root)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
firstNode, err := dec.convertToYamlNode(root)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
} else if firstNode.Tag == "!!null" {
|
||||
dec.finished = true
|
||||
if dec.readAnything {
|
||||
return io.EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
}
|
||||
dec.readAnything = true
|
||||
rootYamlNode.Kind = yaml.DocumentNode
|
||||
rootYamlNode.Content = []*yaml.Node{firstNode}
|
||||
dec.finished = true
|
||||
return nil
|
||||
|
||||
return &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{firstNode},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type xmlNode struct {
|
||||
|
||||
@@ -1,23 +1,114 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type yamlDecoder struct {
|
||||
decoder yaml.Decoder
|
||||
// work around of various parsing issues by yaml.v3 with document headers
|
||||
prefs YamlPreferences
|
||||
leadingContent string
|
||||
readAnything bool
|
||||
firstFile bool
|
||||
}
|
||||
|
||||
func NewYamlDecoder() Decoder {
|
||||
return &yamlDecoder{}
|
||||
func NewYamlDecoder(prefs YamlPreferences) Decoder {
|
||||
return &yamlDecoder{prefs: prefs, firstFile: true}
|
||||
}
|
||||
|
||||
func (dec *yamlDecoder) Init(reader io.Reader) {
|
||||
dec.decoder = *yaml.NewDecoder(reader)
|
||||
func (dec *yamlDecoder) processReadStream(reader *bufio.Reader) (io.Reader, string, error) {
|
||||
var commentLineRegEx = regexp.MustCompile(`^\s*#`)
|
||||
var sb strings.Builder
|
||||
for {
|
||||
peekBytes, err := reader.Peek(3)
|
||||
if errors.Is(err, io.EOF) {
|
||||
// EOF are handled else where..
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
} else if string(peekBytes) == "---" {
|
||||
_, err := reader.ReadString('\n')
|
||||
sb.WriteString("$yqDocSeperator$\n")
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else if commentLineRegEx.MatchString(string(peekBytes)) {
|
||||
line, err := reader.ReadString('\n')
|
||||
sb.WriteString(line)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else {
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *yamlDecoder) Decode(rootYamlNode *yaml.Node) error {
|
||||
return dec.decoder.Decode(rootYamlNode)
|
||||
func (dec *yamlDecoder) Init(reader io.Reader) error {
|
||||
readerToUse := reader
|
||||
leadingContent := ""
|
||||
var err error
|
||||
// if we 'evaluating together' - we only process the leading content
|
||||
// of the first file - this ensures comments from subsequent files are
|
||||
// merged together correctly.
|
||||
if dec.prefs.LeadingContentPreProcessing && (!dec.prefs.EvaluateTogether || dec.firstFile) {
|
||||
readerToUse, leadingContent, err = dec.processReadStream(bufio.NewReader(reader))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dec.leadingContent = leadingContent
|
||||
dec.readAnything = false
|
||||
dec.decoder = *yaml.NewDecoder(readerToUse)
|
||||
dec.firstFile = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *yamlDecoder) Decode() (*CandidateNode, error) {
|
||||
var dataBucket yaml.Node
|
||||
|
||||
err := dec.decoder.Decode(&dataBucket)
|
||||
if errors.Is(err, io.EOF) && dec.leadingContent != "" && !dec.readAnything {
|
||||
// force returning an empty node with a comment.
|
||||
dec.readAnything = true
|
||||
return dec.blankNodeWithComment(), nil
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidateNode := &CandidateNode{
|
||||
Node: &dataBucket,
|
||||
}
|
||||
|
||||
if dec.leadingContent != "" {
|
||||
candidateNode.LeadingContent = dec.leadingContent
|
||||
dec.leadingContent = ""
|
||||
}
|
||||
// move document comments into candidate node
|
||||
// otherwise unwrap drops them.
|
||||
candidateNode.TrailingContent = dataBucket.FootComment
|
||||
dataBucket.FootComment = ""
|
||||
return candidateNode, nil
|
||||
}
|
||||
|
||||
func (dec *yamlDecoder) blankNodeWithComment() *CandidateNode {
|
||||
return &CandidateNode{
|
||||
Document: 0,
|
||||
Filename: "",
|
||||
Node: &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Tag: "!!null", Kind: yaml.ScalarNode}}},
|
||||
FileIndex: 0,
|
||||
LeadingContent: dec.leadingContent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Array to Map
|
||||
|
||||
Use this operator to convert an array to..a map. Skips over null values.
|
||||
|
||||
Behind the scenes, this is implemented using reduce:
|
||||
|
||||
```
|
||||
(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)
|
||||
```
|
||||
|
||||
## Simple example
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
cool:
|
||||
- null
|
||||
- null
|
||||
- hello
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.cool |= array_to_map' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
cool:
|
||||
2: hello
|
||||
```
|
||||
|
||||
@@ -246,6 +246,7 @@ Note the use of `...` to ensure key nodes are included.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# hi
|
||||
a: cat # comment
|
||||
# great
|
||||
b: # key comment
|
||||
@@ -263,6 +264,7 @@ b:
|
||||
## Get line comment
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# welcome!
|
||||
a: cat # meow
|
||||
# have a great day
|
||||
```
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Contains
|
||||
|
||||
This returns `true` if the context contains the passed in parameter, and false otherwise.
|
||||
This returns `true` if the context contains the passed in parameter, and false otherwise. For arrays, this will return true if the passed in array is contained within the array. For strings, it will return true if the string is a substring.
|
||||
|
||||
{% hint style="warning" %}
|
||||
|
||||
_Note_ that, just like jq, when checking if an array of strings `contains` another, this will use `contains` and _not_ equals to check each string. This means an array like `contains(["cat"])` will return true for an array `["cats"]`.
|
||||
|
||||
See the "Array has a subset array" example below on how to check for a subset.
|
||||
|
||||
{% endhint %}
|
||||
|
||||
## Array contains array
|
||||
Array is equal or subset of
|
||||
@@ -20,6 +28,24 @@ will output
|
||||
true
|
||||
```
|
||||
|
||||
## Array has a subset array
|
||||
Subtract the superset array from the subset, if there's anything left, it's not a subset
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- foobar
|
||||
- foobaz
|
||||
- blarp
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '["baz", "bar"] - . | length == 0' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
false
|
||||
```
|
||||
|
||||
## Object included in array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -60,7 +60,7 @@ a: 2001-12-15
|
||||
## Format: get the day of the week
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 2001-12-15T02:59:43.1Z
|
||||
a: 2001-12-15
|
||||
```
|
||||
then
|
||||
```bash
|
||||
|
||||
@@ -337,7 +337,7 @@ Given a sample.yml file of:
|
||||
a:
|
||||
cool:
|
||||
foo: bar
|
||||
+id: hi
|
||||
+@id: hi
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@@ -357,7 +357,7 @@ Given a sample.yml file of:
|
||||
a:
|
||||
cool:
|
||||
foo: bar
|
||||
+id: hi
|
||||
+@id: hi
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@@ -375,7 +375,7 @@ Given a sample.yml file of:
|
||||
a:
|
||||
cool:
|
||||
foo: bar
|
||||
+id: hi
|
||||
+@id: hi
|
||||
```
|
||||
then
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Array to Map
|
||||
|
||||
Use this operator to convert an array to..a map. Skips over null values.
|
||||
|
||||
Behind the scenes, this is implemented using reduce:
|
||||
|
||||
```
|
||||
(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)
|
||||
```
|
||||
@@ -1,3 +1,11 @@
|
||||
# Contains
|
||||
|
||||
This returns `true` if the context contains the passed in parameter, and false otherwise.
|
||||
This returns `true` if the context contains the passed in parameter, and false otherwise. For arrays, this will return true if the passed in array is contained within the array. For strings, it will return true if the string is a substring.
|
||||
|
||||
{% hint style="warning" %}
|
||||
|
||||
_Note_ that, just like jq, when checking if an array of strings `contains` another, this will use `contains` and _not_ equals to check each string. This means an array like `contains(["cat"])` will return true for an array `["cats"]`.
|
||||
|
||||
See the "Array has a subset array" example below on how to check for a subset.
|
||||
|
||||
{% endhint %}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Slice Array
|
||||
|
||||
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
|
||||
|
||||
You may leave out the first or second number, which will will refer to the start or end of the array respectively.
|
||||
@@ -122,6 +122,32 @@ a:
|
||||
b: things
|
||||
```
|
||||
|
||||
## Set path to prune deep paths
|
||||
Like pick but recursive. This uses `ireduce` to deeply set the selected paths into an empty object,
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
parentA: bob
|
||||
parentB:
|
||||
child1: i am child1
|
||||
child2: i am child2
|
||||
parentC:
|
||||
child1: me child1
|
||||
child2: me child2
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '(.parentB.child2, .parentC.child1) as $i
|
||||
ireduce({}; setpath($i | path; $i))' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
parentB:
|
||||
child2: i am child2
|
||||
parentC:
|
||||
child1: me child1
|
||||
```
|
||||
|
||||
## Set array path
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Slice Array
|
||||
|
||||
The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
|
||||
|
||||
You may leave out the first or second number, which will will refer to the start or end of the array respectively.
|
||||
|
||||
## Slicing arrays
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[1:3]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- dog
|
||||
- frog
|
||||
```
|
||||
|
||||
## Slicing arrays - without the first number
|
||||
Starts from the start of the array
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[:2]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
```
|
||||
|
||||
## Slicing arrays - without the second number
|
||||
Finishes at the end of the array
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[2:]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
|
||||
## Slicing arrays - use negative numbers to count backwards from the end
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[1:-1]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- dog
|
||||
- frog
|
||||
```
|
||||
|
||||
## Inserting into the middle of an array
|
||||
using an expression to find the index
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '(.[] | select(. == "dog") | key + 1) as $pos | .[0:($pos)] + ["rabbit"] + .[$pos:]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- cat
|
||||
- dog
|
||||
- rabbit
|
||||
- frog
|
||||
- cow
|
||||
```
|
||||
|
||||
@@ -135,6 +135,24 @@ will output
|
||||
- a: 100
|
||||
```
|
||||
|
||||
## Sort by custom date field
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: 12-Jun-2011
|
||||
- a: 23-Dec-2010
|
||||
- a: 10-Aug-2011
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'with_dtf("02-Jan-2006"; sort_by(.a))' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- a: 23-Dec-2010
|
||||
- a: 12-Jun-2011
|
||||
- a: 10-Aug-2011
|
||||
```
|
||||
|
||||
## Sort, nulls come first
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
|
||||
@@ -9,7 +9,7 @@ Note that empty arrays and maps are not encoded by default.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# block comments don't come through
|
||||
# block comments come through
|
||||
person: # neither do comments on maps
|
||||
name: Mike Wazowski # comments on values appear
|
||||
pets:
|
||||
@@ -25,6 +25,7 @@ yq -o=props sample.yml
|
||||
```
|
||||
will output
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
@@ -38,7 +39,7 @@ Note that string values with blank characters in them are encapsulated with doub
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# block comments don't come through
|
||||
# block comments come through
|
||||
person: # neither do comments on maps
|
||||
name: Mike Wazowski # comments on values appear
|
||||
pets:
|
||||
@@ -54,6 +55,7 @@ yq -o=props --unwrapScalar=false sample.yml
|
||||
```
|
||||
will output
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = "Mike Wazowski"
|
||||
|
||||
@@ -65,7 +67,7 @@ person.food.0 = pizza
|
||||
## Encode properties: no comments
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# block comments don't come through
|
||||
# block comments come through
|
||||
person: # neither do comments on maps
|
||||
name: Mike Wazowski # comments on values appear
|
||||
pets:
|
||||
@@ -91,7 +93,7 @@ Use a yq expression to set the empty maps and sequences to your desired value.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# block comments don't come through
|
||||
# block comments come through
|
||||
person: # neither do comments on maps
|
||||
name: Mike Wazowski # comments on values appear
|
||||
pets:
|
||||
@@ -107,6 +109,7 @@ yq -o=props '(.. | select( (tag == "!!map" or tag =="!!seq") and length == 0)) =
|
||||
```
|
||||
will output
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
@@ -120,6 +123,7 @@ emptyMap =
|
||||
## Decode properties
|
||||
Given a sample.properties file of:
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
@@ -135,16 +139,37 @@ yq -p=props sample.properties
|
||||
will output
|
||||
```yaml
|
||||
person:
|
||||
name: Mike Wazowski # comments on values appear
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
name: Mike Wazowski
|
||||
pets:
|
||||
- cat # comments on array values appear
|
||||
# comments on array values appear
|
||||
- cat
|
||||
food:
|
||||
- pizza
|
||||
```
|
||||
|
||||
## Decode properties - array should be a map
|
||||
If you have a numeric map key in your property files, use array_to_map to convert them to maps.
|
||||
|
||||
Given a sample.properties file of:
|
||||
```properties
|
||||
things.10 = mike
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=props '.things |= array_to_map' sample.properties
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
things:
|
||||
10: mike
|
||||
```
|
||||
|
||||
## Roundtrip
|
||||
Given a sample.properties file of:
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
@@ -159,6 +184,7 @@ yq -p=props -o=props '.person.pets.0 = "dog"' sample.properties
|
||||
```
|
||||
will output
|
||||
```properties
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
|
||||
+44
-40
@@ -128,7 +128,7 @@ will output
|
||||
```yaml
|
||||
+p_xml: version="1.0" encoding="UTF-8"
|
||||
cat:
|
||||
+legs: "4"
|
||||
+@legs: "4"
|
||||
legs: "7"
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ will output
|
||||
+p_xml: version="1.0" encoding="UTF-8"
|
||||
cat:
|
||||
+content: meow
|
||||
+legs: "4"
|
||||
+@legs: "4"
|
||||
```
|
||||
|
||||
## Parse xml: custom dtd
|
||||
@@ -258,55 +258,55 @@ cat:
|
||||
```
|
||||
|
||||
## Parse xml: keep attribute namespace
|
||||
Defaults to true
|
||||
|
||||
Given a sample.xml file of:
|
||||
```xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
</map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=xml -o=xml --xml-keep-namespace '.' sample.xml
|
||||
yq -p=xml -o=xml --xml-keep-namespace=false '.' sample.xml
|
||||
```
|
||||
will output
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
## Parse xml: keep raw attribute namespace
|
||||
Given a sample.xml file of:
|
||||
```xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
</map>
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=xml -o=xml --xml-keep-namespace --xml-raw-token '.' sample.xml
|
||||
```
|
||||
will output
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xsi="some-instance" schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
## Parse xml: keep raw attribute namespace
|
||||
Defaults to true
|
||||
|
||||
Given a sample.xml file of:
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=xml -o=xml --xml-raw-token=false '.' sample.xml
|
||||
```
|
||||
will output
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
```
|
||||
|
||||
## Encode xml: simple
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -347,7 +347,7 @@ Fields with the matching xml-attribute-prefix are assumed to be attributes.
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
cat:
|
||||
+name: tiger
|
||||
+@name: tiger
|
||||
meows: true
|
||||
|
||||
```
|
||||
@@ -368,7 +368,7 @@ Fields with the matching xml-content-name is assumed to be content.
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
cat:
|
||||
+name: tiger
|
||||
+@name: tiger
|
||||
+content: cool
|
||||
|
||||
```
|
||||
@@ -386,6 +386,7 @@ A best attempt is made to copy comments to xml.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
# header comment
|
||||
# above_cat
|
||||
cat: # inline_cat
|
||||
# above_array
|
||||
@@ -402,7 +403,10 @@ yq -o=xml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```xml
|
||||
<!-- above_cat inline_cat --><cat><!-- above_array inline_array -->
|
||||
<!--
|
||||
header comment
|
||||
above_cat
|
||||
--><!-- inline_cat --><cat><!-- above_array inline_array -->
|
||||
<array>val1<!-- inline_val1 --></array>
|
||||
<array><!-- above_val2 -->val2<!-- inline_val2 --></array>
|
||||
</cat><!-- below_cat -->
|
||||
|
||||
@@ -45,7 +45,7 @@ func (o *orderedMap) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// cycle through k/v
|
||||
var tok json.Token
|
||||
for tok, err = dec.Token(); !errors.Is(err, io.EOF); tok, err = dec.Token() {
|
||||
for tok, err = dec.Token(); err == nil; tok, err = dec.Token() {
|
||||
// we can expect two types: string or Delim. Delim automatically means
|
||||
// that it is the closing bracket of the object, whereas string means
|
||||
// that there is another key.
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type jsonEncoder struct {
|
||||
indentString string
|
||||
colorise bool
|
||||
UnwrapScalar bool
|
||||
}
|
||||
|
||||
func mapKeysToStrings(node *yaml.Node) {
|
||||
@@ -28,14 +29,14 @@ func mapKeysToStrings(node *yaml.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewJSONEncoder(indent int, colorise bool) Encoder {
|
||||
func NewJSONEncoder(indent int, colorise bool, unwrapScalar bool) Encoder {
|
||||
var indentString = ""
|
||||
|
||||
for index := 0; index < indent; index++ {
|
||||
indentString = indentString + " "
|
||||
}
|
||||
|
||||
return &jsonEncoder{indentString, colorise}
|
||||
return &jsonEncoder{indentString, colorise, unwrapScalar}
|
||||
}
|
||||
|
||||
func (je *jsonEncoder) CanHandleAliases() bool {
|
||||
@@ -52,6 +53,10 @@ func (je *jsonEncoder) PrintLeadingContent(writer io.Writer, content string) err
|
||||
|
||||
func (je *jsonEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
|
||||
if node.Kind == yaml.ScalarNode && je.UnwrapScalar {
|
||||
return writeString(writer, node.Value+"\n")
|
||||
}
|
||||
|
||||
destination := writer
|
||||
tempBuffer := bytes.NewBuffer(nil)
|
||||
if je.colorise {
|
||||
|
||||
@@ -65,7 +65,7 @@ func (pe *propertiesEncoder) PrintLeadingContent(writer io.Writer, content strin
|
||||
func (pe *propertiesEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
mapKeysToStrings(node)
|
||||
p := properties.NewProperties()
|
||||
err := pe.doEncode(p, node, "")
|
||||
err := pe.doEncode(p, node, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,8 +74,17 @@ func (pe *propertiesEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yaml.Node, path string) error {
|
||||
p.SetComment(path, headAndLineComment(node))
|
||||
func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yaml.Node, path string, keyNode *yaml.Node) error {
|
||||
|
||||
comments := ""
|
||||
if keyNode != nil {
|
||||
// include the key node comments if present
|
||||
comments = headAndLineComment(keyNode)
|
||||
}
|
||||
comments = comments + headAndLineComment(node)
|
||||
commentsWithSpaces := strings.ReplaceAll(comments, "\n", "\n ")
|
||||
p.SetComments(path, strings.Split(commentsWithSpaces, "\n"))
|
||||
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var nodeValue string
|
||||
@@ -87,13 +96,13 @@ func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yaml.Node,
|
||||
_, _, err := p.Set(path, nodeValue)
|
||||
return err
|
||||
case yaml.DocumentNode:
|
||||
return pe.doEncode(p, node.Content[0], path)
|
||||
return pe.doEncode(p, node.Content[0], path, node)
|
||||
case yaml.SequenceNode:
|
||||
return pe.encodeArray(p, node.Content, path)
|
||||
case yaml.MappingNode:
|
||||
return pe.encodeMap(p, node.Content, path)
|
||||
case yaml.AliasNode:
|
||||
return pe.doEncode(p, node.Alias, path)
|
||||
return pe.doEncode(p, node.Alias, path, nil)
|
||||
default:
|
||||
return fmt.Errorf("Unsupported node %v", node.Tag)
|
||||
}
|
||||
@@ -108,7 +117,7 @@ func (pe *propertiesEncoder) appendPath(path string, key interface{}) string {
|
||||
|
||||
func (pe *propertiesEncoder) encodeArray(p *properties.Properties, kids []*yaml.Node, path string) error {
|
||||
for index, child := range kids {
|
||||
err := pe.doEncode(p, child, pe.appendPath(path, index))
|
||||
err := pe.doEncode(p, child, pe.appendPath(path, index), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,7 +129,7 @@ func (pe *propertiesEncoder) encodeMap(p *properties.Properties, kids []*yaml.No
|
||||
for index := 0; index < len(kids); index = index + 2 {
|
||||
key := kids[index]
|
||||
value := kids[index+1]
|
||||
err := pe.doEncode(p, value, pe.appendPath(path, key.Value))
|
||||
err := pe.doEncode(p, value, pe.appendPath(path, key.Value), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func yamlToProps(sampleYaml string, unwrapScalar bool) string {
|
||||
writer := bufio.NewWriter(&output)
|
||||
|
||||
var propsEncoder = NewPropertiesEncoder(unwrapScalar)
|
||||
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ func yamlToJSON(sampleYaml string, indent int) string {
|
||||
var output bytes.Buffer
|
||||
writer := bufio.NewWriter(&output)
|
||||
|
||||
var jsonEncoder = NewJSONEncoder(indent, false)
|
||||
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder())
|
||||
var jsonEncoder = NewJSONEncoder(indent, false, false)
|
||||
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -4,24 +4,28 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type xmlEncoder struct {
|
||||
indentString string
|
||||
writer io.Writer
|
||||
prefs XmlPreferences
|
||||
indentString string
|
||||
writer io.Writer
|
||||
prefs XmlPreferences
|
||||
leadingContent string
|
||||
}
|
||||
|
||||
var commentPrefix = regexp.MustCompile(`(^|\n)\s*#`)
|
||||
|
||||
func NewXMLEncoder(indent int, prefs XmlPreferences) Encoder {
|
||||
var indentString = ""
|
||||
|
||||
for index := 0; index < indent; index++ {
|
||||
indentString = indentString + " "
|
||||
}
|
||||
return &xmlEncoder{indentString, nil, prefs}
|
||||
return &xmlEncoder{indentString, nil, prefs, ""}
|
||||
}
|
||||
|
||||
func (e *xmlEncoder) CanHandleAliases() bool {
|
||||
@@ -33,6 +37,7 @@ func (e *xmlEncoder) PrintDocumentSeparator(writer io.Writer) error {
|
||||
}
|
||||
|
||||
func (e *xmlEncoder) PrintLeadingContent(writer io.Writer, content string) error {
|
||||
e.leadingContent = commentPrefix.ReplaceAllString(content, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,6 +47,13 @@ func (e *xmlEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
e.writer = writer
|
||||
encoder.Indent("", e.indentString)
|
||||
|
||||
if e.leadingContent != "" {
|
||||
err := e.encodeComment(encoder, e.leadingContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
err := e.encodeTopLevelMap(encoder, node)
|
||||
@@ -53,7 +65,11 @@ func (e *xmlEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.encodeTopLevelMap(encoder, unwrapDoc(node))
|
||||
unwrappedNode := unwrapDoc(node)
|
||||
if unwrappedNode.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("cannot encode %v to XML - only maps can be encoded", unwrappedNode.Tag)
|
||||
}
|
||||
err = e.encodeTopLevelMap(encoder, unwrappedNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,17 +11,16 @@ import (
|
||||
)
|
||||
|
||||
type yamlEncoder struct {
|
||||
indent int
|
||||
colorise bool
|
||||
printDocSeparators bool
|
||||
unwrapScalar bool
|
||||
indent int
|
||||
colorise bool
|
||||
prefs YamlPreferences
|
||||
}
|
||||
|
||||
func NewYamlEncoder(indent int, colorise bool, printDocSeparators bool, unwrapScalar bool) Encoder {
|
||||
func NewYamlEncoder(indent int, colorise bool, prefs YamlPreferences) Encoder {
|
||||
if indent < 0 {
|
||||
indent = 0
|
||||
}
|
||||
return &yamlEncoder{indent, colorise, printDocSeparators, unwrapScalar}
|
||||
return &yamlEncoder{indent, colorise, prefs}
|
||||
}
|
||||
|
||||
func (ye *yamlEncoder) CanHandleAliases() bool {
|
||||
@@ -29,7 +28,7 @@ func (ye *yamlEncoder) CanHandleAliases() bool {
|
||||
}
|
||||
|
||||
func (ye *yamlEncoder) PrintDocumentSeparator(writer io.Writer) error {
|
||||
if ye.printDocSeparators {
|
||||
if ye.prefs.PrintDocSeparators {
|
||||
log.Debug("-- writing doc sep")
|
||||
if err := writeString(writer, "---\n"); err != nil {
|
||||
return err
|
||||
@@ -76,7 +75,7 @@ func (ye *yamlEncoder) PrintLeadingContent(writer io.Writer, content string) err
|
||||
|
||||
func (ye *yamlEncoder) Encode(writer io.Writer, node *yaml.Node) error {
|
||||
|
||||
if node.Kind == yaml.ScalarNode && ye.unwrapScalar {
|
||||
if node.Kind == yaml.ScalarNode && ye.prefs.UnwrapScalar {
|
||||
return writeString(writer, node.Value+"\n")
|
||||
}
|
||||
|
||||
|
||||
+22
-9
@@ -84,6 +84,13 @@ var jsonScenarios = []formatScenario{
|
||||
input: `{"cat": "meow"}`,
|
||||
expected: "D0, P[], (!!map)::cat: meow\n",
|
||||
},
|
||||
{
|
||||
description: "bad json",
|
||||
skipDoc: true,
|
||||
input: `{"a": 1 "b": 2}`,
|
||||
expectedError: `bad file 'sample.yml': invalid character '"' after object key:value pair`,
|
||||
scenarioType: "decode-error",
|
||||
},
|
||||
{
|
||||
description: "Parse json: complex",
|
||||
subdescription: "JSON is a subset of yaml, so all you need to do is prettify the output",
|
||||
@@ -237,7 +244,7 @@ func documentRoundtripNdJsonScenario(w *bufio.Writer, s formatScenario, indent i
|
||||
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(indent, false))))
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(indent, false, false))))
|
||||
}
|
||||
|
||||
func documentDecodeNdJsonScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -262,11 +269,11 @@ func documentDecodeNdJsonScenario(w *bufio.Writer, s formatScenario) {
|
||||
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewJSONDecoder(), NewYamlEncoder(s.indent, false, true, true))))
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewJSONDecoder(), NewYamlEncoder(s.indent, false, ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func decodeJSON(t *testing.T, jsonString string) *CandidateNode {
|
||||
docs, err := readDocumentWithLeadingContent(jsonString, "sample.json", 0)
|
||||
docs, err := readDocument(jsonString, "sample.json", 0)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -293,17 +300,23 @@ func decodeJSON(t *testing.T, jsonString string) *CandidateNode {
|
||||
func testJSONScenario(t *testing.T, s formatScenario) {
|
||||
switch s.scenarioType {
|
||||
case "encode", "decode":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewJSONEncoder(s.indent, false)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewJSONEncoder(s.indent, false, false)), s.description)
|
||||
case "":
|
||||
var actual = resultToString(t, decodeJSON(t, s.input))
|
||||
test.AssertResultWithContext(t, s.expected, actual, s.description)
|
||||
case "decode-ndjson":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJSONDecoder(), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewJSONDecoder(), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "roundtrip-ndjson":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(0, false)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(0, false, false)), s.description)
|
||||
case "roundtrip-multi":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(2, false)), s.description)
|
||||
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(2, false, false)), s.description)
|
||||
case "decode-error":
|
||||
result, err := processFormatScenario(s, NewJSONDecoder(), NewJSONEncoder(2, false, false))
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
|
||||
} else {
|
||||
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
@@ -385,7 +398,7 @@ func documentJSONEncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
}
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewJSONEncoder(s.indent, false))))
|
||||
writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewJSONEncoder(s.indent, false, false))))
|
||||
}
|
||||
|
||||
func TestJSONScenarios(t *testing.T) {
|
||||
|
||||
+30
-6
@@ -3,7 +3,6 @@ package yqlib
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type expressionTokeniser interface {
|
||||
@@ -64,11 +63,11 @@ func unwrap(value string) string {
|
||||
func extractNumberParameter(value string) (int, error) {
|
||||
parameterParser := regexp.MustCompile(`.*\(([0-9]+)\)`)
|
||||
matches := parameterParser.FindStringSubmatch(value)
|
||||
var indent, errParsingInt = strconv.ParseInt(matches[1], 10, 32)
|
||||
var indent, errParsingInt = parseInt(matches[1])
|
||||
if errParsingInt != nil {
|
||||
return 0, errParsingInt
|
||||
}
|
||||
return int(indent), nil
|
||||
return indent, nil
|
||||
}
|
||||
|
||||
func hasOptionParameter(value string, option string) bool {
|
||||
@@ -98,6 +97,10 @@ func postProcessTokens(tokens []*token) []*token {
|
||||
return postProcessedTokens
|
||||
}
|
||||
|
||||
func tokenIsOpType(token *token, opType *operationType) bool {
|
||||
return token.TokenType == operationToken && token.Operation.OperationType == opType
|
||||
}
|
||||
|
||||
func handleToken(tokens []*token, index int, postProcessedTokens []*token) (tokensAccum []*token, skipNextToken bool) {
|
||||
skipNextToken = false
|
||||
currentToken := tokens[index]
|
||||
@@ -121,9 +124,18 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
|
||||
|
||||
}
|
||||
|
||||
if tokenIsOpType(currentToken, createMapOpType) {
|
||||
log.Debugf("tokenIsOpType: createMapOpType")
|
||||
// check the previous token is '[', means we are slice, but dont have a first number
|
||||
if tokens[index-1].TokenType == traverseArrayCollect {
|
||||
log.Debugf("previous token is : traverseArrayOpType")
|
||||
// need to put the number 0 before this token, as that is implied
|
||||
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
|
||||
}
|
||||
}
|
||||
|
||||
if index != len(tokens)-1 && currentToken.AssignOperation != nil &&
|
||||
tokens[index+1].TokenType == operationToken &&
|
||||
tokens[index+1].Operation.OperationType == assignOpType {
|
||||
tokenIsOpType(tokens[index+1], assignOpType) {
|
||||
log.Debug(" its an update assign")
|
||||
currentToken.Operation = currentToken.AssignOperation
|
||||
currentToken.Operation.UpdateAssign = tokens[index+1].Operation.UpdateAssign
|
||||
@@ -133,6 +145,17 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
|
||||
log.Debug(" adding token to the fixed list")
|
||||
postProcessedTokens = append(postProcessedTokens, currentToken)
|
||||
|
||||
if tokenIsOpType(currentToken, createMapOpType) {
|
||||
log.Debugf("tokenIsOpType: createMapOpType")
|
||||
// check the next token is ']', means we are slice, but dont have a second number
|
||||
if index != len(tokens)-1 && tokens[index+1].TokenType == closeCollect {
|
||||
log.Debugf("next token is : closeCollect")
|
||||
// need to put the number 0 before this token, as that is implied
|
||||
lengthOp := &Operation{OperationType: lengthOpType}
|
||||
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: lengthOp})
|
||||
}
|
||||
}
|
||||
|
||||
if index != len(tokens)-1 &&
|
||||
((currentToken.TokenType == openCollect && tokens[index+1].TokenType == closeCollect) ||
|
||||
(currentToken.TokenType == openCollectObject && tokens[index+1].TokenType == closeCollectObject)) {
|
||||
@@ -142,7 +165,8 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
|
||||
}
|
||||
|
||||
if index != len(tokens)-1 && currentToken.CheckForPostTraverse &&
|
||||
((tokens[index+1].TokenType == operationToken && (tokens[index+1].Operation.OperationType == traversePathOpType)) ||
|
||||
|
||||
(tokenIsOpType(tokens[index+1], traversePathOpType) ||
|
||||
(tokens[index+1].TokenType == traverseArrayCollect)) {
|
||||
log.Debug(" adding pipe because the next thing is traverse")
|
||||
op := &Operation{OperationType: shortPipeOpType, Value: "PIPE", StringValue: "."}
|
||||
|
||||
@@ -50,6 +50,8 @@ var participleYqRules = []*participleYqRule{
|
||||
simpleOp("sortKeys", sortKeysOpType),
|
||||
simpleOp("sort_?keys", sortKeysOpType),
|
||||
|
||||
{"ArrayToMap", "array_?to_?map", expressionOpToken(`(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)`), 0},
|
||||
|
||||
{"YamlEncodeWithIndent", `to_?yaml\([0-9]+\)`, encodeParseIndent(YamlOutputFormat), 0},
|
||||
{"XMLEncodeWithIndent", `to_?xml\([0-9]+\)`, encodeParseIndent(XMLOutputFormat), 0},
|
||||
{"JSONEncodeWithIndent", `to_?json\([0-9]+\)`, encodeParseIndent(JSONOutputFormat), 0},
|
||||
@@ -84,7 +86,7 @@ var participleYqRules = []*participleYqRule{
|
||||
|
||||
{"LoadString", `load_?str|str_?load`, loadOp(nil, true), 0},
|
||||
|
||||
{"LoadYaml", `load`, loadOp(NewYamlDecoder(), false), 0},
|
||||
{"LoadYaml", `load`, loadOp(NewYamlDecoder(ConfiguredYamlPreferences), false), 0},
|
||||
|
||||
{"SplitDocument", `splitDoc|split_?doc`, opToken(splitDocumentOpType), 0},
|
||||
|
||||
@@ -286,6 +288,14 @@ func opTokenWithPrefs(opType *operationType, assignOpType *operationType, prefer
|
||||
}
|
||||
}
|
||||
|
||||
func expressionOpToken(expression string) yqAction {
|
||||
return func(rawToken lexer.Token) (*token, error) {
|
||||
prefs := expressionOpPreferences{expression: expression}
|
||||
expressionOp := &Operation{OperationType: expressionOpType, Preferences: prefs}
|
||||
return &token{TokenType: operationToken, Operation: expressionOp}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func flattenWithDepth() yqAction {
|
||||
return func(rawToken lexer.Token) (*token, error) {
|
||||
value := rawToken.Value
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/alecthomas/repr"
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
"gopkg.in/yaml.v3"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type participleLexerScenario struct {
|
||||
@@ -14,6 +14,127 @@ type participleLexerScenario struct {
|
||||
}
|
||||
|
||||
var participleLexerScenarios = []participleLexerScenario{
|
||||
{
|
||||
expression: ".[:3]",
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: selfReferenceOpType,
|
||||
StringValue: "SELF",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: traverseArrayOpType,
|
||||
StringValue: "TRAVERSE_ARRAY",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: openCollect,
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: valueOpType,
|
||||
Value: 0,
|
||||
StringValue: "0",
|
||||
CandidateNode: &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!int",
|
||||
Value: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: createMapOpType,
|
||||
Value: "CREATE_MAP",
|
||||
StringValue: ":",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: valueOpType,
|
||||
Value: 3,
|
||||
StringValue: "3",
|
||||
CandidateNode: &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.Kind(8),
|
||||
Tag: "!!int",
|
||||
Value: "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: closeCollect,
|
||||
CheckForPostTraverse: true,
|
||||
Match: "]",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: ".[-2:]",
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: selfReferenceOpType,
|
||||
StringValue: "SELF",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: traverseArrayOpType,
|
||||
StringValue: "TRAVERSE_ARRAY",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: openCollect,
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: valueOpType,
|
||||
Value: -2,
|
||||
StringValue: "-2",
|
||||
CandidateNode: &CandidateNode{
|
||||
Node: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!int",
|
||||
Value: "-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: createMapOpType,
|
||||
Value: "CREATE_MAP",
|
||||
StringValue: ":",
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: lengthOpType,
|
||||
},
|
||||
},
|
||||
{
|
||||
TokenType: closeCollect,
|
||||
CheckForPostTraverse: true,
|
||||
Match: "]",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: ".a",
|
||||
tokens: []*token{
|
||||
|
||||
+21
-8
@@ -80,6 +80,8 @@ var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Ha
|
||||
var lineOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: lineOperator}
|
||||
var columnOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: columnOperator}
|
||||
|
||||
var expressionOpType = &operationType{Type: "EXP", NumArgs: 0, Precedence: 50, Handler: expressionOperator}
|
||||
|
||||
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator}
|
||||
var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator}
|
||||
var errorOpType = &operationType{Type: "ERROR", NumArgs: 1, Precedence: 50, Handler: errorOperator}
|
||||
@@ -248,14 +250,25 @@ func guessTagFromCustomType(node *yaml.Node) string {
|
||||
}
|
||||
|
||||
func parseSnippet(value string) (*yaml.Node, error) {
|
||||
decoder := NewYamlDecoder()
|
||||
decoder.Init(strings.NewReader(value))
|
||||
var dataBucket yaml.Node
|
||||
err := decoder.Decode(&dataBucket)
|
||||
if len(dataBucket.Content) == 0 {
|
||||
if value == "" {
|
||||
return &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!null",
|
||||
}, nil
|
||||
}
|
||||
decoder := NewYamlDecoder(ConfiguredYamlPreferences)
|
||||
err := decoder.Init(strings.NewReader(value))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedNode, err := decoder.Decode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(parsedNode.Node.Content) == 0 {
|
||||
return nil, fmt.Errorf("bad data")
|
||||
}
|
||||
return dataBucket.Content[0], err
|
||||
return unwrapDoc(parsedNode.Node), err
|
||||
}
|
||||
|
||||
func recursiveNodeEqual(lhs *yaml.Node, rhs *yaml.Node) bool {
|
||||
@@ -350,8 +363,8 @@ func parseInt(numberString string) (int, error) {
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if parsed > math.MaxInt {
|
||||
return 0, fmt.Errorf("%v is too big (larger than %v)", parsed, math.MaxInt)
|
||||
} else if parsed > math.MaxInt || parsed < math.MinInt {
|
||||
return 0, fmt.Errorf("%v is not within [%v, %v]", parsed, math.MinInt, math.MaxInt)
|
||||
}
|
||||
|
||||
return int(parsed), err
|
||||
|
||||
+95
-1
@@ -1,6 +1,11 @@
|
||||
package yqlib
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestGetLogger(t *testing.T) {
|
||||
l := GetLogger()
|
||||
@@ -8,3 +13,92 @@ func TestGetLogger(t *testing.T) {
|
||||
t.Fatal("GetLogger should return the yq logger instance, not a copy")
|
||||
}
|
||||
}
|
||||
|
||||
type parseSnippetScenario struct {
|
||||
snippet string
|
||||
expected *yaml.Node
|
||||
expectedError string
|
||||
}
|
||||
|
||||
var parseSnippetScenarios = []parseSnippetScenario{
|
||||
{
|
||||
snippet: ":",
|
||||
expectedError: "yaml: did not find expected key",
|
||||
},
|
||||
{
|
||||
snippet: "",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!null",
|
||||
},
|
||||
},
|
||||
{
|
||||
snippet: "null",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!null",
|
||||
Value: "null",
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
snippet: "3",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!int",
|
||||
Value: "3",
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
snippet: "cat",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: "cat",
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
snippet: "3.1",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!float",
|
||||
Value: "3.1",
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
snippet: "true",
|
||||
expected: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!bool",
|
||||
Value: "true",
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseSnippet(t *testing.T) {
|
||||
for _, tt := range parseSnippetScenarios {
|
||||
actual, err := parseSnippet(tt.snippet)
|
||||
if tt.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%v' but it worked!", tt.expectedError)
|
||||
} else {
|
||||
test.AssertResultComplexWithContext(t, tt.expectedError, err.Error(), tt.snippet)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(tt.snippet)
|
||||
t.Error(err)
|
||||
}
|
||||
test.AssertResultComplexWithContext(t, tt.expected, actual, tt.snippet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func addScalars(context Context, target *CandidateNode, lhs *yaml.Node, rhs *yam
|
||||
|
||||
// if the lhs is a string, it might be a timestamp in a custom format.
|
||||
if lhsTag == "!!str" && context.GetDateTimeLayout() != time.RFC3339 {
|
||||
_, err := time.Parse(context.GetDateTimeLayout(), lhs.Value)
|
||||
_, err := parseDateTime(context.GetDateTimeLayout(), lhs.Value)
|
||||
isDateTime = err == nil
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func addDateTimes(layout string, target *CandidateNode, lhs *yaml.Node, rhs *yam
|
||||
return fmt.Errorf("unable to parse duration [%v]: %w", rhs.Value, err)
|
||||
}
|
||||
|
||||
currentTime, err := time.Parse(layout, lhs.Value)
|
||||
currentTime, err := parseDateTime(layout, lhs.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -252,6 +252,15 @@ var addOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (doc)::a: 2021-01-01T03:10:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date addition -date only",
|
||||
skipDoc: true,
|
||||
document: `a: 2021-01-01`,
|
||||
expression: `.a += "24h"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 2021-01-02T00:00:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date addition - custom format",
|
||||
subdescription: "You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
|
||||
@@ -2,6 +2,7 @@ package yqlib
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -244,6 +245,9 @@ func applyAlias(node *yaml.Node, alias *yaml.Node, aliasIndex int, newContent Co
|
||||
if alias == nil {
|
||||
return nil
|
||||
}
|
||||
if alias.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("merge anchor only supports maps, got %v instead", alias.Tag)
|
||||
}
|
||||
for index := 0; index < len(alias.Content); index = index + 2 {
|
||||
keyNode := alias.Content[index]
|
||||
log.Debugf("applying alias key %v", keyNode.Value)
|
||||
|
||||
@@ -36,6 +36,13 @@ thingTwo:
|
||||
`
|
||||
|
||||
var anchorOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "merge anchor not map",
|
||||
document: "a: &a\n - 0\nc:\n <<: [*a]\n",
|
||||
expectedError: "merge anchor only supports maps, got !!seq instead",
|
||||
expression: "explode(.)",
|
||||
},
|
||||
{
|
||||
description: "Merge one map",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var arrayToMapScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Simple example",
|
||||
document: `cool: [null, null, hello]`,
|
||||
expression: `.cool |= array_to_map`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::cool:\n 2: hello\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestArrayToMapScenarios(t *testing.T) {
|
||||
for _, tt := range arrayToMapScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "array-to-map", arrayToMapScenarios)
|
||||
}
|
||||
@@ -83,6 +83,10 @@ func getCommentsOperator(d *dataTreeNavigator, context Context, expressionNode *
|
||||
log.Debugf("GetComments operator!")
|
||||
var results = list.New()
|
||||
|
||||
yamlPrefs := NewDefaultYamlPreferences()
|
||||
yamlPrefs.PrintDocSeparators = false
|
||||
yamlPrefs.UnwrapScalar = false
|
||||
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
comment := ""
|
||||
@@ -92,7 +96,7 @@ func getCommentsOperator(d *dataTreeNavigator, context Context, expressionNode *
|
||||
var chompRegexp = regexp.MustCompile(`\n$`)
|
||||
var output bytes.Buffer
|
||||
var writer = bufio.NewWriter(&output)
|
||||
var encoder = NewYamlEncoder(2, false, false, false)
|
||||
var encoder = NewYamlEncoder(2, false, yamlPrefs)
|
||||
if err := encoder.PrintLeadingContent(writer, candidate.LeadingContent); err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ var containsOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!bool)::true\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Array has a subset array",
|
||||
subdescription: "Subtract the superset array from the subset, if there's anything left, it's not a subset",
|
||||
document: `["foobar", "foobaz", "blarp"]`,
|
||||
expression: `["baz", "bar"] - . | length == 0`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!bool)::false\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
expression: `["dog", "cat", "giraffe"] | contains(["camel"])`,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -51,10 +50,20 @@ func nowOp(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode
|
||||
|
||||
}
|
||||
|
||||
func parseDateTime(layout string, datestring string) (time.Time, error) {
|
||||
|
||||
parsedTime, err := time.Parse(layout, datestring)
|
||||
if err != nil && layout == time.RFC3339 {
|
||||
// try parsing the date time with only the date
|
||||
return time.Parse("2006-01-02", datestring)
|
||||
}
|
||||
return parsedTime, err
|
||||
|
||||
}
|
||||
|
||||
func formatDateTime(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
format, err := getStringParamter("format", d, context, expressionNode.RHS)
|
||||
layout := context.GetDateTimeLayout()
|
||||
decoder := NewYamlDecoder()
|
||||
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
@@ -64,24 +73,20 @@ func formatDateTime(d *dataTreeNavigator, context Context, expressionNode *Expre
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
|
||||
parsedTime, err := time.Parse(layout, candidate.Node.Value)
|
||||
parsedTime, err := parseDateTime(layout, candidate.Node.Value)
|
||||
if err != nil {
|
||||
return Context{}, fmt.Errorf("could not parse datetime of [%v]: %w", candidate.GetNicePath(), err)
|
||||
}
|
||||
formattedTimeStr := parsedTime.Format(format)
|
||||
decoder.Init(strings.NewReader(formattedTimeStr))
|
||||
var dataBucket yaml.Node
|
||||
errorReading := decoder.Decode(&dataBucket)
|
||||
var node *yaml.Node
|
||||
|
||||
node, errorReading := parseSnippet(formattedTimeStr)
|
||||
if errorReading != nil {
|
||||
log.Debugf("could not parse %v - lets just leave it as a string", formattedTimeStr)
|
||||
log.Debugf("could not parse %v - lets just leave it as a string: %w", formattedTimeStr, errorReading)
|
||||
node = &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: formattedTimeStr,
|
||||
}
|
||||
} else {
|
||||
node = unwrapDoc(&dataBucket)
|
||||
}
|
||||
|
||||
results.PushBack(candidate.CreateReplacement(node))
|
||||
@@ -107,7 +112,7 @@ func tzOp(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode)
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
|
||||
parsedTime, err := time.Parse(layout, candidate.Node.Value)
|
||||
parsedTime, err := parseDateTime(layout, candidate.Node.Value)
|
||||
if err != nil {
|
||||
return Context{}, fmt.Errorf("could not parse datetime of [%v] using layout [%v]: %w", candidate.GetNicePath(), layout, err)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ var dateTimeOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
{
|
||||
description: "Format: get the day of the week",
|
||||
document: `a: 2001-12-15T02:59:43.1Z`,
|
||||
document: `a: 2001-12-15`,
|
||||
expression: `.a | format_datetime("Monday")`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!str)::Saturday\n",
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func configureEncoder(format PrinterOutputFormat, indent int) Encoder {
|
||||
switch format {
|
||||
case JSONOutputFormat:
|
||||
return NewJSONEncoder(indent, false)
|
||||
return NewJSONEncoder(indent, false, false)
|
||||
case PropsOutputFormat:
|
||||
return NewPropertiesEncoder(true)
|
||||
case CSVOutputFormat:
|
||||
@@ -21,7 +21,7 @@ func configureEncoder(format PrinterOutputFormat, indent int) Encoder {
|
||||
case TSVOutputFormat:
|
||||
return NewCsvEncoder('\t')
|
||||
case YamlOutputFormat:
|
||||
return NewYamlEncoder(indent, false, true, true)
|
||||
return NewYamlEncoder(indent, false, ConfiguredYamlPreferences)
|
||||
case XMLOutputFormat:
|
||||
return NewXMLEncoder(indent, ConfiguredXMLPreferences)
|
||||
case Base64OutputFormat:
|
||||
@@ -102,7 +102,7 @@ func decodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
|
||||
var decoder Decoder
|
||||
switch preferences.format {
|
||||
case YamlInputFormat:
|
||||
decoder = NewYamlDecoder()
|
||||
decoder = NewYamlDecoder(ConfiguredYamlPreferences)
|
||||
case XMLInputFormat:
|
||||
decoder = NewXMLDecoder(ConfiguredXMLPreferences)
|
||||
case Base64InputFormat:
|
||||
@@ -121,17 +121,19 @@ func decodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
|
||||
|
||||
context.SetVariable("decoded: "+candidate.GetKey(), candidate.AsList())
|
||||
|
||||
var dataBucket yaml.Node
|
||||
log.Debugf("got: [%v]", candidate.Node.Value)
|
||||
|
||||
decoder.Init(strings.NewReader(unwrapDoc(candidate.Node).Value))
|
||||
err := decoder.Init(strings.NewReader(unwrapDoc(candidate.Node).Value))
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
errorReading := decoder.Decode(&dataBucket)
|
||||
decodedNode, errorReading := decoder.Decode()
|
||||
if errorReading != nil {
|
||||
return Context{}, errorReading
|
||||
}
|
||||
//first node is a doc
|
||||
node := unwrapDoc(&dataBucket)
|
||||
node := unwrapDoc(decodedNode.Node)
|
||||
|
||||
results.PushBack(candidate.CreateReplacement(node))
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
{
|
||||
description: "Encode value as xml string",
|
||||
document: `{a: {cool: {foo: "bar", +id: hi}}}`,
|
||||
document: `{a: {cool: {foo: "bar", +@id: hi}}}`,
|
||||
expression: `.a | to_xml`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!str)::<cool id=\"hi\">\n <foo>bar</foo>\n</cool>\n\n",
|
||||
@@ -202,7 +202,7 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
{
|
||||
description: "Encode value as xml string on a single line",
|
||||
document: `{a: {cool: {foo: "bar", +id: hi}}}`,
|
||||
document: `{a: {cool: {foo: "bar", +@id: hi}}}`,
|
||||
expression: `.a | @xml`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!str)::<cool id=\"hi\"><foo>bar</foo></cool>\n\n",
|
||||
@@ -210,7 +210,7 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
{
|
||||
description: "Encode value as xml string with custom indentation",
|
||||
document: `{a: {cool: {foo: "bar", +id: hi}}}`,
|
||||
document: `{a: {cool: {foo: "bar", +@id: hi}}}`,
|
||||
expression: `{"cat": .a | to_xml(1)}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::cat: |\n <cool id=\"hi\">\n <foo>bar</foo>\n </cool>\n",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package yqlib
|
||||
|
||||
type expressionOpPreferences struct {
|
||||
expression string
|
||||
}
|
||||
|
||||
func expressionOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
prefs := expressionNode.Operation.Preferences.(expressionOpPreferences)
|
||||
expNode, err := ExpressionParser.ParseExpression(prefs.expression)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
return d.GetMatchingNodes(context, expNode)
|
||||
}
|
||||
@@ -7,8 +7,16 @@ import (
|
||||
var loadScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Load empty file",
|
||||
description: "Load empty file with a comment",
|
||||
expression: `load("../../examples/empty.yaml")`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::# comment\n\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Load empty file with no comment",
|
||||
expression: `load("../../examples/empty-no-comment.yaml")`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!null)::\n",
|
||||
},
|
||||
|
||||
@@ -4,6 +4,16 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var documentToPrune = `
|
||||
parentA: bob
|
||||
parentB:
|
||||
child1: i am child1
|
||||
child2: i am child2
|
||||
parentC:
|
||||
child1: me child1
|
||||
child2: me child2
|
||||
`
|
||||
|
||||
var pathOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Map path",
|
||||
@@ -78,6 +88,15 @@ var pathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], ()::a:\n b: things\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Set path to prune deep paths",
|
||||
subdescription: "Like pick but recursive. This uses `ireduce` to deeply set the selected paths into an empty object,",
|
||||
document: documentToPrune,
|
||||
expression: "(.parentB.child2, .parentC.child1) as $i\n ireduce({}; setpath($i | path; $i))",
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::parentB:\n child2: i am child2\nparentC:\n child1: me child1\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Set array path",
|
||||
document: `a: [cat, frog]`,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func getSliceNumber(d *dataTreeNavigator, context Context, node *CandidateNode, expressionNode *ExpressionNode) (int, error) {
|
||||
result, err := d.GetMatchingNodes(context.SingleChildContext(node), expressionNode)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if result.MatchingNodes.Len() != 1 {
|
||||
return 0, fmt.Errorf("expected to find 1 number, got %v instead", result.MatchingNodes.Len())
|
||||
}
|
||||
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Node.Value)
|
||||
}
|
||||
|
||||
func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
log.Debug("slice array operator!")
|
||||
log.Debug("lhs: %v", expressionNode.LHS.Operation.toString())
|
||||
log.Debug("rhs: %v", expressionNode.RHS.Operation.toString())
|
||||
|
||||
results := list.New()
|
||||
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
lhsNode := el.Value.(*CandidateNode)
|
||||
original := unwrapDoc(lhsNode.Node)
|
||||
|
||||
firstNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.LHS)
|
||||
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
relativeFirstNumber := firstNumber
|
||||
if relativeFirstNumber < 0 {
|
||||
relativeFirstNumber = len(original.Content) + firstNumber
|
||||
}
|
||||
|
||||
secondNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.RHS)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
relativeSecondNumber := secondNumber
|
||||
if relativeSecondNumber < 0 {
|
||||
relativeSecondNumber = len(original.Content) + secondNumber
|
||||
}
|
||||
|
||||
log.Debug("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
||||
|
||||
var newResults []*yaml.Node
|
||||
for i := relativeFirstNumber; i < relativeSecondNumber; i++ {
|
||||
newResults = append(newResults, original.Content[i])
|
||||
}
|
||||
|
||||
slicedArrayNode := &yaml.Node{
|
||||
Kind: yaml.SequenceNode,
|
||||
Tag: original.Tag,
|
||||
Content: newResults,
|
||||
}
|
||||
results.PushBack(lhsNode.CreateReplacement(slicedArrayNode))
|
||||
|
||||
}
|
||||
|
||||
// result is now the context that has the nodes we need to put back into a sequence.
|
||||
//what about multiple arrays in the context? I think we need to create an array for each one
|
||||
return context.ChildContext(results), nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package yqlib
|
||||
|
||||
import "testing"
|
||||
|
||||
var sliceArrayScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Slicing arrays",
|
||||
document: `[cat, dog, frog, cow]`,
|
||||
expression: `.[1:3]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- dog\n- frog\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Slicing arrays - without the first number",
|
||||
subdescription: "Starts from the start of the array",
|
||||
document: `[cat, dog, frog, cow]`,
|
||||
expression: `.[:2]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- cat\n- dog\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Slicing arrays - without the second number",
|
||||
subdescription: "Finishes at the end of the array",
|
||||
document: `[cat, dog, frog, cow]`,
|
||||
expression: `.[2:]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- frog\n- cow\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Slicing arrays - use negative numbers to count backwards from the end",
|
||||
document: `[cat, dog, frog, cow]`,
|
||||
expression: `.[1:-1]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- dog\n- frog\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Inserting into the middle of an array",
|
||||
subdescription: "using an expression to find the index",
|
||||
document: `[cat, dog, frog, cow]`,
|
||||
expression: `(.[] | select(. == "dog") | key + 1) as $pos | .[0:($pos)] + ["rabbit"] + .[$pos:]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- cat\n- dog\n- rabbit\n- frog\n- cow\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[[cat, dog, frog, cow], [apple, banana, grape, mango]]`,
|
||||
expression: `.[] | .[1:3]`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!seq)::- dog\n- frog\n",
|
||||
"D0, P[1], (!!seq)::- banana\n- grape\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[[cat, dog, frog, cow], [apple, banana, grape, mango]]`,
|
||||
expression: `.[] | .[-2:-1]`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!seq)::- frog\n",
|
||||
"D0, P[1], (!!seq)::- grape\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[cat1, cat2, cat3, cat4, cat5, cat6, cat7, cat8, cat9, cat10, cat11]`,
|
||||
expression: `.[10:11]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- cat11\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[cat1, cat2, cat3, cat4, cat5, cat6, cat7, cat8, cat9, cat10, cat11]`,
|
||||
expression: `.[-11:-10]`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- cat1\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSliceOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range sliceArrayScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "slice-array", sliceArrayScenarios)
|
||||
}
|
||||
+47
-13
@@ -6,6 +6,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -48,7 +49,7 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
|
||||
|
||||
log.Debug("going to compare %v by %v", NodeToString(candidate.CreateReplacement(originalNode)), NodeToString(candidate.CreateReplacement(nodeToCompare)))
|
||||
|
||||
sortableArray[i] = sortableNode{Node: originalNode, NodeToCompare: nodeToCompare}
|
||||
sortableArray[i] = sortableNode{Node: originalNode, NodeToCompare: nodeToCompare, dateTimeLayout: context.GetDateTimeLayout()}
|
||||
|
||||
if nodeToCompare.Kind != yaml.ScalarNode {
|
||||
return Context{}, fmt.Errorf("sort only works for scalars, got %v", nodeToCompare.Tag)
|
||||
@@ -70,8 +71,9 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
|
||||
}
|
||||
|
||||
type sortableNode struct {
|
||||
Node *yaml.Node
|
||||
NodeToCompare *yaml.Node
|
||||
Node *yaml.Node
|
||||
NodeToCompare *yaml.Node
|
||||
dateTimeLayout string
|
||||
}
|
||||
|
||||
type sortableNodeArray []sortableNode
|
||||
@@ -83,15 +85,37 @@ func (a sortableNodeArray) Less(i, j int) bool {
|
||||
lhs := a[i].NodeToCompare
|
||||
rhs := a[j].NodeToCompare
|
||||
|
||||
if lhs.Tag == "!!null" && rhs.Tag != "!!null" {
|
||||
lhsTag := lhs.Tag
|
||||
rhsTag := rhs.Tag
|
||||
|
||||
if !strings.HasPrefix(lhsTag, "!!") {
|
||||
// custom tag - we have to have a guess
|
||||
lhsTag = guessTagFromCustomType(lhs)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(rhsTag, "!!") {
|
||||
// custom tag - we have to have a guess
|
||||
rhsTag = guessTagFromCustomType(rhs)
|
||||
}
|
||||
|
||||
isDateTime := lhsTag == "!!timestamp" && rhsTag == "!!timestamp"
|
||||
layout := a[i].dateTimeLayout
|
||||
// if the lhs is a string, it might be a timestamp in a custom format.
|
||||
if lhsTag == "!!str" && layout != time.RFC3339 {
|
||||
_, errLhs := parseDateTime(layout, lhs.Value)
|
||||
_, errRhs := parseDateTime(layout, rhs.Value)
|
||||
isDateTime = errLhs == nil && errRhs == nil
|
||||
}
|
||||
|
||||
if lhsTag == "!!null" && rhsTag != "!!null" {
|
||||
return true
|
||||
} else if lhs.Tag != "!!null" && rhs.Tag == "!!null" {
|
||||
} else if lhsTag != "!!null" && rhsTag == "!!null" {
|
||||
return false
|
||||
} else if lhs.Tag == "!!bool" && rhs.Tag != "!!bool" {
|
||||
} else if lhsTag == "!!bool" && rhsTag != "!!bool" {
|
||||
return true
|
||||
} else if lhs.Tag != "!!bool" && rhs.Tag == "!!bool" {
|
||||
} else if lhsTag != "!!bool" && rhsTag == "!!bool" {
|
||||
return false
|
||||
} else if lhs.Tag == "!!bool" && rhs.Tag == "!!bool" {
|
||||
} else if lhsTag == "!!bool" && rhsTag == "!!bool" {
|
||||
lhsTruthy, err := isTruthyNode(lhs)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not parse %v as boolean: %w", lhs.Value, err))
|
||||
@@ -103,9 +127,19 @@ func (a sortableNodeArray) Less(i, j int) bool {
|
||||
}
|
||||
|
||||
return !lhsTruthy && rhsTruthy
|
||||
} else if lhs.Tag != rhs.Tag || lhs.Tag == "!!str" {
|
||||
return strings.Compare(lhs.Value, rhs.Value) < 0
|
||||
} else if lhs.Tag == "!!int" && rhs.Tag == "!!int" {
|
||||
} else if isDateTime {
|
||||
lhsTime, err := parseDateTime(layout, lhs.Value)
|
||||
if err != nil {
|
||||
log.Warningf("Could not parse time %v with layout %v for sort, sorting by string instead: %w", lhs.Value, layout, err)
|
||||
return strings.Compare(lhs.Value, rhs.Value) < 0
|
||||
}
|
||||
rhsTime, err := parseDateTime(layout, rhs.Value)
|
||||
if err != nil {
|
||||
log.Warningf("Could not parse time %v with layout %v for sort, sorting by string instead: %w", rhs.Value, layout, err)
|
||||
return strings.Compare(lhs.Value, rhs.Value) < 0
|
||||
}
|
||||
return lhsTime.Before(rhsTime)
|
||||
} else if lhsTag == "!!int" && rhsTag == "!!int" {
|
||||
_, lhsNum, err := parseInt64(lhs.Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -115,7 +149,7 @@ func (a sortableNodeArray) Less(i, j int) bool {
|
||||
panic(err)
|
||||
}
|
||||
return lhsNum < rhsNum
|
||||
} else if (lhs.Tag == "!!int" || lhs.Tag == "!!float") && (rhs.Tag == "!!int" || rhs.Tag == "!!float") {
|
||||
} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
|
||||
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -127,5 +161,5 @@ func (a sortableNodeArray) Less(i, j int) bool {
|
||||
return lhsNum < rhsNum
|
||||
}
|
||||
|
||||
return true
|
||||
return strings.Compare(lhs.Value, rhs.Value) < 0
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ var sortByOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!seq)::[{a: 1}, {a: 10}, {a: 100}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Sort by custom date field",
|
||||
document: `[{a: "12-Jun-2011"},{a: "23-Dec-2010"},{a: "10-Aug-2011"}]`,
|
||||
expression: `with_dtf("02-Jan-2006"; sort_by(.a))`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::[{a: \"23-Dec-2010\"}, {a: \"12-Jun-2011\"}, {a: \"10-Aug-2011\"}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "[{a: 1.1},{a: 1.001},{a: 1.01}]",
|
||||
|
||||
@@ -92,10 +92,10 @@ func subtractScalars(context Context, target *CandidateNode, lhs *yaml.Node, rhs
|
||||
rhsTag = guessTagFromCustomType(rhs)
|
||||
}
|
||||
|
||||
isDateTime := lhs.Tag == "!!timestamp"
|
||||
isDateTime := lhsTag == "!!timestamp"
|
||||
// if the lhs is a string, it might be a timestamp in a custom format.
|
||||
if lhsTag == "!!str" && context.GetDateTimeLayout() != time.RFC3339 {
|
||||
_, err := time.Parse(context.GetDateTimeLayout(), lhs.Value)
|
||||
_, err := parseDateTime(context.GetDateTimeLayout(), lhs.Value)
|
||||
isDateTime = err == nil
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func subtractDateTime(layout string, target *CandidateNode, lhs *yaml.Node, rhs
|
||||
return fmt.Errorf("unable to parse duration [%v]: %w", rhs.Value, err)
|
||||
}
|
||||
|
||||
currentTime, err := time.Parse(layout, lhs.Value)
|
||||
currentTime, err := parseDateTime(layout, lhs.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,6 +102,15 @@ var subtractOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (doc)::a: 2021-01-01T00:00:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date subtraction - only date",
|
||||
skipDoc: true,
|
||||
document: `a: 2021-01-01`,
|
||||
expression: `.a -= "24h"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 2020-12-31T00:00:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date subtraction - custom format",
|
||||
subdescription: "Use with_dtf to specify your datetime format. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
|
||||
@@ -76,11 +76,14 @@ func traverse(context Context, matchingNode *CandidateNode, operation *Operation
|
||||
}
|
||||
|
||||
func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
//lhs may update the variable context, we should pass that into the RHS
|
||||
// BUT we still return the original context back (see jq)
|
||||
// https://stedolan.github.io/jq/manual/#Variable/SymbolicBindingOperator:...as$identifier|...
|
||||
|
||||
if expressionNode.RHS != nil && expressionNode.RHS.RHS != nil && expressionNode.RHS.RHS.Operation.OperationType == createMapOpType {
|
||||
return sliceArrayOperator(d, context, expressionNode.RHS.RHS)
|
||||
}
|
||||
|
||||
lhs, err := d.GetMatchingNodes(context, expressionNode.LHS)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
|
||||
+12
-17
@@ -40,21 +40,16 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func NewSimpleYamlPrinter(writer io.Writer, outputFormat PrinterOutputFormat, unwrapScalar bool, colorsEnabled bool, indent int, printDocSeparators bool) Printer {
|
||||
return NewPrinter(NewYamlEncoder(indent, colorsEnabled, printDocSeparators, unwrapScalar), NewSinglePrinterWriter(writer))
|
||||
prefs := NewDefaultYamlPreferences()
|
||||
prefs.PrintDocSeparators = printDocSeparators
|
||||
prefs.UnwrapScalar = unwrapScalar
|
||||
return NewPrinter(NewYamlEncoder(indent, colorsEnabled, prefs), NewSinglePrinterWriter(writer))
|
||||
}
|
||||
|
||||
func readDocumentWithLeadingContent(content string, fakefilename string, fakeFileIndex int) (*list.List, error) {
|
||||
reader, firstFileLeadingContent, err := processReadStream(bufio.NewReader(strings.NewReader(content)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func readDocument(content string, fakefilename string, fakeFileIndex int) (*list.List, error) {
|
||||
reader := bufio.NewReader(strings.NewReader(content))
|
||||
|
||||
inputs, err := readDocuments(reader, fakefilename, fakeFileIndex, NewYamlDecoder())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputs.Front().Value.(*CandidateNode).LeadingContent = firstFileLeadingContent
|
||||
return inputs, nil
|
||||
return readDocuments(reader, fakefilename, fakeFileIndex, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
}
|
||||
|
||||
func testScenario(t *testing.T, s *expressionScenario) {
|
||||
@@ -67,7 +62,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
|
||||
inputs := list.New()
|
||||
|
||||
if s.document != "" {
|
||||
inputs, err = readDocumentWithLeadingContent(s.document, "sample.yml", 0)
|
||||
inputs, err = readDocument(s.document, "sample.yml", 0)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err, s.document, s.expression)
|
||||
@@ -75,7 +70,7 @@ func testScenario(t *testing.T, s *expressionScenario) {
|
||||
}
|
||||
|
||||
if s.document2 != "" {
|
||||
moreInputs, err := readDocumentWithLeadingContent(s.document2, "another.yml", 1)
|
||||
moreInputs, err := readDocument(s.document2, "another.yml", 1)
|
||||
if err != nil {
|
||||
t.Error(err, s.document2, s.expression)
|
||||
return
|
||||
@@ -176,7 +171,7 @@ func formatYaml(yaml string, filename string) string {
|
||||
panic(err)
|
||||
}
|
||||
streamEvaluator := NewStreamEvaluator()
|
||||
_, err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer, "", NewYamlDecoder())
|
||||
_, err = streamEvaluator.Evaluate(filename, strings.NewReader(yaml), node, printer, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -322,13 +317,13 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
|
||||
|
||||
if s.document != "" {
|
||||
|
||||
inputs, err = readDocumentWithLeadingContent(formattedDoc, "sample.yml", 0)
|
||||
inputs, err = readDocument(formattedDoc, "sample.yml", 0)
|
||||
if err != nil {
|
||||
t.Error(err, s.document, s.expression)
|
||||
return
|
||||
}
|
||||
if s.document2 != "" {
|
||||
moreInputs, err := readDocumentWithLeadingContent(formattedDoc2, "another.yml", 1)
|
||||
moreInputs, err := readDocument(formattedDoc2, "another.yml", 1)
|
||||
if err != nil {
|
||||
t.Error(err, s.document, s.expression)
|
||||
return
|
||||
|
||||
@@ -111,7 +111,7 @@ func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error {
|
||||
|
||||
mappedDoc := el.Value.(*CandidateNode)
|
||||
log.Debug("-- print sep logic: p.firstTimePrinting: %v, previousDocIndex: %v, mappedDoc.Document: %v", p.firstTimePrinting, p.previousDocIndex, mappedDoc.Document)
|
||||
|
||||
log.Debug("%v", NodeToString(mappedDoc))
|
||||
writer, errorWriting := p.printerWriter.GetWriter(mappedDoc)
|
||||
if errorWriting != nil {
|
||||
return errorWriting
|
||||
|
||||
+10
-10
@@ -38,7 +38,7 @@ func TestPrinterMultipleDocsInSequenceOnly(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func TestPrinterMultipleDocsInSequenceWithLeadingContent(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestPrinterMultipleFilesInSequence(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func TestPrinterMultipleFilesInSequenceWithLeadingContent(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func TestPrinterMultipleDocsInSinglePrint(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func TestPrinterMultipleDocsInSinglePrintWithLeadingDoc(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func TestPrinterMultipleDocsInSinglePrintWithLeadingDocTrailing(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, YamlOutputFormat, true, false, 2, true)
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func TestPrinterScalarWithLeadingCont(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
streamEvaluator := NewStreamEvaluator()
|
||||
_, err = streamEvaluator.Evaluate("sample", strings.NewReader(multiDocSample), node, printer, "# blah\n", NewYamlDecoder())
|
||||
_, err = streamEvaluator.Evaluate("sample", strings.NewReader(multiDocSample), node, printer, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -314,9 +314,9 @@ func TestPrinterMultipleDocsJson(t *testing.T) {
|
||||
var writer = bufio.NewWriter(&output)
|
||||
// note printDocSeparators is true, it should still not print document separators
|
||||
// when outputing JSON.
|
||||
printer := NewPrinter(NewJSONEncoder(0, false), NewSinglePrinterWriter(writer))
|
||||
printer := NewPrinter(NewJSONEncoder(0, false, false), NewSinglePrinterWriter(writer))
|
||||
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder())
|
||||
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+100
-15
@@ -8,7 +8,49 @@ import (
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
)
|
||||
|
||||
const samplePropertiesYaml = `# block comments don't come through
|
||||
const propertiesWithCommentsOnMap = `this.thing = hi hi
|
||||
# important notes
|
||||
# about this value
|
||||
this.value = cool
|
||||
`
|
||||
|
||||
const expectedPropertiesWithCommentsOnMapProps = `this.thing = hi hi
|
||||
|
||||
# important notes
|
||||
# about this value
|
||||
this.value = cool
|
||||
`
|
||||
|
||||
const expectedPropertiesWithCommentsOnMapYaml = `this:
|
||||
thing: hi hi
|
||||
# important notes
|
||||
# about this value
|
||||
value: cool
|
||||
`
|
||||
|
||||
const propertiesWithCommentInArray = `
|
||||
this.array.0 = cat
|
||||
# important notes
|
||||
# about dogs
|
||||
this.array.1 = dog
|
||||
`
|
||||
|
||||
const expectedPropertiesWithCommentInArrayProps = `this.array.0 = cat
|
||||
|
||||
# important notes
|
||||
# about dogs
|
||||
this.array.1 = dog
|
||||
`
|
||||
|
||||
const expectedPropertiesWithCommentInArrayYaml = `this:
|
||||
array:
|
||||
- cat
|
||||
# important notes
|
||||
# about dogs
|
||||
- dog
|
||||
`
|
||||
|
||||
const samplePropertiesYaml = `# block comments come through
|
||||
person: # neither do comments on maps
|
||||
name: Mike Wazowski # comments on values appear
|
||||
pets:
|
||||
@@ -18,7 +60,8 @@ emptyArray: []
|
||||
emptyMap: []
|
||||
`
|
||||
|
||||
const expectedPropertiesUnwrapped = `# comments on values appear
|
||||
const expectedPropertiesUnwrapped = `# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
# comments on array values appear
|
||||
@@ -26,7 +69,8 @@ person.pets.0 = cat
|
||||
person.food.0 = pizza
|
||||
`
|
||||
|
||||
const expectedPropertiesWrapped = `# comments on values appear
|
||||
const expectedPropertiesWrapped = `# block comments come through
|
||||
# comments on values appear
|
||||
person.name = "Mike Wazowski"
|
||||
|
||||
# comments on array values appear
|
||||
@@ -34,7 +78,8 @@ person.pets.0 = cat
|
||||
person.food.0 = pizza
|
||||
`
|
||||
|
||||
const expectedUpdatedProperties = `# comments on values appear
|
||||
const expectedUpdatedProperties = `# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
# comments on array values appear
|
||||
@@ -43,9 +88,12 @@ person.food.0 = pizza
|
||||
`
|
||||
|
||||
const expectedDecodedYaml = `person:
|
||||
name: Mike Wazowski # comments on values appear
|
||||
# block comments come through
|
||||
# comments on values appear
|
||||
name: Mike Wazowski
|
||||
pets:
|
||||
- cat # comments on array values appear
|
||||
# comments on array values appear
|
||||
- cat
|
||||
food:
|
||||
- pizza
|
||||
`
|
||||
@@ -55,7 +103,8 @@ person.pets.0 = cat
|
||||
person.food.0 = pizza
|
||||
`
|
||||
|
||||
const expectedPropertiesWithEmptyMapsAndArrays = `# comments on values appear
|
||||
const expectedPropertiesWithEmptyMapsAndArrays = `# block comments come through
|
||||
# comments on values appear
|
||||
person.name = Mike Wazowski
|
||||
|
||||
# comments on array values appear
|
||||
@@ -98,6 +147,14 @@ var propertyScenarios = []formatScenario{
|
||||
expected: expectedDecodedYaml,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Decode properties - array should be a map",
|
||||
subdescription: "If you have a numeric map key in your property files, use array_to_map to convert them to maps.",
|
||||
input: `things.10 = mike`,
|
||||
expression: `.things |= array_to_map`,
|
||||
expected: "things:\n 10: mike\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "does not expand automatically",
|
||||
skipDoc: true,
|
||||
@@ -112,6 +169,34 @@ var propertyScenarios = []formatScenario{
|
||||
expected: expectedUpdatedProperties,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "comments on arrays roundtrip",
|
||||
input: propertiesWithCommentInArray,
|
||||
expected: expectedPropertiesWithCommentInArrayProps,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "comments on arrays decode",
|
||||
input: propertiesWithCommentInArray,
|
||||
expected: expectedPropertiesWithCommentInArrayYaml,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "comments on map roundtrip",
|
||||
input: propertiesWithCommentsOnMap,
|
||||
expected: expectedPropertiesWithCommentsOnMapProps,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "comments on map decode",
|
||||
input: propertiesWithCommentsOnMap,
|
||||
expected: expectedPropertiesWithCommentsOnMapYaml,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Empty doc",
|
||||
skipDoc: true,
|
||||
@@ -143,7 +228,7 @@ func documentUnwrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario)
|
||||
}
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder(true))))
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true))))
|
||||
}
|
||||
|
||||
func documentWrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -168,7 +253,7 @@ func documentWrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
}
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder(false))))
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false))))
|
||||
}
|
||||
|
||||
func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -193,7 +278,7 @@ func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(s.indent, false, true, true))))
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(s.indent, false, ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func documentRoundTripPropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -218,7 +303,7 @@ func documentRoundTripPropertyScenario(w *bufio.Writer, s formatScenario) {
|
||||
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true))))
|
||||
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true))))
|
||||
}
|
||||
|
||||
func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
|
||||
@@ -245,13 +330,13 @@ func TestPropertyScenarios(t *testing.T) {
|
||||
for _, s := range propertyScenarios {
|
||||
switch s.scenarioType {
|
||||
case "":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder(true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true)), s.description)
|
||||
case "decode":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "encode-wrapped":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewPropertiesEncoder(false)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false)), s.description)
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true)), s.description)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
// Uses less memory than loading all documents and running the expression once, but this cannot process
|
||||
// cross document expressions.
|
||||
type StreamEvaluator interface {
|
||||
Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string, decoder Decoder) (uint, error)
|
||||
EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error
|
||||
EvaluateNew(expression string, printer Printer, leadingContent string) error
|
||||
Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, decoder Decoder) (uint, error)
|
||||
EvaluateFiles(expression string, filenames []string, printer Printer, decoder Decoder) error
|
||||
EvaluateNew(expression string, printer Printer) error
|
||||
}
|
||||
|
||||
type streamEvaluator struct {
|
||||
@@ -28,17 +28,16 @@ func NewStreamEvaluator() StreamEvaluator {
|
||||
return &streamEvaluator{treeNavigator: NewDataTreeNavigator()}
|
||||
}
|
||||
|
||||
func (s *streamEvaluator) EvaluateNew(expression string, printer Printer, leadingContent string) error {
|
||||
func (s *streamEvaluator) EvaluateNew(expression string, printer Printer) error {
|
||||
node, err := ExpressionParser.ParseExpression(expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
candidateNode := &CandidateNode{
|
||||
Document: 0,
|
||||
Filename: "",
|
||||
Node: &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Tag: "!!null", Kind: yaml.ScalarNode}}},
|
||||
FileIndex: 0,
|
||||
LeadingContent: leadingContent,
|
||||
Document: 0,
|
||||
Filename: "",
|
||||
Node: &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Tag: "!!null", Kind: yaml.ScalarNode}}},
|
||||
FileIndex: 0,
|
||||
}
|
||||
inputList := list.New()
|
||||
inputList.PushBack(candidateNode)
|
||||
@@ -50,27 +49,20 @@ func (s *streamEvaluator) EvaluateNew(expression string, printer Printer, leadin
|
||||
return printer.PrintResults(result.MatchingNodes)
|
||||
}
|
||||
|
||||
func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, leadingContentPreProcessing bool, decoder Decoder) error {
|
||||
func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer, decoder Decoder) error {
|
||||
var totalProcessDocs uint
|
||||
node, err := ExpressionParser.ParseExpression(expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var firstFileLeadingContent string
|
||||
|
||||
for index, filename := range filenames {
|
||||
reader, leadingContent, err := readStream(filename, leadingContentPreProcessing)
|
||||
log.Debug("leadingContent: %v", leadingContent)
|
||||
|
||||
if index == 0 {
|
||||
firstFileLeadingContent = leadingContent
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
reader, err := readStream(filename)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
processedDocs, err := s.Evaluate(filename, reader, node, printer, leadingContent, decoder)
|
||||
processedDocs, err := s.Evaluate(filename, reader, node, printer, decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,19 +75,22 @@ func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, p
|
||||
}
|
||||
|
||||
if totalProcessDocs == 0 {
|
||||
return s.EvaluateNew(expression, printer, firstFileLeadingContent)
|
||||
// problem is I've already slurped the leading content sadface
|
||||
return s.EvaluateNew(expression, printer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, leadingContent string, decoder Decoder) (uint, error) {
|
||||
func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *ExpressionNode, printer Printer, decoder Decoder) (uint, error) {
|
||||
|
||||
var currentIndex uint
|
||||
decoder.Init(reader)
|
||||
err := decoder.Init(reader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for {
|
||||
var dataBucket yaml.Node
|
||||
errorReading := decoder.Decode(&dataBucket)
|
||||
candidateNode, errorReading := decoder.Decode()
|
||||
|
||||
if errors.Is(errorReading, io.EOF) {
|
||||
s.fileIndex = s.fileIndex + 1
|
||||
@@ -103,21 +98,10 @@ func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *Expr
|
||||
} else if errorReading != nil {
|
||||
return currentIndex, fmt.Errorf("bad file '%v': %w", filename, errorReading)
|
||||
}
|
||||
candidateNode.Document = currentIndex
|
||||
candidateNode.Filename = filename
|
||||
candidateNode.FileIndex = s.fileIndex
|
||||
|
||||
candidateNode := &CandidateNode{
|
||||
Document: currentIndex,
|
||||
Filename: filename,
|
||||
Node: &dataBucket,
|
||||
FileIndex: s.fileIndex,
|
||||
}
|
||||
// move document comments into candidate node
|
||||
// otherwise unwrap drops them.
|
||||
candidateNode.TrailingContent = dataBucket.FootComment
|
||||
dataBucket.FootComment = ""
|
||||
|
||||
if currentIndex == 0 {
|
||||
candidateNode.LeadingContent = leadingContent
|
||||
}
|
||||
inputList := list.New()
|
||||
inputList.PushBack(candidateNode)
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StringEvaluator interface {
|
||||
Evaluate(expression string, input string, encoder Encoder, leadingContentPreProcessing bool, decoder Decoder) (string, error)
|
||||
Evaluate(expression string, input string, encoder Encoder, decoder Decoder) (string, error)
|
||||
}
|
||||
|
||||
type stringEvaluator struct {
|
||||
@@ -25,7 +25,7 @@ func NewStringEvaluator() StringEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringEvaluator) Evaluate(expression string, input string, encoder Encoder, leadingContentPreProcessing bool, decoder Decoder) (string, error) {
|
||||
func (s *stringEvaluator) Evaluate(expression string, input string, encoder Encoder, decoder Decoder) (string, error) {
|
||||
|
||||
// Use bytes.Buffer for output of string
|
||||
out := new(bytes.Buffer)
|
||||
@@ -37,16 +37,15 @@ func (s *stringEvaluator) Evaluate(expression string, input string, encoder Enco
|
||||
return "", err
|
||||
}
|
||||
|
||||
reader, leadingContent, err := readString(input, leadingContentPreProcessing)
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
var currentIndex uint
|
||||
err = decoder.Init(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var currentIndex uint
|
||||
decoder.Init(reader)
|
||||
for {
|
||||
var dataBucket yaml.Node
|
||||
errorReading := decoder.Decode(&dataBucket)
|
||||
candidateNode, errorReading := decoder.Decode()
|
||||
|
||||
if errors.Is(errorReading, io.EOF) {
|
||||
s.fileIndex = s.fileIndex + 1
|
||||
@@ -54,20 +53,9 @@ func (s *stringEvaluator) Evaluate(expression string, input string, encoder Enco
|
||||
} else if errorReading != nil {
|
||||
return "", fmt.Errorf("bad input '%v': %w", input, errorReading)
|
||||
}
|
||||
candidateNode.Document = currentIndex
|
||||
candidateNode.FileIndex = s.fileIndex
|
||||
|
||||
candidateNode := &CandidateNode{
|
||||
Document: currentIndex,
|
||||
Node: &dataBucket,
|
||||
FileIndex: s.fileIndex,
|
||||
}
|
||||
// move document comments into candidate node
|
||||
// otherwise unwrap drops them.
|
||||
candidateNode.TrailingContent = dataBucket.FootComment
|
||||
dataBucket.FootComment = ""
|
||||
|
||||
if currentIndex == 0 {
|
||||
candidateNode.LeadingContent = leadingContent
|
||||
}
|
||||
inputList := list.New()
|
||||
inputList.PushBack(candidateNode)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ func TestStringEvaluator_Evaluate_Nominal(t *testing.T) {
|
||||
`---` + "\n" +
|
||||
` - name: jq` + "\n" +
|
||||
` description: Command-line JSON processor` + "\n"
|
||||
encoder := NewYamlEncoder(2, true, true, true)
|
||||
decoder := NewYamlDecoder()
|
||||
encoder := NewYamlEncoder(2, true, ConfiguredYamlPreferences)
|
||||
decoder := NewYamlDecoder(ConfiguredYamlPreferences)
|
||||
|
||||
result, err := NewStringEvaluator().Evaluate(expression, input, encoder, true, decoder)
|
||||
result, err := NewStringEvaluator().Evaluate(expression, input, encoder, decoder)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
+12
-65
@@ -7,13 +7,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func readStream(filename string, leadingContentPreProcessing bool) (io.Reader, string, error) {
|
||||
func readStream(filename string) (io.Reader, error) {
|
||||
var reader *bufio.Reader
|
||||
if filename == "-" {
|
||||
reader = bufio.NewReader(os.Stdin)
|
||||
@@ -22,23 +18,12 @@ func readStream(filename string, leadingContentPreProcessing bool) (io.Reader, s
|
||||
// and ensuring that it's not possible to give a path to a file outside thar directory.
|
||||
file, err := os.Open(filename) // #nosec
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
reader = bufio.NewReader(file)
|
||||
}
|
||||
return reader, nil
|
||||
|
||||
if !leadingContentPreProcessing {
|
||||
return reader, "", nil
|
||||
}
|
||||
return processReadStream(reader)
|
||||
}
|
||||
|
||||
func readString(input string, leadingContentPreProcessing bool) (io.Reader, string, error) {
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
if !leadingContentPreProcessing {
|
||||
return reader, "", nil
|
||||
}
|
||||
return processReadStream(reader)
|
||||
}
|
||||
|
||||
func writeString(writer io.Writer, txt string) error {
|
||||
@@ -46,46 +31,16 @@ func writeString(writer io.Writer, txt string) error {
|
||||
return errorWriting
|
||||
}
|
||||
|
||||
func processReadStream(reader *bufio.Reader) (io.Reader, string, error) {
|
||||
var commentLineRegEx = regexp.MustCompile(`^\s*#`)
|
||||
var sb strings.Builder
|
||||
for {
|
||||
peekBytes, err := reader.Peek(3)
|
||||
if errors.Is(err, io.EOF) {
|
||||
// EOF are handled else where..
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
} else if string(peekBytes) == "---" {
|
||||
_, err := reader.ReadString('\n')
|
||||
sb.WriteString("$yqDocSeperator$\n")
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else if commentLineRegEx.MatchString(string(peekBytes)) {
|
||||
line, err := reader.ReadString('\n')
|
||||
sb.WriteString(line)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return reader, sb.String(), nil
|
||||
} else if err != nil {
|
||||
return reader, sb.String(), err
|
||||
}
|
||||
} else {
|
||||
return reader, sb.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readDocuments(reader io.Reader, filename string, fileIndex int, decoder Decoder) (*list.List, error) {
|
||||
decoder.Init(reader)
|
||||
err := decoder.Init(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputList := list.New()
|
||||
var currentIndex uint
|
||||
|
||||
for {
|
||||
var dataBucket yaml.Node
|
||||
errorReading := decoder.Decode(&dataBucket)
|
||||
candidateNode, errorReading := decoder.Decode()
|
||||
|
||||
if errors.Is(errorReading, io.EOF) {
|
||||
switch reader := reader.(type) {
|
||||
@@ -96,18 +51,10 @@ func readDocuments(reader io.Reader, filename string, fileIndex int, decoder Dec
|
||||
} else if errorReading != nil {
|
||||
return nil, fmt.Errorf("bad file '%v': %w", filename, errorReading)
|
||||
}
|
||||
candidateNode := &CandidateNode{
|
||||
Document: currentIndex,
|
||||
Filename: filename,
|
||||
Node: &dataBucket,
|
||||
FileIndex: fileIndex,
|
||||
EvaluateTogether: true,
|
||||
}
|
||||
|
||||
//move document comments into candidate node
|
||||
// otherwise unwrap drops them.
|
||||
candidateNode.TrailingContent = dataBucket.FootComment
|
||||
dataBucket.FootComment = ""
|
||||
candidateNode.Document = currentIndex
|
||||
candidateNode.Filename = filename
|
||||
candidateNode.FileIndex = fileIndex
|
||||
candidateNode.EvaluateTogether = true
|
||||
|
||||
inputList.PushBack(candidateNode)
|
||||
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ type XmlPreferences struct {
|
||||
|
||||
func NewDefaultXmlPreferences() XmlPreferences {
|
||||
return XmlPreferences{
|
||||
AttributePrefix: "+",
|
||||
AttributePrefix: "+@",
|
||||
ContentName: "+content",
|
||||
StrictMode: false,
|
||||
KeepNamespace: true,
|
||||
UseRawToken: false,
|
||||
UseRawToken: true,
|
||||
ProcInstPrefix: "+p_",
|
||||
DirectiveName: "+directive",
|
||||
SkipProcInst: false,
|
||||
|
||||
+105
-47
@@ -58,7 +58,7 @@ cat:
|
||||
d:
|
||||
# in d before
|
||||
z:
|
||||
+sweet: cool
|
||||
+@sweet: cool
|
||||
# in d after
|
||||
# in y after
|
||||
# in_cat_after
|
||||
@@ -98,11 +98,11 @@ cat:
|
||||
d:
|
||||
- # in d before
|
||||
z:
|
||||
+sweet: cool
|
||||
+@sweet: cool
|
||||
# in d after
|
||||
- # in d2 before
|
||||
z:
|
||||
+sweet: cool2
|
||||
+@sweet: cool2
|
||||
# in d2 after
|
||||
# in y after
|
||||
# in_cat_after
|
||||
@@ -137,7 +137,8 @@ in d before -->
|
||||
</cat><!-- after cat -->
|
||||
`
|
||||
|
||||
const yamlWithComments = `# above_cat
|
||||
const yamlWithComments = `# header comment
|
||||
# above_cat
|
||||
cat: # inline_cat
|
||||
# above_array
|
||||
array: # inline_array
|
||||
@@ -147,30 +148,38 @@ cat: # inline_cat
|
||||
# below_cat
|
||||
`
|
||||
|
||||
const expectedXMLWithComments = `<!-- above_cat inline_cat --><cat><!-- above_array inline_array -->
|
||||
const expectedXMLWithComments = `<!--
|
||||
header comment
|
||||
above_cat
|
||||
--><!-- inline_cat --><cat><!-- above_array inline_array -->
|
||||
<array>val1<!-- inline_val1 --></array>
|
||||
<array><!-- above_val2 -->val2<!-- inline_val2 --></array>
|
||||
</cat><!-- below_cat -->
|
||||
`
|
||||
|
||||
const inputXMLWithNamespacedAttr = `
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
</map>
|
||||
const inputXMLWithNamespacedAttr = `<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
`
|
||||
|
||||
const expectedYAMLWithNamespacedAttr = `+p_xml: version="1.0"
|
||||
map:
|
||||
+xmlns: some-namespace
|
||||
+xmlns:xsi: some-instance
|
||||
+some-instance:schemaLocation: some-url
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@xsi:schemaLocation: some-url
|
||||
`
|
||||
|
||||
const expectedYAMLWithRawNamespacedAttr = `+p_xml: version="1.0"
|
||||
map:
|
||||
+xmlns: some-namespace
|
||||
+xmlns:xsi: some-instance
|
||||
+xsi:schemaLocation: some-url
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@xsi:schemaLocation: some-url
|
||||
`
|
||||
|
||||
const expectedYAMLWithoutRawNamespacedAttr = `+p_xml: version="1.0"
|
||||
map:
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@some-instance:schemaLocation: some-url
|
||||
`
|
||||
|
||||
const xmlWithCustomDtd = `
|
||||
@@ -247,13 +256,13 @@ var xmlScenarios = []formatScenario{
|
||||
description: "Parse xml: attributes",
|
||||
subdescription: "Attributes are converted to fields, with the default attribute prefix '+'. Use '--xml-attribute-prefix` to set your own.",
|
||||
input: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat legs=\"4\">\n <legs>7</legs>\n</cat>",
|
||||
expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +legs: \"4\"\n legs: \"7\"\n",
|
||||
expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +@legs: \"4\"\n legs: \"7\"\n",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: attributes with content",
|
||||
subdescription: "Content is added as a field, using the default content name of `+content`. Use `--xml-content-name` to set your own.",
|
||||
input: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<cat legs=\"4\">meow</cat>",
|
||||
expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +content: meow\n +legs: \"4\"\n",
|
||||
expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +content: meow\n +@legs: \"4\"\n",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: custom dtd",
|
||||
@@ -262,6 +271,13 @@ var xmlScenarios = []formatScenario{
|
||||
expected: expectedDtd,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip with name spaced attributes",
|
||||
skipDoc: true,
|
||||
input: inputXMLWithNamespacedAttr,
|
||||
expected: inputXMLWithNamespacedAttr,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: skip custom dtd",
|
||||
subdescription: "DTDs are directives, skip over directives to skip DTDs.",
|
||||
@@ -319,19 +335,28 @@ var xmlScenarios = []formatScenario{
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: keep attribute namespace",
|
||||
skipDoc: false,
|
||||
input: inputXMLWithNamespacedAttr,
|
||||
expected: expectedYAMLWithNamespacedAttr,
|
||||
scenarioType: "decode-keep-ns",
|
||||
description: "Parse xml: keep attribute namespace",
|
||||
subdescription: fmt.Sprintf(`Defaults to %v`, ConfiguredXMLPreferences.KeepNamespace),
|
||||
skipDoc: false,
|
||||
input: inputXMLWithNamespacedAttr,
|
||||
expected: expectedYAMLWithNamespacedAttr,
|
||||
scenarioType: "decode-keep-ns",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: keep raw attribute namespace",
|
||||
skipDoc: false,
|
||||
skipDoc: true,
|
||||
input: inputXMLWithNamespacedAttr,
|
||||
expected: expectedYAMLWithRawNamespacedAttr,
|
||||
scenarioType: "decode-raw-token",
|
||||
},
|
||||
{
|
||||
description: "Parse xml: keep raw attribute namespace",
|
||||
subdescription: fmt.Sprintf(`Defaults to %v`, ConfiguredXMLPreferences.UseRawToken),
|
||||
skipDoc: false,
|
||||
input: inputXMLWithNamespacedAttr,
|
||||
expected: expectedYAMLWithoutRawNamespacedAttr,
|
||||
scenarioType: "decode-raw-token-off",
|
||||
},
|
||||
{
|
||||
description: "Encode xml: simple",
|
||||
input: "cat: purrs",
|
||||
@@ -363,21 +388,42 @@ var xmlScenarios = []formatScenario{
|
||||
{
|
||||
description: "Encode xml: attributes",
|
||||
subdescription: "Fields with the matching xml-attribute-prefix are assumed to be attributes.",
|
||||
input: "cat:\n +name: tiger\n meows: true\n",
|
||||
input: "cat:\n +@name: tiger\n meows: true\n",
|
||||
expected: "<cat name=\"tiger\">\n <meows>true</meows>\n</cat>\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "double prefix",
|
||||
skipDoc: true,
|
||||
input: "cat:\n ++@name: tiger\n meows: true\n",
|
||||
input: "cat:\n +@+@name: tiger\n meows: true\n",
|
||||
expected: "<cat +@name=\"tiger\">\n <meows>true</meows>\n</cat>\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "arrays cannot be encoded",
|
||||
skipDoc: true,
|
||||
input: "[cat, dog, fish]",
|
||||
expectedError: "cannot encode !!seq to XML - only maps can be encoded",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "arrays cannot be encoded - 2",
|
||||
skipDoc: true,
|
||||
input: "[cat, dog]",
|
||||
expectedError: "cannot encode !!seq to XML - only maps can be encoded",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "scalars cannot be encoded",
|
||||
skipDoc: true,
|
||||
input: "mike",
|
||||
expectedError: "cannot encode !!str to XML - only maps can be encoded",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "Encode xml: attributes with content",
|
||||
subdescription: "Fields with the matching xml-content-name is assumed to be content.",
|
||||
input: "cat:\n +name: tiger\n +content: cool\n",
|
||||
input: "cat:\n +@name: tiger\n +content: cool\n",
|
||||
expected: "<cat name=\"tiger\">cool</cat>\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
@@ -414,23 +460,35 @@ var xmlScenarios = []formatScenario{
|
||||
func testXMLScenario(t *testing.T, s formatScenario) {
|
||||
switch s.scenarioType {
|
||||
case "", "decode":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(4, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(4, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "encode":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlDecoder(), NewXMLEncoder(2, ConfiguredXMLPreferences)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences)), s.description)
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences)), s.description)
|
||||
case "decode-keep-ns":
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.KeepNamespace = true
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "decode-raw-token":
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.UseRawToken = true
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(2, false, true, true)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "decode-raw-token-off":
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.UseRawToken = false
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
|
||||
case "roundtrip-skip-directives":
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.SkipDirectives = true
|
||||
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs)), s.description)
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs)), s.description)
|
||||
case "encode-error":
|
||||
result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(2, NewDefaultXmlPreferences()))
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
|
||||
} else {
|
||||
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
@@ -451,7 +509,7 @@ func documentXMLScenario(t *testing.T, w *bufio.Writer, i interface{}) {
|
||||
documentXMLRoundTripScenario(w, s)
|
||||
case "decode-keep-ns":
|
||||
documentXMLDecodeKeepNsScenario(w, s)
|
||||
case "decode-raw-token":
|
||||
case "decode-raw-token-off":
|
||||
documentXMLDecodeKeepNsRawTokenScenario(w, s)
|
||||
case "roundtrip-skip-directives":
|
||||
documentXMLSkipDirectrivesScenario(w, s)
|
||||
@@ -480,7 +538,7 @@ func documentXMLDecodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=xml '%v' sample.xml\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(2, false, true, true))))
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(2, false, ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func documentXMLDecodeKeepNsScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -495,16 +553,16 @@ func documentXMLDecodeKeepNsScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
writeOrPanic(w, "```bash\nyq -p=xml -o=xml --xml-keep-namespace '.' sample.xml\n```\n")
|
||||
writeOrPanic(w, "```bash\nyq -p=xml -o=xml --xml-keep-namespace=false '.' sample.xml\n```\n")
|
||||
writeOrPanic(w, "will output\n")
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.KeepNamespace = true
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
prefs.KeepNamespace = false
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
|
||||
prefsWithout := NewDefaultXmlPreferences()
|
||||
prefs.KeepNamespace = false
|
||||
prefs.KeepNamespace = true
|
||||
writeOrPanic(w, "instead of\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(2, prefsWithout))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(2, prefsWithout))))
|
||||
}
|
||||
|
||||
func documentXMLDecodeKeepNsRawTokenScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -519,19 +577,19 @@ func documentXMLDecodeKeepNsRawTokenScenario(w *bufio.Writer, s formatScenario)
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
writeOrPanic(w, "```bash\nyq -p=xml -o=xml --xml-keep-namespace --xml-raw-token '.' sample.xml\n```\n")
|
||||
writeOrPanic(w, "```bash\nyq -p=xml -o=xml --xml-raw-token=false '.' sample.xml\n```\n")
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.KeepNamespace = true
|
||||
prefs.UseRawToken = false
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
|
||||
prefsWithout := NewDefaultXmlPreferences()
|
||||
prefsWithout.KeepNamespace = false
|
||||
prefsWithout.UseRawToken = true
|
||||
|
||||
writeOrPanic(w, "instead of\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(2, prefsWithout))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(2, prefsWithout))))
|
||||
}
|
||||
|
||||
func documentXMLEncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -549,7 +607,7 @@ func documentXMLEncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, "```bash\nyq -o=xml '.' sample.yml\n```\n")
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewYamlDecoder(), NewXMLEncoder(2, ConfiguredXMLPreferences))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences))))
|
||||
}
|
||||
|
||||
func documentXMLRoundTripScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -567,7 +625,7 @@ func documentXMLRoundTripScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, "```bash\nyq -p=xml -o=xml '.' sample.xml\n```\n")
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(2, ConfiguredXMLPreferences))))
|
||||
}
|
||||
|
||||
func documentXMLSkipDirectrivesScenario(w *bufio.Writer, s formatScenario) {
|
||||
@@ -587,7 +645,7 @@ func documentXMLSkipDirectrivesScenario(w *bufio.Writer, s formatScenario) {
|
||||
prefs := NewDefaultXmlPreferences()
|
||||
prefs.SkipDirectives = true
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(2, prefs))))
|
||||
}
|
||||
|
||||
func TestXMLScenarios(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package yqlib
|
||||
|
||||
type YamlPreferences struct {
|
||||
LeadingContentPreProcessing bool
|
||||
PrintDocSeparators bool
|
||||
UnwrapScalar bool
|
||||
EvaluateTogether bool
|
||||
}
|
||||
|
||||
func NewDefaultYamlPreferences() YamlPreferences {
|
||||
return YamlPreferences{
|
||||
LeadingContentPreProcessing: true,
|
||||
PrintDocSeparators: true,
|
||||
UnwrapScalar: true,
|
||||
EvaluateTogether: false,
|
||||
}
|
||||
}
|
||||
|
||||
var ConfiguredYamlPreferences = NewDefaultYamlPreferences()
|
||||
@@ -1,7 +1,31 @@
|
||||
4.30.2:
|
||||
- Actually updated the default xml prefix :facepalm:
|
||||
|
||||
4.30.1:
|
||||
- XML users note: the default attribute prefix has change to `+@` to avoid naming conflicts!
|
||||
- Can use expressions in slice #1419
|
||||
- Fixed unhandled exception when decoding CSV thanks @washanhanzi
|
||||
- Added array_to_map operator for #1415
|
||||
- Fixed sorting by date #1412
|
||||
- Added check to ensure only maps can be encoded to XML #1408
|
||||
- Check merge alias is a map #1425
|
||||
- Explicity setting unwrap flag works for json output #437, #1409
|
||||
- Bumped go version
|
||||
|
||||
|
||||
|
||||
4.29.2:
|
||||
- Fixed null pointer exception when parsing CSV with empty field #1404
|
||||
|
||||
4.29.1:
|
||||
- Fixed Square brackets removing update #1342
|
||||
- Added slice array operator (.[10:15]) #44
|
||||
- XML decoder/encoder now parses directives and proc instructions (#1344). Please use the new skip flags [documented here](https://mikefarah.gitbook.io/yq/usage/xml) to ignore them.
|
||||
- XML users note that the default attribute prefix will change to `+@` in the 4.30 release to avoid naming conflicts!
|
||||
- Improved comment handling of decoders (breaking change for yqlib users sorry)
|
||||
- Fixed load operator bug when loading yaml file with multiple documents
|
||||
- Bumped Go compiler version
|
||||
- Bumped dependencies
|
||||
|
||||
4.28.2:
|
||||
- Fixed Github Actions issues (thanks @mattphelps-8451)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: yq
|
||||
version: '4.28.2'
|
||||
version: '4.30.1'
|
||||
summary: A lightweight and portable command-line YAML processor
|
||||
description: |
|
||||
The aim of the project is to be the jq or sed of yaml files.
|
||||
|
||||
Reference in New Issue
Block a user