1
0
mirror of https://github.com/taigrr/yq synced 2025-01-18 04:53:17 -08:00

Compare commits

...

36 Commits

Author SHA1 Message Date
Mike Farah
0a66bb797d 4 alpha2 2020-11-25 13:32:32 +11:00
Mike Farah
1ce30b25dc Add operator! 2020-11-24 13:07:19 +11:00
Mike Farah
3d6a231722 Added has operator 2020-11-24 11:38:39 +11:00
Mike Farah
3f04a1b52e Fixed empty array op 2020-11-22 13:50:32 +11:00
Mike Farah
aed598c736 Fixing docs 2020-11-22 13:16:54 +11:00
Mike Farah
e9fa873af8 path operator singular 2020-11-22 12:22:15 +11:00
Mike Farah
064cff1341 added path operator! 2020-11-22 12:19:57 +11:00
Mike Farah
fc3af441e5 Extracted out evaluators 2020-11-22 11:56:28 +11:00
Mike Farah
e451119014 Added File operators 2020-11-20 23:08:12 +11:00
Mike Farah
d38caf6bc2 Added File operators 2020-11-20 22:57:32 +11:00
Mike Farah
4e385a1b93 get file wip 2020-11-20 15:50:15 +11:00
Mike Farah
356aac5a1f fixed boolean example 2020-11-20 15:33:21 +11:00
Mike Farah
663413cd7a Fixed typo 2020-11-20 15:31:49 +11:00
Mike Farah
f03005f86d Fixed boolean ops 2020-11-20 15:29:53 +11:00
Mike Farah
bc87aca8d7 wip 2020-11-20 14:35:34 +11:00
Mike Farah
c08980e70f Set entrypoint to yq 2020-11-20 13:53:44 +11:00
Mike Farah
9674acf684 Fixed docker file, fixed doco 2020-11-19 22:53:05 +11:00
Mike Farah
8e1ce4ca70 Updated todo 2020-11-19 22:12:34 +11:00
Mike Farah
9bd9468526 Minor fixes 2020-11-19 22:11:26 +11:00
Mike Farah
75044e480c Added plain assignment 2020-11-19 17:08:13 +11:00
Mike Farah
36084a60a9 Added tag operator 2020-11-19 16:45:05 +11:00
Mike Farah
9b48cf80e0 updated todo 2020-11-18 20:43:36 +11:00
Mike Farah
bb3b08e648 wip style docs and test 2020-11-18 20:42:37 +11:00
Mike Farah
dcacad1e7e docs 2020-11-18 10:32:30 +11:00
Mike Farah
3356061e1e select doc 2020-11-18 09:52:03 +11:00
Mike Farah
2c062bc2a5 Added printer test 2020-11-18 09:52:03 +11:00
Mike Farah
088ec36acd include docs for tracking 2020-11-18 09:50:52 +11:00
Mike Farah
83cb6421df added test to ensure json keys remain in order 2020-11-17 16:17:38 +11:00
Mike Farah
a57944d123 Fixed printer 2020-11-16 12:09:57 +11:00
Mike Farah
79867473d5 updating release 2020-11-16 10:28:57 +11:00
Mike Farah
b3efcdc202 more docs 2020-11-15 10:58:47 +11:00
Mike Farah
af2aa9ad91 more docs 2020-11-15 10:50:30 +11:00
Mike Farah
db4762ef7c more docs 2020-11-14 13:38:44 +11:00
Mike Farah
860655b4cd Better documentation generation 2020-11-13 21:34:43 +11:00
Mike Farah
d91b25840a Better documentation generation 2020-11-13 21:22:05 +11:00
Mike Farah
019acfe456 Better documentation generation 2020-11-13 20:58:01 +11:00
105 changed files with 3800 additions and 1283 deletions

View File

@@ -22,4 +22,4 @@ LABEL version=${VERSION}
WORKDIR /workdir WORKDIR /workdir
ENTRYPOINT [/usr/bin/yq] ENTRYPOINT ["/usr/bin/yq"]

View File

@@ -14,6 +14,5 @@ var noDocSeparators = false
var nullInput = false var nullInput = false
var verbose = false var verbose = false
var version = false var version = false
var shellCompletion = ""
// var log = logging.MustGetLogger("yq") // var log = logging.MustGetLogger("yq")

View File

@@ -40,23 +40,23 @@ func evaluateAll(cmd *cobra.Command, args []string) error {
colorsEnabled = true colorsEnabled = true
} }
printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators) printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
allAtOnceEvaluator := yqlib.NewAllAtOnceEvaluator()
switch len(args) { switch len(args) {
case 0: case 0:
if pipingStdIn { if pipingStdIn {
err = yqlib.EvaluateAllFileStreams("", []string{"-"}, printer) err = allAtOnceEvaluator.EvaluateFiles("", []string{"-"}, printer)
} else { } else {
cmd.Println(cmd.UsageString()) cmd.Println(cmd.UsageString())
return nil return nil
} }
case 1: case 1:
if nullInput { if nullInput {
err = yqlib.EvaluateAllFileStreams(args[0], []string{}, printer) err = allAtOnceEvaluator.EvaluateFiles(args[0], []string{}, printer)
} else { } else {
err = yqlib.EvaluateAllFileStreams("", []string{args[0]}, printer) err = allAtOnceEvaluator.EvaluateFiles("", []string{args[0]}, printer)
} }
default: default:
err = yqlib.EvaluateAllFileStreams(args[0], args[1:], printer) err = allAtOnceEvaluator.EvaluateFiles(args[0], args[1:], printer)
} }
cmd.SilenceUsage = true cmd.SilenceUsage = true

View File

@@ -41,22 +41,25 @@ func evaluateSequence(cmd *cobra.Command, args []string) error {
} }
printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators) printer := yqlib.NewPrinter(out, outputToJSON, unwrapScalar, colorsEnabled, indent, !noDocSeparators)
streamEvaluator := yqlib.NewStreamEvaluator()
allAtOnceEvaluator := yqlib.NewAllAtOnceEvaluator()
switch len(args) { switch len(args) {
case 0: case 0:
if pipingStdIn { if pipingStdIn {
err = yqlib.EvaluateFileStreamsSequence("", []string{"-"}, printer) err = streamEvaluator.EvaluateFiles("", []string{"-"}, printer)
} else { } else {
cmd.Println(cmd.UsageString()) cmd.Println(cmd.UsageString())
return nil return nil
} }
case 1: case 1:
if nullInput { if nullInput {
err = yqlib.EvaluateAllFileStreams(args[0], []string{}, printer) err = allAtOnceEvaluator.EvaluateFiles(args[0], []string{}, printer)
} else { } else {
err = yqlib.EvaluateFileStreamsSequence("", []string{args[0]}, printer) err = streamEvaluator.EvaluateFiles("", []string{args[0]}, printer)
} }
default: default:
err = yqlib.EvaluateFileStreamsSequence(args[0], args[1:], printer) err = streamEvaluator.EvaluateFiles(args[0], args[1:], printer)
} }
cmd.SilenceUsage = true cmd.SilenceUsage = true

View File

@@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -18,20 +17,6 @@ func New() *cobra.Command {
cmd.Print(GetVersionDisplay()) cmd.Print(GetVersionDisplay())
return nil return nil
} }
if shellCompletion != "" {
switch shellCompletion {
case "bash", "":
return cmd.GenBashCompletion(os.Stdout)
case "zsh":
return cmd.GenZshCompletion(os.Stdout)
case "fish":
return cmd.GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.GenPowerShellCompletion(os.Stdout)
default:
return fmt.Errorf("Unknown variant %v", shellCompletion)
}
}
cmd.Println(cmd.UsageString()) cmd.Println(cmd.UsageString())
return nil return nil
@@ -62,10 +47,12 @@ func New() *cobra.Command {
rootCmd.PersistentFlags().IntVarP(&indent, "indent", "I", 2, "sets indent level for output") 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.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
rootCmd.Flags().StringVarP(&shellCompletion, "shellCompletion", "", "", "[bash/zsh/powershell/fish] prints shell completion script")
rootCmd.PersistentFlags().BoolVarP(&forceColor, "colors", "C", false, "force print with colors") rootCmd.PersistentFlags().BoolVarP(&forceColor, "colors", "C", false, "force print with colors")
rootCmd.PersistentFlags().BoolVarP(&forceNoColor, "no-colors", "M", false, "force print with no colors") rootCmd.PersistentFlags().BoolVarP(&forceNoColor, "no-colors", "M", false, "force print with no colors")
rootCmd.AddCommand(createEvaluateSequenceCommand(), createEvaluateAllCommand()) rootCmd.AddCommand(
createEvaluateSequenceCommand(),
createEvaluateAllCommand(),
completionCmd,
)
return rootCmd return rootCmd
} }

61
cmd/shell-completion.go Normal file
View File

@@ -0,0 +1,61 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "shell-completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(yq shell-completion bash)
# To load completions for each session, execute once:
Linux:
$ yq shell-completion bash > /etc/bash_completion.d/yq
MacOS:
$ yq shell-completion bash > /usr/local/etc/bash_completion.d/yq
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ yq shell-completion zsh > "${fpath[1]}/_yq"
# You will need to start a new shell for this setup to take effect.
Fish:
$ yq shell-completion fish | source
# To load completions for each session, execute once:
$ yq shell-completion fish > ~/.config/fish/completions/yq.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error = nil
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
err = cmd.Root().GenPowerShellCompletion(os.Stdout)
}
return err
},
}

View File

@@ -11,7 +11,7 @@ var (
GitDescribe string GitDescribe string
// Version is main version number that is being run at the moment. // Version is main version number that is being run at the moment.
Version = "4.0.0-alpha1" Version = "4.0.0-alpha2"
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string) // 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 // then it means that it is a final release. Otherwise, this is a pre-release

View File

@@ -1,13 +0,0 @@
GREEN='\033[0;32m'
NC='\033[0m'
echo "${GREEN}---Old---${NC}"
yq $@ > /tmp/yq-old-output
cat /tmp/yq-old-output
echo "${GREEN}---New---${NC}"
./yq $@ > /tmp/yq-new-output
cat /tmp/yq-new-output
echo "${GREEN}---Diff---${NC}"
colordiff /tmp/yq-old-output /tmp/yq-new-output

View File

@@ -0,0 +1,45 @@
package yqlib
import "container/list"
/**
Loads all yaml documents of all files given into memory, then runs the given expression once.
**/
type Evaluator interface {
EvaluateFiles(expression string, filenames []string, printer Printer) error
}
type allAtOnceEvaluator struct {
treeNavigator DataTreeNavigator
treeCreator PathTreeCreator
}
func NewAllAtOnceEvaluator() Evaluator {
return &allAtOnceEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewPathTreeCreator()}
}
func (e *allAtOnceEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
fileIndex := 0
node, err := treeCreator.ParsePath(expression)
if err != nil {
return err
}
var allDocuments *list.List = list.New()
for _, filename := range filenames {
reader, err := readStream(filename)
if err != nil {
return err
}
fileDocuments, err := readDocuments(reader, filename, fileIndex)
if err != nil {
return err
}
allDocuments.PushBackList(fileDocuments)
fileIndex = fileIndex + 1
}
matches, err := treeNavigator.GetMatchingNodes(allDocuments, node)
if err != nil {
return err
}
return printer.PrintResults(matches)
}

View File

