Support negative parent indices

This commit is contained in:
Mike Farah 2025-12-20 14:59:26 +11:00
parent 66ec487792
commit 64516c639a
5 changed files with 107 additions and 5 deletions

View File

@ -79,6 +79,46 @@ will output
c: cat 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 ## N-th parent
You can optionally supply the number of levels to go up for the parent, the default being 1. You can optionally supply the number of levels to go up for the parent, the default being 1.

View File

@ -61,7 +61,7 @@ func unwrap(value string) string {
} }
func extractNumberParameter(value string) (int, error) { func extractNumberParameter(value string) (int, error) {
parameterParser := regexp.MustCompile(`.*\(([0-9]+)\)`) parameterParser := regexp.MustCompile(`.*\((-?[0-9]+)\)`)
matches := parameterParser.FindStringSubmatch(value) matches := parameterParser.FindStringSubmatch(value)
var indent, errParsingInt = parseInt(matches[1]) var indent, errParsingInt = parseInt(matches[1])
if errParsingInt != nil { if errParsingInt != nil {

View File

@ -57,7 +57,7 @@ var participleYqRules = []*participleYqRule{
simpleOp("sort_?keys", sortKeysOpType), simpleOp("sort_?keys", sortKeysOpType),
{"ArrayToMap", "array_?to_?map", expressionOpToken(`(.[] | select(. != null) ) as $i ireduce({}; .[$i | key] = $i)`), 0}, {"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}, {"YamlEncodeWithIndent", `to_?yaml\([0-9]+\)`, encodeParseIndent(YamlFormat), 0},
{"XMLEncodeWithIndent", `to_?xml\([0-9]+\)`, encodeParseIndent(XMLFormat), 0}, {"XMLEncodeWithIndent", `to_?xml\([0-9]+\)`, encodeParseIndent(XMLFormat), 0},
{"JSONEncodeWithIndent", `to_?json\([0-9]+\)`, encodeParseIndent(JSONFormat), 0}, {"JSONEncodeWithIndent", `to_?json\([0-9]+\)`, encodeParseIndent(JSONFormat), 0},
@ -132,7 +132,7 @@ var participleYqRules = []*participleYqRule{
simpleOp("split", splitStringOpType), simpleOp("split", splitStringOpType),
simpleOp("parents", getParentsOpType), simpleOp("parents", getParentsOpType),
{"ParentWithLevel", `parent\([0-9]+\)`, parentWithLevel(), 0}, {"ParentWithLevel", `parent\(-?[0-9]+\)`, parentWithLevel(), 0},
{"ParentWithDefaultLevel", `parent`, parentWithDefaultLevel(), 0}, {"ParentWithDefaultLevel", `parent`, parentWithDefaultLevel(), 0},
simpleOp("keys", keysOpType), simpleOp("keys", keysOpType),

View File

@ -35,9 +35,28 @@ func getParentOperator(_ *dataTreeNavigator, context Context, expressionNode *Ex
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) 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 currentLevel := 0
for currentLevel < prefs.Level && candidate != nil { for currentLevel < levelsToGoUp && candidate != nil {
log.Debugf("currentLevel: %v, desired: %v", currentLevel, prefs.Level) log.Debugf("currentLevel: %v, desired: %v", currentLevel, levelsToGoUp)
log.Debugf("candidate: %v", NodeToString(candidate)) log.Debugf("candidate: %v", NodeToString(candidate))
candidate = candidate.Parent candidate = candidate.Parent
currentLevel++ currentLevel++

View File

@ -38,6 +38,49 @@ var parentOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::- {c: cat}\n- {b: {c: cat}}\n- {a: {b: {c: cat}}}\n", "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", description: "N-th parent",
subdescription: "You can optionally supply the number of levels to go up for the parent, the default being 1.", subdescription: "You can optionally supply the number of levels to go up for the parent, the default being 1.",