2021-01-14 03:46:50 +00:00
package yqlib
import (
"container/list"
"fmt"
2021-04-15 00:09:41 +00:00
"regexp"
2021-01-14 03:46:50 +00:00
"strings"
)
2022-02-22 05:17:23 +00:00
type changeCasePrefs struct {
ToUpperCase bool
}
2024-01-11 02:17:34 +00:00
func trimSpaceOperator ( _ * dataTreeNavigator , context Context , _ * ExpressionNode ) ( Context , error ) {
2022-08-08 03:35:57 +00:00
results := list . New ( )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
2022-08-08 03:35:57 +00:00
2023-10-18 01:11:53 +00:00
if node . guessTagFromCustomType ( ) != "!!str" {
2022-08-08 03:35:57 +00:00
return Context { } , fmt . Errorf ( "cannot trim %v, can only operate on strings. " , node . Tag )
}
2023-10-18 01:11:53 +00:00
newStringNode := node . CreateReplacement ( ScalarNode , node . Tag , strings . TrimSpace ( node . Value ) )
newStringNode . Style = node . Style
results . PushBack ( newStringNode )
2022-08-08 03:35:57 +00:00
}
return context . ChildContext ( results ) , nil
}
2024-01-11 02:17:34 +00:00
func changeCaseOperator ( _ * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
2022-02-22 05:17:23 +00:00
results := list . New ( )
prefs := expressionNode . Operation . Preferences . ( changeCasePrefs )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
2022-02-22 05:17:23 +00:00
2023-10-18 01:11:53 +00:00
if node . guessTagFromCustomType ( ) != "!!str" {
2022-02-22 05:17:23 +00:00
return Context { } , fmt . Errorf ( "cannot change case with %v, can only operate on strings. " , node . Tag )
}
2023-10-18 01:11:53 +00:00
value := ""
2022-02-22 05:17:23 +00:00
if prefs . ToUpperCase {
2023-10-18 01:11:53 +00:00
value = strings . ToUpper ( node . Value )
2022-02-22 05:17:23 +00:00
} else {
2023-10-18 01:11:53 +00:00
value = strings . ToLower ( node . Value )
2022-02-22 05:17:23 +00:00
}
2023-10-18 01:11:53 +00:00
newStringNode := node . CreateReplacement ( ScalarNode , node . Tag , value )
newStringNode . Style = node . Style
results . PushBack ( newStringNode )
2022-02-22 05:17:23 +00:00
}
return context . ChildContext ( results ) , nil
}
2021-04-15 00:09:41 +00:00
func getSubstituteParameters ( d * dataTreeNavigator , block * ExpressionNode , context Context ) ( string , string , error ) {
regEx := ""
replacementText := ""
2022-02-07 00:55:55 +00:00
regExNodes , err := d . GetMatchingNodes ( context . ReadOnlyClone ( ) , block . LHS )
2021-04-15 00:09:41 +00:00
if err != nil {
return "" , "" , err
}
if regExNodes . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
regEx = regExNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-04-15 00:09:41 +00:00
}
log . Debug ( "regEx %v" , regEx )
2022-02-07 00:55:55 +00:00
replacementNodes , err := d . GetMatchingNodes ( context , block . RHS )
2021-04-15 00:09:41 +00:00
if err != nil {
return "" , "" , err
}
if replacementNodes . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
replacementText = replacementNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-04-15 00:09:41 +00:00
}
return regEx , replacementText , nil
}
2023-10-18 01:11:53 +00:00
func substitute ( original string , regex * regexp . Regexp , replacement string ) ( Kind , string , string ) {
2021-04-15 00:09:41 +00:00
replacedString := regex . ReplaceAllString ( original , replacement )
2023-10-18 01:11:53 +00:00
return ScalarNode , "!!str" , replacedString
2021-04-15 00:09:41 +00:00
}
func substituteStringOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
//rhs block operator
//lhs of block = regex
//rhs of block = replacement expression
2022-02-07 00:55:55 +00:00
block := expressionNode . RHS
2021-04-15 00:09:41 +00:00
regExStr , replacementText , err := getSubstituteParameters ( d , block , context )
if err != nil {
return Context { } , err
}
regEx , err := regexp . Compile ( regExStr )
if err != nil {
return Context { } , err
}
var results = list . New ( )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
if node . guessTagFromCustomType ( ) != "!!str" {
2021-12-20 22:30:08 +00:00
return Context { } , fmt . Errorf ( "cannot substitute with %v, can only substitute strings. Hint: Most often you'll want to use '|=' over '=' for this operation" , node . Tag )
2021-04-15 00:09:41 +00:00
}
2023-10-18 01:11:53 +00:00
result := node . CreateReplacement ( substitute ( node . Value , regEx , replacementText ) )
2021-04-15 00:09:41 +00:00
results . PushBack ( result )
}
return context . ChildContext ( results ) , nil
}
2023-10-18 01:11:53 +00:00
func addMatch ( original [ ] * CandidateNode , match string , offset int , name string ) [ ] * CandidateNode {
2021-07-11 01:08:18 +00:00
2021-07-07 12:47:16 +00:00
newContent := append ( original ,
2021-07-11 01:08:18 +00:00
createScalarNode ( "string" , "string" ) )
if offset < 0 {
// offset of -1 means there was no match, force a null value like jq
newContent = append ( newContent ,
createScalarNode ( nil , "null" ) ,
)
} else {
newContent = append ( newContent ,
createScalarNode ( match , match ) ,
)
}
newContent = append ( newContent ,
2021-07-07 12:40:46 +00:00
createScalarNode ( "offset" , "offset" ) ,
createScalarNode ( offset , fmt . Sprintf ( "%v" , offset ) ) ,
createScalarNode ( "length" , "length" ) ,
createScalarNode ( len ( match ) , fmt . Sprintf ( "%v" , len ( match ) ) ) )
2021-07-07 12:47:16 +00:00
if name != "" {
newContent = append ( newContent ,
createScalarNode ( "name" , "name" ) ,
createScalarNode ( name , name ) ,
)
}
return newContent
2021-07-07 12:40:46 +00:00
}
2021-07-09 05:33:41 +00:00
type matchPreferences struct {
Global bool
}
2021-07-11 01:08:18 +00:00
func getMatches ( matchPrefs matchPreferences , regEx * regexp . Regexp , value string ) ( [ ] [ ] string , [ ] [ ] int ) {
2021-07-09 05:33:41 +00:00
var allMatches [ ] [ ] string
var allIndices [ ] [ ] int
if matchPrefs . Global {
allMatches = regEx . FindAllStringSubmatch ( value , - 1 )
allIndices = regEx . FindAllStringSubmatchIndex ( value , - 1 )
} else {
allMatches = [ ] [ ] string { regEx . FindStringSubmatch ( value ) }
allIndices = [ ] [ ] int { regEx . FindStringSubmatchIndex ( value ) }
}
2021-07-07 12:40:46 +00:00
2021-07-09 05:54:56 +00:00
log . Debug ( "allMatches, %v" , allMatches )
2021-07-11 01:08:18 +00:00
return allMatches , allIndices
}
func match ( matchPrefs matchPreferences , regEx * regexp . Regexp , candidate * CandidateNode , value string , results * list . List ) {
subNames := regEx . SubexpNames ( )
allMatches , allIndices := getMatches ( matchPrefs , regEx , value )
2021-07-09 05:54:56 +00:00
// if all matches just has an empty array in it,
// then nothing matched
if len ( allMatches ) > 0 && len ( allMatches [ 0 ] ) == 0 {
return
}
2021-07-07 12:40:46 +00:00
for i , matches := range allMatches {
2023-10-18 01:11:53 +00:00
capturesListNode := & CandidateNode { Kind : SequenceNode }
2021-07-07 12:40:46 +00:00
match , submatches := matches [ 0 ] , matches [ 1 : ]
for j , submatch := range submatches {
2023-10-18 01:11:53 +00:00
captureNode := & CandidateNode { Kind : MappingNode }
captureNode . AddChildren ( addMatch ( captureNode . Content , submatch , allIndices [ i ] [ 2 + j * 2 ] , subNames [ j + 1 ] ) )
capturesListNode . AddChild ( captureNode )
2021-07-07 12:40:46 +00:00
}
2023-10-18 01:11:53 +00:00
node := candidate . CreateReplacement ( MappingNode , "!!map" , "" )
node . AddChildren ( addMatch ( node . Content , match , allIndices [ i ] [ 0 ] , "" ) )
node . AddKeyValueChild ( createScalarNode ( "captures" , "captures" ) , capturesListNode )
results . PushBack ( node )
2021-07-07 12:40:46 +00:00
}
}
2021-07-11 01:08:18 +00:00
func capture ( matchPrefs matchPreferences , regEx * regexp . Regexp , candidate * CandidateNode , value string , results * list . List ) {
subNames := regEx . SubexpNames ( )
allMatches , allIndices := getMatches ( matchPrefs , regEx , value )
// if all matches just has an empty array in it,
// then nothing matched
if len ( allMatches ) > 0 && len ( allMatches [ 0 ] ) == 0 {
return
}
for i , matches := range allMatches {
2023-10-18 01:11:53 +00:00
capturesNode := candidate . CreateReplacement ( MappingNode , "!!map" , "" )
2021-07-11 01:08:18 +00:00
_ , submatches := matches [ 0 ] , matches [ 1 : ]
for j , submatch := range submatches {
2023-10-18 01:11:53 +00:00
keyNode := createScalarNode ( subNames [ j + 1 ] , subNames [ j + 1 ] )
var valueNode * CandidateNode
2021-07-11 01:08:18 +00:00
offset := allIndices [ i ] [ 2 + j * 2 ]
// offset of -1 means there was no match, force a null value like jq
if offset < 0 {
2023-10-18 01:11:53 +00:00
valueNode = createScalarNode ( nil , "null" )
2021-07-11 01:08:18 +00:00
} else {
2023-10-18 01:11:53 +00:00
valueNode = createScalarNode ( submatch , submatch )
2021-07-11 01:08:18 +00:00
}
2023-10-18 01:11:53 +00:00
capturesNode . AddKeyValueChild ( keyNode , valueNode )
2021-07-11 01:08:18 +00:00
}
2023-10-18 01:11:53 +00:00
results . PushBack ( capturesNode )
2021-07-11 01:08:18 +00:00
}
}
2021-07-09 05:54:56 +00:00
func extractMatchArguments ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( * regexp . Regexp , matchPreferences , error ) {
2022-02-07 00:55:55 +00:00
regExExpNode := expressionNode . RHS
2021-07-09 05:33:41 +00:00
matchPrefs := matchPreferences { }
2021-07-07 12:40:46 +00:00
2021-07-09 05:33:41 +00:00
// we got given parameters e.g. match(exp; params)
2022-02-07 00:55:55 +00:00
if expressionNode . RHS . Operation . OperationType == blockOpType {
block := expressionNode . RHS
regExExpNode = block . LHS
replacementNodes , err := d . GetMatchingNodes ( context , block . RHS )
2021-07-09 05:33:41 +00:00
if err != nil {
2021-07-09 05:54:56 +00:00
return nil , matchPrefs , err
2021-07-09 05:33:41 +00:00
}
paramText := ""
if replacementNodes . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
paramText = replacementNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-07-09 05:33:41 +00:00
}
if strings . Contains ( paramText , "g" ) {
paramText = strings . ReplaceAll ( paramText , "g" , "" )
matchPrefs . Global = true
}
if strings . Contains ( paramText , "i" ) {
2021-07-09 05:54:56 +00:00
return nil , matchPrefs , fmt . Errorf ( ` 'i' is not a valid option for match. To ignore case, use an expression like match("(?i)cat") ` )
2021-07-09 05:33:41 +00:00
}
if len ( paramText ) > 0 {
2021-07-09 05:54:56 +00:00
return nil , matchPrefs , fmt . Errorf ( ` Unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators ` , paramText )
2021-07-09 05:33:41 +00:00
}
}
regExNodes , err := d . GetMatchingNodes ( context . ReadOnlyClone ( ) , regExExpNode )
2021-07-07 12:40:46 +00:00
if err != nil {
2021-07-09 05:54:56 +00:00
return nil , matchPrefs , err
2021-07-07 12:40:46 +00:00
}
log . Debug ( NodesToString ( regExNodes . MatchingNodes ) )
regExStr := ""
if regExNodes . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
regExStr = regExNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-07-07 12:40:46 +00:00
}
log . Debug ( "regEx %v" , regExStr )
2021-07-09 05:54:56 +00:00
regEx , err := regexp . Compile ( regExStr )
return regEx , matchPrefs , err
2021-07-09 05:33:41 +00:00
}
func matchOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
2021-07-09 05:54:56 +00:00
regEx , matchPrefs , err := extractMatchArguments ( d , context , expressionNode )
2021-07-09 05:33:41 +00:00
if err != nil {
return Context { } , err
}
2021-07-07 12:40:46 +00:00
2021-07-09 05:54:56 +00:00
var results = list . New ( )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
if node . guessTagFromCustomType ( ) != "!!str" {
2021-07-09 05:54:56 +00:00
return Context { } , fmt . Errorf ( "cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation" , node . Tag )
}
2023-10-18 01:11:53 +00:00
match ( matchPrefs , regEx , node , node . Value , results )
2021-07-09 05:54:56 +00:00
}
return context . ChildContext ( results ) , nil
}
2021-07-11 01:08:18 +00:00
func captureOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
regEx , matchPrefs , err := extractMatchArguments ( d , context , expressionNode )
if err != nil {
return Context { } , err
}
var results = list . New ( )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
if node . guessTagFromCustomType ( ) != "!!str" {
2021-07-11 01:08:18 +00:00
return Context { } , fmt . Errorf ( "cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation" , node . Tag )
}
2023-10-18 01:11:53 +00:00
capture ( matchPrefs , regEx , node , node . Value , results )
2021-07-11 01:08:18 +00:00
}
return context . ChildContext ( results ) , nil
}
2021-07-09 05:54:56 +00:00
func testOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
regEx , _ , err := extractMatchArguments ( d , context , expressionNode )
2021-07-07 12:40:46 +00:00
if err != nil {
return Context { } , err
}
var results = list . New ( )
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
if node . guessTagFromCustomType ( ) != "!!str" {
2021-07-09 05:33:41 +00:00
return Context { } , fmt . Errorf ( "cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation" , node . Tag )
2021-07-07 12:40:46 +00:00
}
2021-07-09 05:54:56 +00:00
matches := regEx . FindStringSubmatch ( node . Value )
2023-10-18 01:11:53 +00:00
results . PushBack ( createBooleanCandidate ( node , len ( matches ) > 0 ) )
2021-07-07 12:40:46 +00:00
}
return context . ChildContext ( results ) , nil
}
2021-02-02 07:17:59 +00:00
func joinStringOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
2021-01-14 03:46:50 +00:00
log . Debugf ( "-- joinStringOperator" )
joinStr := ""
2022-02-07 00:55:55 +00:00
rhs , err := d . GetMatchingNodes ( context . ReadOnlyClone ( ) , expressionNode . RHS )
2021-01-14 03:46:50 +00:00
if err != nil {
2021-02-02 07:17:59 +00:00
return Context { } , err
2021-01-14 03:46:50 +00:00
}
2021-02-02 07:17:59 +00:00
if rhs . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
joinStr = rhs . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-01-14 03:46:50 +00:00
}
var results = list . New ( )
2021-02-02 07:17:59 +00:00
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
if node . Kind != SequenceNode {
2021-04-15 00:09:41 +00:00
return Context { } , fmt . Errorf ( "cannot join with %v, can only join arrays of scalars" , node . Tag )
2021-01-14 03:46:50 +00:00
}
2023-10-18 01:11:53 +00:00
result := node . CreateReplacement ( join ( node . Content , joinStr ) )
2021-01-14 03:46:50 +00:00
results . PushBack ( result )
}
2021-02-02 07:17:59 +00:00
return context . ChildContext ( results ) , nil
2021-01-14 03:46:50 +00:00
}
2023-10-18 01:11:53 +00:00
func join ( content [ ] * CandidateNode , joinStr string ) ( Kind , string , string ) {
2021-01-14 03:46:50 +00:00
var stringsToJoin [ ] string
for _ , node := range content {
str := node . Value
if node . Tag == "!!null" {
str = ""
}
stringsToJoin = append ( stringsToJoin , str )
}
2023-10-18 01:11:53 +00:00
return ScalarNode , "!!str" , strings . Join ( stringsToJoin , joinStr )
2021-01-14 03:46:50 +00:00
}
2021-01-14 04:05:50 +00:00
2021-02-02 07:17:59 +00:00
func splitStringOperator ( d * dataTreeNavigator , context Context , expressionNode * ExpressionNode ) ( Context , error ) {
2021-01-14 04:05:50 +00:00
log . Debugf ( "-- splitStringOperator" )
splitStr := ""
2022-02-07 00:55:55 +00:00
rhs , err := d . GetMatchingNodes ( context . ReadOnlyClone ( ) , expressionNode . RHS )
2021-01-14 04:05:50 +00:00
if err != nil {
2021-02-02 07:17:59 +00:00
return Context { } , err
2021-01-14 04:05:50 +00:00
}
2021-02-02 07:17:59 +00:00
if rhs . MatchingNodes . Front ( ) != nil {
2023-10-18 01:11:53 +00:00
splitStr = rhs . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Value
2021-01-14 04:05:50 +00:00
}
var results = list . New ( )
2021-02-02 07:17:59 +00:00
for el := context . MatchingNodes . Front ( ) ; el != nil ; el = el . Next ( ) {
2023-10-18 01:11:53 +00:00
node := el . Value . ( * CandidateNode )
2021-01-14 04:05:50 +00:00
if node . Tag == "!!null" {
continue
}
2022-02-22 03:50:45 +00:00
2023-10-18 01:11:53 +00:00
if node . guessTagFromCustomType ( ) != "!!str" {
return Context { } , fmt . Errorf ( "cannot split %v, can only split strings" , node . Tag )
2021-01-14 04:05:50 +00:00
}
2023-10-18 01:11:53 +00:00
kind , tag , content := split ( node . Value , splitStr )
result := node . CreateReplacement ( kind , tag , "" )
result . AddChildren ( content )
2021-01-14 04:05:50 +00:00
results . PushBack ( result )
}
2021-02-02 07:17:59 +00:00
return context . ChildContext ( results ) , nil
2021-01-14 04:05:50 +00:00
}
2023-10-18 01:11:53 +00:00
func split ( value string , spltStr string ) ( Kind , string , [ ] * CandidateNode ) {
var contents [ ] * CandidateNode
2021-01-14 04:05:50 +00:00
if value != "" {
2022-11-14 05:40:41 +00:00
log . Debug ( "going to spltStr[%v]" , spltStr )
2021-01-14 04:05:50 +00:00
var newStrings = strings . Split ( value , spltStr )
2023-10-18 01:11:53 +00:00
contents = make ( [ ] * CandidateNode , len ( newStrings ) )
2021-01-14 04:05:50 +00:00
for index , str := range newStrings {
2023-10-18 01:11:53 +00:00
contents [ index ] = & CandidateNode { Kind : ScalarNode , Tag : "!!str" , Value : str }
2021-01-14 04:05:50 +00:00
}
}
2023-10-18 01:11:53 +00:00
return SequenceNode , "!!seq" , contents
2021-01-14 04:05:50 +00:00
}