mirror of
https://github.com/mikefarah/yq.git
synced 2026-10-12 02:19:06 +00:00
* 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]>
157 lines
5.2 KiB
Go
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
|
|
}
|