From 064cff13418f470a1a42d7a1403a0b44f196421b Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sun, 22 Nov 2020 12:19:57 +1100 Subject: [PATCH] added path operator! --- pkg/yqlib/doc/Path Operators.md | 59 +++++++++++++++++++++++++ pkg/yqlib/doc/headers/Path Operators.md | 1 + pkg/yqlib/lib.go | 2 +- pkg/yqlib/operator_path.go | 39 ++++++++++++++++ pkg/yqlib/operator_path_test.go | 45 +++++++++++++++++++ pkg/yqlib/path_tokeniser.go | 1 + 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 pkg/yqlib/doc/Path Operators.md create mode 100644 pkg/yqlib/doc/headers/Path Operators.md create mode 100644 pkg/yqlib/operator_path.go create mode 100644 pkg/yqlib/operator_path_test.go diff --git a/pkg/yqlib/doc/Path Operators.md b/pkg/yqlib/doc/Path Operators.md new file mode 100644 index 00000000..27ec6988 --- /dev/null +++ b/pkg/yqlib/doc/Path Operators.md @@ -0,0 +1,59 @@ +The path operator can be used to find 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. +## Examples +### 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 +``` + diff --git a/pkg/yqlib/doc/headers/Path Operators.md b/pkg/yqlib/doc/headers/Path Operators.md new file mode 100644 index 00000000..a731d40e --- /dev/null +++ b/pkg/yqlib/doc/headers/Path Operators.md @@ -0,0 +1 @@ +The path operator can be used to find 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. \ No newline at end of file diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index b2ea9350..837ae4f8 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -17,7 +17,6 @@ type OperationType struct { } // operators TODO: -// - get path operator (like doc index) // - write in place // - mergeAppend (merges and appends arrays) // - mergeEmpty (sets only if the document is empty, do I do that now?) @@ -51,6 +50,7 @@ var GetComment = &OperationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, 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} diff --git a/pkg/yqlib/operator_path.go b/pkg/yqlib/operator_path.go new file mode 100644 index 00000000..2f27f48e --- /dev/null +++ b/pkg/yqlib/operator_path.go @@ -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 +} diff --git a/pkg/yqlib/operator_path_test.go b/pkg/yqlib/operator_path_test.go new file mode 100644 index 00000000..ecc59f23 --- /dev/null +++ b/pkg/yqlib/operator_path_test.go @@ -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 Operators", pathOperatorScenarios) +} diff --git a/pkg/yqlib/path_tokeniser.go b/pkg/yqlib/path_tokeniser.go index 1fc0ed3b..17af2a25 100644 --- a/pkg/yqlib/path_tokeniser.go +++ b/pkg/yqlib/path_tokeniser.go @@ -206,6 +206,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`tag`), opAssignableToken(GetTag, AssignTag)) lexer.Add([]byte(`filename`), opToken(GetFilename)) lexer.Add([]byte(`fileIndex`), opToken(GetFileIndex)) + lexer.Add([]byte(`path`), opToken(GetPath)) lexer.Add([]byte(`lineComment`), opTokenWithPrefs(GetComment, AssignComment, &CommentOpPreferences{LineComment: true}))