@@ -6,14 +6,15 @@ import (
"strings" "strings"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type CandidateNode struct { type CandidateNode struct {
Node *yaml.Node // the actual node Node *yaml.Node // the actual node
Path []interface{} /// the path we took to get to this node Path []interface{} /// the path we took to get to this node
Document uint // the document index of this node Document uint // the document index of this node
Filename string Filename string
FileIndex int
} }
func (n *CandidateNode) GetKey() string { func (n *CandidateNode) GetKey() string {

View File

@@ -5,17 +5,9 @@ import (
"container/list" "container/list"
"gopkg.in/op/go-logging.v1" logging "gopkg.in/op/go-logging.v1"
) )
type dataTreeNavigator struct {
navigationPrefs NavigationPrefs
}
type NavigationPrefs struct {
FollowAlias bool
}
type DataTreeNavigator interface { type DataTreeNavigator interface {
// given a list of CandidateEntities and a pathNode, // given a list of CandidateEntities and a pathNode,
// this will process the list against the given pathNode and return // this will process the list against the given pathNode and return
@@ -23,8 +15,11 @@ type DataTreeNavigator interface {
GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error)
} }
func NewDataTreeNavigator(navigationPrefs NavigationPrefs) DataTreeNavigator { type dataTreeNavigator struct {
return &dataTreeNavigator{navigationPrefs} }
func NewDataTreeNavigator() DataTreeNavigator {
return &dataTreeNavigator{}
} }
func (d *dataTreeNavigator) GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func (d *dataTreeNavigator) GetMatchingNodes(matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {

View File

@@ -1,664 +0,0 @@
package yqlib
import (
"container/list"
)
func resultsToString(results *list.List) []string {
var pretty []string = make([]string, 0)
for el := results.Front(); el != nil; el = el.Next() {
n := el.Value.(*CandidateNode)
pretty = append(pretty, NodeToString(n))
}
return pretty
}
// func TestDataTreeNavigatorDeleteSimple(t *testing.T) {
// nodes := readDoc(t, `a:
// b: apple
// c: camel`)
// path, errPath := treeCreator.ParsePath("a .- b")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!map, Kind: MappingNode, Anchor:
// c: camel
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteTwice(t *testing.T) {
// nodes := readDoc(t, `a:
// b: apple
// c: camel
// d: dingo`)
// path, errPath := treeCreator.ParsePath("a .- b OR a .- c")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!map, Kind: MappingNode, Anchor:
// d: dingo
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteWithUnion(t *testing.T) {
// nodes := readDoc(t, `a:
// b: apple
// c: camel
// d: dingo`)
// path, errPath := treeCreator.ParsePath("a .- (b OR c)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!map, Kind: MappingNode, Anchor:
// d: dingo
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteByIndex(t *testing.T) {
// nodes := readDoc(t, `a:
// - b: apple
// - b: sdfsd
// - b: apple`)
// path, errPath := treeCreator.ParsePath("(a .- (0 or 1))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - b: apple
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteByFind(t *testing.T) {
// nodes := readDoc(t, `a:
// - b: apple
// - b: sdfsd
// - b: apple`)
// path, errPath := treeCreator.ParsePath("(a .- (* == apple))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - b: sdfsd
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteArrayByFind(t *testing.T) {
// nodes := readDoc(t, `a:
// - apple
// - sdfsd
// - apple`)
// path, errPath := treeCreator.ParsePath("(a .- (. == apple))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - sdfsd
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteViaSelf(t *testing.T) {
// nodes := readDoc(t, `- apple
// - sdfsd
// - apple`)
// path, errPath := treeCreator.ParsePath(". .- (. == apple)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: []
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - sdfsd
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorFilterWithSplat(t *testing.T) {
// nodes := readDoc(t, `f:
// a: frog
// b: dally
// c: log`)
// path, errPath := treeCreator.ParsePath(".f | .[] == \"frog\"")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 2
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountAndCollectWithFilterCmd(t *testing.T) {
// nodes := readDoc(t, `f:
// a: frog
// b: dally
// c: log`)
// path, errPath := treeCreator.ParsePath(".f | .[] == *og ")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 2
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCollectWithFilter(t *testing.T) {
// nodes := readDoc(t, `f:
// a: frog
// b: dally
// c: log`)
// path, errPath := treeCreator.ParsePath("f(collect(. == *og))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: , Kind: SequenceNode, Anchor:
// - frog
// - log
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountWithFilter2(t *testing.T) {
// nodes := readDoc(t, `f:
// a: frog
// b: dally
// c: log`)
// path, errPath := treeCreator.ParsePath("count(f(. == *og))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: []
// Tag: !!int, Kind: ScalarNode, Anchor:
// 2
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCollectWithFilter2(t *testing.T) {
// nodes := readDoc(t, `f:
// a: frog
// b: dally
// c: log`)
// path, errPath := treeCreator.ParsePath("collect(f(. == *og))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: []
// Tag: , Kind: SequenceNode, Anchor:
// - frog
// - log
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountMultipleMatchesInside(t *testing.T) {
// nodes := readDoc(t, `f:
// a: [1,2]
// b: dally
// c: [3,4,5]`)
// path, errPath := treeCreator.ParsePath("f | count(a or c)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 2
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCollectMultipleMatchesInside(t *testing.T) {
// nodes := readDoc(t, `f:
// a: [1,2]
// b: dally
// c: [3,4,5]`)
// path, errPath := treeCreator.ParsePath("f | collect(a or c)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: , Kind: SequenceNode, Anchor:
// - [1, 2]
// - [3, 4, 5]
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountMultipleMatchesInsideSplat(t *testing.T) {
// nodes := readDoc(t, `f:
// a: [1,2,3]
// b: [1,2,3,4]
// c: [1,2,3,4,5]`)
// path, errPath := treeCreator.ParsePath("f(count( (a or c)*))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 8
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountMultipleMatchesOutside(t *testing.T) {
// nodes := readDoc(t, `f:
// a: [1,2,3]
// b: [1,2,3,4]
// c: [1,2,3,4,5]`)
// path, errPath := treeCreator.ParsePath("f(a or c)(count(*))")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [f a]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 3
// -- Node --
// Document 0, path: [f c]
// Tag: !!int, Kind: ScalarNode, Anchor:
// 5
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountOfResults(t *testing.T) {
// nodes := readDoc(t, `- apple
// - sdfsd
// - apple`)
// path, errPath := treeCreator.ParsePath("count(*)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: []
// Tag: !!int, Kind: ScalarNode, Anchor:
// 3
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorCountNoMatches(t *testing.T) {
// nodes := readDoc(t, `- apple
// - sdfsd
// - apple`)
// path, errPath := treeCreator.ParsePath("count(5)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: []
// Tag: !!int, Kind: ScalarNode, Anchor:
// 0
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteAndWrite(t *testing.T) {
// nodes := readDoc(t, `a:
// - b: apple
// - b: sdfsd
// - { b: apple, c: cat }`)
// path, errPath := treeCreator.ParsePath("(a .- (0 or 1)) or (a[0].b := frog)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - {b: frog, c: cat}
// -- Node --
// Document 0, path: [a 0 b]
// Tag: !!str, Kind: ScalarNode, Anchor:
// frog
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorDeleteArray(t *testing.T) {
// nodes := readDoc(t, `a:
// - b: apple
// - b: sdfsd
// - b: apple`)
// path, errPath := treeCreator.ParsePath("a .- (b == a*)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a]
// Tag: !!seq, Kind: SequenceNode, Anchor:
// - b: sdfsd
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorArraySimple(t *testing.T) {
// nodes := readDoc(t, `- b: apple`)
// path, errPath := treeCreator.ParsePath("[0]")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [0]
// Tag: !!map, Kind: MappingNode, Anchor:
// b: apple
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorSimpleAssignByFind(t *testing.T) {
// nodes := readDoc(t, `a:
// b: apple`)
// path, errPath := treeCreator.ParsePath("a(. == apple) := frog")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a b]
// Tag: !!str, Kind: ScalarNode, Anchor:
// frog
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorOrDeDupes(t *testing.T) {
// nodes := readDoc(t, `a:
// cat: apple
// mad: things`)
// path, errPath := treeCreator.ParsePath("a.(cat or cat)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a cat]
// Tag: !!str, Kind: ScalarNode, Anchor:
// apple
// `
// test.AssertResult(t, expected, resultsToString(results))
// }
// func TestDataTreeNavigatorAnd(t *testing.T) {
// nodes := readDoc(t, `a:
// cat: apple
// pat: apple
// cow: apple
// mad: things`)
// path, errPath := treeCreator.ParsePath("a.(*t and c*)")
// if errPath != nil {
// t.Error(errPath)
// }
// results, errNav := treeNavigator.GetMatchingNodes(nodes, path)
// if errNav != nil {
// t.Error(errNav)
// }
// expected := `
// -- Node --
// Document 0, path: [a cat]
// Tag: !!str, Kind: ScalarNode, Anchor:
// apple
// `
// test.AssertResult(t, expected, resultsToString(results))
// }

View File

@@ -1 +0,0 @@
*.md

107
pkg/yqlib/doc/Add.md Normal file
View File

@@ -0,0 +1,107 @@
Add behaves differently according to the type of the LHS:
- arrays: concatenate
- number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon)
## Concatenate arrays
Given a sample.yml file of:
```yaml
a:
- 1
- 2
b:
- 3
- 4
```
then
```bash
yq eval '.a + .b' sample.yml
```
will output
```yaml
- 1
- 2
- 3
- 4
```
## Concatenate null to array
Given a sample.yml file of:
```yaml
a:
- 1
- 2
```
then
```bash
yq eval '.a + null' sample.yml
```
will output
```yaml
- 1
- 2
```
## Add object to array
Given a sample.yml file of:
```yaml
a:
- 1
- 2
c:
cat: meow
```
then
```bash
yq eval '.a + .c' sample.yml
```
will output
```yaml
- 1
- 2
- cat: meow
```
## Add string to array
Given a sample.yml file of:
```yaml
a:
- 1
- 2
```
then
```bash
yq eval '.a + "hello"' sample.yml
```
will output
```yaml
- 1
- 2
- hello
```
## Update array (append)
Given a sample.yml file of:
```yaml
a:
- 1
- 2
b:
- 3
- 4
```
then
```bash
yq eval '.a = .a + .b' sample.yml
```
will output
```yaml
a:
- 1
- 2
- 3
- 4
b:
- 3
- 4
```

157
pkg/yqlib/doc/Assign.md Normal file
View File

@@ -0,0 +1,157 @@
This operator is used to update node values. It can be used in either the:
### plain form: `=`
Which will assign the LHS node values to the RHS node values. The RHS expression is run against the matching nodes in the pipeline.
### relative form: `|=`
This will do a similar thing to the plain form, however, the RHS expression is run against _the LHS nodes_. This is useful for updating values based on old values, e.g. increment.
## Update node to be the child value
Given a sample.yml file of:
```yaml
a:
b:
g: foof
```
then
```bash
yq eval '.a |= .b' sample.yml
```
will output
```yaml
a:
g: foof
```
## Update node to be the sibling value
Given a sample.yml file of:
```yaml
a:
b: child
b: sibling
```
then
```bash
yq eval '.a = .b' sample.yml
```
will output
```yaml
a: sibling
b: sibling
```
## Updated multiple paths
Given a sample.yml file of:
```yaml
a: fieldA
b: fieldB
c: fieldC
```
then
```bash
yq eval '(.a, .c) |= "potatoe"' sample.yml
```
will output
```yaml
a: potatoe
b: fieldB
c: potatoe
```
## Update string value
Given a sample.yml file of:
```yaml
a:
b: apple
```
then
```bash
yq eval '.a.b = "frog"' sample.yml
```
will output
```yaml
a:
b: frog
```
## Update string value via |=
Note there is no difference between `=` and `|=` when the RHS is a scalar
Given a sample.yml file of:
```yaml
a:
b: apple
```
then
```bash
yq eval '.a.b |= "frog"' sample.yml
```
will output
```yaml
a:
b: frog
```
## Update selected results
Given a sample.yml file of:
```yaml
a:
b: apple
c: cactus
```
then
```bash
yq eval '.a.[] | select(. == "apple") |= "frog"' sample.yml
```
will output
```yaml
a:
b: frog
c: cactus
```
## Update array values
Given a sample.yml file of:
```yaml
- candy
- apple
- sandy
```
then
```bash
yq eval '.[] | select(. == "*andy") |= "bogs"' sample.yml
```
will output
```yaml
- bogs
- apple
- bogs
```
## Update empty object
Given a sample.yml file of:
```yaml
{}
```
then
```bash
yq eval '.a.b |= "bogs"' sample.yml
```
will output
```yaml
{a: {b: bogs}}
```
## Update empty object and array
Given a sample.yml file of:
```yaml
{}
```
then
```bash
yq eval '.a.b[0] |= "bogs"' sample.yml
```
will output
```yaml
{a: {b: [bogs]}}
```

View File

@@ -0,0 +1,113 @@
The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes.
## OR example
Running
```bash
yq eval --null-input 'true or false'
```
will output
```yaml
true
```
## AND example
Running
```bash
yq eval --null-input 'true and false'
```
will output
```yaml
false
```
## Matching nodes with select, equals and or
Given a sample.yml file of:
```yaml
- a: bird
b: dog
- a: frog
b: bird
- a: cat
b: fly
```
then
```bash
yq eval '[.[] | select(.a == "cat" or .b == "dog")]' sample.yml
```
will output
```yaml
- a: bird
b: dog
- a: cat
b: fly
```
## Not true is false
Running
```bash
yq eval --null-input 'true | not'
```
will output
```yaml
false
```
## Not false is true
Running
```bash
yq eval --null-input 'false | not'
```
will output
```yaml
true
```
## String values considered to be true
Running
```bash
yq eval --null-input '"cat" | not'
```
will output
```yaml
false
```
## Empty string value considered to be true
Running
```bash
yq eval --null-input '"" | not'
```
will output
```yaml
false
```
## Numbers are considered to be true
Running
```bash
yq eval --null-input '1 | not'
```
will output
```yaml
false
```
## Zero is considered to be true
Running
```bash
yq eval --null-input '0 | not'
```
will output
```yaml
false
```
## Null is considered to be false
Running
```bash
yq eval --null-input '~ | not'
```
will output
```yaml
true
```

View File

@@ -0,0 +1,41 @@
# Collect into Array
This creates an array using the expression between the square brackets.
## Collect empty
Running
```bash
yq eval --null-input '[]'
```
will output
```yaml
[]
```
## Collect single
Running
```bash
yq eval --null-input '["cat"]'
```
will output
```yaml
- cat
```
## Collect many
Given a sample.yml file of:
```yaml
a: cat
b: dog
```
then
```bash
yq eval '[.a, .b]' sample.yml
```
will output
```yaml
- cat
- dog
```

View File

@@ -0,0 +1,79 @@
This is used to construct objects (or maps). This can be used against existing yaml, or to create fresh yaml documents.
## Collect empty object
Running
```bash
yq eval --null-input '{}'
```
will output
```yaml
{}
```
## Wrap (prefix) existing object
Given a sample.yml file of:
```yaml
name: Mike
```
then
```bash
yq eval '{"wrap": .}' sample.yml
```
will output
```yaml
wrap:
name: Mike
```
## Using splat to create multiple objects
Given a sample.yml file of:
```yaml
name: Mike
pets:
- cat
- dog
```
then
```bash
yq eval '{.name: .pets.[]}' sample.yml
```
will output
```yaml
Mike: cat
Mike: dog
```
## Working with multiple documents
Given a sample.yml file of:
```yaml
name: Mike
pets:
- cat
- dog
---
name: Rosey
pets:
- monkey
- sheep
```
then
```bash
yq eval '{.name: .pets.[]}' sample.yml
```
will output
```yaml
Mike: cat
Mike: dog
Rosey: monkey
Rosey: sheep
```
## Creating yaml from scratch
Running
```bash
yq eval --null-input '{"wrap": "frog"}'
```
will output
```yaml
wrap: frog
```

View File

@@ -0,0 +1,119 @@
Use these comment operators to set or retrieve comments.
## Set line comment
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval '.a lineComment="single"' sample.yml
```
will output
```yaml
a: cat # single
```
## Set head comment
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval '. headComment="single"' sample.yml
```
will output
```yaml
# single
a: cat
```
## Set foot comment, using an expression
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval '. footComment=.a' sample.yml
```
will output
```yaml
a: cat
# cat
```
## Remove comment
Given a sample.yml file of:
```yaml
a: cat # comment
b: dog # leave this
```
then
```bash
yq eval '.a lineComment=""' sample.yml
```
will output
```yaml
a: cat
b: dog # leave this
```
## Remove all comments
Given a sample.yml file of:
```yaml
a: cat # comment
```
then
```bash
yq eval '.. comments=""' sample.yml
```
will output
```yaml
a: cat
```
## Get line comment
Given a sample.yml file of:
```yaml
a: cat # meow
```
then
```bash
yq eval '.a | lineComment' sample.yml
```
will output
```yaml
meow
```
## Get head comment
Given a sample.yml file of:
```yaml
a: cat # meow
```
then
```bash
yq eval '. | headComment' sample.yml
```
will output
```yaml
```
## Get foot comment
Given a sample.yml file of:
```yaml
a: cat # meow
```
then
```bash
yq eval '. | footComment' sample.yml
```
will output
```yaml
```

65
pkg/yqlib/doc/Delete.md Normal file
View File

@@ -0,0 +1,65 @@
Deletes matching entries in maps or arrays.
## Delete entry in map
Given a sample.yml file of:
```yaml
a: cat
b: dog
```
then
```bash
yq eval 'del(.b)' sample.yml
```
will output
```yaml
a: cat
```
## Delete entry in array
Given a sample.yml file of:
```yaml
- 1
- 2
- 3
```
then
```bash
yq eval 'del(.[1])' sample.yml
```
will output
```yaml
- 1
- 3
```
## Delete no matches
Given a sample.yml file of:
```yaml
a: cat
b: dog
```
then
```bash
yq eval 'del(.c)' sample.yml
```
will output
```yaml
a: cat
b: dog
```
## Delete matching entries
Given a sample.yml file of:
```yaml
a: cat
b: dog
c: bat
```
then
```bash
yq eval 'del( .[] | select(. == "*at") )' sample.yml
```
will output
```yaml
b: dog
```

View File

@@ -0,0 +1,54 @@
Use the `documentIndex` operator to select nodes of a particular document.
## Retrieve a document index
Given a sample.yml file of:
```yaml
a: cat
---
a: frog
```
then
```bash
yq eval '.a | documentIndex' sample.yml
```
will output
```yaml
0
---
1
```
## Filter by document index
Given a sample.yml file of:
```yaml
a: cat
---
a: frog
```
then
```bash
yq eval 'select(. | documentIndex == 1)' sample.yml
```
will output
```yaml
a: frog
```
## Print Document Index with matches
Given a sample.yml file of:
```yaml
a: cat
---
a: frog
```
then
```bash
yq eval '.a | ({"match": ., "doc": (. | documentIndex)})' sample.yml
```
will output
```yaml
match: cat
doc: 0
match: frog
doc: 1
```

59
pkg/yqlib/doc/Equals.md Normal file
View File

@@ -0,0 +1,59 @@
This is a boolean operator that will return ```true``` if the LHS is equal to the RHS and ``false`` otherwise.
```
.a == .b
```
It is most often used with the select operator to find particular nodes:
```
select(.a == .b)
```
## Match string
Given a sample.yml file of:
```yaml
- cat
- goat
- dog
```
then
```bash
yq eval '.[] | (. == "*at")' sample.yml
```
will output
```yaml
true
true
false
```
## Match number
Given a sample.yml file of:
```yaml
- 3
- 4
- 5
```
then
```bash
yq eval '.[] | (. == 4)' sample.yml
```
will output
```yaml
false
true
false
```
## Match nulls
Running
```bash
yq eval --null-input 'null == ~'
```
will output
```yaml
true
```

98
pkg/yqlib/doc/Explode.md Normal file
View File

@@ -0,0 +1,98 @@
Explodes (or dereferences) aliases and anchors.
## Explode alias and anchor
Given a sample.yml file of:
```yaml
f:
a: &a cat
b: *a
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
b: cat
```
## Explode with no aliases or anchors
Given a sample.yml file of:
```yaml
a: mike
```
then
```bash
yq eval 'explode(.a)' sample.yml
```
will output
```yaml
a: mike
```
## Explode with alias keys
Given a sample.yml file of:
```yaml
f:
a: &a cat
*a: b
```
then
```bash
yq eval 'explode(.f)' sample.yml
```
will output
```yaml
f:
a: cat
cat: b
```
## Explode with merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval 'explode(.)' sample.yml
```
will output
```yaml
foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: bar_b
a: foo_a
thing: bar_thing
c: foobarList_c
foobar:
c: foo_c
a: foo_a
thing: foobar_thing
```

View File

@@ -0,0 +1,35 @@
File operators are most often used with merge when needing to merge specific files together. Note that when doing this, you will need to use `eval-all` to ensure all yaml documents are loaded into memory before performing the merge (as opposed to `eval` which runs the expression once per document).
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
```
## Get filename
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval 'filename' sample.yml
```
will output
```yaml
sample.yaml
```
## Get file index
Given a sample.yml file of:
```yaml
a: cat
```
then
```bash
yq eval 'fileIndex' sample.yml
```
will output
```yaml
0
```

44
pkg/yqlib/doc/Has.md Normal file
View File

@@ -0,0 +1,44 @@
This is operation that returns true if the key exists in a map (or index in an array), false otherwise.
## Has map key
Given a sample.yml file of:
```yaml
- a: yes
- a: ~
- a:
- b: nope
```
then
```bash
yq eval '.[] | has("a")' sample.yml
```
will output
```yaml
true
true
true
false
```
## Has array index
Given a sample.yml file of:
```yaml
- []
- [1]
- [1, 2]
- [1, null]
- [1, 2, 3]
```
then
```bash
yq eval '.[] | has(1)' sample.yml
```
will output
```yaml
false
false
true
true
true
```

204
pkg/yqlib/doc/Multiply.md Normal file
View File

@@ -0,0 +1,204 @@
Like the multiple operator in `jq`, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS.
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings).
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml
```
## Merge objects together, returning merged result only
Given a sample.yml file of:
```yaml
a:
field: me
fieldA: cat
b:
field:
g: wizz
fieldB: dog
```
then
```bash
yq eval '.a * .b' sample.yml
```
will output
```yaml
field:
g: wizz
fieldA: cat
fieldB: dog
```
## Merge objects together, returning parent object
Given a sample.yml file of:
```yaml
a:
field: me
fieldA: cat
b:
field:
g: wizz
fieldB: dog
```
then
```bash
yq eval '. * {"a":.b}' sample.yml
```
will output
```yaml
a:
field:
g: wizz
fieldA: cat
fieldB: dog
b:
field:
g: wizz
fieldB: dog
```
## Merge keeps style of LHS
Given a sample.yml file of:
```yaml
a: {things: great}
b:
also: "me"
```
then
```bash
yq eval '. * {"a":.b}' sample.yml
```
will output
```yaml
a: {things: great, also: "me"}
b:
also: "me"
```
## Merge arrays
Given a sample.yml file of:
```yaml
a:
- 1
- 2
- 3
b:
- 3
- 4
- 5
```
then
```bash
yq eval '. * {"a":.b}' sample.yml
```
will output
```yaml
a:
- 3
- 4
- 5
b:
- 3
- 4
- 5
```
## Merge to prefix an element
Given a sample.yml file of:
```yaml
a: cat
b: dog
```
then
```bash
yq eval '. * {"a": {"c": .a}}' sample.yml
```
will output
```yaml
a:
c: cat
b: dog
```
## Merge with simple aliases
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b:
f: *cat
c:
g: thongs
```
then
```bash
yq eval '.c * .b' sample.yml
```
will output
```yaml
g: thongs
f: *cat
```
## Merge does not copy anchor names
Given a sample.yml file of:
```yaml
a:
c: &cat frog
b:
f: *cat
c:
g: thongs
```
then
```bash
yq eval '.c * .a' sample.yml
```
will output
```yaml
g: thongs
c: frog
```
## Merge with merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar * .foobarList' sample.yml
```
will output
```yaml
c: foobarList_c
<<:
- *foo
- *bar
thing: foobar_thing
b: foobarList_b
```

58
pkg/yqlib/doc/Path.md Normal file
View File

@@ -0,0 +1,58 @@
The path operator can be used to get the traversal paths of matching nodes in an expression. The path is returned as an array, which if traversed in order will lead to the matching node.
## Map path
Given a sample.yml file of:
```yaml
a:
b: cat
```
then
```bash
yq eval '.a.b | path' sample.yml
```
will output
```yaml
- a
- b
```
## Array path
Given a sample.yml file of:
```yaml
a:
- cat
- dog
```
then
```bash
yq eval '.a.[] | select(. == "dog") | path' sample.yml
```
will output
```yaml
- a
- 1
```
## Print path and value
Given a sample.yml file of:
```yaml
a:
- cat
- dog
- frog
```
then
```bash
yq eval '.a.[] | select(. == "*og") | [{"path":path, "value":.}]' sample.yml
```
will output
```yaml
- path:
- a
- 1
value: dog
- path:
- a
- 2
value: frog
```

View File

@@ -0,0 +1,63 @@
This operator recursively matches all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches, for instance to set the `style` of all nodes in a yaml doc:
```bash
yq eval '.. style= "flow"' file.yaml
```
## Aliases are not traversed
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '[..]' sample.yml
```
will output
```yaml
- a: &cat
c: frog
b: *cat
- &cat
c: frog
- frog
- *cat
```
## Merge docs are not traversed
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar | [..]' sample.yml
```
will output
```yaml
- c: foobar_c
!!merge <<: *foo
thing: foobar_thing
- foobar_c
- *foo
- foobar_thing
```

38
pkg/yqlib/doc/Select.md Normal file
View File

@@ -0,0 +1,38 @@
Select is used to filter arrays and maps by a boolean expression.
## Select elements from array
Given a sample.yml file of:
```yaml
- cat
- goat
- dog
```
then
```bash
yq eval '.[] | select(. == "*at")' sample.yml
```
will output
```yaml
cat
goat
```
## Select and update matching values in map
Given a sample.yml file of:
```yaml
a:
things: cat
bob: goat
horse: dog
```
then
```bash
yq eval '(.a.[] | select(. == "*at")) |= "rabbit"' sample.yml
```
will output
```yaml
a:
things: rabbit
bob: rabbit
horse: dog
```

165
pkg/yqlib/doc/Style.md Normal file
View File

@@ -0,0 +1,165 @@
The style operator can be used to get or set the style of nodes (e.g. string style, yaml style)
## Set tagged style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="tagged"' sample.yml
```
will output
```yaml
!!map
a: !!str cat
b: !!int 5
c: !!float 3.2
e: !!bool true
```
## Set double quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="double"' sample.yml
```
will output
```yaml
a: "cat"
b: "5"
c: "3.2"
e: "true"
```
## Set single quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="single"' sample.yml
```
will output
```yaml
a: 'cat'
b: '5'
c: '3.2'
e: 'true'
```
## Set literal quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="literal"' sample.yml
```
will output
```yaml
a: |-
cat
b: |-
5
c: |-
3.2
e: |-
true
```
## Set folded quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="folded"' sample.yml
```
will output
```yaml
a: >-
cat
b: >-
5
c: >-
3.2
e: >-
true
```
## Set flow quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style="flow"' sample.yml
```
will output
```yaml
{a: cat, b: 5, c: 3.2, e: true}
```
## Pretty print
Set empty (default) quote style
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '.. style=""' sample.yml
```
will output
```yaml
a: cat
b: 5
c: 3.2
e: true
```
## Read style
Given a sample.yml file of:
```yaml
{a: "cat", b: 'thing'}
```
then
```bash
yq eval '.. | style' sample.yml
```
will output
```yaml
flow
double
single
```

44
pkg/yqlib/doc/Tag.md Normal file
View File

@@ -0,0 +1,44 @@
The tag operator can be used to get or set the tag of nodes (e.g. `!!str`, `!!int`, `!!bool`).
## Get tag
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
f: []
```
then
```bash
yq eval '.. | tag' sample.yml
```
will output
```yaml
!!map
!!str
!!int
!!float
!!bool
!!seq
```
## Convert numbers to strings
Given a sample.yml file of:
```yaml
a: cat
b: 5
c: 3.2
e: true
```
then
```bash
yq eval '(.. | select(tag == "!!int")) tag= "!!str"' sample.yml
```
will output
```yaml
a: cat
b: "5"
c: 3.2
e: true
```

368
pkg/yqlib/doc/Traverse.md Normal file
View File

@@ -0,0 +1,368 @@
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structurse.
## Simple map navigation
Given a sample.yml file of:
```yaml
a:
b: apple
```
then
```bash
yq eval '.a' sample.yml
```
will output
```yaml
b: apple
```
## Splat
Often used to pipe children into other operators
Given a sample.yml file of:
```yaml
- b: apple
- c: banana
```
then
```bash
yq eval '.[]' sample.yml
```
will output
```yaml
b: apple
c: banana
```
## Special characters
Use quotes around path elements with special characters
Given a sample.yml file of:
```yaml
"{}": frog
```
then
```bash
yq eval '."{}"' sample.yml
```
will output
```yaml
frog
```
## Children don't exist
Nodes are added dynamically while traversing
Given a sample.yml file of:
```yaml
c: banana
```
then
```bash
yq eval '.a.b' sample.yml
```
will output
```yaml
null
```
## Wildcard matching
Given a sample.yml file of:
```yaml
a:
cat: apple
mad: things
```
then
```bash
yq eval '.a."*a*"' sample.yml
```
will output
```yaml
apple
things
```
## Aliases
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '.b' sample.yml
```
will output
```yaml
*cat
```
## Traversing aliases with splat
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '.b.[]' sample.yml
```
will output
```yaml
frog
```
## Traversing aliases explicitly
Given a sample.yml file of:
```yaml
a: &cat
c: frog
b: *cat
```
then
```bash
yq eval '.b.c' sample.yml
```
will output
```yaml
frog
```
## Traversing arrays by index
Given a sample.yml file of:
```yaml
- 1
- 2
- 3
```
then
```bash
yq eval '[0]' sample.yml
```
will output
```yaml
1
```
## Maps with numeric keys
Given a sample.yml file of:
```yaml
2: cat
```
then
```bash
yq eval '[2]' sample.yml
```
will output
```yaml
cat
```
## Maps with non existing numeric keys
Given a sample.yml file of:
```yaml
a: b
```
then
```bash
yq eval '[0]' sample.yml
```
will output
```yaml
null
```
## Traversing merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar.a' sample.yml
```
will output
```yaml
foo_a
```
## Traversing merge anchors with override
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar.c' sample.yml
```
will output
```yaml
foo_c
```
## Traversing merge anchors with local override
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar.thing' sample.yml
```
will output
```yaml
foobar_thing
```
## Splatting merge anchors
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobar.[]' sample.yml
```
will output
```yaml
foo_c
foo_a
foobar_thing
```
## Traversing merge anchor lists
Note that the later merge anchors override previous
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobarList.thing' sample.yml
```
will output
```yaml
bar_thing
```
## Splatting merge anchor lists
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq eval '.foobarList.[]' sample.yml
```
will output
```yaml
bar_b
foo_a
bar_thing
foobarList_c
```

30
pkg/yqlib/doc/Union.md Normal file
View File

@@ -0,0 +1,30 @@
This operator is used to combine different results together.
## Combine scalars
Running
```bash
yq eval --null-input '1, true, "cat"'
```
will output
```yaml
1
true
cat
```
## Combine selected paths
Given a sample.yml file of:
```yaml
a: fieldA
b: fieldB
c: fieldC
```
then
```bash
yq eval '.a, .c' sample.yml
```
will output
```yaml
fieldA
fieldC
```

View File

@@ -0,0 +1,4 @@
Add behaves differently according to the type of the LHS:
- arrays: concatenate
- number scalars: arithmetic addition (soon)
- string scalars: concatenate (soon)

View File

@@ -0,0 +1,7 @@
This operator is used to update node values. It can be used in either the:
### plain form: `=`
Which will assign the LHS node values to the RHS node values. The RHS expression is run against the matching nodes in the pipeline.
### relative form: `|=`
This will do a similar thing to the plain form, however, the RHS expression is run against _the LHS nodes_. This is useful for updating values based on old values, e.g. increment.

View File

@@ -0,0 +1 @@
The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes.

View File

@@ -0,0 +1,4 @@
# Collect into Array
This creates an array using the expression between the square brackets.

View File

@@ -0,0 +1 @@
This is used to construct objects (or maps). This can be used against existing yaml, or to create fresh yaml documents.

View File

@@ -0,0 +1 @@
Use these comment operators to set or retrieve comments.

View File

@@ -0,0 +1 @@
Deletes matching entries in maps or arrays.

View File

@@ -0,0 +1 @@
Use the `documentIndex` operator to select nodes of a particular document.

View File

@@ -0,0 +1,12 @@
This is a boolean operator that will return ```true``` if the LHS is equal to the RHS and ``false`` otherwise.
```
.a == .b
```
It is most often used with the select operator to find particular nodes:
```
select(.a == .b)
```

View File

@@ -0,0 +1 @@
Explodes (or dereferences) aliases and anchors.

View File

@@ -0,0 +1,7 @@
File operators are most often used with merge when needing to merge specific files together. Note that when doing this, you will need to use `eval-all` to ensure all yaml documents are loaded into memory before performing the merge (as opposed to `eval` which runs the expression once per document).
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(filename == "file2.yaml")' file1.yaml file2.yaml
```

View File

@@ -0,0 +1 @@
This is operation that returns true if the key exists in a map (or index in an array), false otherwise.

View File

@@ -0,0 +1,12 @@
Like the multiple operator in `jq`, depending on the operands, this multiply operator will do different things. Currently only objects are supported, which have the effect of merging the RHS into the LHS.
Upcoming versions of `yq` will add support for other types of multiplication (numbers, strings).
Note that when merging objects, this operator returns the merged object (not the parent). This will be clearer in the examples below.
## Merging files
Note the use of eval-all to ensure all documents are loaded into memory.
```bash
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' file1.yaml file2.yaml
```

View File

@@ -0,0 +1 @@
The path operator can be used to get the traversal paths of matching nodes in an expression. The path is returned as an array, which if traversed in order will lead to the matching node.

View File

@@ -0,0 +1,5 @@
This operator recursively matches all children nodes given of a particular element, including that node itself. This is most often used to apply a filter recursively against all matches, for instance to set the `style` of all nodes in a yaml doc:
```bash
yq eval '.. style= "flow"' file.yaml
```

View File

@@ -0,0 +1 @@
Select is used to filter arrays and maps by a boolean expression.

View File

@@ -0,0 +1 @@
The style operator can be used to get or set the style of nodes (e.g. string style, yaml style)

View File

@@ -0,0 +1 @@
The tag operator can be used to get or set the tag of nodes (e.g. `!!str`, `!!int`, `!!bool`).

View File

@@ -0,0 +1 @@
This is the simplest (and perhaps most used) operator, it is used to navigate deeply into yaml structurse.

View File

@@ -0,0 +1 @@
This operator is used to combine different results together.

47
pkg/yqlib/encoder_test.go Normal file
View File

@@ -0,0 +1,47 @@
package yqlib
import (
"bufio"
"bytes"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
var sampleYaml = `zabbix: winner
apple: great
banana:
- {cobra: kai, angus: bob}
`
var expectedJson = `{
"zabbix": "winner",
"apple": "great",
"banana": [
{
"cobra": "kai",
"angus": "bob"
}
]
}
`
func TestJsonEncoderPreservesObjectOrder(t *testing.T) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
var jsonEncoder = NewJsonEncoder(writer, 2)
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0)
if err != nil {
panic(err)
}
node := inputs.Front().Value.(*CandidateNode).Node
err = jsonEncoder.Encode(node)
if err != nil {
panic(err)
}
writer.Flush()
test.AssertResult(t, expectedJson, output.String())
}

View File

@@ -5,8 +5,8 @@ import (
"container/list" "container/list"
"fmt" "fmt"
"gopkg.in/op/go-logging.v1" logging "gopkg.in/op/go-logging.v1"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type OperationType struct { type OperationType struct {
@@ -17,16 +17,9 @@ type OperationType struct {
} }
// operators TODO: // operators TODO:
// - generator doc from operator tests
// - slurp - stdin, read in sequence, vs read all
// - write in place // - write in place
// - get path operator (like doc index)
// - get file index op (like doc index)
// - get file name op (like doc index)
// - mergeAppend (merges and appends arrays) // - mergeAppend (merges and appends arrays)
// - mergeEmpty (sets only if the document is empty, do I do that now?) // - mergeEmpty (sets only if the document is empty, do I do that now?)
// - updateTag - not recursive
// - get tag (tag)
// - compare ?? // - compare ??
// - validate ?? // - validate ??
// - exists // - exists
@@ -37,11 +30,14 @@ var And = &OperationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: AndOp
var Union = &OperationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: UnionOperator} var Union = &OperationType{Type: "UNION", NumArgs: 2, Precedence: 10, Handler: UnionOperator}
var Assign = &OperationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: AssignUpdateOperator} var Assign = &OperationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: AssignUpdateOperator}
var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: AssignAttributesOperator} var AssignAttributes = &OperationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: AssignAttributesOperator}
var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator} var AssignStyle = &OperationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: AssignStyleOperator}
var AssignTag = &OperationType{Type: "ASSIGN_TAG", NumArgs: 2, Precedence: 40, Handler: AssignTagOperator}
var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator} var AssignComment = &OperationType{Type: "ASSIGN_COMMENT", NumArgs: 2, Precedence: 40, Handler: AssignCommentsOperator}
var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 40, Handler: MultiplyOperator} var Multiply = &OperationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 45, Handler: MultiplyOperator}
var Add = &OperationType{Type: "ADD", NumArgs: 2, Precedence: 45, Handler: AddOperator}
var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator} var Equals = &OperationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: EqualsOperator}
var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator} var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, Handler: CreateMapOperator}
@@ -50,8 +46,12 @@ var Pipe = &OperationType{Type: "PIPE", NumArgs: 2, Precedence: 45, Handler: Pip
var Length = &OperationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: LengthOperator} var Length = &OperationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: LengthOperator}
var Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: CollectOperator} var Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: CollectOperator}
var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator} var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator}
var GetTag = &OperationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: GetTagOperator}
var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator} var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: GetCommentsOperator}
var GetDocumentIndex = &OperationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: GetDocumentIndexOperator} var GetDocumentIndex = &OperationType{Type: "GET_DOCUMENT_INDEX", NumArgs: 0, Precedence: 50, Handler: GetDocumentIndexOperator}
var GetFilename = &OperationType{Type: "GET_FILENAME", NumArgs: 0, Precedence: 50, Handler: GetFilenameOperator}
var GetFileIndex = &OperationType{Type: "GET_FILE_INDEX", NumArgs: 0, Precedence: 50, Handler: GetFileIndexOperator}
var GetPath = &OperationType{Type: "GET_PATH", NumArgs: 0, Precedence: 50, Handler: GetPathOperator}
var Explode = &OperationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: ExplodeOperator} var Explode = &OperationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, Handler: ExplodeOperator}
@@ -66,11 +66,9 @@ var Empty = &OperationType{Type: "EMPTY", NumArgs: 50, Handler: EmptyOperator}
var RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: RecursiveDescentOperator} var RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: RecursiveDescentOperator}
// not sure yet
var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator} var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator}
var Has = &OperationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: HasOperator}
var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 2, Precedence: 40, Handler: DeleteChildOperator} var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: DeleteChildOperator}
// var Exists = &OperationType{Type: "Length", NumArgs: 2, Precedence: 35} // var Exists = &OperationType{Type: "Length", NumArgs: 2, Precedence: 35}
// filters matches if they have the existing path // filters matches if they have the existing path

