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"
"gopkg.in/yaml.v3"
)
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 {
regEx = regExNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . Value
}
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 {
replacementText = replacementNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . Value
}
return regEx , replacementText , nil
}
func substitute ( original string , regex * regexp . Regexp , replacement string ) * yaml . Node {
replacedString := regex . ReplaceAllString ( original , replacement )
return & yaml . Node { Kind : yaml . ScalarNode , Value : replacedString , Tag : "!!str" }
}
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 ( ) {
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
2022-02-22 03:50:45 +00:00
if guessTagFromCustomType ( node ) != "!!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
}
targetNode := substitute ( node . Value , regEx , replacementText )
2021-11-23 22:57:35 +00:00
result := candidate . CreateReplacement ( targetNode )
2021-04-15 00:09:41 +00:00
results . PushBack ( result )
}
return context . ChildContext ( results ) , nil
}
2021-07-07 12:47:16 +00:00
func addMatch ( original [ ] * yaml . Node , match string , offset int , name string ) [ ] * yaml . Node {
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 {
2022-02-20 02:28:13 +00:00
capturesListNode := & yaml . Node { Kind : yaml . SequenceNode }
2021-07-07 12:40:46 +00:00
match , submatches := matches [ 0 ] , matches [ 1 : ]
for j , submatch := range submatches {
captureNode := & yaml . Node { Kind : yaml . MappingNode }
2022-02-20 02:28:13 +00:00
captureNode . Content = addMatch ( captureNode . Content , submatch , allIndices [ i ] [ 2 + j * 2 ] , subNames [ j + 1 ] )
capturesListNode . Content = append ( capturesListNode . Content , captureNode )
2021-07-07 12:40:46 +00:00
}
node := & yaml . Node { Kind : yaml . MappingNode }
2021-07-07 12:47:16 +00:00
node . Content = addMatch ( node . Content , match , allIndices [ i ] [ 0 ] , "" )
2021-07-07 12:40:46 +00:00
node . Content = append ( node . Content ,
createScalarNode ( "captures" , "captures" ) ,
2022-02-20 02:28:13 +00:00
capturesListNode ,
2021-07-07 12:40:46 +00:00
)
2021-11-23 22:57:35 +00:00
results . PushBack ( candidate . CreateReplacement ( 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 {
capturesNode := & yaml . Node { Kind : yaml . MappingNode }
_ , submatches := matches [ 0 ] , matches [ 1 : ]
for j , submatch := range submatches {
capturesNode . Content = append ( capturesNode . Content ,
createScalarNode ( subNames [ j + 1 ] , subNames [ j + 1 ] ) )
offset := allIndices [ i ] [ 2 + j * 2 ]
// offset of -1 means there was no match, force a null value like jq
if offset < 0 {
capturesNode . Content = append ( capturesNode . Content ,
createScalarNode ( nil , "null" ) ,
)
} else {
capturesNode . Content = append ( capturesNode . Content ,
createScalarNode ( submatch , submatch ) ,
)
}
}
2021-11-23 22:57:35 +00:00
results . PushBack ( candidate . CreateReplacement ( 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 {
paramText = replacementNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . Value
}
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 {
regExStr = regExNodes . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . Value
}
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 ( ) {
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
2022-02-22 03:50:45 +00:00
if guessTagFromCustomType ( node ) != "!!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 )
}
match ( matchPrefs , regEx , candidate , node . Value , results )
}
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 ( ) {
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
2022-02-22 03:50:45 +00:00
if guessTagFromCustomType ( node ) != "!!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 )
}
capture ( matchPrefs , regEx , candidate , node . Value , results )
}
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 ( ) {
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
2022-02-22 03:50:45 +00:00
if guessTagFromCustomType ( node ) != "!!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 )
results . PushBack ( createBooleanCandidate ( candidate , 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 {
joinStr = rhs . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . 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 ( ) {
2021-01-14 03:46:50 +00:00
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
if node . Kind != yaml . 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
}
targetNode := join ( node . Content , joinStr )
2021-11-23 22:57:35 +00:00
result := candidate . CreateReplacement ( targetNode )
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
}
func join ( content [ ] * yaml . Node , joinStr string ) * yaml . Node {
var stringsToJoin [ ] string
for _ , node := range content {
str := node . Value
if node . Tag == "!!null" {
str = ""
}
stringsToJoin = append ( stringsToJoin , str )
}
return & yaml . Node { Kind : yaml . ScalarNode , Value : strings . Join ( stringsToJoin , joinStr ) , Tag : "!!str" }
}
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 {
splitStr = rhs . MatchingNodes . Front ( ) . Value . ( * CandidateNode ) . Node . 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 ( ) {
2021-01-14 04:05:50 +00:00
candidate := el . Value . ( * CandidateNode )
node := unwrapDoc ( candidate . Node )
if node . Tag == "!!null" {
continue
}
2022-02-22 03:50:45 +00:00
if guessTagFromCustomType ( node ) != "!!str" {
2021-02-02 07:17:59 +00:00
return Context { } , fmt . Errorf ( "Cannot split %v, can only split strings" , node . Tag )
2021-01-14 04:05:50 +00:00
}
targetNode := split ( node . Value , splitStr )
2021-11-23 22:57:35 +00:00
result := candidate . CreateReplacement ( targetNode )
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
}
func split ( value string , spltStr string ) * yaml . Node {
var contents [ ] * yaml . Node
if value != "" {
var newStrings = strings . Split ( value , spltStr )
contents = make ( [ ] * yaml . Node , len ( newStrings ) )
for index , str := range newStrings {
contents [ index ] = & yaml . Node { Kind : yaml . ScalarNode , Tag : "!!str" , Value : str }
}
}
return & yaml . Node { Kind : yaml . SequenceNode , Tag : "!!seq" , Content : contents }
}