Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pkg/yqlib/doc/operators/headers/slice-array.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Slice/Splice Array
# Slice/Splice Array or String

The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.

You may leave out the first or second number, which will refer to the start or end of the array respectively.
You may leave out the first or second number, which will refer to the start or end of the array or string respectively.
68 changes: 65 additions & 3 deletions pkg/yqlib/doc/operators/slice-array.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Slice/Splice Array
# Slice/Splice Array or String

The slice array operator takes an array as input and returns a subarray. Like the `jq` equivalent, `.[10:15]` will return an array of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array.
The slice operator works on both arrays and strings. Like the `jq` equivalent, `.[10:15]` will return a subarray (or substring) of length 5, starting from index 10 inclusive, up to index 15 exclusive. Negative numbers count backwards from the end of the array or string.

You may leave out the first or second number, which will refer to the start or end of the array respectively.
You may leave out the first or second number, which will refer to the start or end of the array or string respectively.

## Slicing arrays
Given a sample.yml file of:
Expand Down Expand Up @@ -103,3 +103,65 @@ will output
- cow
```

## Slicing strings
Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[4:]' sample.yml
```
will output
```yaml
ralia
```

## Slicing strings - without the second number
Finishes at the end of the string

Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[0:5]' sample.yml
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test says without the second number, but both numbers are present

```
will output
```yaml
Austr
```

## Slicing strings - without the first number
Starts from the start of the string

Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[:5]' sample.yml
```
will output
```yaml
Austr
```

## Slicing strings - use negative numbers to count backwards from the end
Negative indices count from the end of the string

Given a sample.yml file of:
```yaml
country: Australia
```
then
```bash
yq '.country[-5:]' sample.yml
```
will output
```yaml
ralia
```