69
pkg/yqlib/operator_add.go Normal file
View File

@@ -0,0 +1,69 @@
package yqlib
import (
"fmt"
"container/list"
yaml "gopkg.in/yaml.v3"
)
func toNodes(candidates *list.List) []*yaml.Node {
if candidates.Len() == 0 {
return []*yaml.Node{}
}
candidate := candidates.Front().Value.(*CandidateNode)
if candidate.Node.Tag == "!!null" {
return []*yaml.Node{}
}
switch candidate.Node.Kind {
case yaml.SequenceNode:
return candidate.Node.Content
default:
return []*yaml.Node{candidate.Node}
}
}
func AddOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("Add operator")
var results = list.New()
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil {
return nil, err
}
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
lhsCandidate := el.Value.(*CandidateNode)
lhsNode := UnwrapDoc(lhsCandidate.Node)
var newBlank = &CandidateNode{
Path: lhsCandidate.Path,
Document: lhsCandidate.Document,
Filename: lhsCandidate.Filename,
Node: &yaml.Node{},
}
switch lhsNode.Kind {
case yaml.MappingNode:
return nil, fmt.Errorf("Maps not yet supported for addition")
case yaml.SequenceNode:
newBlank.Node.Kind = yaml.SequenceNode
newBlank.Node.Style = lhsNode.Style
newBlank.Node.Tag = "!!seq"
newBlank.Node.Content = append(lhsNode.Content, toNodes(rhs)...)
results.PushBack(newBlank)
case yaml.ScalarNode:
return nil, fmt.Errorf("Scalars not yet supported for addition")
}
}
return results, nil
}

