mirror of
				https://github.com/taigrr/yq
				synced 2025-01-18 04:53:17 -08:00 
			
		
		
		
	Traverse Array Operator
This commit is contained in:
		
							parent
							
								
									ea231006ed
								
							
						
					
					
						commit
						6a13c8b78f
					
				| @ -65,6 +65,7 @@ var SortKeys = &OperationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Han | |||||||
| 
 | 
 | ||||||
| var CollectObject = &OperationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: CollectObjectOperator} | var CollectObject = &OperationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: CollectObjectOperator} | ||||||
| var TraversePath = &OperationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} | var TraversePath = &OperationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} | ||||||
|  | var TraverseArray = &OperationType{Type: "TRAVERSE_ARRAY", NumArgs: 2, Precedence: 50, Handler: TraverseArrayOperator} | ||||||
| 
 | 
 | ||||||
| var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} | var DocumentFilter = &OperationType{Type: "DOCUMENT_FILTER", NumArgs: 0, Precedence: 50, Handler: TraversePathOperator} | ||||||
| var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator} | var SelfReference = &OperationType{Type: "SELF", NumArgs: 0, Precedence: 50, Handler: SelfOperator} | ||||||
|  | |||||||
							
								
								
									
										91
									
								
								pkg/yqlib/operator_array_traverse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								pkg/yqlib/operator_array_traverse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | package yqlib | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"container/list" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 
 | ||||||
