diff --git a/pkg/yqlib/doc/Traverse (Read).md b/pkg/yqlib/doc/Traverse (Read).md index f5acb2d7..b949ba45 100644 --- a/pkg/yqlib/doc/Traverse (Read).md +++ b/pkg/yqlib/doc/Traverse (Read).md @@ -32,6 +32,21 @@ b: apple c: banana ``` +## Optional Splat +Just like splat, but won't error if you run it against scalars + +Given a sample.yml file of: +```yaml +cat +``` +then +```bash +yq eval '.[]' sample.yml +``` +will output +```yaml +``` + ## Special characters Use quotes with brackets around path elements with special characters diff --git a/pkg/yqlib/expression_postfix.go b/pkg/yqlib/expression_postfix.go index c814269c..101dfe29 100644 --- a/pkg/yqlib/expression_postfix.go +++ b/pkg/yqlib/expression_postfix.go @@ -55,7 +55,15 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope log.Debugf("deleteing open bracket from opstack") //and append a collect to the opStack - result = append(result, &Operation{OperationType: collectOperator}) + // hack - see if there's the optional traverse flag + // on the close op - move it to the collect op. + // allows for .["cat"]? + prefs := traversePreferences{} + closeTokenMatch := string(currentToken.Match.Bytes) + if closeTokenMatch[len(closeTokenMatch)-1:] == "?" { + prefs.OptionalTraverse = true + } + result = append(result, &Operation{OperationType: collectOperator, Preferences: prefs}) log.Debugf("put collect onto the result") result = append(result, &Operation{OperationType: shortPipeOpType}) log.Debugf("put shortpipe onto the result") diff --git a/pkg/yqlib/expression_processing_test.go b/pkg/yqlib/expression_processing_test.go index b72e6e94..93cb2047 100644 --- a/pkg/yqlib/expression_processing_test.go +++ b/pkg/yqlib/expression_processing_test.go @@ -62,6 +62,11 @@ var pathTests = []struct { append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"), append(make([]interface{}, 0), "b", "a", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), }, + { + `.b[.a]?`, + append(make([]interface{}, 0), "b", "TRAVERSE_ARRAY", "[", "a", "]"), + append(make([]interface{}, 0), "b", "a", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), + }, { `.[]`, append(make([]interface{}, 0), "SELF", "TRAVERSE_ARRAY", "[", "EMPTY", "]"), @@ -72,6 +77,11 @@ var pathTests = []struct { append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"), append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), }, + { + `.a[]?`, + append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"), + append(make([]interface{}, 0), "a", "EMPTY", "COLLECT", "SHORT_PIPE", "TRAVERSE_ARRAY"), + }, { `.a.[]`, append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "EMPTY", "]"), @@ -82,6 +92,11 @@ var pathTests = []struct { 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.[0]`, append(make([]interface{}, 0), "a", "TRAVERSE_ARRAY", "[", "0 (int64)", "]"), diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 64e672c7..ad0dd2cf 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -29,8 +29,9 @@ const ( type token struct { TokenType tokenType 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 + AssignOperation *Operation // e.g. tag (GetTag) op becomes AssignTag if '=' follows it + CheckForPostTraverse bool // e.g. [1]cat should really be [1].cat + Match *machines.Match // match that created this token } @@ -145,7 +146,7 @@ func assignAllCommentsOp(updateAssign bool) lex.Action { func literalToken(pType tokenType, checkForPost bool) lex.Action { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { - return &token{TokenType: pType, CheckForPostTraverse: checkForPost}, nil + return &token{TokenType: pType, CheckForPostTraverse: checkForPost, Match: m}, nil } } @@ -331,7 +332,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`env\([^\)]+\)`), envOp(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(closeCollectObject, true)) lexer.Add([]byte(`\*[\+|\?d]*`), multiplyWithPrefs()) diff --git a/pkg/yqlib/operator_traverse_path.go b/pkg/yqlib/operator_traverse_path.go index db24ac80..c2c82174 100644 --- a/pkg/yqlib/operator_traverse_path.go +++ b/pkg/yqlib/operator_traverse_path.go @@ -90,14 +90,19 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode // rhs is a collect expression that will yield indexes to retreive of the arrays rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) + if err != nil { return Context{}, err } + prefs := traversePreferences{} + if expressionNode.Rhs.Rhs != nil && expressionNode.Rhs.Rhs.Operation.Preferences != nil { + prefs = expressionNode.Rhs.Rhs.Operation.Preferences.(traversePreferences) + } var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Content //now we traverse the result of the lhs against the indices we found - result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, traversePreferences{}) + result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs) if err != nil { return Context{}, err } diff --git a/pkg/yqlib/operator_traverse_path_test.go b/pkg/yqlib/operator_traverse_path_test.go index f6869461..b2a31f9a 100644 --- a/pkg/yqlib/operator_traverse_path_test.go +++ b/pkg/yqlib/operator_traverse_path_test.go @@ -45,14 +45,13 @@ var traversePathOperatorScenarios = []expressionScenario{ "D0, P[1], (!!map)::{c: banana}\n", }, }, - // { - // description: "Optional Splat", - // subdescription: "Just like splat, but won't error if you run it against scalars", - // document: `"cat"`, - // expression: `.[]?`, - // expected: []string{ - // }, - // }, + { + description: "Optional Splat", + subdescription: "Just like splat, but won't error if you run it against scalars", + document: `"cat"`, + expression: `.[]`, + expected: []string{}, + }, { description: "Special characters", subdescription: "Use quotes with brackets around path elements with special characters", @@ -112,13 +111,12 @@ var traversePathOperatorScenarios = []expressionScenario{ expression: `.a?`, expected: []string{}, }, - // { - // skipDoc: true, - // document: `[1,2,3]`, - // expression: `.["a"]?`, - // expected: []string{ - // }, - // }, + { + skipDoc: true, + document: `[[1,2,3], {a: frog}]`, + expression: `.[] | .["a"]?`, + expected: []string{"D0, P[1 a], (!!str)::frog\n"}, + }, { skipDoc: true, document: ``,