diff --git a/commands_test.go b/commands_test.go index 487bb7fd..370a0a97 100644 --- a/commands_test.go +++ b/commands_test.go @@ -94,6 +94,15 @@ func TestReadCmd(t *testing.T) { test.AssertResult(t, "2", result.Output) } +func TestReadWithAdvancedFilterCmd(t *testing.T) { + cmd := getRootCommand() + result := test.RunCmd(cmd, "read examples/sample.yaml b.e(name == sam).value") + if result.Error != nil { + t.Error(result.Error) + } + test.AssertResult(t, "4", result.Output) +} + func TestReadWithKeyAndValueCmd(t *testing.T) { cmd := getRootCommand() result := test.RunCmd(cmd, "read -p pv examples/sample.yaml b.c") diff --git a/examples/sample.yaml b/examples/sample.yaml index 1fb93352..603dc54d 100644 --- a/examples/sample.yaml +++ b/examples/sample.yaml @@ -1,9 +1,4 @@ -a: true -b: - c: 2 - d: [3, 4, 5] - e: - - name: fred - value: 3 - - name: sam - value: 4 \ No newline at end of file +- name: fred + value: 3 +- name: sam + value: 4 \ No newline at end of file diff --git a/pkg/yqlib/data_navigator.go b/pkg/yqlib/data_navigator.go index 440331ce..8dedf9a8 100644 --- a/pkg/yqlib/data_navigator.go +++ b/pkg/yqlib/data_navigator.go @@ -2,6 +2,7 @@ package yqlib import ( "strconv" + "strings" errors "github.com/pkg/errors" yaml "gopkg.in/yaml.v3" @@ -69,7 +70,7 @@ func (n *navigator) recurse(value *yaml.Node, head string, tail []string, pathSt return n.recurseMap(value, head, tail, pathStack) case yaml.SequenceNode: log.Debug("its a sequence of %v things!", len(value.Content)) - if head == "*" || head == "**" { + if head == "*" || head == "**" || strings.Contains(head, "==") { return n.splatArray(value, head, tail, pathStack) } else if head == "+" { return n.appendArray(value, head, tail, pathStack) @@ -96,7 +97,7 @@ func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, pat newPathStack := append(pathStack, contents[indexInMap].Value) log.Debug("appended %v", contents[indexInMap].Value) n.navigationStrategy.DebugVisitedNodes() - log.Debug("should I traverse? %v, %v", head, pathStackToString(newPathStack)) + log.Debug("should I traverse? head: %v, path: %v", head, pathStackToString(newPathStack)) DebugNode(value) if n.navigationStrategy.ShouldTraverse(NewNodeContext(contents[indexInMap+1], head, tail, newPathStack), contents[indexInMap].Value) { log.Debug("recurseMap: Going to traverse") @@ -214,9 +215,13 @@ func (n *navigator) splatArray(value *yaml.Node, head string, tail []string, pat log.Debug("processing") DebugNode(childValue) childValue = n.getOrReplace(childValue, guessKind(head, tail, childValue.Kind)) - var err = n.doTraverse(childValue, head, tail, append(pathStack, index)) - if err != nil { - return err + + newPathStack := append(pathStack, index) + if n.navigationStrategy.ShouldTraverse(NewNodeContext(childValue, head, tail, newPathStack), childValue.Value) { + var err = n.doTraverse(childValue, head, tail, newPathStack) + if err != nil { + return err + } } } return nil diff --git a/pkg/yqlib/filter_matching_node_navigation_strategy.go b/pkg/yqlib/filter_matching_node_navigation_strategy.go new file mode 100644 index 00000000..dde3b8a3 --- /dev/null +++ b/pkg/yqlib/filter_matching_node_navigation_strategy.go @@ -0,0 +1,20 @@ +package yqlib + +func FilterMatchingNodesNavigationStrategy(value string) NavigationStrategy { + return &NavigationStrategyImpl{ + visitedNodes: []*NodeContext{}, + followAlias: func(nodeContext NodeContext) bool { + return true + }, + autoCreateMap: func(nodeContext NodeContext) bool { + return false + }, + visit: func(nodeContext NodeContext) error { + return nil + }, + shouldVisitExtraFn: func(nodeContext NodeContext) bool { + log.Debug("does %v match %v ? %v", nodeContext.Node.Value, value, nodeContext.Node.Value == value) + return nodeContext.Node.Value == value + }, + } +} diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index e455d7ff..108ef3ac 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -108,10 +108,10 @@ func NewYqLib() YqLib { func (l *lib) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error) { var paths = l.parser.ParsePath(path) - NavigationStrategy := ReadNavigationStrategy() - navigator := NewDataNavigator(NavigationStrategy) + navigationStrategy := ReadNavigationStrategy() + navigator := NewDataNavigator(navigationStrategy) error := navigator.Traverse(rootNode, paths) - return NavigationStrategy.GetVisitedNodes(), error + return navigationStrategy.GetVisitedNodes(), error } diff --git a/pkg/yqlib/navigation_strategy.go b/pkg/yqlib/navigation_strategy.go index d1d72ca0..a5ae1f1f 100644 --- a/pkg/yqlib/navigation_strategy.go +++ b/pkg/yqlib/navigation_strategy.go @@ -39,10 +39,11 @@ type NavigationStrategy interface { } type NavigationStrategyImpl struct { - followAlias func(nodeContext NodeContext) bool - autoCreateMap func(nodeContext NodeContext) bool - visit func(nodeContext NodeContext) error - visitedNodes []*NodeContext + followAlias func(nodeContext NodeContext) bool + autoCreateMap func(nodeContext NodeContext) bool + visit func(nodeContext NodeContext) error + shouldVisitExtraFn func(nodeContext NodeContext) bool + visitedNodes []*NodeContext } func (ns *NavigationStrategyImpl) GetVisitedNodes() []*NodeContext { @@ -90,8 +91,8 @@ func (ns *NavigationStrategyImpl) shouldVisit(nodeContext NodeContext) bool { parser := NewPathParser() // only visit aliases if its an exact match - return (nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" && - parser.MatchesNextPathElement(nodeContext, nodeKey)) + return ((nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" && + parser.MatchesNextPathElement(nodeContext, nodeKey))) && (ns.shouldVisitExtraFn == nil || ns.shouldVisitExtraFn(nodeContext)) } func (ns *NavigationStrategyImpl) Visit(nodeContext NodeContext) error { diff --git a/pkg/yqlib/path_parser.go b/pkg/yqlib/path_parser.go index b18ff5d8..112d7912 100644 --- a/pkg/yqlib/path_parser.go +++ b/pkg/yqlib/path_parser.go @@ -28,6 +28,26 @@ func (p *pathParser) MatchesNextPathElement(nodeContext NodeContext, nodeKey str if head == "**" || head == "*" { return true } + if strings.Contains(head, "==") { + log.Debug("ooh deep recursion time") + result := strings.SplitN(head, "==", 2) + path := strings.TrimSpace(result[0]) + value := strings.TrimSpace(result[1]) + log.Debug("path %v", path) + log.Debug("value %v", value) + DebugNode(nodeContext.Node) + navigationStrategy := FilterMatchingNodesNavigationStrategy(value) + + navigator := NewDataNavigator(navigationStrategy) + err := navigator.Traverse(nodeContext.Node, p.ParsePath(path)) + if err != nil { + log.Error(err.Error()) + } + //crap handle error + log.Debug("done deep recursing, found %v matches", len(navigationStrategy.GetVisitedNodes())) + return len(navigationStrategy.GetVisitedNodes()) > 0 + } + if head == "+" { log.Debug("head is +, nodeKey is %v", nodeKey) var _, err = strconv.ParseInt(nodeKey, 10, 64) // nolint @@ -66,9 +86,12 @@ func (p *pathParser) nextYamlPath(path string) (pathElement string, remaining st case '"': // e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat" return p.search(path[1:], []uint8{'"'}, true) + case '(': + // e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat" + return p.search(path[1:], []uint8{')'}, true) default: // e.g "a.blah.cat" -> return "a" and "blah.cat" - return p.search(path[0:], []uint8{'.', '['}, false) + return p.search(path[0:], []uint8{'.', '[', '"', '('}, false) } }