View File

@@ -0,0 +1,55 @@
package yqlib
import (
"testing"
)
var addOperatorScenarios = []expressionScenario{
{
description: "Concatenate arrays",
document: `{a: [1,2], b: [3,4]}`,
expression: `.a + .b`,
expected: []string{
"D0, P[a], (!!seq)::[1, 2, 3, 4]\n",
},
},
{
description: "Concatenate null to array",
document: `{a: [1,2]}`,
expression: `.a + null`,
expected: []string{
"D0, P[a], (!!seq)::[1, 2]\n",
},
},
{
description: "Add object to array",
document: `{a: [1,2], c: {cat: meow}}`,
expression: `.a + .c`,
expected: []string{
"D0, P[a], (!!seq)::[1, 2, {cat: meow}]\n",
},
},
{
description: "Add string to array",
document: `{a: [1,2]}`,
expression: `.a + "hello"`,
expected: []string{
"D0, P[a], (!!seq)::[1, 2, hello]\n",
},
},
{
description: "Update array (append)",
document: `{a: [1,2], b: [3,4]}`,
expression: `.a = .a + .b`,
expected: []string{
"D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n",
},
},
}
func TestAddOperatorScenarios(t *testing.T) {
for _, tt := range addOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Add", addOperatorScenarios)
}

View File

@@ -2,15 +2,28 @@ package yqlib
import "container/list" import "container/list"
type AssignOpPreferences struct {
UpdateAssign bool
}
func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func AssignUpdateOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs) lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
preferences := pathNode.Operation.Preferences.(*AssignOpPreferences)
var rhs *list.List
if !preferences.UpdateAssign {
rhs, err = d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
}
for el := lhs.Front(); el != nil; el = el.Next() { for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs) if preferences.UpdateAssign {
rhs, err = d.GetMatchingNodes(nodeToMap(candidate), pathNode.Rhs)
}
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,122 @@
package yqlib
import (
"testing"
)
var assignOperatorScenarios = []expressionScenario{
{
description: "Update node to be the child value",
document: `{a: {b: {g: foof}}}`,
expression: `.a |= .b`,
expected: []string{
"D0, P[], (doc)::{a: {g: foof}}\n",
},
},
{
description: "Update node to be the sibling value",
document: `{a: {b: child}, b: sibling}`,
expression: `.a = .b`,
expected: []string{
"D0, P[], (doc)::{a: sibling, b: sibling}\n",
},
},
{
description: "Updated multiple paths",
document: `{a: fieldA, b: fieldB, c: fieldC}`,
expression: `(.a, .c) |= "potatoe"`,
expected: []string{
"D0, P[], (doc)::{a: potatoe, b: fieldB, c: potatoe}\n",
},
},
{
description: "Update string value",
document: `{a: {b: apple}}`,
expression: `.a.b = "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog}}\n",
},
},
{
description: "Update string value via |=",
subdescription: "Note there is no difference between `=` and `|=` when the RHS is a scalar",
document: `{a: {b: apple}}`,
expression: `.a.b |= "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog}}\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple}}`,
expression: `.a.b | (. |= "frog")`,
expected: []string{
"D0, P[a b], (!!str)::frog\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple}}`,
expression: `.a.b |= 5`,
expected: []string{
"D0, P[], (doc)::{a: {b: 5}}\n",
},
},
{
skipDoc: true,
document: `{a: {b: apple}}`,
expression: `.a.b |= 3.142`,
expected: []string{
"D0, P[], (doc)::{a: {b: 3.142}}\n",
},
},
{
description: "Update selected results",
document: `{a: {b: apple, c: cactus}}`,
expression: `.a.[] | select(. == "apple") |= "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog, c: cactus}}\n",
},
},
{
description: "Update array values",
document: `[candy, apple, sandy]`,
expression: `.[] | select(. == "*andy") |= "bogs"`,
expected: []string{
"D0, P[], (doc)::[bogs, apple, bogs]\n",
},
},
{
description: "Update empty object",
dontFormatInputForDoc: true,
document: `{}`,
expression: `.a.b |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: bogs}}\n",
},
},
{
description: "Update empty object and array",
dontFormatInputForDoc: true,
document: `{}`,
expression: `.a.b[0] |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: [bogs]}}\n",
},
},
{
skipDoc: true,
document: `{}`,
expression: `.a.b[1].c |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: [null, {c: bogs}]}}\n",
},
},
}
func TestAssignOperatorScenarios(t *testing.T) {
for _, tt := range assignOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Assign", assignOperatorScenarios)
}

View File

