diff --git a/pkg/yqlib/doc/operators/parent.md b/pkg/yqlib/doc/operators/parent.md index 02375f87..7870aa3f 100644 --- a/pkg/yqlib/doc/operators/parent.md +++ b/pkg/yqlib/doc/operators/parent.md @@ -79,6 +79,46 @@ will output c: cat ``` +## Get the top (root) parent +Use negative numbers to get the top parents + +Given a sample.yml file of: +```yaml +a: + b: + c: cat +``` +then +```bash +yq '.a.b.c | parent(-1)' sample.yml +``` +will output +```yaml +a: + b: + c: cat +``` + +## Root +Alias for parent(-1), returns the top level parent. This is usually the document node. + +Given a sample.yml file of: +```yaml +a: + b: + c: cat +``` +then +```bash +yq '.a.b.c | root' sample.yml +``` +will output +```yaml +a: + b: + c: cat +``` + ## N-th parent You can optionally supply the number of levels to go up for the parent, the default being 1. diff --git a/pkg/yqlib/lexer.go b/pkg/yqlib/lexer.go index 8e04761d..509d07b0 100644 --- a/pkg/yqlib/lexer.go +++ b/pkg/yqlib/lexer.go @@ -61,7 +61,7 @@ func unwrap(value string) string { } func extractNumberParameter(value string) (int, error) { - parameterParser := regexp.MustCompile(`.*\(([0-9]+)\)`) + parameterParser := regexp.MustCompile(`.*\((-?[0-9]+)\)`) matches := parameterParser.FindStringSubmatch(value) var indent, errParsingInt = parseInt(matches[1]) if errParsingInt != nil { diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index f7c9d056..d4451257 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -57,7 +57,7 @@ var participleYqRules = []*participleYqRule{ simpleOp("sort_?keys", sortKeysOpType), {"ArrayToMap", "array_?to_?map", expressionOpToken(`(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)`), 0}, - + {"Root", "root", expressionOpToken(`parent(-1)`), 0}, {"YamlEncodeWithIndent", `to_?yaml\([0-9]+\)`, encodeParseIndent(YamlFormat), 0}, {"XMLEncodeWithIndent", `to_?xml\([0-9]+\)`, encodeParseIndent(XMLFormat), 0}, {"JSONEncodeWithIndent", `to_?json\([0-9]+\)`, encodeParseIndent(JSONFormat), 0}, @@ -132,7 +132,7 @@ var participleYqRules = []*participleYqRule{ simpleOp("split", splitStringOpType), simpleOp("parents", getParentsOpType), - {"ParentWithLevel", `parent\([0-9]+\)`, parentWithLevel(), 0}, + {"ParentWithLevel", `parent\(-?[0-9]+\)`, parentWithLevel(), 0}, {"ParentWithDefaultLevel", `parent`, parentWithDefaultLevel(), 0}, simpleOp("keys", keysOpType), diff --git a/pkg/yqlib/operator_parent.go b/pkg/yqlib/operator_parent.go index a85c9ca5..3644db0f 100644 --- a/pkg/yqlib/operator_parent.go +++ b/pkg/yqlib/operator_parent.go @@ -35,9 +35,28 @@ func getParentOperator(_ *dataTreeNavigator, context Context, expressionNode *Ex for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) + + // Handle negative levels: count total parents first + levelsToGoUp := prefs.Level + if prefs.Level < 0 { + // Count all parents + totalParents := 0 + temp := candidate.Parent + for temp != nil { + totalParents++ + temp = temp.Parent + } + // Convert negative index to positive + // -1 means last parent (root), -2 means second to last, etc. + levelsToGoUp = totalParents + prefs.Level + 1 + if levelsToGoUp < 0 { + levelsToGoUp = 0 + } + } + currentLevel := 0 - for currentLevel < prefs.Level && candidate != nil { - log.Debugf("currentLevel: %v, desired: %v", currentLevel, prefs.Level) + for currentLevel < levelsToGoUp && candidate != nil { + log.Debugf("currentLevel: %v, desired: %v", currentLevel, levelsToGoUp) log.Debugf("candidate: %v", NodeToString(candidate)) candidate = candidate.Parent currentLevel++ diff --git a/pkg/yqlib/operator_parent_test.go b/pkg/yqlib/operator_parent_test.go index a1709895..0f0295ce 100644 --- a/pkg/yqlib/operator_parent_test.go +++ b/pkg/yqlib/operator_parent_test.go @@ -38,6 +38,49 @@ var parentOperatorScenarios = []expressionScenario{ "D0, P[], (!!seq)::- {c: cat}\n- {b: {c: cat}}\n- {a: {b: {c: cat}}}\n", }, }, + { + description: "Get the top (root) parent", + subdescription: "Use negative numbers to get the top parents", + document: "a:\n b:\n c: cat\n", + expression: `.a.b.c | parent(-1)`, + expected: []string{ + "D0, P[], (!!map)::a:\n b:\n c: cat\n", + }, + }, + { + description: "Root", + subdescription: "Alias for parent(-1), returns the top level parent. This is usually the document node.", + document: "a:\n b:\n c: cat\n", + expression: `.a.b.c | root`, + expected: []string{ + "D0, P[], (!!map)::a:\n b:\n c: cat\n", + }, + }, + { + description: "N-th negative", + skipDoc: true, + document: "a:\n b:\n c: cat\n", + expression: `.a.b.c | parent(-2)`, + expected: []string{ + "D0, P[a], (!!map)::b:\n c: cat\n", + }, + }, + { + description: "large negative", + skipDoc: true, + document: "a:\n b:\n c: cat\n", + expression: `.a.b.c | parent(-10)`, + expected: []string{ + "D0, P[a b c], (!!str)::cat\n", + }, + }, + { + description: "large positive", + skipDoc: true, + document: "a:\n b:\n c: cat\n", + expression: `.a.b.c | parent(10)`, + expected: []string{}, + }, { description: "N-th parent", subdescription: "You can optionally supply the number of levels to go up for the parent, the default being 1.",