diff --git a/pkg/yqlib/operator_first.go b/pkg/yqlib/operator_first.go index cdc4d289..e287ca12 100644 --- a/pkg/yqlib/operator_first.go +++ b/pkg/yqlib/operator_first.go @@ -8,9 +8,20 @@ func firstOperator(d *dataTreeNavigator, context Context, expressionNode *Expres for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) - // If no RHS expression is provided, simply return the first entry in candidate.Content + // If no RHS expression is provided, simply return the first entry if expressionNode == nil || expressionNode.RHS == nil { - if len(candidate.Content) > 0 { + if candidate.Kind == MappingNode { + // candidate.Content is [key0, value0, ...]; the first entry of a + // map is its first VALUE, not the key (first.md: "first matching + // value in a map"). Splat to get map values with correct paths. + splatted, err := splat(context.SingleChildContext(candidate), traversePreferences{}) + if err != nil { + return Context{}, err + } + if splatted.MatchingNodes.Front() != nil { + results.PushBack(splatted.MatchingNodes.Front().Value.(*CandidateNode)) + } + } else if len(candidate.Content) > 0 { results.PushBack(candidate.Content[0]) } continue diff --git a/pkg/yqlib/operator_first_test.go b/pkg/yqlib/operator_first_test.go index a83af4f6..e44f7a04 100644 --- a/pkg/yqlib/operator_first_test.go +++ b/pkg/yqlib/operator_first_test.go @@ -176,6 +176,15 @@ var firstOperatorScenarios = []expressionScenario{ expression: `first`, expected: []string{}, }, + { + description: "First value with no filter from a map", + skipDoc: true, + document: "{x: {a: banana}, y: {a: cat}}", + expression: `first`, + expected: []string{ + "D0, P[x], (!!map)::{a: banana}\n", + }, + }, } func TestFirstOperatorScenarios(t *testing.T) {