|  | 	yaml "gopkg.in/yaml.v3" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TraverseArrayOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { | ||||||
|  | 	// lhs is an expression that will yield a bunch of arrays | ||||||
|  | 	// rhs is a collect expression that will yield indexes to retreive of the arrays | ||||||
|  | 
 | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var indicesToTraverse = rhs.Front().Value.(*CandidateNode).Node.Content | ||||||
|  | 
 | ||||||
|  | 	var matchingNodeMap = list.New() | ||||||
|  | 	for el := lhs.Front(); el != nil; el = el.Next() { | ||||||
|  | 		candidate := el.Value.(*CandidateNode) | ||||||
|  | 
 | ||||||
|  | 		if candidate.Node.Kind == yaml.SequenceNode { | ||||||
|  | 			newNodes, err := traverseArrayWithIndices(candidate, indicesToTraverse) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			matchingNodeMap.PushBackList(newNodes) | ||||||
|  | 		} else { | ||||||
|  | 			log.Debugf("OperatorArray Traverse skipping %v as its a %v", candidate, candidate.Node.Tag) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return matchingNodeMap, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func traverseArrayWithIndices(candidate *CandidateNode, indices []*yaml.Node) (*list.List, error) { | ||||||
|  | 	log.Debug("traverseArrayWithIndices") | ||||||
|  | 	var newMatches = list.New() | ||||||
|  | 
 | ||||||
|  | 	if len(indices) == 0 { | ||||||
|  | 		var contents = candidate.Node.Content | ||||||
|  | 		var index int64 | ||||||
|  | 		for index = 0; index < int64(len(contents)); index = index + 1 { | ||||||
|  | 
 | ||||||
|  | 			newMatches.PushBack(&CandidateNode{ | ||||||
|  | 				Document: candidate.Document, | ||||||
|  | 				Path:     candidate.CreateChildPath(index), | ||||||
|  | 				Node:     contents[index], | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 		return newMatches, nil | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, indexNode := range indices { | ||||||
|  | 		index, err := strconv.ParseInt(indexNode.Value, 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		indexToUse := index | ||||||
|  | 		contentLength := int64(len(candidate.Node.Content)) | ||||||
|  | 		for contentLength <= index { | ||||||
|  | 			candidate.Node.Content = append(candidate.Node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}) | ||||||
|  | 			contentLength = int64(len(candidate.Node.Content)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if indexToUse < 0 { | ||||||
|  | 			indexToUse = contentLength + indexToUse | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if indexToUse < 0 { | ||||||
|  | 			return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		newMatches.PushBack(&CandidateNode{ | ||||||
|  | 			Node:     candidate.Node.Content[indexToUse], | ||||||
|  | 			Document: candidate.Document, | ||||||
|  | 			Path:     candidate.CreateChildPath(index), | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	return newMatches, nil | ||||||
|  | } | ||||||
							
								
								
									
										91
									
								
								pkg/yqlib/operator_array_traverse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								pkg/yqlib/operator_array_traverse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | package yqlib | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var traverseArrayOperatorScenarios = []expressionScenario{ | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a[0]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a[0, 2]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 			"D0, P[a 2], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a.[0]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a[-1]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a -1], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a.[-1]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a -1], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a[-2]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a -2], (!!str)::b\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a.[-2]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a -2], (!!str)::b\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a[]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 			"D0, P[a 1], (!!str)::b\n", | ||||||
|  | 			"D0, P[a 2], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a.[]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 			"D0, P[a 1], (!!str)::b\n", | ||||||
|  | 			"D0, P[a 2], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		document:   `{a: [a,b,c]}`, | ||||||
|  | 		expression: `.a | .[]`, | ||||||
|  | 		expected: []string{ | ||||||
|  | 			"D0, P[a 0], (!!str)::a\n", | ||||||
|  | 			"D0, P[a 1], (!!str)::b\n", | ||||||
|  | 			"D0, P[a 2], (!!str)::c\n", | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestTraverseArrayOperatorScenarios(t *testing.T) { | ||||||
|  | 	for _, tt := range traverseArrayOperatorScenarios { | ||||||
|  | 		testScenario(t, &tt) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -19,8 +19,18 @@ var pathTests = []struct { | |||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		`.a[]`, | 		`.a[]`, | ||||||
| 		append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]"), | 		append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "]"), | ||||||
| 		append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE"), | 		append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		`.a[0]`, | ||||||
|  | 		append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"), | ||||||
|  | 		append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		`.a.[0]`, | ||||||
|  | 		append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"), | ||||||
|  | 		append(make([]interface{}, 0), "a", "0 (int64)", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		`.a.[]`, | 		`.a.[]`, | ||||||
| @ -29,8 +39,8 @@ var pathTests = []struct { | |||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		`.a[].c`, | 		`.a[].c`, | ||||||
| 		append(make([]interface{}, 0), "a", "SHORT_PIPE", "[]", "SHORT_PIPE", "c"), | 		append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "]", "SHORT_PIPE", "c"), | ||||||
| 		append(make([]interface{}, 0), "a", "[]", "SHORT_PIPE", "c", "SHORT_PIPE"), | 		append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY", "c", "SHORT_PIPE"), | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		`[3]`, | 		`[3]`, | ||||||
|  | |||||||
| @ -22,7 +22,6 @@ const ( | |||||||
| 	CloseCollect | 	CloseCollect | ||||||
| 	OpenCollectObject | 	OpenCollectObject | ||||||
| 	CloseCollectObject | 	CloseCollectObject | ||||||
| 	SplatOrEmptyCollect |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Token struct { | type Token struct { | ||||||
| @ -49,8 +48,6 @@ func (t *Token) toString() string { | |||||||
| 		return "{" | 		return "{" | ||||||
| 	} else if t.TokenType == CloseCollectObject { | 	} else if t.TokenType == CloseCollectObject { | ||||||
| 		return "}" | 		return "}" | ||||||
| 	} else if t.TokenType == SplatOrEmptyCollect { |  | ||||||
| 		return "[]?" |  | ||||||
| 	} else { | 	} else { | ||||||
| 		return "NFI" | 		return "NFI" | ||||||
| 	} | 	} | ||||||
| @ -254,8 +251,6 @@ func initLexer() (*lex.Lexer, error) { | |||||||
| 
 | 
 | ||||||
| 	lexer.Add([]byte(`"[^"]*"`), stringValue(true)) | 	lexer.Add([]byte(`"[^"]*"`), stringValue(true)) | ||||||
| 
 | 
 | ||||||
| 	lexer.Add([]byte(`\[\]`), literalToken(SplatOrEmptyCollect, true)) |  | ||||||
| 
 |  | ||||||
| 	lexer.Add([]byte(`\[`), literalToken(OpenCollect, false)) | 	lexer.Add([]byte(`\[`), literalToken(OpenCollect, false)) | ||||||
| 	lexer.Add([]byte(`\]`), literalToken(CloseCollect, true)) | 	lexer.Add([]byte(`\]`), literalToken(CloseCollect, true)) | ||||||
| 	lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false)) | 	lexer.Add([]byte(`\{`), literalToken(OpenCollectObject, false)) | ||||||
| @ -324,25 +319,6 @@ func (p *pathTokeniser) Tokenise(path string) ([]*Token, error) { | |||||||
| func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTokens []*Token) (tokensAccum []*Token, skipNextToken bool) { | func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTokens []*Token) (tokensAccum []*Token, skipNextToken bool) { | ||||||
| 	skipNextToken = false | 	skipNextToken = false | ||||||
| 	token := tokens[index] | 	token := tokens[index] | ||||||
| 	if token.TokenType == SplatOrEmptyCollect { |  | ||||||
| 		if index > 0 && tokens[index-1].TokenType == OperationToken && |  | ||||||
| 			tokens[index-1].Operation.OperationType == TraversePath { |  | ||||||
| 			// must be a splat without a preceding dot , e.g. .a[] |  | ||||||
| 			// lets put a pipe in front of it, and convert it to a traverse "[]" token |  | ||||||
| 			pipeOp := &Operation{OperationType: ShortPipe, Value: "PIPE"} |  | ||||||
| 
 |  | ||||||
| 			postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: pipeOp}) |  | ||||||
| 
 |  | ||||||
| 			traverseOp := &Operation{OperationType: TraversePath, Value: "[]", StringValue: "[]"} |  | ||||||
| 			token = &Token{TokenType: OperationToken, Operation: traverseOp, CheckForPostTraverse: true} |  | ||||||
| 
 |  | ||||||
| 		} else { |  | ||||||
| 			// gotta be a collect empty array, we need to split this into two tokens |  | ||||||
| 			// one OpenCollect, the other CloseCollect |  | ||||||
| 			postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OpenCollect}) |  | ||||||
| 			token = &Token{TokenType: CloseCollect, CheckForPostTraverse: true} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if index != len(tokens)-1 && token.AssignOperation != nil && | 	if index != len(tokens)-1 && token.AssignOperation != nil && | ||||||
| 		tokens[index+1].TokenType == OperationToken && | 		tokens[index+1].TokenType == OperationToken && | ||||||
| @ -359,5 +335,10 @@ func (p *pathTokeniser) handleToken(tokens []*Token, index int, postProcessedTok | |||||||
| 		op := &Operation{OperationType: ShortPipe, Value: "PIPE"} | 		op := &Operation{OperationType: ShortPipe, Value: "PIPE"} | ||||||
| 		postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op}) | 		postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op}) | ||||||
| 	} | 	} | ||||||
|  | 	if index != len(tokens)-1 && token.CheckForPostTraverse && | ||||||
|  | 		tokens[index+1].TokenType == OpenCollect { | ||||||
|  | 		op := &Operation{OperationType: TraverseArray} | ||||||
|  | 		postProcessedTokens = append(postProcessedTokens, &Token{TokenType: OperationToken, Operation: op}) | ||||||
|  | 	} | ||||||
| 	return postProcessedTokens, skipNextToken | 	return postProcessedTokens, skipNextToken | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										60
									
								
								yq_test.go
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								yq_test.go
									
									
									
									
									
								
							| @ -1,60 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| // import ( |  | ||||||
| // 	"fmt" |  | ||||||
| // 	"runtime" |  | ||||||
| // 	"testing" |  | ||||||
| 
 |  | ||||||
| // 	"github.com/mikefarah/yq/v2/pkg/marshal" |  | ||||||
| // 	"github.com/mikefarah/yq/v2/test" |  | ||||||
| // ) |  | ||||||
| 
 |  | ||||||
| // func TestMultilineString(t *testing.T) { |  | ||||||
| // 	testString := ` |  | ||||||
| // 	abcd |  | ||||||
| // 	efg` |  | ||||||
| // 	formattedResult, _ := marshal.NewYamlConverter().YamlToString(testString, false) |  | ||||||
| // 	test.AssertResult(t, testString, formattedResult) |  | ||||||
| // } |  | ||||||
| 
 |  | ||||||
| // func TestNewYaml(t *testing.T) { |  | ||||||
| // 	result, _ := newYaml([]string{"b.c", "3"}) |  | ||||||
| // 	formattedResult := fmt.Sprintf("%v", result) |  | ||||||
| // 	test.AssertResult(t, |  | ||||||
| // 		"[{b [{c 3}]}]", |  | ||||||
| // 		formattedResult) |  | ||||||
| // } |  | ||||||
| 
 |  | ||||||
| // func TestNewYamlArray(t *testing.T) { |  | ||||||
| // 	result, _ := newYaml([]string{"[0].cat", "meow"}) |  | ||||||
| // 	formattedResult := fmt.Sprintf("%v", result) |  | ||||||
| // 	test.AssertResult(t, |  | ||||||
| // 		"[[{cat meow}]]", |  | ||||||
| // 		formattedResult) |  | ||||||
| // } |  | ||||||
| 
 |  | ||||||
| // func TestNewYaml_WithScript(t *testing.T) { |  | ||||||
| // 	writeScript = "examples/instruction_sample.yaml" |  | ||||||
| // 	expectedResult := `b: |  | ||||||
| //   c: cat |  | ||||||
| //   e: |  | ||||||
| //   - name: Mike Farah` |  | ||||||
| // 	result, _ := newYaml([]string{""}) |  | ||||||
| // 	actualResult, _ := marshal.NewYamlConverter().YamlToString(result, true) |  | ||||||
| // 	test.AssertResult(t, expectedResult, actualResult) |  | ||||||
| // } |  | ||||||
| 
 |  | ||||||
| // func TestNewYaml_WithUnknownScript(t *testing.T) { |  | ||||||
| // 	writeScript = "fake-unknown" |  | ||||||
| // 	_, err := newYaml([]string{""}) |  | ||||||
| // 	if err == nil { |  | ||||||
| // 		t.Error("Expected error due to unknown file") |  | ||||||
| // 	} |  | ||||||
| // 	var expectedOutput string |  | ||||||
| // 	if runtime.GOOS == "windows" { |  | ||||||
| // 		expectedOutput = `open fake-unknown: The system cannot find the file specified.` |  | ||||||
| // 	} else { |  | ||||||
| // 		expectedOutput = `open fake-unknown: no such file or directory` |  | ||||||
| // 	} |  | ||||||
| // 	test.AssertResult(t, expectedOutput, err.Error()) |  | ||||||
| // } |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user