@@ -1,84 +0,0 @@
package yqlib
import (
"testing"
)
var assignOperatorScenarios = []expressionScenario{
{
document: `{a: {b: apple}}`,
expression: `.a.b |= "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog}}\n",
},
},
{
document: `{a: {b: apple}}`,
expression: `.a.b | (. |= "frog")`,
expected: []string{
"D0, P[a b], (!!str)::frog\n",
},
},
{
document: `{a: {b: apple}}`,
expression: `.a.b |= 5`,
expected: []string{
"D0, P[], (doc)::{a: {b: 5}}\n",
},
},
{
document: `{a: {b: apple}}`,
expression: `.a.b |= 3.142`,
expected: []string{
"D0, P[], (doc)::{a: {b: 3.142}}\n",
},
},
{
document: `{a: {b: {g: foof}}}`,
expression: `.a |= .b`,
expected: []string{
"D0, P[], (doc)::{a: {g: foof}}\n",
},
},
{
document: `{a: {b: apple, c: cactus}}`,
expression: `.a[] | select(. == "apple") |= "frog"`,
expected: []string{
"D0, P[], (doc)::{a: {b: frog, c: cactus}}\n",
},
},
{
document: `[candy, apple, sandy]`,
expression: `.[] | select(. == "*andy") |= "bogs"`,
expected: []string{
"D0, P[], (doc)::[bogs, apple, bogs]\n",
},
},
{
document: `{}`,
expression: `.a.b |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: bogs}}\n",
},
},
{
document: `{}`,
expression: `.a.b[0] |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: [bogs]}}\n",
},
},
{
document: `{}`,
expression: `.a.b[1].c |= "bogs"`,
expected: []string{
"D0, P[], (doc)::{a: {b: [null, {c: bogs}]}}\n",
},
},
}
func TestAssignOperatorScenarios(t *testing.T) {
for _, tt := range assignOperatorScenarios {
testScenario(t, &tt)
}
}

View File

@@ -3,7 +3,7 @@ package yqlib
import ( import (
"container/list" "container/list"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func isTruthy(c *CandidateNode) (bool, error) { func isTruthy(c *CandidateNode) (bool, error) {
@@ -25,9 +25,42 @@ func isTruthy(c *CandidateNode) (bool, error) {
type boolOp func(bool, bool) bool type boolOp func(bool, bool) bool
func performBoolOp(results *list.List, lhs *list.List, rhs *list.List, op boolOp) error {
for lhsChild := lhs.Front(); lhsChild != nil; lhsChild = lhsChild.Next() {
lhsCandidate := lhsChild.Value.(*CandidateNode)
lhsTrue, errDecoding := isTruthy(lhsCandidate)
if errDecoding != nil {
return errDecoding
}
for rhsChild := rhs.Front(); rhsChild != nil; rhsChild = rhsChild.Next() {
rhsCandidate := rhsChild.Value.(*CandidateNode)
rhsTrue, errDecoding := isTruthy(rhsCandidate)
if errDecoding != nil {
return errDecoding
}
boolResult := createBooleanCandidate(lhsCandidate, op(lhsTrue, rhsTrue))
results.PushBack(boolResult)
}
}
return nil
}
func booleanOp(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, op boolOp) (*list.List, error) { func booleanOp(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode, op boolOp) (*list.List, error) {
var results = list.New() var results = list.New()
if matchingNodes.Len() == 0 {
lhs, err := d.GetMatchingNodes(list.New(), pathNode.Lhs)
if err != nil {
return nil, err
}
rhs, err := d.GetMatchingNodes(list.New(), pathNode.Rhs)
if err != nil {
return nil, err
}
return results, performBoolOp(results, lhs, rhs, op)
}
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
lhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Lhs) lhs, err := d.GetMatchingNodes(nodeToMap(candidate), pathNode.Lhs)
@@ -39,23 +72,9 @@ func booleanOp(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTre
return nil, err return nil, err
} }
for lhsChild := lhs.Front(); lhsChild != nil; lhsChild = lhsChild.Next() { err = performBoolOp(results, lhs, rhs, op)
lhsCandidate := lhsChild.Value.(*CandidateNode) if err != nil {
lhsTrue, errDecoding := isTruthy(lhsCandidate) return nil, err
if errDecoding != nil {
return nil, errDecoding
}
for rhsChild := rhs.Front(); rhsChild != nil; rhsChild = rhsChild.Next() {
rhsCandidate := rhsChild.Value.(*CandidateNode)
rhsTrue, errDecoding := isTruthy(rhsCandidate)
if errDecoding != nil {
return nil, errDecoding
}
boolResult := createBooleanCandidate(lhsCandidate, op(lhsTrue, rhsTrue))
results.PushBack(boolResult)
}
} }
} }
@@ -75,3 +94,20 @@ func AndOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathT
return b1 && b2 return b1 && b2
}) })
} }
func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- notOperation")
var results = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debug("notOperation checking %v", candidate)
truthy, errDecoding := isTruthy(candidate)
if errDecoding != nil {
return nil, errDecoding
}
result := createBooleanCandidate(candidate, !truthy)
results.PushBack(result)
}
return results, nil
}

View File

@@ -6,18 +6,36 @@ import (
var booleanOperatorScenarios = []expressionScenario{ var booleanOperatorScenarios = []expressionScenario{
{ {
document: `{}`, description: "OR example",
expression: `true or false`, expression: `true or false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
}, },
}, { },
document: `{}`, {
description: "AND example",
expression: `true and false`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
document: "[{a: bird, b: dog}, {a: frog, b: bird}, {a: cat, b: fly}]",
description: "Matching nodes with select, equals and or",
expression: `[.[] | select(.a == "cat" or .b == "dog")]`,
expected: []string{
"D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n",
},
},
{
skipDoc: true,
expression: `false or false`, expression: `false or false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
}, },
}, { },
{
skipDoc: true,
document: `{a: true, b: false}`, document: `{a: true, b: false}`,
expression: `.[] or (false, true)`, expression: `.[] or (false, true)`,
expected: []string{ expected: []string{
@@ -27,10 +45,61 @@ var booleanOperatorScenarios = []expressionScenario{
"D0, P[b], (!!bool)::true\n", "D0, P[b], (!!bool)::true\n",
}, },
}, },
{
description: "Not true is false",
expression: `true | not`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Not false is true",
expression: `false | not`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "String values considered to be true",
expression: `"cat" | not`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Empty string value considered to be true",
expression: `"" | not`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Numbers are considered to be true",
expression: `1 | not`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Zero is considered to be true",
expression: `0 | not`,
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
description: "Null is considered to be false",
expression: `~ | not`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
} }
func TestBooleanOperatorScenarios(t *testing.T) { func TestBooleanOperatorScenarios(t *testing.T) {
for _, tt := range booleanOperatorScenarios { for _, tt := range booleanOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Boolean Operators", booleanOperatorScenarios)
} }

View File

@@ -3,12 +3,18 @@ package yqlib
import ( import (
"container/list" "container/list"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func CollectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { func CollectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- collectOperation") log.Debugf("-- collectOperation")
if matchMap.Len() == 0 {
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Value: "[]"}
candidate := &CandidateNode{Node: node}
return nodeToMap(candidate), nil
}
var results = list.New() var results = list.New()
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}

View File

@@ -2,6 +2,8 @@ package yqlib
import ( import (
"container/list" "container/list"
yaml "gopkg.in/yaml.v3"
) )
/* /*
@@ -19,7 +21,9 @@ func CollectObjectOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *
log.Debugf("-- collectObjectOperation") log.Debugf("-- collectObjectOperation")
if matchMap.Len() == 0 { if matchMap.Len() == 0 {
return list.New(), nil node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Value: "{}"}
candidate := &CandidateNode{Node: node}
return nodeToMap(candidate), nil
} }
first := matchMap.Front().Value.(*CandidateNode) first := matchMap.Front().Value.(*CandidateNode)
var rotated []*list.List = make([]*list.List, len(first.Node.Content)) var rotated []*list.List = make([]*list.List, len(first.Node.Content))

View File

@@ -6,18 +6,23 @@ import (
var collectObjectOperatorScenarios = []expressionScenario{ var collectObjectOperatorScenarios = []expressionScenario{
{ {
document: ``, description: `Collect empty object`,
expression: `{}`, document: ``,
expected: []string{}, expression: `{}`,
expected: []string{
"D0, P[], (!!map)::{}\n",
},
}, },
{ {
document: "{name: Mike}\n", description: `Wrap (prefix) existing object`,
expression: `{"wrap": .}`, document: "{name: Mike}\n",
expression: `{"wrap": .}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::wrap: {name: Mike}\n", "D0, P[], (!!map)::wrap: {name: Mike}\n",
}, },
}, },
{ {
skipDoc: true,
document: "{name: Mike}\n---\n{name: Bob}", document: "{name: Mike}\n---\n{name: Bob}",
expression: `{"wrap": .}`, expression: `{"wrap": .}`,
expected: []string{ expected: []string{
@@ -26,6 +31,7 @@ var collectObjectOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{name: Mike, age: 32}`, document: `{name: Mike, age: 32}`,
expression: `{.name: .age}`, expression: `{.name: .age}`,
expected: []string{ expected: []string{
@@ -33,16 +39,19 @@ var collectObjectOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: `{name: Mike, pets: [cat, dog]}`, description: `Using splat to create multiple objects`,
expression: `{.name: .pets[]}`, document: `{name: Mike, pets: [cat, dog]}`,
expression: `{.name: .pets.[]}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::Mike: cat\n", "D0, P[], (!!map)::Mike: cat\n",
"D0, P[], (!!map)::Mike: dog\n", "D0, P[], (!!map)::Mike: dog\n",
}, },
}, },
{ {
document: "{name: Mike, pets: [cat, dog]}\n---\n{name: Rosey, pets: [monkey, sheep]}", description: `Working with multiple documents`,
expression: `{.name: .pets[]}`, dontFormatInputForDoc: false,
document: "{name: Mike, pets: [cat, dog]}\n---\n{name: Rosey, pets: [monkey, sheep]}",
expression: `{.name: .pets.[]}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::Mike: cat\n", "D0, P[], (!!map)::Mike: cat\n",
"D0, P[], (!!map)::Mike: dog\n", "D0, P[], (!!map)::Mike: dog\n",
@@ -51,8 +60,9 @@ var collectObjectOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{name: Mike, pets: [cat, dog], food: [hotdog, burger]}`, document: `{name: Mike, pets: [cat, dog], food: [hotdog, burger]}`,
expression: `{.name: .pets[], "f":.food[]}`, expression: `{.name: .pets.[], "f":.food.[]}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::Mike: cat\nf: hotdog\n", "D0, P[], (!!map)::Mike: cat\nf: hotdog\n",
"D0, P[], (!!map)::Mike: cat\nf: burger\n", "D0, P[], (!!map)::Mike: cat\nf: burger\n",
@@ -61,6 +71,7 @@ var collectObjectOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{name: Mike, pets: {cows: [apl, bba]}}`, document: `{name: Mike, pets: {cows: [apl, bba]}}`,
expression: `{"a":.name, "b":.pets}`, expression: `{"a":.name, "b":.pets}`,
expected: []string{ expected: []string{
@@ -70,13 +81,15 @@ b: {cows: [apl, bba]}
}, },
}, },
{ {
document: ``, description: "Creating yaml from scratch",
expression: `{"wrap": "frog"}`, document: ``,
expression: `{"wrap": "frog"}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::wrap: frog\n", "D0, P[], (!!map)::wrap: frog\n",
}, },
}, },
{ {
skipDoc: true,
document: `{name: Mike}`, document: `{name: Mike}`,
expression: `{"wrap": .}`, expression: `{"wrap": .}`,
expected: []string{ expected: []string{
@@ -84,6 +97,7 @@ b: {cows: [apl, bba]}
}, },
}, },
{ {
skipDoc: true,
document: `{name: Mike}`, document: `{name: Mike}`,
expression: `{"wrap": {"further": .}} | (.. style= "flow")`, expression: `{"wrap": {"further": .}} | (.. style= "flow")`,
expected: []string{ expected: []string{

View File

@@ -6,43 +6,56 @@ import (
var collectOperatorScenarios = []expressionScenario{ var collectOperatorScenarios = []expressionScenario{
{ {
document: ``, description: "Collect empty",
expression: `[]`, document: ``,
expected: []string{}, expression: `[]`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
}, },
{ {
document: ``, description: "Collect single",
expression: `["cat"]`, document: ``,
expression: `["cat"]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- cat\n", "D0, P[], (!!seq)::- cat\n",
}, },
}, { }, {
document: ``, document: ``,
skipDoc: true,
expression: `[true]`, expression: `[true]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- true\n", "D0, P[], (!!seq)::- true\n",
}, },
}, { },
document: ``, {
expression: `["cat", "dog"]`, description: "Collect many",
document: `{a: cat, b: dog}`,
expression: `[.a, .b]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- cat\n- dog\n", "D0, P[], (!!seq)::- cat\n- dog\n",
}, },
}, { },
{
document: ``, document: ``,
skipDoc: true,
expression: `1 | collect`, expression: `1 | collect`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- 1\n", "D0, P[], (!!seq)::- 1\n",
}, },
}, { },
{
document: `[1,2,3]`, document: `[1,2,3]`,
skipDoc: true,
expression: `[.[]]`, expression: `[.[]]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- 1\n- 2\n- 3\n", "D0, P[], (!!seq)::- 1\n- 2\n- 3\n",
}, },
}, { },
{
document: `a: {b: [1,2,3]}`, document: `a: {b: [1,2,3]}`,
expression: `[.a.b[]]`, expression: `[.a.b.[]]`,
skipDoc: true,
expected: []string{ expected: []string{
"D0, P[a b], (!!seq)::- 1\n- 2\n- 3\n", "D0, P[a b], (!!seq)::- 1\n- 2\n- 3\n",
}, },

View File

@@ -75,5 +75,5 @@ func TestCommentOperatorScenarios(t *testing.T) {
for _, tt := range commentOperatorScenarios { for _, tt := range commentOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Comments Operator", commentOperatorScenarios) documentScenarios(t, "Comment Operators", commentOperatorScenarios)
} }

View File

@@ -21,14 +21,14 @@ var createMapOperatorScenarios = []expressionScenario{
}, },
{ {
document: `{name: Mike, pets: [cat, dog]}`, document: `{name: Mike, pets: [cat, dog]}`,
expression: `.name: .pets[]`, expression: `.name: .pets.[]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n", "D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n",
}, },
}, },
{ {
document: `{name: Mike, pets: [cat, dog], food: [hotdog, burger]}`, document: `{name: Mike, pets: [cat, dog], food: [hotdog, burger]}`,
expression: `.name: .pets[], "f":.food[]`, expression: `.name: .pets.[], "f":.food.[]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n", "D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n",
"D0, P[], (!!seq)::- [{f: hotdog}, {f: burger}]\n", "D0, P[], (!!seq)::- [{f: hotdog}, {f: burger}]\n",
@@ -36,7 +36,7 @@ var createMapOperatorScenarios = []expressionScenario{
}, },
{ {
document: "{name: Mike, pets: [cat, dog], food: [hotdog, burger]}\n---\n{name: Fred, pets: [mouse], food: [pizza, onion, apple]}", document: "{name: Mike, pets: [cat, dog], food: [hotdog, burger]}\n---\n{name: Fred, pets: [mouse], food: [pizza, onion, apple]}",
expression: `.name: .pets[], "f":.food[]`, expression: `.name: .pets.[], "f":.food.[]`,
expected: []string{ expected: []string{
"D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n- [{Fred: mouse}]\n", "D0, P[], (!!seq)::- [{Mike: cat}, {Mike: dog}]\n- [{Fred: mouse}]\n",
"D0, P[], (!!seq)::- [{f: hotdog}, {f: burger}]\n- [{f: pizza}, {f: onion}, {f: apple}]\n", "D0, P[], (!!seq)::- [{f: hotdog}, {f: burger}]\n- [{f: pizza}, {f: onion}, {f: apple}]\n",

View File

@@ -3,19 +3,15 @@ package yqlib
import ( import (
"container/list" "container/list"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil {
return nil, err
}
// for each lhs, splat the node, // for each lhs, splat the node,
// the intersect it against the rhs expression // the intersect it against the rhs expression
// recreate the contents using only the intersection result. // recreate the contents using only the intersection result.
for el := lhs.Front(); el != nil; el = el.Next() { for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
elMap := list.New() elMap := list.New()
elMap.PushBack(candidate) elMap.PushBack(candidate)
@@ -25,20 +21,22 @@ func DeleteChildOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNod
return nil, err return nil, err
} }
if candidate.Node.Kind == yaml.SequenceNode { realNode := UnwrapDoc(candidate.Node)
if realNode.Kind == yaml.SequenceNode {
deleteFromArray(candidate, nodesToDelete) deleteFromArray(candidate, nodesToDelete)
} else if candidate.Node.Kind == yaml.MappingNode { } else if realNode.Kind == yaml.MappingNode {
deleteFromMap(candidate, nodesToDelete) deleteFromMap(candidate, nodesToDelete)
} else { } else {
log.Debug("Cannot delete from node that's not a map or array %v", NodeToString(candidate)) log.Debug("Cannot delete from node that's not a map or array %v", NodeToString(candidate))
} }
} }
return lhs, nil return matchingNodes, nil
} }
func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) { func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
log.Debug("deleteFromMap") log.Debug("deleteFromMap")
node := candidate.Node node := UnwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
newContents := make([]*yaml.Node, 0) newContents := make([]*yaml.Node, 0)
@@ -51,8 +49,13 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
Document: candidate.Document, Document: candidate.Document,
Path: append(candidate.Path, key.Value), Path: append(candidate.Path, key.Value),
} }
// _, shouldDelete := nodesToDelete.Get(childCandidate.GetKey())
shouldDelete := true shouldDelete := false
for el := nodesToDelete.Front(); el != nil && !shouldDelete; el = el.Next() {
if el.Value.(*CandidateNode).GetKey() == childCandidate.GetKey() {
shouldDelete = true
}
}
log.Debugf("shouldDelete %v ? %v", childCandidate.GetKey(), shouldDelete) log.Debugf("shouldDelete %v ? %v", childCandidate.GetKey(), shouldDelete)
@@ -65,21 +68,26 @@ func deleteFromMap(candidate *CandidateNode, nodesToDelete *list.List) {
func deleteFromArray(candidate *CandidateNode, nodesToDelete *list.List) { func deleteFromArray(candidate *CandidateNode, nodesToDelete *list.List) {
log.Debug("deleteFromArray") log.Debug("deleteFromArray")
node := candidate.Node node := UnwrapDoc(candidate.Node)
contents := node.Content contents := node.Content
newContents := make([]*yaml.Node, 0) newContents := make([]*yaml.Node, 0)
for index := 0; index < len(contents); index = index + 1 { for index := 0; index < len(contents); index = index + 1 {
value := contents[index] value := contents[index]
// childCandidate := &CandidateNode{ childCandidate := &CandidateNode{
// Node: value, Node: value,
// Document: candidate.Document, Document: candidate.Document,
// Path: append(candidate.Path, index), Path: append(candidate.Path, index),
// } }
shouldDelete := false
for el := nodesToDelete.Front(); el != nil && !shouldDelete; el = el.Next() {
if el.Value.(*CandidateNode).GetKey() == childCandidate.GetKey() {
shouldDelete = true
}
}
// _, shouldDelete := nodesToDelete.Get(childCandidate.GetKey())
shouldDelete := true
if !shouldDelete { if !shouldDelete {
newContents = append(newContents, value) newContents = append(newContents, value)
} }

View File

@@ -0,0 +1,47 @@
package yqlib
import (
"testing"
)
var deleteOperatorScenarios = []expressionScenario{
{
description: "Delete entry in map",
document: `{a: cat, b: dog}`,
expression: `del(.b)`,
expected: []string{
"D0, P[], (doc)::{a: cat}\n",
},
},
{
description: "Delete entry in array",
document: `[1,2,3]`,
expression: `del(.[1])`,
expected: []string{
"D0, P[], (doc)::[1, 3]\n",
},
},
{
description: "Delete no matches",
document: `{a: cat, b: dog}`,
expression: `del(.c)`,
expected: []string{
"D0, P[], (doc)::{a: cat, b: dog}\n",
},
},
{
description: "Delete matching entries",
document: `{a: cat, b: dog, c: bat}`,
expression: `del( .[] | select(. == "*at") )`,
expected: []string{
"D0, P[], (doc)::{b: dog}\n",
},
},
}
func TestDeleteOperatorScenarios(t *testing.T) {
for _, tt := range deleteOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Delete", deleteOperatorScenarios)
}

View File

@@ -37,5 +37,5 @@ func TestDocumentIndexScenarios(t *testing.T) {
for _, tt := range documentIndexScenarios { for _, tt := range documentIndexScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Document Index Operator", documentIndexScenarios) documentScenarios(t, "Document Index", documentIndexScenarios)
} }

View File

@@ -6,22 +6,25 @@ import (
var equalsOperatorScenarios = []expressionScenario{ var equalsOperatorScenarios = []expressionScenario{
{ {
document: `[cat,goat,dog]`, description: "Match string",
expression: `.[] | (. == "*at")`, document: `[cat,goat,dog]`,
expression: `.[] | (. == "*at")`,
expected: []string{ expected: []string{
"D0, P[0], (!!bool)::true\n", "D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n", "D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::false\n", "D0, P[2], (!!bool)::false\n",
}, },
}, { }, {
document: `[3, 4, 5]`, description: "Match number",
expression: `.[] | (. == 4)`, document: `[3, 4, 5]`,
expression: `.[] | (. == 4)`,
expected: []string{ expected: []string{
"D0, P[0], (!!bool)::false\n", "D0, P[0], (!!bool)::false\n",
"D0, P[1], (!!bool)::true\n", "D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::false\n", "D0, P[2], (!!bool)::false\n",
}, },
}, { }, {
skipDoc: true,
document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`, document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`,
expression: `.a | (.[].b == "apple")`, expression: `.a | (.[].b == "apple")`,
expected: []string{ expected: []string{
@@ -30,6 +33,7 @@ var equalsOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: ``, document: ``,
expression: `null == null`, expression: `null == null`,
expected: []string{ expected: []string{
@@ -37,8 +41,9 @@ var equalsOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: ``, description: "Match nulls",
expression: `null == ~`, document: ``,
expression: `null == ~`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
}, },
@@ -49,5 +54,5 @@ func TestEqualOperatorScenarios(t *testing.T) {
for _, tt := range equalsOperatorScenarios { for _, tt := range equalsOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Equals Operator", equalsOperatorScenarios) documentScenarios(t, "Equals", equalsOperatorScenarios)
} }

View File

@@ -6,27 +6,54 @@ import (
var explodeTest = []expressionScenario{ var explodeTest = []expressionScenario{
{ {
document: `{a: mike}`, description: "Explode alias and anchor",
expression: `explode(.a)`, document: `{f : {a: &a cat, b: *a}}`,
expected: []string{ expression: `explode(.f)`,
"D0, P[], (doc)::{a: mike}\n",
},
},
{
document: `{f : {a: &a cat, b: *a}}`,
expression: `explode(.f)`,
expected: []string{ expected: []string{
"D0, P[], (doc)::{f: {a: cat, b: cat}}\n", "D0, P[], (doc)::{f: {a: cat, b: cat}}\n",
}, },
}, },
{ {
document: `{f : {a: &a cat, *a: b}}`, description: "Explode with no aliases or anchors",
expression: `explode(.f)`, document: `a: mike`,
expression: `explode(.a)`,
expected: []string{
"D0, P[], (doc)::a: mike\n",
},
},
{
description: "Explode with alias keys",
document: `{f : {a: &a cat, *a: b}}`,
expression: `explode(.f)`,
expected: []string{ expected: []string{
"D0, P[], (doc)::{f: {a: cat, cat: b}}\n", "D0, P[], (doc)::{f: {a: cat, cat: b}}\n",
}, },
}, },
{ {
description: "Explode with merge anchors",
document: mergeDocSample,
expression: `explode(.)`,
expected: []string{`D0, P[], (doc)::foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: bar_b
a: foo_a
thing: bar_thing
c: foobarList_c
foobar:
c: foo_c
a: foo_a
thing: foobar_thing
`},
},
{
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foo* | explode(.) | (. style="flow")`, expression: `.foo* | explode(.) | (. style="flow")`,
expected: []string{ expected: []string{
@@ -36,6 +63,7 @@ var explodeTest = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foo* | explode(explode(.)) | (. style="flow")`, expression: `.foo* | explode(explode(.)) | (. style="flow")`,
expected: []string{ expected: []string{
@@ -45,6 +73,7 @@ var explodeTest = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{f : {a: &a cat, b: &b {f: *a}, *a: *b}}`, document: `{f : {a: &a cat, b: &b {f: *a}, *a: *b}}`,
expression: `explode(.f)`, expression: `explode(.f)`,
expected: []string{ expected: []string{
@@ -57,4 +86,5 @@ func TestExplodeOperatorScenarios(t *testing.T) {
for _, tt := range explodeTest { for _, tt := range explodeTest {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Explode", explodeTest)
} }

View File

@@ -0,0 +1,38 @@
package yqlib
import (
"container/list"
"fmt"
yaml "gopkg.in/yaml.v3"
)
func GetFilenameOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("GetFilename")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Filename, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}
func GetFileIndexOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("GetFileIndex")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.FileIndex), Tag: "!!int"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}

View File

@@ -0,0 +1,31 @@
package yqlib
import (
"testing"
)
var fileOperatorScenarios = []expressionScenario{
{
description: "Get filename",
document: `{a: cat}`,
expression: `filename`,
expected: []string{
"D0, P[], (!!str)::sample.yml\n",
},
},
{
description: "Get file index",
document: `{a: cat}`,
expression: `fileIndex`,
expected: []string{
"D0, P[], (!!int)::0\n",
},
},
}
func TestFileOperatorsScenarios(t *testing.T) {
for _, tt := range fileOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "File Operators", fileOperatorScenarios)
}

53
pkg/yqlib/operator_has.go Normal file
View File

@@ -0,0 +1,53 @@
package yqlib
import (
"container/list"
"strconv"
yaml "gopkg.in/yaml.v3"
)
func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- hasOperation")
var results = list.New()
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
wanted := rhs.Front().Value.(*CandidateNode).Node
wantedKey := wanted.Value
if err != nil {
return nil, err
}
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
// grab the first value
var contents = candidate.Node.Content
switch candidate.Node.Kind {
case yaml.MappingNode:
candidateHasKey := false
for index := 0; index < len(contents) && !candidateHasKey; index = index + 2 {
key := contents[index]
if key.Value == wantedKey {
candidateHasKey = true
}
}
results.PushBack(createBooleanCandidate(candidate, candidateHasKey))
case yaml.SequenceNode:
candidateHasKey := false
if wanted.Tag == "!!int" {
var number, errParsingInt = strconv.ParseInt(wantedKey, 10, 64) // nolint
if errParsingInt != nil {
return nil, errParsingInt
}
candidateHasKey = int64(len(contents)) > number
}
results.PushBack(createBooleanCandidate(candidate, candidateHasKey))
default:
results.PushBack(createBooleanCandidate(candidate, false))
}
}
return results, nil
}

View File

@@ -0,0 +1,48 @@
package yqlib
import (
"testing"
)
var hasOperatorScenarios = []expressionScenario{
{
description: "Has map key",
document: `- a: "yes"
- a: ~
- a:
- b: nope
`,
expression: `.[] | has("a")`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::true\n",
"D0, P[3], (!!bool)::false\n",
},
},
{
dontFormatInputForDoc: true,
description: "Has array index",
document: `- []
- [1]
- [1, 2]
- [1, null]
- [1, 2, 3]
`,
expression: `.[] | has(1)`,
expected: []string{
"D0, P[0], (!!bool)::false\n",
"D0, P[1], (!!bool)::false\n",
"D0, P[2], (!!bool)::true\n",
"D0, P[3], (!!bool)::true\n",
"D0, P[4], (!!bool)::true\n",
},
},
}
func TestHasOperatorScenarios(t *testing.T) {
for _, tt := range hasOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Has", hasOperatorScenarios)
}

View File

