Files
yq/pkg/yqlib/expression_postfix.go
4d5f0bda59 feat: add jq style if-then-elif-else-end conditionals (#2871)
* feat: add jq style if-then-elif-else-end conditionals

Adds `if A then B elif C then D else E end`, following jq semantics:

- the condition is evaluated against each input, using the same
  truthiness as select / and / or (null and false are falsy)
- each result of the condition produces a result
- `else` is optional; when missing, falsy inputs are returned unchanged
  (jq 1.7 behaviour)
- a condition with no results (e.g. a missing key) is treated as null,
  consistent with `and`, `or` and `//`

`if` / `end` are lexed as brackets so the conditional can be followed by
traversals and used anywhere an expression can. `then` / `elif` / `else`
are binary ops with the lowest precedence, and the postfixer checks that
the keywords are in the right order inside an if block. Path elements like
`.if.then.else.end` are unaffected.

Closes #95

* updating tools

* build(deps): bump golang from 1.26.6 to 1.27.1 (#2855)

Bumps golang from 1.26.6 to 1.27.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.24 (#2866)

Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.24.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.24)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/text from 0.41.0 to 0.42.0 (#2867)

Bumps [golang.org/x/text](https://github.com/golang/text) from 0.41.0 to 0.42.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/mod from 0.40.0 to 0.41.0 (#2858)

Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/mod/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(toml): quote empty keys (#2870)

* build(deps): bump alpine from `28bd5fe` to `294b683` (#2875)

Bumps alpine from `28bd5fe` to `294b683`.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang from `512690a` to `3680233` (#2876)

Bumps golang from `512690a` to `3680233`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/net from 0.58.0 to 0.59.0 (#2868)

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.58.0 to 0.59.0.
- [Commits](https://github.com/golang/net/compare/v0.58.0...v0.59.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/hashicorp/hcl/v2 from 2.24.0 to 2.25.0 (#2878)

Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.24.0 to 2.25.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.24.0...v2.25.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hcl/v2
  dependency-version: 2.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/magiconair/properties (#2877)

Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.18.11 to 1.18.12.
- [Release notes](https://github.com/magiconair/properties/releases)
- [Commits](https://github.com/magiconair/properties/compare/v1.18.11...v1.18.12)

---
updated-dependencies:
- dependency-name: github.com/magiconair/properties
  dependency-version: 1.18.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: preserve explicit assignments in read-only expressions (#2850)

* fix: preserve explicit assignments in read-only expressions

AI agent disclosure: This commit was prepared by Codex acting on RRXXZZYY's behalf.

* fix: allow attribute assignments to create paths

---------

Co-authored-by: RRXXZZYY <[email protected]>

* Fixing assignment with merge (#2883)

* Writable context for compound assignments

* Fixing assignment with merge

* test: cover explicit YAML null conditions in if-then-else

Add a documented scenario showing that empty, ~, null and Null values
are falsy, plus tests for a tagged !!null, a null condition with no
else, and the remaining truthiness cases (only null and false are
falsy).

* Extracting if logic from expression_postfix

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Mike Farah <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jake Wang <[email protected]>
Co-authored-by: ZhongYu Li <[email protected]>
Co-authored-by: RRXXZZYY <[email protected]>
2026-09-29 13:43:28 +10:00

157 lines
5.2 KiB
Go

package yqlib
import (
"errors"
"fmt"
"log/slog"
)
type expressionPostFixer interface {
ConvertToPostfix([]*token) ([]*Operation, error)
}
type expressionPostFixerImpl struct {
}
func newExpressionPostFixer() expressionPostFixer {
return &expressionPostFixerImpl{}
}
func popOpToResult(opStack []*token, result []*Operation) ([]*token, []*Operation) {
var newOp *token
opStack, newOp = opStack[0:len(opStack)-1], opStack[len(opStack)-1]
log.Debugf("popped %v from opstack to results", newOp.toString(true))
return opStack, append(result, newOp.Operation)
}
func validateNoOpenTokens(token *token) error {
switch token.TokenType {
case openCollect:
return fmt.Errorf(("bad expression, could not find matching `]`"))
case openCollectObject:
return fmt.Errorf(("bad expression, could not find matching `}`"))
case openBracket:
closer := ")"
if token.ExpectedCloser != "" {
closer = token.ExpectedCloser
}
return fmt.Errorf("bad expression, could not find matching `%v`", closer)
}
return nil
}
func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Operation, error) {
var result []*Operation
// surround the whole thing with brackets
var opStack = []*token{{TokenType: openBracket}}
var tokens = append(infixTokens, &token{TokenType: closeBracket})
ifValidator := newIfBlockValidator()
for _, currentToken := range tokens {
log.Debugf("postfix processing currentToken %v", currentToken.toString(true))
switch currentToken.TokenType {
case openBracket, openCollect, openCollectObject:
ifValidator.onOpen(currentToken)
opStack = append(opStack, currentToken)
log.Debugf("put %v onto the opstack", currentToken.toString(true))
case closeCollect, closeCollectObject:
var opener tokenType = openCollect
var collectOperator = collectOpType
if currentToken.TokenType == closeCollectObject {
opener = openCollectObject
collectOperator = collectObjectOpType
}
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != opener {
missingClosingTokenErr := validateNoOpenTokens(opStack[len(opStack)-1])
if missingClosingTokenErr != nil {
return nil, missingClosingTokenErr
}
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("bad path expression, got close collect brackets without matching opening bracket")
}
// now we should have [ as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
log.Debugf("deleting open bracket from opstack")
//and append a collect to the result
// hack - see if there's the optional traverse flag
// on the close op - move it to the traverse array op
// allows for .["cat"]?
prefs := traversePreferences{}
closeTokenMatch := currentToken.Match
if closeTokenMatch[len(closeTokenMatch)-1:] == "?" {
prefs.OptionalTraverse = true
}
result = append(result, &Operation{OperationType: collectOperator})
log.Debugf("put collect onto the result")
if opener != openCollect {
result = append(result, &Operation{OperationType: shortPipeOpType})
log.Debugf("put shortpipe onto the result")
}
//traverseArrayCollect is a sneaky op that needs to be included too
//when closing a ]
if len(opStack) > 0 && opStack[len(opStack)-1].Operation != nil && opStack[len(opStack)-1].Operation.OperationType == traverseArrayOpType {
opStack[len(opStack)-1].Operation.Preferences = prefs
opStack, result = popOpToResult(opStack, result)
}
case closeBracket:
for len(opStack) > 0 && opStack[len(opStack)-1].TokenType != openBracket {
missingClosingTokenErr := validateNoOpenTokens(opStack[len(opStack)-1])
if missingClosingTokenErr != nil {
return nil, missingClosingTokenErr
}
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("bad expression, got close brackets without matching opening bracket")
}
if err := ifValidator.onClose(opStack[len(opStack)-1], currentToken); err != nil {
return nil, err
}
// now we should have ( as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]
default:
if err := ifValidator.onKeyword(opStack, currentToken); err != nil {
return nil, err
}
var currentPrecedence = currentToken.Operation.OperationType.Precedence
// pop off higher precedent operators onto the result
for len(opStack) > 0 &&
opStack[len(opStack)-1].TokenType == operationToken &&
opStack[len(opStack)-1].Operation.OperationType.Precedence > currentPrecedence {
opStack, result = popOpToResult(opStack, result)
}
// add this operator to the opStack
opStack = append(opStack, currentToken)
log.Debugf("put %v onto the opstack", currentToken.toString(true))
}
}
log.Debugf("opstackLen: %v", len(opStack))
if len(opStack) > 0 {
log.Debugf("opstack:")
for _, token := range opStack {
log.Debugf("- %v", token.toString(true))
}
return nil, fmt.Errorf("bad expression - probably missing close bracket on %v", opStack[len(opStack)-1].toString(false))
}
if log.IsEnabledFor(slog.LevelDebug) {
log.Debugf("PostFix Result:")
for _, currentToken := range result {
log.Debugf("> %v", currentToken.toString())
}
}
return result, nil
}