2 changes: 1 addition & 1 deletion pkg/yqlib/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func handleToken(tokens []*token, index int, postProcessedTokens []*token) (toke
if tokenIsOpType(currentToken, createMapOpType) {
log.Debugf("tokenIsOpType: createMapOpType")
// check the previous token is '[', means we are slice, but dont have a first number
if index > 0 && tokens[index-1].TokenType == traverseArrayCollect {
if index > 0 && (tokens[index-1].TokenType == traverseArrayCollect || tokens[index-1].TokenType == openCollect) {
log.Debugf("previous token is : traverseArrayOpType")
// need to put the number 0 before this token, as that is implied
postProcessedTokens = append(postProcessedTokens, &token{TokenType: operationToken, Operation: createValueOperation(0, "0")})
Expand Down
51 changes: 46 additions & 5 deletions pkg/yqlib/operator_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
}

func sliceStringNode(lhsNode *CandidateNode, firstNumber int, secondNumber int) (*CandidateNode, error) {

Check failure on line 19 in pkg/yqlib/operator_slice.go

View workflow job for this annotation

GitHub Actions / Build

sliceStringNode - result 1 (error) is always nil (unparam)

Check failure on line 19 in pkg/yqlib/operator_slice.go

View workflow job for this annotation

GitHub Actions / Build

sliceStringNode - result 1 (error) is always nil (unparam)
runes := []rune(lhsNode.Value)
length := len(runes)

relativeFirstNumber := firstNumber
if relativeFirstNumber < 0 {
relativeFirstNumber = length + firstNumber
}
if relativeFirstNumber < 0 {
relativeFirstNumber = 0
}

relativeSecondNumber := secondNumber
if relativeSecondNumber < 0 {
relativeSecondNumber = length + secondNumber
} else if relativeSecondNumber > length {
relativeSecondNumber = length
}

log.Debugf("sliceStringNode: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)

if relativeFirstNumber > length {
relativeFirstNumber = length
}
if relativeSecondNumber < relativeFirstNumber {
relativeSecondNumber = relativeFirstNumber
}

slicedString := string(runes[relativeFirstNumber:relativeSecondNumber])
return lhsNode.CreateReplacement(ScalarNode, "!!str", slicedString), nil
}

func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {

log.Debug("slice array operator!")
Expand All @@ -28,20 +60,29 @@
lhsNode := el.Value.(*CandidateNode)

firstNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.LHS)

if err != nil {
return Context{}, err
}
relativeFirstNumber := firstNumber
if relativeFirstNumber < 0 {
relativeFirstNumber = len(lhsNode.Content) + firstNumber
}

secondNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.RHS)
if err != nil {
return Context{}, err
}

if lhsNode.Kind == ScalarNode && lhsNode.guessTagFromCustomType() == "!!str" {
slicedNode, err := sliceStringNode(lhsNode, firstNumber, secondNumber)
if err != nil {
return Context{}, err
}
results.PushBack(slicedNode)
continue
}

relativeFirstNumber := firstNumber
if relativeFirstNumber < 0 {
relativeFirstNumber = len(lhsNode.Content) + firstNumber
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array slicing can still panic when the start index is more negative than the array length (e.g. .[-100:]): relativeFirstNumber is adjusted by len(lhsNode.Content)+firstNumber but is not clamped back to 0, so the loop can index lhsNode.Content with a negative i. Clamp relativeFirstNumber (and similarly relativeSecondNumber) into [0,len] before iterating to avoid runtime panics and match the existing out-of-bounds clamping behavior for other cases.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 341e252. Array slice now clamps both relativeFirstNumber and relativeSecondNumber into [0, len(Content)] after adjusting for negative indices, preventing a panic on expressions like .[-100:]. Also added test cases for both directions of out-of-bounds negative indices.


relativeSecondNumber := secondNumber
if relativeSecondNumber < 0 {
relativeSecondNumber = len(lhsNode.Content) + secondNumber
Expand Down
78 changes: 78 additions & 0 deletions pkg/yqlib/operator_slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,84 @@ var sliceArrayScenarios = []expressionScenario{
"D0, P[], (!!seq)::- cat1\n",
},
},
{
description: "Slicing strings",
document: `country: Australia`,
expression: `.country[4:]`,
expected: []string{
"D0, P[country], (!!str)::ralia\n",
},
},
{
description: "Slicing strings - without the second number",
subdescription: "Finishes at the end of the string",
document: `country: Australia`,
expression: `.country[0:5]`,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep this has the second number...should this be .country[5:] ?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix this ^;

also the build is failing with:

Error: pkg/yqlib/operator_slice.go:19:98: sliceStringNode - result 1 (error) is always nil (unparam)
func sliceStringNode(lhsNode *CandidateNode, firstNumber int, secondNumber int) (*CandidateNode, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9a9399a:

  • Changed "Slicing strings - without the second number" expression from .country[0:5] to .country[5:] (now actually omits the second number, giving alia)
  • Changed the first "Slicing strings" test to use both numbers: .country[0:5]Austr
  • Removed the always-nil error return from sliceStringNode to fix the build warning

expected: []string{
"D0, P[country], (!!str)::Austr\n",
},
},
{
description: "Slicing strings - without the first number",
subdescription: "Starts from the start of the string",
document: `country: Australia`,
expression: `.country[:5]`,
expected: []string{
"D0, P[country], (!!str)::Austr\n",
},
},
{
description: "Slicing strings - use negative numbers to count backwards from the end",
subdescription: "Negative indices count from the end of the string",
document: `country: Australia`,
expression: `.country[-5:]`,
expected: []string{
"D0, P[country], (!!str)::ralia\n",
},
},
{
skipDoc: true,
document: `country: Australia`,
expression: `.country[1:-1]`,
expected: []string{
"D0, P[country], (!!str)::ustrali\n",
},
},
{
skipDoc: true,
document: `country: Australia`,
expression: `.country[:]`,
expected: []string{
"D0, P[country], (!!str)::Australia\n",
},
},
{
skipDoc: true,
description: "second index beyond string length clamps",
document: `country: Australia`,
expression: `.country[:100]`,
expected: []string{
"D0, P[country], (!!str)::Australia\n",
},
},
{
skipDoc: true,
description: "first index beyond string length returns empty string",
document: `country: Australia`,
expression: `.country[100:]`,
expected: []string{
"D0, P[country], (!!str)::\n",
},
},
{
skipDoc: true,
description: "Unicode string slicing",
document: `greeting: héllo`,
expression: `.greeting[1:3]`,
expected: []string{
"D0, P[greeting], (!!str)::él\n",
},
},
}

func TestSliceOperatorScenarios(t *testing.T) {
Expand Down
6 changes: 5 additions & 1 deletion pkg/yqlib/operator_traverse_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode
log.Debugf("--traverseArrayOperator")

if expressionNode.RHS != nil && expressionNode.RHS.RHS != nil && expressionNode.RHS.RHS.Operation.OperationType == createMapOpType {
return sliceArrayOperator(d, context, expressionNode.RHS.RHS)
lhsContext, err := d.GetMatchingNodes(context, expressionNode.LHS)
if err != nil {
return Context{}, err
}
return sliceArrayOperator(d, lhsContext, expressionNode.RHS.RHS)
}

lhs, err := d.GetMatchingNodes(context, expressionNode.LHS)
Expand Down
6 changes: 5 additions & 1 deletion project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,8 @@ subsubarray
Ffile
Fquery
coverpkg
gsub
gsub
ralia
Austr
ustrali
héllo
Loading