@@ -5,7 +5,7 @@ import (
"container/list" "container/list"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
type CrossFunctionCalculation func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) type CrossFunctionCalculation func(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error)
@@ -15,12 +15,14 @@ func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, pathNode *Pat
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("crossFunction LHS len: %v", lhs.Len())
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("crossFunction RHS len: %v", rhs.Len())
var results = list.New() var results = list.New()
@@ -28,6 +30,7 @@ func crossFunction(d *dataTreeNavigator, matchingNodes *list.List, pathNode *Pat
lhsCandidate := el.Value.(*CandidateNode) lhsCandidate := el.Value.(*CandidateNode)
for rightEl := rhs.Front(); rightEl != nil; rightEl = rightEl.Next() { for rightEl := rhs.Front(); rightEl != nil; rightEl = rightEl.Next() {
log.Debugf("Applying calc")
rhsCandidate := rightEl.Value.(*CandidateNode) rhsCandidate := rightEl.Value.(*CandidateNode)
resultCandidate, err := calculation(d, lhsCandidate, rhsCandidate) resultCandidate, err := calculation(d, lhsCandidate, rhsCandidate)
if err != nil { if err != nil {
@@ -48,8 +51,8 @@ func MultiplyOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *
func multiply(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func multiply(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
lhs.Node = UnwrapDoc(lhs.Node) lhs.Node = UnwrapDoc(lhs.Node)
rhs.Node = UnwrapDoc(rhs.Node) rhs.Node = UnwrapDoc(rhs.Node)
log.Debugf("Multipling LHS: %v", NodeToString(lhs)) log.Debugf("Multipling LHS: %v", lhs.Node.Tag)
log.Debugf("- RHS: %v", NodeToString(rhs)) log.Debugf("- RHS: %v", rhs.Node.Tag)
if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode || if lhs.Node.Kind == yaml.MappingNode && rhs.Node.Kind == yaml.MappingNode ||
(lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) { (lhs.Node.Kind == yaml.SequenceNode && rhs.Node.Kind == yaml.SequenceNode) {
@@ -67,7 +70,7 @@ func multiply(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*Ca
return mergeObjects(d, newThing, rhs) return mergeObjects(d, newThing, rhs)
} }
return nil, fmt.Errorf("Cannot multiply %v with %v", NodeToString(lhs), NodeToString(rhs)) return nil, fmt.Errorf("Cannot multiply %v with %v", lhs.Node.Tag, rhs.Node.Tag)
} }
func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { func mergeObjects(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
@@ -112,6 +115,7 @@ func applyAssignment(d *dataTreeNavigator, pathIndexToStartFrom int, lhs *Candid
assignmentOp := &Operation{OperationType: AssignAttributes} assignmentOp := &Operation{OperationType: AssignAttributes}
if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode { if rhs.Node.Kind == yaml.ScalarNode || rhs.Node.Kind == yaml.AliasNode {
assignmentOp.OperationType = Assign assignmentOp.OperationType = Assign
assignmentOp.Preferences = &AssignOpPreferences{false}
} }
rhsOp := &Operation{OperationType: ValueOp, CandidateNode: rhs} rhsOp := &Operation{OperationType: ValueOp, CandidateNode: rhs}

View File

@@ -22,11 +22,19 @@ var multiplyOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "Merge objects together", description: "Merge objects together, returning merged result only",
document: `{a: {also: me}, b: {also: {g: wizz}}}`, document: `{a: {field: me, fieldA: cat}, b: {field: {g: wizz}, fieldB: dog}}`,
expression: `.a * .b`,
expected: []string{
"D0, P[a], (!!map)::{field: {g: wizz}, fieldA: cat, fieldB: dog}\n",
},
},
{
description: "Merge objects together, returning parent object",
document: `{a: {field: me, fieldA: cat}, b: {field: {g: wizz}, fieldB: dog}}`,
expression: `. * {"a":.b}`, expression: `. * {"a":.b}`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: {also: {g: wizz}}, b: {also: {g: wizz}}}\n", "D0, P[], (!!map)::{a: {field: {g: wizz}, fieldA: cat, fieldB: dog}, b: {field: {g: wizz}, fieldB: dog}}\n",
}, },
}, },
{ {
@@ -62,7 +70,8 @@ var multiplyOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "Merge keeps style of LHS", description: "Merge keeps style of LHS",
dontFormatInputForDoc: true,
document: `a: {things: great} document: `a: {things: great}
b: b:
also: "me" also: "me"
@@ -121,5 +130,5 @@ func TestMultiplyOperatorScenarios(t *testing.T) {
for _, tt := range multiplyOperatorScenarios { for _, tt := range multiplyOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Mulitply Operator", multiplyOperatorScenarios) documentScenarios(t, "Multiply", multiplyOperatorScenarios)
} }

View File

@@ -1,20 +0,0 @@
package yqlib
import "container/list"
func NotOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("-- notOperation")
var results = list.New()
for el := matchMap.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debug("notOperation checking %v", candidate)
truthy, errDecoding := isTruthy(candidate)
if errDecoding != nil {
return nil, errDecoding
}
result := createBooleanCandidate(candidate, !truthy)
results.PushBack(result)
}
return results, nil
}

View File

@@ -1,56 +0,0 @@
package yqlib
import (
"testing"
)
var notOperatorScenarios = []expressionScenario{
// {
// document: `cat`,
// expression: `. | not`,
// expected: []string{
// "D0, P[], (!!bool)::false\n",
// },
// },
// {
// document: `1`,
// expression: `. | not`,
// expected: []string{
// "D0, P[], (!!bool)::false\n",
// },
// },
// {
// document: `0`,
// expression: `. | not`,
// expected: []string{
// "D0, P[], (!!bool)::false\n",
// },
// },
{
document: `~`,
expression: `. | not`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
// {
// document: `false`,
// expression: `. | not`,
// expected: []string{
// "D0, P[], (!!bool)::true\n",
// },
// },
// {
// document: `true`,
// expression: `. | not`,
// expected: []string{
// "D0, P[], (!!bool)::false\n",
// },
// },
}
func TestNotOperatorScenarios(t *testing.T) {
for _, tt := range notOperatorScenarios {
testScenario(t, &tt)
}
}

View File

@@ -0,0 +1,39 @@
package yqlib
import (
"container/list"
"fmt"
yaml "gopkg.in/yaml.v3"
)
func createPathNodeFor(pathElement interface{}) *yaml.Node {
switch pathElement := pathElement.(type) {
case string:
return &yaml.Node{Kind: yaml.ScalarNode, Value: pathElement, Tag: "!!str"}
default:
return &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", pathElement), Tag: "!!int"}
}
}
func GetPathOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("GetPath")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
content := make([]*yaml.Node, len(candidate.Path))
for pathIndex := 0; pathIndex < len(candidate.Path); pathIndex++ {
path := candidate.Path[pathIndex]
content[pathIndex] = createPathNodeFor(path)
}
node.Content = content
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}

View File

@@ -0,0 +1,45 @@
package yqlib
import (
"testing"
)
var pathOperatorScenarios = []expressionScenario{
{
description: "Map path",
document: `{a: {b: cat}}`,
expression: `.a.b | path`,
expected: []string{
"D0, P[a b], (!!seq)::- a\n- b\n",
},
},
{
description: "Array path",
document: `{a: [cat, dog]}`,
expression: `.a.[] | select(. == "dog") | path`,
expected: []string{
"D0, P[a 1], (!!seq)::- a\n- 1\n",
},
},
{
description: "Print path and value",
document: `{a: [cat, dog, frog]}`,
expression: `.a.[] | select(. == "*og") | [{"path":path, "value":.}]`,
expected: []string{`D0, P[], (!!seq)::- path:
- a
- 1
value: dog
- path:
- a
- 2
value: frog
`},
},
}
func TestPathOperatorsScenarios(t *testing.T) {
for _, tt := range pathOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Path", pathOperatorScenarios)
}

View File

@@ -6,6 +6,7 @@ import (
var recursiveDescentOperatorScenarios = []expressionScenario{ var recursiveDescentOperatorScenarios = []expressionScenario{
{ {
skipDoc: true,
document: `cat`, document: `cat`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -13,6 +14,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{a: frog}`, document: `{a: frog}`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -21,6 +23,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{a: {b: apple}}`, document: `{a: {b: apple}}`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -30,6 +33,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `[1,2,3]`, document: `[1,2,3]`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -40,6 +44,7 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `[{a: cat},2,true]`, document: `[{a: cat},2,true]`,
expression: `..`, expression: `..`,
expected: []string{ expected: []string{
@@ -51,26 +56,23 @@ var recursiveDescentOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: `{a: &cat {c: frog}, b: *cat}`, description: "Aliases are not traversed",
expression: `..`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `[..]`,
expected: []string{ expected: []string{
"D0, P[], (!!map)::{a: &cat {c: frog}, b: *cat}\n", "D0, P[a], (!!seq)::- {a: &cat {c: frog}, b: *cat}\n- &cat {c: frog}\n- frog\n- *cat\n",
"D0, P[a], (!!map)::&cat {c: frog}\n",
"D0, P[a c], (!!str)::frog\n",
"D0, P[b], (alias)::*cat\n",
}, },
}, },
{ {
document: mergeDocSample, description: "Merge docs are not traversed",
expression: `.foobar | ..`, document: mergeDocSample,
expression: `.foobar | [..]`,
expected: []string{ expected: []string{
"D0, P[foobar], (!!map)::c: foobar_c\n!!merge <<: *foo\nthing: foobar_thing\n", "D0, P[foobar], (!!seq)::- c: foobar_c\n !!merge <<: *foo\n thing: foobar_thing\n- foobar_c\n- *foo\n- foobar_thing\n",
"D0, P[foobar c], (!!str)::foobar_c\n",
"D0, P[foobar <<], (alias)::*foo\n",
"D0, P[foobar thing], (!!str)::foobar_thing\n",
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList | ..`, expression: `.foobarList | ..`,
expected: []string{ expected: []string{
@@ -88,4 +90,5 @@ func TestRecursiveDescentOperatorScenarios(t *testing.T) {
for _, tt := range recursiveDescentOperatorScenarios { for _, tt := range recursiveDescentOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Recursive Descent", recursiveDescentOperatorScenarios)
} }

View File

@@ -6,36 +6,47 @@ import (
var selectOperatorScenarios = []expressionScenario{ var selectOperatorScenarios = []expressionScenario{
{ {
document: `[cat,goat,dog]`, description: "Select elements from array",
expression: `.[] | select(. == "*at")`, document: `[cat,goat,dog]`,
expression: `.[] | select(. == "*at")`,
expected: []string{ expected: []string{
"D0, P[0], (!!str)::cat\n", "D0, P[0], (!!str)::cat\n",
"D0, P[1], (!!str)::goat\n", "D0, P[1], (!!str)::goat\n",
}, },
}, { },
{
skipDoc: true,
document: `[hot, fot, dog]`, document: `[hot, fot, dog]`,
expression: `.[] | select(. == "*at")`, expression: `.[] | select(. == "*at")`,
expected: []string{}, expected: []string{},
}, { },
{
skipDoc: true,
document: `a: [cat,goat,dog]`, document: `a: [cat,goat,dog]`,
expression: `.a[] | select(. == "*at")`, expression: `.a.[] | select(. == "*at")`,
expected: []string{ expected: []string{
"D0, P[a 0], (!!str)::cat\n", "D0, P[a 0], (!!str)::cat\n",
"D0, P[a 1], (!!str)::goat\n"}, "D0, P[a 1], (!!str)::goat\n"},
}, { },
document: `a: { things: cat, bob: goat, horse: dog }`, {
expression: `.a[] | select(. == "*at")`, description: "Select and update matching values in map",
document: `a: { things: cat, bob: goat, horse: dog }`,
expression: `(.a.[] | select(. == "*at")) |= "rabbit"`,
expected: []string{ expected: []string{
"D0, P[a things], (!!str)::cat\n", "D0, P[], (doc)::a: {things: rabbit, bob: rabbit, horse: dog}\n",
"D0, P[a bob], (!!str)::goat\n"}, },
}, { },
{
skipDoc: true,
document: `a: { things: {include: true}, notMe: {include: false}, andMe: {include: fold} }`, document: `a: { things: {include: true}, notMe: {include: false}, andMe: {include: fold} }`,
expression: `.a[] | select(.include)`, expression: `.a.[] | select(.include)`,
expected: []string{ expected: []string{
"D0, P[a things], (!!map)::{include: true}\n", "D0, P[a things], (!!map)::{include: true}\n",
"D0, P[a andMe], (!!map)::{include: fold}\n", "D0, P[a andMe], (!!map)::{include: fold}\n",
}, },
}, { },
{
skipDoc: true,
document: `[cat,~,dog]`, document: `[cat,~,dog]`,
expression: `.[] | select(. == ~)`, expression: `.[] | select(. == ~)`,
expected: []string{ expected: []string{
@@ -48,4 +59,5 @@ func TestSelectOperatorScenarios(t *testing.T) {
for _, tt := range selectOperatorScenarios { for _, tt := range selectOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Select", selectOperatorScenarios)
} }

View File

@@ -0,0 +1,116 @@
package yqlib
import (
"testing"
)
var styleOperatorScenarios = []expressionScenario{
{
description: "Set tagged style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="tagged"`,
expected: []string{
"D0, P[], (!!map)::!!map\na: !!str cat\nb: !!int 5\nc: !!float 3.2\ne: !!bool true\n",
},
},
{
description: "Set double quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="double"`,
expected: []string{
"D0, P[], (!!map)::a: \"cat\"\nb: \"5\"\nc: \"3.2\"\ne: \"true\"\n",
},
},
{
description: "Set single quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="single"`,
expected: []string{
"D0, P[], (!!map)::a: 'cat'\nb: '5'\nc: '3.2'\ne: 'true'\n",
},
},
{
description: "Set literal quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="literal"`,
expected: []string{
`D0, P[], (!!map)::a: |-
cat
b: |-
5
c: |-
3.2
e: |-
true
`,
},
},
{
description: "Set folded quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="folded"`,
expected: []string{
`D0, P[], (!!map)::a: >-
cat
b: >-
5
c: >-
3.2
e: >-
true
`,
},
},
{
description: "Set flow quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style="flow"`,
expected: []string{
"D0, P[], (!!map)::{a: cat, b: 5, c: 3.2, e: true}\n",
},
},
{
description: "Pretty print",
subdescription: "Set empty (default) quote style",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `.. style=""`,
expected: []string{
"D0, P[], (!!map)::a: cat\nb: 5\nc: 3.2\ne: true\n",
},
},
{
skipDoc: true,
document: `{a: cat, b: double}`,
expression: `.a style=.b`,
expected: []string{
"D0, P[], (doc)::{a: \"cat\", b: double}\n",
},
},
{
description: "Read style",
document: `{a: "cat", b: 'thing'}`,
dontFormatInputForDoc: true,
expression: `.. | style`,
expected: []string{
"D0, P[], (!!str)::flow\n",
"D0, P[a], (!!str)::double\n",
"D0, P[b], (!!str)::single\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: `.. | style`,
expected: []string{
"D0, P[], (!!str)::\"\"\n",
"D0, P[a], (!!str)::\"\"\n",
},
},
}
func TestStyleOperatorScenarios(t *testing.T) {
for _, tt := range styleOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Style", styleOperatorScenarios)
}

51
pkg/yqlib/operator_tag.go Normal file
View File

@@ -0,0 +1,51 @@
package yqlib
import (
"container/list"
"gopkg.in/yaml.v3"
)
func AssignTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("AssignTagOperator: %v")
rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs)
if err != nil {
return nil, err
}
tag := ""
if rhs.Front() != nil {
tag = rhs.Front().Value.(*CandidateNode).Node.Value
}
lhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Lhs)
if err != nil {
return nil, err
}
for el := lhs.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
log.Debugf("Setting tag of : %v", candidate.GetKey())
candidate.Node.Tag = tag
}
return matchingNodes, nil
}
func GetTagOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) {
log.Debugf("GetTagOperator")
var results = list.New()
for el := matchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
node := &yaml.Node{Kind: yaml.ScalarNode, Value: candidate.Node.Tag, Tag: "!!str"}
lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path}
results.PushBack(lengthCand)
}
return results, nil
}

View File

@@ -0,0 +1,36 @@
package yqlib
import (
"testing"
)
var tagOperatorScenarios = []expressionScenario{
{
description: "Get tag",
document: `{a: cat, b: 5, c: 3.2, e: true, f: []}`,
expression: `.. | tag`,
expected: []string{
"D0, P[], (!!str)::'!!map'\n",
"D0, P[a], (!!str)::'!!str'\n",
"D0, P[b], (!!str)::'!!int'\n",
"D0, P[c], (!!str)::'!!float'\n",
"D0, P[e], (!!str)::'!!bool'\n",
"D0, P[f], (!!str)::'!!seq'\n",
},
},
{
description: "Convert numbers to strings",
document: `{a: cat, b: 5, c: 3.2, e: true}`,
expression: `(.. | select(tag == "!!int")) tag= "!!str"`,
expected: []string{
"D0, P[], (!!map)::{a: cat, b: \"5\", c: 3.2, e: true}\n",
},
},
}
func TestTagOperatorScenarios(t *testing.T) {
for _, tt := range tagOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Tag", tagOperatorScenarios)
}

View File

@@ -28,28 +28,43 @@ foobar:
var traversePathOperatorScenarios = []expressionScenario{ var traversePathOperatorScenarios = []expressionScenario{
{ {
document: `{a: {b: apple}}`, description: "Simple map navigation",
expression: `.a`, document: `{a: {b: apple}}`,
expression: `.a`,
expected: []string{ expected: []string{
"D0, P[a], (!!map)::{b: apple}\n", "D0, P[a], (!!map)::{b: apple}\n",
}, },
}, },
{ {
document: `[{b: apple}, {c: banana}]`, description: "Splat",
expression: `.[]`, subdescription: "Often used to pipe children into other operators",
document: `[{b: apple}, {c: banana}]`,
expression: `.[]`,
expected: []string{ expected: []string{
"D0, P[0], (!!map)::{b: apple}\n", "D0, P[0], (!!map)::{b: apple}\n",
"D0, P[1], (!!map)::{c: banana}\n", "D0, P[1], (!!map)::{c: banana}\n",
}, },
}, },
{ {
document: `{}`, description: "Special characters",
expression: `.a.b`, subdescription: "Use quotes around path elements with special characters",
document: `{"{}": frog}`,
expression: `."{}"`,
expected: []string{
"D0, P[{}], (!!str)::frog\n",
},
},
{
description: "Children don't exist",
subdescription: "Nodes are added dynamically while traversing",
document: `{c: banana}`,
expression: `.a.b`,
expected: []string{ expected: []string{
"D0, P[a b], (!!null)::null\n", "D0, P[a b], (!!null)::null\n",
}, },
}, },
{ {
skipDoc: true,
document: `{}`, document: `{}`,
expression: `.[1].a`, expression: `.[1].a`,
expected: []string{ expected: []string{
@@ -57,6 +72,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{}`, document: `{}`,
expression: `.a.[1]`, expression: `.a.[1]`,
expected: []string{ expected: []string{
@@ -64,14 +80,16 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: `{a: {cat: apple, mad: things}}`, description: "Wildcard matching",
expression: `.a."*a*"`, document: `{a: {cat: apple, mad: things}}`,
expression: `.a."*a*"`,
expected: []string{ expected: []string{
"D0, P[a cat], (!!str)::apple\n", "D0, P[a cat], (!!str)::apple\n",
"D0, P[a mad], (!!str)::things\n", "D0, P[a mad], (!!str)::things\n",
}, },
}, },
{ {
skipDoc: true,
document: `{a: {cat: {b: 3}, mad: {b: 4}, fad: {c: t}}}`, document: `{a: {cat: {b: 3}, mad: {b: 4}, fad: {c: t}}}`,
expression: `.a."*a*".b`, expression: `.a."*a*".b`,
expected: []string{ expected: []string{
@@ -81,6 +99,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{a: {cat: apple, mad: things}}`, document: `{a: {cat: apple, mad: things}}`,
expression: `.a | (.cat, .mad)`, expression: `.a | (.cat, .mad)`,
expected: []string{ expected: []string{
@@ -89,6 +108,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{a: {cat: apple, mad: things}}`, document: `{a: {cat: apple, mad: things}}`,
expression: `.a | (.cat, .mad, .fad)`, expression: `.a | (.cat, .mad, .fad)`,
expected: []string{ expected: []string{
@@ -98,6 +118,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: `{a: {cat: apple, mad: things}}`, document: `{a: {cat: apple, mad: things}}`,
expression: `.a | (.cat, .mad, .fad) | select( (. == null) | not)`, expression: `.a | (.cat, .mad, .fad) | select( (. == null) | not)`,
expected: []string{ expected: []string{
@@ -106,40 +127,53 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: `{a: &cat {c: frog}, b: *cat}`, description: "Aliases",
expression: `.b`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b`,
expected: []string{ expected: []string{
"D0, P[b], (alias)::*cat\n", "D0, P[b], (alias)::*cat\n",
}, },
}, },
{ {
document: `{a: &cat {c: frog}, b: *cat}`, description: "Traversing aliases with splat",
expression: `.b.[]`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.[]`,
expected: []string{ expected: []string{
"D0, P[b c], (!!str)::frog\n", "D0, P[b c], (!!str)::frog\n",
}, },
}, },
{ {
document: `{a: &cat {c: frog}, b: *cat}`, description: "Traversing aliases explicitly",
expression: `.b.c`, document: `{a: &cat {c: frog}, b: *cat}`,
expression: `.b.c`,
expected: []string{ expected: []string{
"D0, P[b c], (!!str)::frog\n", "D0, P[b c], (!!str)::frog\n",
}, },
}, },
{ {
skipDoc: true,
document: `[1,2,3]`, document: `[1,2,3]`,
expression: `.b`, expression: `.b`,
expected: []string{}, expected: []string{},
}, },
{ {
document: `[1,2,3]`, description: "Traversing arrays by index",
expression: `[0]`, document: `[1,2,3]`,
expression: `[0]`,
expected: []string{ expected: []string{
"D0, P[0], (!!int)::1\n", "D0, P[0], (!!int)::1\n",
}, },
}, },
{ {
description: `Maps can have numbers as keys, so this default to a non-exisiting key behaviour.`, description: "Maps with numeric keys",
document: `{2: cat}`,
expression: `[2]`,
expected: []string{
"D0, P[2], (!!str)::cat\n",
},
},
{
description: "Maps with non existing numeric keys",
document: `{a: b}`, document: `{a: b}`,
expression: `[0]`, expression: `[0]`,
expected: []string{ expected: []string{
@@ -147,6 +181,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobar`, expression: `.foobar`,
expected: []string{ expected: []string{
@@ -154,29 +189,33 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: mergeDocSample, description: "Traversing merge anchors",
expression: `.foobar.a`, document: mergeDocSample,
expression: `.foobar.a`,
expected: []string{ expected: []string{
"D0, P[foobar a], (!!str)::foo_a\n", "D0, P[foobar a], (!!str)::foo_a\n",
}, },
}, },
{ {
document: mergeDocSample, description: "Traversing merge anchors with override",
expression: `.foobar.c`, document: mergeDocSample,
expression: `.foobar.c`,
expected: []string{ expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n", "D0, P[foobar c], (!!str)::foo_c\n",
}, },
}, },
{ {
document: mergeDocSample, description: "Traversing merge anchors with local override",
expression: `.foobar.thing`, document: mergeDocSample,
expression: `.foobar.thing`,
expected: []string{ expected: []string{
"D0, P[foobar thing], (!!str)::foobar_thing\n", "D0, P[foobar thing], (!!str)::foobar_thing\n",
}, },
}, },
{ {
document: mergeDocSample, description: "Splatting merge anchors",
expression: `.foobar.[]`, document: mergeDocSample,
expression: `.foobar.[]`,
expected: []string{ expected: []string{
"D0, P[foobar c], (!!str)::foo_c\n", "D0, P[foobar c], (!!str)::foo_c\n",
"D0, P[foobar a], (!!str)::foo_a\n", "D0, P[foobar a], (!!str)::foo_a\n",
@@ -184,6 +223,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList`, expression: `.foobarList`,
expected: []string{ expected: []string{
@@ -191,6 +231,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList.a`, expression: `.foobarList.a`,
expected: []string{ expected: []string{
@@ -198,13 +239,16 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: mergeDocSample, description: "Traversing merge anchor lists",
expression: `.foobarList.thing`, subdescription: "Note that the later merge anchors override previous",
document: mergeDocSample,
expression: `.foobarList.thing`,
expected: []string{ expected: []string{
"D0, P[foobarList thing], (!!str)::bar_thing\n", "D0, P[foobarList thing], (!!str)::bar_thing\n",
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList.c`, expression: `.foobarList.c`,
expected: []string{ expected: []string{
@@ -212,6 +256,7 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
skipDoc: true,
document: mergeDocSample, document: mergeDocSample,
expression: `.foobarList.b`, expression: `.foobarList.b`,
expected: []string{ expected: []string{
@@ -219,8 +264,9 @@ var traversePathOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
document: mergeDocSample, description: "Splatting merge anchor lists",
expression: `.foobarList.[]`, document: mergeDocSample,
expression: `.foobarList.[]`,
expected: []string{ expected: []string{
"D0, P[foobarList b], (!!str)::bar_b\n", "D0, P[foobarList b], (!!str)::bar_b\n",
"D0, P[foobarList a], (!!str)::foo_a\n", "D0, P[foobarList a], (!!str)::foo_a\n",
@@ -234,4 +280,5 @@ func TestTraversePathOperatorScenarios(t *testing.T) {
for _, tt := range traversePathOperatorScenarios { for _, tt := range traversePathOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Traverse", traversePathOperatorScenarios)
} }

View File

@@ -6,20 +6,21 @@ import (
var unionOperatorScenarios = []expressionScenario{ var unionOperatorScenarios = []expressionScenario{
{ {
document: `{}`, description: "Combine scalars",
expression: `"cat", "dog"`, expression: `1, true, "cat"`,
expected: []string{
"D0, P[], (!!str)::cat\n",
"D0, P[], (!!str)::dog\n",
},
}, {
document: `{a: frog}`,
expression: `1, true, "cat", .a`,
expected: []string{ expected: []string{
"D0, P[], (!!int)::1\n", "D0, P[], (!!int)::1\n",
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
"D0, P[], (!!str)::cat\n", "D0, P[], (!!str)::cat\n",
"D0, P[a], (!!str)::frog\n", },
},
{
description: "Combine selected paths",
document: `{a: fieldA, b: fieldB, c: fieldC}`,
expression: `.a, .c`,
expected: []string{
"D0, P[a], (!!str)::fieldA\n",
"D0, P[c], (!!str)::fieldC\n",
}, },
}, },
} }
@@ -28,4 +29,5 @@ func TestUnionOperatorScenarios(t *testing.T) {
for _, tt := range unionOperatorScenarios { for _, tt := range unionOperatorScenarios {
testScenario(t, &tt) testScenario(t, &tt)
} }
documentScenarios(t, "Union", unionOperatorScenarios)
} }

View File

@@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"container/list" "container/list"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"testing" "testing"
@@ -13,11 +14,13 @@ import (
) )
type expressionScenario struct { type expressionScenario struct {
description string description string
document string subdescription string
expression string document string
expected []string expression string
skipDoc bool expected []string
skipDoc bool
dontFormatInputForDoc bool // dont format input doc for documentation generation
} }
func testScenario(t *testing.T, s *expressionScenario) { func testScenario(t *testing.T, s *expressionScenario) {
@@ -26,13 +29,13 @@ func testScenario(t *testing.T, s *expressionScenario) {
node, err := treeCreator.ParsePath(s.expression) node, err := treeCreator.ParsePath(s.expression)
if err != nil { if err != nil {
t.Error(err) t.Error(fmt.Errorf("Error parsing expression %v of %v: %v", s.expression, s.description, err))
return return
} }
inputs := list.New() inputs := list.New()
if s.document != "" { if s.document != "" {
inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml") inputs, err = readDocuments(strings.NewReader(s.document), "sample.yml", 0)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -48,6 +51,15 @@ func testScenario(t *testing.T, s *expressionScenario) {
test.AssertResultComplexWithContext(t, s.expected, resultsToString(results), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document)) test.AssertResultComplexWithContext(t, s.expected, resultsToString(results), fmt.Sprintf("exp: %v\ndoc: %v", s.expression, s.document))
} }
func resultsToString(results *list.List) []string {
var pretty []string = make([]string, 0)
for el := results.Front(); el != nil; el = el.Next() {
n := el.Value.(*CandidateNode)
pretty = append(pretty, NodeToString(n))
}
return pretty
}
func writeOrPanic(w *bufio.Writer, text string) { func writeOrPanic(w *bufio.Writer, text string) {
_, err := w.WriteString(text) _, err := w.WriteString(text)
if err != nil { if err != nil {
@@ -55,35 +67,87 @@ func writeOrPanic(w *bufio.Writer, text string) {
} }
} }
func copyFromHeader(title string, out *os.File) error {
source := fmt.Sprintf("doc/headers/%v.md", title)
_, err := os.Stat(source)
if os.IsNotExist(err) {
return nil
}
in, err := os.Open(source) // nolint gosec
if err != nil {
return err
}
defer safelyCloseFile(in)
_, err = io.Copy(out, in)
return err
}
func formatYaml(yaml string) string {
var output bytes.Buffer
printer := NewPrinter(bufio.NewWriter(&output), false, true, false, 2, true)
node, err := treeCreator.ParsePath(".. style= \"\"")
if err != nil {
panic(err)
}
streamEvaluator := NewStreamEvaluator()
err = streamEvaluator.Evaluate("sample.yaml", strings.NewReader(yaml), node, printer)
if err != nil {
panic(err)
}
return output.String()
}
func documentScenarios(t *testing.T, title string, scenarios []expressionScenario) { func documentScenarios(t *testing.T, title string, scenarios []expressionScenario) {
f, err := os.Create(fmt.Sprintf("doc/%v.md", title)) f, err := os.Create(fmt.Sprintf("doc/%v.md", title))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return
} }
defer f.Close() defer f.Close()
w := bufio.NewWriter(f)
writeOrPanic(w, fmt.Sprintf("# %v\n", title))
writeOrPanic(w, "## Examples\n")
for index, s := range scenarios { err = copyFromHeader(title, f)
if err != nil {
t.Error(err)
return
}
w := bufio.NewWriter(f)
writeOrPanic(w, "\n")
for _, s := range scenarios {
if !s.skipDoc { if !s.skipDoc {
if s.description != "" { writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
writeOrPanic(w, fmt.Sprintf("### %v\n", s.description))
} else { if s.subdescription != "" {
writeOrPanic(w, fmt.Sprintf("### Example %v\n", index)) writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
} }
formattedDoc := ""
if s.document != "" { if s.document != "" {
writeOrPanic(w, "sample.yml:\n") if s.dontFormatInputForDoc {
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.document)) formattedDoc = s.document + "\n"
} } else {
if s.expression != "" { formattedDoc = formatYaml(s.document)
writeOrPanic(w, "Expression\n") }
writeOrPanic(w, fmt.Sprintf("```bash\nyq '%v' < sample.yml\n```\n", s.expression)) //TODO: pretty here
writeOrPanic(w, "Given a sample.yml file of:\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n", formattedDoc))
writeOrPanic(w, "then\n")
if s.expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq eval '%v' sample.yml\n```\n", s.expression))
} else {
writeOrPanic(w, "```bash\nyq eval sample.yml\n```\n")
}
} else {
writeOrPanic(w, "Running\n")
writeOrPanic(w, fmt.Sprintf("```bash\nyq eval --null-input '%v'\n```\n", s.expression))
} }
writeOrPanic(w, "Result\n") writeOrPanic(w, "will output\n")
var output bytes.Buffer var output bytes.Buffer
var err error var err error
@@ -94,18 +158,20 @@ func documentScenarios(t *testing.T, title string, scenarios []expressionScenari
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
err = EvaluateStream("sample.yaml", strings.NewReader(s.document), node, printer) streamEvaluator := NewStreamEvaluator()
err = streamEvaluator.Evaluate("sample.yaml", strings.NewReader(formattedDoc), node, printer)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
} else { } else {
err = EvaluateAllFileStreams(s.expression, []string{}, printer) allAtOnceEvaluator := NewAllAtOnceEvaluator()
err = allAtOnceEvaluator.EvaluateFiles(s.expression, []string{}, printer)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
} }
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n", output.String())) writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", output.String()))
} }

View File

@@ -1,54 +0,0 @@
package yqlib
import (
"testing"
)
var styleOperatorScenarios = []expressionScenario{
{
document: `{a: cat}`,
expression: `.a style="single"`,
expected: []string{
"D0, P[], (doc)::{a: 'cat'}\n",
},
},
{
description: "Set style using a path",
document: `{a: cat, b: double}`,
expression: `.a style=.b`,
expected: []string{
"D0, P[], (doc)::{a: \"cat\", b: double}\n",
},
},
{
document: `{a: "cat", b: 'dog'}`,
expression: `.. style=""`,
expected: []string{
"D0, P[], (!!map)::a: cat\nb: dog\n",
},
},
{
document: `{a: "cat", b: 'thing'}`,
expression: `.. | style`,
expected: []string{
"D0, P[], (!!str)::flow\n",
"D0, P[a], (!!str)::double\n",
"D0, P[b], (!!str)::single\n",
},
},
{
document: `a: cat`,
expression: `.. | style`,
expected: []string{
"D0, P[], (!!str)::\"\"\n",
"D0, P[a], (!!str)::\"\"\n",
},
},
}
func TestStyleOperatorScenarios(t *testing.T) {
for _, tt := range styleOperatorScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "Style Operator", styleOperatorScenarios)
}

View File

@@ -37,8 +37,18 @@ var pathTests = []struct {
// {`.a, .b`, append(make([]interface{}, 0), "a", "OR", "b")}, // {`.a, .b`, append(make([]interface{}, 0), "a", "OR", "b")},
// {`[.a, .b]`, append(make([]interface{}, 0), "[", "a", "OR", "b", "]")}, // {`[.a, .b]`, append(make([]interface{}, 0), "[", "a", "OR", "b", "]")},
// {`."[a", ."b]"`, append(make([]interface{}, 0), "[a", "OR", "b]")}, // {`."[a", ."b]"`, append(make([]interface{}, 0), "[a", "OR", "b]")},
// {`.a[]`, append(make([]interface{}, 0), "a", "PIPE", "[]")}, // {`.a.[]`, append(make([]interface{}, 0), "a", "PIPE", "[]")},
// {`.[].a`, append(make([]interface{}, 0), "[]", "PIPE", "a")}, // {`.[].a`, append(make([]interface{}, 0), "[]", "PIPE", "a")},
// {
// `["cat"]`,
// append(make([]interface{}, 0), "[", "cat (string)", "]"),
// append(make([]interface{}, 0), "cat (string)", "COLLECT", "PIPE"),
// },
{
`[]`,
append(make([]interface{}, 0), "[", "]"),
append(make([]interface{}, 0), "EMPTY", "COLLECT", "PIPE"),
},
{ {
`d0.a`, `d0.a`,
append(make([]interface{}, 0), "d0", "PIPE", "a"), append(make([]interface{}, 0), "d0", "PIPE", "a"),
@@ -85,7 +95,7 @@ var pathTests = []struct {
append(make([]interface{}, 0), "a", "mike (string)", "CREATE_MAP", "COLLECT_OBJECT", "PIPE"), append(make([]interface{}, 0), "a", "mike (string)", "CREATE_MAP", "COLLECT_OBJECT", "PIPE"),
}, },
{ {
`{.a: .c, .b[]: .f.g[]}`, `{.a: .c, .b.[]: .f.g.[]}`,
append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "PIPE", "[]", "CREATE_MAP", "f", "PIPE", "g", "PIPE", "[]", "}"), append(make([]interface{}, 0), "{", "a", "CREATE_MAP", "c", "UNION", "b", "PIPE", "[]", "CREATE_MAP", "f", "PIPE", "g", "PIPE", "[]", "}"),
append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "[]", "PIPE", "f", "g", "PIPE", "[]", "PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "PIPE"), append(make([]interface{}, 0), "a", "c", "CREATE_MAP", "b", "[]", "PIPE", "f", "g", "PIPE", "[]", "PIPE", "CREATE_MAP", "UNION", "COLLECT_OBJECT", "PIPE"),
}, },
@@ -99,6 +109,27 @@ var pathTests = []struct {
append(make([]interface{}, 0), "a", "PIPE", "b", "ASSIGN_STYLE", "folded (string)"), append(make([]interface{}, 0), "a", "PIPE", "b", "ASSIGN_STYLE", "folded (string)"),
append(make([]interface{}, 0), "a", "b", "PIPE", "folded (string)", "ASSIGN_STYLE"), append(make([]interface{}, 0), "a", "b", "PIPE", "folded (string)", "ASSIGN_STYLE"),
}, },
{
`tag == "str"`,
append(make([]interface{}, 0), "GET_TAG", "EQUALS", "str (string)"),
append(make([]interface{}, 0), "GET_TAG", "str (string)", "EQUALS"),
},
{
`. tag= "str"`,
append(make([]interface{}, 0), "SELF", "ASSIGN_TAG", "str (string)"),
append(make([]interface{}, 0), "SELF", "str (string)", "ASSIGN_TAG"),
},
{
`lineComment == "str"`,
append(make([]interface{}, 0), "GET_COMMENT", "EQUALS", "str (string)"),
append(make([]interface{}, 0), "GET_COMMENT", "str (string)", "EQUALS"),
},
{
`. lineComment= "str"`,
append(make([]interface{}, 0), "SELF", "ASSIGN_COMMENT", "str (string)"),
append(make([]interface{}, 0), "SELF", "str (string)", "ASSIGN_COMMENT"),
},
// { // {
// `.a.b tag="!!str"`, // `.a.b tag="!!str"`,
// append(make([]interface{}, 0), "EXPLODE", "(", "a", "PIPE", "b", ")"), // append(make([]interface{}, 0), "EXPLODE", "(", "a", "PIPE", "b", ")"),

View File

@@ -3,7 +3,7 @@ package yqlib
import ( import (
"errors" "errors"
"gopkg.in/op/go-logging.v1" logging "gopkg.in/op/go-logging.v1"
) )
type PathPostFixer interface { type PathPostFixer interface {

View File

@@ -24,14 +24,16 @@ const (
) )
type Token struct { type Token struct {
TokenType TokenType TokenType TokenType
Operation *Operation Operation *Operation
AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it
CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat
CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat
} }
func (t *Token) toString() string { func (t *Token) toString() string {
if t.TokenType == OperationToken { if t.TokenType == OperationToken {
log.Debug("toString, its an op")
return t.Operation.toString() return t.Operation.toString()
} else if t.TokenType == OpenBracket { } else if t.TokenType == OpenBracket {
return "(" return "("
@@ -57,13 +59,7 @@ func pathToken(wrapped bool) lex.Action {
if wrapped { if wrapped {
value = unwrap(value) value = unwrap(value)
} }
op := &Operation{OperationType: TraversePath, Value: value, StringValue: value} log.Debug("PathToken %v", value)
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
}
}
func literalPathToken(value string) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
op := &Operation{OperationType: TraversePath, Value: value, StringValue: value} op := &Operation{OperationType: TraversePath, Value: value, StringValue: value}
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
} }
@@ -77,20 +73,30 @@ func documentToken() lex.Action {
if errParsingInt != nil { if errParsingInt != nil {
return nil, errParsingInt return nil, errParsingInt
} }
log.Debug("documentToken %v", string(m.Bytes))
op := &Operation{OperationType: DocumentFilter, Value: number, StringValue: numberString} op := &Operation{OperationType: DocumentFilter, Value: number, StringValue: numberString}
return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil return &Token{TokenType: OperationToken, Operation: op, CheckForPostTraverse: true}, nil
} }
} }
func opToken(op *OperationType) lex.Action { func opToken(op *OperationType) lex.Action {
return opTokenWithPrefs(op, nil) return opTokenWithPrefs(op, nil, nil)
} }
func opTokenWithPrefs(op *OperationType, preferences interface{}) lex.Action { func opAssignableToken(opType *OperationType, assignOpType *OperationType) lex.Action {
return opTokenWithPrefs(opType, assignOpType, nil)
}
func opTokenWithPrefs(op *OperationType, assignOpType *OperationType, preferences interface{}) lex.Action {
return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) {
log.Debug("opTokenWithPrefs %v", string(m.Bytes))
value := string(m.Bytes) value := string(m.Bytes)
op := &Operation{OperationType: op, Value: op.Type, StringValue: value, Preferences: preferences} op := &Operation{OperationType: op, Value: op.Type, StringValue: value, Preferences: preferences}
return &Token{TokenType: OperationToken, Operation: op}, nil var assign *Operation
if assignOpType != nil {
assign = &Operation{OperationType: assignOpType, Value: assignOpType.Type, StringValue: value, Preferences: preferences}
}
return &Token{TokenType: OperationToken, Operation: op, AssignOperation: assign}, nil
} }
} }
@@ -178,40 +184,44 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\(`), literalToken(OpenBracket, false)) lexer.Add([]byte(`\(`), literalToken(OpenBracket, false))
lexer.Add([]byte(`\)`), literalToken(CloseBracket, true)) lexer.Add([]byte(`\)`), literalToken(CloseBracket, true))
lexer.Add([]byte(`\.?\[\]`), literalPathToken("[]")) lexer.Add([]byte(`\.\[\]`), pathToken(false))
lexer.Add([]byte(`\.\.`), opToken(RecursiveDescent)) lexer.Add([]byte(`\.\.`), opToken(RecursiveDescent))
lexer.Add([]byte(`,`), opToken(Union)) lexer.Add([]byte(`,`), opToken(Union))
lexer.Add([]byte(`:\s*`), opToken(CreateMap)) lexer.Add([]byte(`:\s*`), opToken(CreateMap))
lexer.Add([]byte(`length`), opToken(Length)) lexer.Add([]byte(`length`), opToken(Length))
lexer.Add([]byte(`select`), opToken(Select)) lexer.Add([]byte(`select`), opToken(Select))
lexer.Add([]byte(`has`), opToken(Has))
lexer.Add([]byte(`explode`), opToken(Explode)) lexer.Add([]byte(`explode`), opToken(Explode))
lexer.Add([]byte(`or`), opToken(Or)) lexer.Add([]byte(`or`), opToken(Or))
lexer.Add([]byte(`and`), opToken(And))
lexer.Add([]byte(`not`), opToken(Not)) lexer.Add([]byte(`not`), opToken(Not))
lexer.Add([]byte(`documentIndex`), opToken(GetDocumentIndex)) lexer.Add([]byte(`documentIndex`), opToken(GetDocumentIndex))
lexer.Add([]byte(`style\s*=`), opToken(AssignStyle)) lexer.Add([]byte(`style`), opAssignableToken(GetStyle, AssignStyle))
lexer.Add([]byte(`style`), opToken(GetStyle))
lexer.Add([]byte(`lineComment\s*=`), opTokenWithPrefs(AssignComment, &CommentOpPreferences{LineComment: true})) lexer.Add([]byte(`tag`), opAssignableToken(GetTag, AssignTag))
lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, &CommentOpPreferences{LineComment: true})) lexer.Add([]byte(`filename`), opToken(GetFilename))
lexer.Add([]byte(`fileIndex`), opToken(GetFileIndex))
lexer.Add([]byte(`path`), opToken(GetPath))
lexer.Add([]byte(`headComment\s*=`), opTokenWithPrefs(AssignComment, &CommentOpPreferences{HeadComment: true})) lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{LineComment: true}))
lexer.Add([]byte(`headComment`), opTokenWithPrefs(GetComment, &CommentOpPreferences{HeadComment: true}))
lexer.Add([]byte(`footComment\s*=`), opTokenWithPrefs(AssignComment, &CommentOpPreferences{FootComment: true})) lexer.Add([]byte(`headComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{HeadComment: true}))
lexer.Add([]byte(`footComment`), opTokenWithPrefs(GetComment, &CommentOpPreferences{FootComment: true}))
lexer.Add([]byte(`comments\s*=`), opTokenWithPrefs(AssignComment, &CommentOpPreferences{LineComment: true, HeadComment: true, FootComment: true})) lexer.Add([]byte(`footComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{FootComment: true}))
lexer.Add([]byte(`comments\s*=`), opTokenWithPrefs(AssignComment, nil, &CommentOpPreferences{LineComment: true, HeadComment: true, FootComment: true}))
lexer.Add([]byte(`collect`), opToken(Collect)) lexer.Add([]byte(`collect`), opToken(Collect))
lexer.Add([]byte(`\s*==\s*`), opToken(Equals)) lexer.Add([]byte(`\s*==\s*`), opToken(Equals))
lexer.Add([]byte(`\s*=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{false}))
lexer.Add([]byte(`\s*.-\s*`), opToken(DeleteChild)) lexer.Add([]byte(`del`), opToken(DeleteChild))
lexer.Add([]byte(`\s*\|=\s*`), opToken(Assign)) lexer.Add([]byte(`\s*\|=\s*`), opTokenWithPrefs(Assign, nil, &AssignOpPreferences{true}))
lexer.Add([]byte(`\[-?[0-9]+\]`), arrayIndextoken(false)) lexer.Add([]byte(`\[-?[0-9]+\]`), arrayIndextoken(false))
lexer.Add([]byte(`\.\[-?[0-9]+\]`), arrayIndextoken(true)) lexer.Add([]byte(`\.\[-?[0-9]+\]`), arrayIndextoken(true))
@@ -242,6 +252,7 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false)) lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false))
lexer.Add([]byte(`\}`), literalToken(CloseCollectObject, true)) lexer.Add([]byte(`\}`), literalToken(CloseCollectObject, true))
lexer.Add([]byte(`\*`), opToken(Multiply)) lexer.Add([]byte(`\*`), opToken(Multiply))
lexer.Add([]byte(`\+`), opToken(Add))
err := lexer.Compile() err := lexer.Compile()
if err != nil { if err != nil {
@@ -286,15 +297,28 @@ func (p *pathTokeniser) Tokenise(path string) ([]*Token, error) {
} }
var postProcessedTokens = make([]*Token, 0) var postProcessedTokens = make([]*Token, 0)
skipNextToken := false
for index, token := range tokens { for index, token := range tokens {
if skipNextToken {
skipNextToken = false
} else {
postProcessedTokens = append(postProcessedTokens, token) if index != len(tokens)-1 && token.AssignOperation != nil &&
tokens[index+1].TokenType == OperationToken &&
tokens[index+1].Operation.OperationType == Assign {
token.Operation = token.AssignOperation
skipNextToken = true
}
if index != len(tokens)-1 && token.CheckForPostTraverse && postProcessedTokens = append(postProcessedTokens, token)
tokens[index+1].TokenType == OperationToken &&
tokens[index+1].Operation.OperationType == TraversePath { if index != len(tokens)-1 && token.CheckForPostTraverse &&
op := &Operation{OperationType: Pipe, Value: "PIPE"} tokens[index+1].TokenType == OperationToken &&
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op}) tokens[index+1].Operation.OperationType == TraversePath {
op := &Operation{OperationType: Pipe, Value: "PIPE"}
postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op})
}
} }
} }

View File

@@ -20,6 +20,7 @@ type resultsPrinter struct {
printDocSeparators bool printDocSeparators bool
writer io.Writer writer io.Writer
firstTimePrinting bool firstTimePrinting bool
previousDocIndex uint
} }
func NewPrinter(writer io.Writer, outputToJSON bool, unwrapScalar bool, colorsEnabled bool, indent int, printDocSeparators bool) Printer { func NewPrinter(writer io.Writer, outputToJSON bool, unwrapScalar bool, colorsEnabled bool, indent int, printDocSeparators bool) Printer {
@@ -53,6 +54,7 @@ func (p *resultsPrinter) writeString(writer io.Writer, txt string) error {
} }
func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error { func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error {
log.Debug("PrintResults for %v matches", matchingNodes.Len())
var err error var err error
if p.outputToJSON { if p.outputToJSON {
explodeOp := Operation{OperationType: Explode} explodeOp := Operation{OperationType: Explode}
@@ -70,13 +72,16 @@ func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error {
log.Debug("no matching results, nothing to print") log.Debug("no matching results, nothing to print")
return nil return nil
} }
if p.firstTimePrinting {
previousDocIndex := matchingNodes.Front().Value.(*CandidateNode).Document p.previousDocIndex = matchingNodes.Front().Value.(*CandidateNode).Document
p.firstTimePrinting = false
}
for el := matchingNodes.Front(); el != nil; el = el.Next() { for el := matchingNodes.Front(); el != nil; el = el.Next() {
mappedDoc := el.Value.(*CandidateNode) mappedDoc := el.Value.(*CandidateNode)
log.Debug("-- print sep logic: p.firstTimePrinting: %v, previousDocIndex: %v, mappedDoc.Document: %v, printDocSeparators: %v", p.firstTimePrinting, p.previousDocIndex, mappedDoc.Document, p.printDocSeparators)
if (!p.firstTimePrinting || (previousDocIndex != mappedDoc.Document)) && p.printDocSeparators { if (p.previousDocIndex != mappedDoc.Document) && p.printDocSeparators {
log.Debug("-- writing doc sep")
if err := p.writeString(bufferedWriter, "---\n"); err != nil { if err := p.writeString(bufferedWriter, "---\n"); err != nil {
return err return err
} }
@@ -87,9 +92,8 @@ func (p *resultsPrinter) PrintResults(matchingNodes *list.List) error {
return err return err
} }
previousDocIndex = mappedDoc.Document p.previousDocIndex = mappedDoc.Document
} }
p.firstTimePrinting = false
return nil return nil
} }

99
pkg/yqlib/printer_test.go Normal file
View File

@@ -0,0 +1,99 @@
package yqlib
import (
"bufio"
"bytes"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
var multiDocSample = `a: banana
---
a: apple
---
a: coconut
`
func TestPrinterMultipleDocsInSequence(t *testing.T) {
var output bytes.Buffer
var writer = bufio.NewWriter(&output)
printer := NewPrinter(writer, false, true, false, 2, true)
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
if err != nil {
panic(err)
}
el := inputs.Front()
sample1 := nodeToMap(el.Value.(*CandidateNode))
el = el.Next()
sample2 := nodeToMap(el.Value.(*CandidateNode))
el = el.Next()
sample3 := nodeToMap(el.Value.(*CandidateNode))
err = printer.PrintResults(sample1)
if err != nil {
panic(err)
}
err = printer.PrintResults(sample2)
if err != nil {
panic(err)
}
err = printer.PrintResults(sample3)
if err != nil {
panic(err)
}
writer.Flush()
test.AssertResult(t, multiDocSample, output.String())
}
func TestPrinterMultipleDocsInSinglePrint(t *testing.T) {
var output bytes.Buffer
var writer = bufio.NewWriter(&output)
printer := NewPrinter(writer, false, true, false, 2, true)
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
if err != nil {
panic(err)
}
err = printer.PrintResults(inputs)
if err != nil {
panic(err)
}
writer.Flush()
test.AssertResult(t, multiDocSample, output.String())
}
func TestPrinterMultipleDocsJson(t *testing.T) {
var output bytes.Buffer
var writer = bufio.NewWriter(&output)
printer := NewPrinter(writer, true, true, false, 0, false)
inputs, err := readDocuments(strings.NewReader(multiDocSample), "sample.yml", 0)
if err != nil {
panic(err)
}
err = printer.PrintResults(inputs)
if err != nil {
panic(err)
}
expected := `{"a":"banana"}
{"a":"apple"}
{"a":"coconut"}
`
writer.Flush()
test.AssertResult(t, expected, output.String())
}

View File

@@ -0,0 +1,85 @@
package yqlib
import (
"container/list"
"io"
"os"
yaml "gopkg.in/yaml.v3"
)
type StreamEvaluator interface {
Evaluate(filename string, reader io.Reader, node *PathTreeNode, printer Printer) error
EvaluateFiles(expression string, filenames []string, printer Printer) error
}
type streamEvaluator struct {
treeNavigator DataTreeNavigator
treeCreator PathTreeCreator
fileIndex int
}
func NewStreamEvaluator() StreamEvaluator {
return &streamEvaluator{treeNavigator: NewDataTreeNavigator(), treeCreator: NewPathTreeCreator()}
}
func (s *streamEvaluator) EvaluateFiles(expression string, filenames []string, printer Printer) error {
node, err := treeCreator.ParsePath(expression)
if err != nil {
return err
}
for _, filename := range filenames {
reader, err := readStream(filename)
if err != nil {
return err
}
err = s.Evaluate(filename, reader, node, printer)
if err != nil {
return err
}
switch reader := reader.(type) {
case *os.File:
safelyCloseFile(reader)
}
}
return nil
}
func (s *streamEvaluator) Evaluate(filename string, reader io.Reader, node *PathTreeNode, printer Printer) error {
var currentIndex uint
decoder := yaml.NewDecoder(reader)
for {
var dataBucket yaml.Node
errorReading := decoder.Decode(&dataBucket)
if errorReading == io.EOF {
s.fileIndex = s.fileIndex + 1
return nil
} else if errorReading != nil {
return errorReading
}
candidateNode := &CandidateNode{
Document: currentIndex,
Filename: filename,
Node: &dataBucket,
FileIndex: s.fileIndex,
}
inputList := list.New()
inputList.PushBack(candidateNode)
matches, errorParsing := treeNavigator.GetMatchingNodes(inputList, node)
if errorParsing != nil {
return errorParsing
}
err := printer.PrintResults(matches)
if err != nil {
return err
}
currentIndex = currentIndex + 1
}
}

Some files were not shown because too many files have changed in this diff Show More