Repository: TomWright/dasel
Pull request #149 (2021-08-08 21:12:52)

Add != comparison operator support in dynamic and search selectors


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2de4ff9..f172528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-- Nothing
+### Added
+
+- Support for `!=` comparison operator in dynamic and search selectors.
+- Support for `-`/`keyValue` key in dynamic selectors.
 
 ## [v1.16.1] - 2021-08-02
 
diff --git a/condition_equal.go b/condition_equal.go
index a92efb3..7b0c4c3 100644
--- a/condition_equal.go
+++ b/condition_equal.go
@@ -12,6 +12,16 @@ type EqualCondition struct {
 	Key string
 	// Value is the value we are looking for.
 	Value string
+	// Not is true if this is a not equal check.
+	Not bool
+}
+
+func (c EqualCondition) check(a interface{}, b interface{}) (bool, error) {
+	var res = fmt.Sprint(a) == b
+	if c.Not {
+		res = !res
+	}
+	return res, nil
 }
 
 // Check checks to see if other contains the required key value pair.
@@ -23,7 +33,7 @@ func (c EqualCondition) Check(other reflect.Value) (bool, error) {
 	value := unwrapValue(other)
 
 	if c.Key == "value" || c.Key == "." {
-		return fmt.Sprint(value.Interface()) == c.Value, nil
+		return c.check(value.Interface(), c.Value)
 	}
 
 	switch value.Kind() {
@@ -39,7 +49,7 @@ func (c EqualCondition) Check(other reflect.Value) (bool, error) {
 			return false, fmt.Errorf("subquery failed: %w", err)
 		}
 
-		return fmt.Sprint(foundNode.InterfaceValue()) == c.Value, nil
+		return c.check(foundNode.InterfaceValue(), c.Value)
 	}
 
 	return false, &UnhandledCheckType{Value: value.String()}
diff --git a/condition_key_equal.go b/condition_key_equal.go
index 65de611..6e59ddc 100644
--- a/condition_key_equal.go
+++ b/condition_key_equal.go
@@ -1,6 +1,7 @@
 package dasel
 
 import (
+	"fmt"
 	"reflect"
 )
 
@@ -8,6 +9,16 @@ import (
 type KeyEqualCondition struct {
 	// Value is the value we are looking for.
 	Value string
+	// Not is true if this is a not equal check.
+	Not bool
+}
+
+func (c KeyEqualCondition) check(a interface{}, b interface{}) (bool, error) {
+	var res = fmt.Sprint(a) == b
+	if c.Not {
+		res = !res
+	}
+	return res, nil
 }
 
 // Check checks to see if other contains the required key value pair.
@@ -18,5 +29,5 @@ func (c KeyEqualCondition) Check(other reflect.Value) (bool, error) {
 
 	value := unwrapValue(other)
 
-	return c.Value == value.String(), nil
+	return c.check(c.Value, value.String())
 }
diff --git a/go.sum b/go.sum
index b948cde..297f70c 100644
--- a/go.sum
+++ b/go.sum
@@ -34,7 +34,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -101,7 +100,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -121,7 +119,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
diff --git a/node_query.go b/node_query.go
index b7cca6a..0b60e1b 100644
--- a/node_query.go
+++ b/node_query.go
@@ -119,12 +119,21 @@ func findNextAvailableIndex(n *Node, createIfNotExists bool) (reflect.Value, err
 }
 
 // processFindDynamicItem is used by findValueDynamic.
-func processFindDynamicItem(n *Node, object reflect.Value) (bool, error) {
+func processFindDynamicItem(n *Node, object reflect.Value, key string) (bool, error) {
 	// Loop through each condition.
 	allConditionsMatched := true
 	for _, c := range n.Selector.Conditions {
 		// If the object doesn't match any checks, return a ValueNotFound.
-		found, err := c.Check(object)
+
+		var found bool
+		var err error
+		switch cond := c.(type) {
+		case *KeyEqualCondition:
+			found, err = cond.Check(reflect.ValueOf(key))
+		default:
+			found, err = cond.Check(object)
+		}
+
 		if err != nil {
 			return false, err
 		}
@@ -152,7 +161,7 @@ func findValueDynamic(n *Node, createIfNotExists bool) (reflect.Value, error) {
 	case reflect.Slice:
 		for i := 0; i < value.Len(); i++ {
 			object := value.Index(i)
-			found, err := processFindDynamicItem(n, object)
+			found, err := processFindDynamicItem(n, object, fmt.Sprint(i))
 			if err != nil {
 				return nilValue(), err
 			}
@@ -171,7 +180,7 @@ func findValueDynamic(n *Node, createIfNotExists bool) (reflect.Value, error) {
 	case reflect.Map:
 		for _, key := range value.MapKeys() {
 			object := value.MapIndex(key)
-			found, err := processFindDynamicItem(n, object)
+			found, err := processFindDynamicItem(n, object, key.String())
 			if err != nil {
 				return nilValue(), err
 			}
diff --git a/node_query_multiple.go b/node_query_multiple.go
index ed2defd..02b8d78 100644
--- a/node_query_multiple.go
+++ b/node_query_multiple.go
@@ -182,12 +182,21 @@ func findNextAvailableIndexNodes(selector Selector, previousValue reflect.Value,
 }
 
 // processFindDynamicItems is used by findNodesDynamic.
-func processFindDynamicItems(selector Selector, object reflect.Value) (bool, error) {
+func processFindDynamicItems(selector Selector, object reflect.Value, key string) (bool, error) {
 	// Loop through each condition.
 	allConditionsMatched := true
 	for _, c := range selector.Conditions {
 		// If the object doesn't match any checks, return a ValueNotFound.
-		found, err := c.Check(object)
+
+		var found bool
+		var err error
+		switch cond := c.(type) {
+		case *KeyEqualCondition:
+			found, err = cond.Check(reflect.ValueOf(key))
+		default:
+			found, err = cond.Check(object)
+		}
+
 		if err != nil {
 			return false, err
 		}
@@ -215,7 +224,7 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
 		results := make([]*Node, 0)
 		for i := 0; i < value.Len(); i++ {
 			object := value.Index(i)
-			found, err := processFindDynamicItems(selector, object)
+			found, err := processFindDynamicItems(selector, object, fmt.Sprint(i))
 			if err != nil {
 				return nil, err
 			}
@@ -246,7 +255,7 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
 		results := make([]*Node, 0)
 		for _, key := range value.MapKeys() {
 			object := value.MapIndex(key)
-			found, err := processFindDynamicItems(selector, object)
+			found, err := processFindDynamicItems(selector, object, key.String())
 			if err != nil {
 				return nil, err
 			}
diff --git a/parse_selector.go b/parse_selector.go
index 077b07c..7196031 100644
--- a/parse_selector.go
+++ b/parse_selector.go
@@ -46,52 +46,82 @@ func ParseSelector(selector string) (Selector, error) {
 	return sel, err
 }
 
-func processParseSelectorDynamic(selector string, sel Selector) (Selector, error) {
-	sel.Type = "DYNAMIC"
-	dynamicGroups, err := DynamicSelectorToGroups(selector)
-	if err != nil {
-		return sel, err
-	}
-
-	for _, g := range dynamicGroups {
-		m := FindDynamicSelectorParts(g)
+func getCondition(parts DynamicSelectorParts) (Condition, error) {
+	switch parts.Key {
+	case "-", "keyValue":
+		switch parts.Comparison {
+		case "=":
+			return &KeyEqualCondition{
+				Value: parts.Value,
+			}, nil
+		case "!=":
+			return &KeyEqualCondition{
+				Value: parts.Value,
+				Not:   true,
+			}, nil
+		default:
+			return nil, &UnknownComparisonOperatorErr{Operator: parts.Comparison}
+		}
+	default:
 
-		var cond Condition
-		switch m.Comparison {
+		switch parts.Comparison {
 		case "=":
-			cond = &EqualCondition{
-				Key:   m.Key,
-				Value: m.Value,
-			}
+			return &EqualCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
+			}, nil
+		case "!=":
+			return &EqualCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
+				Not:   true,
+			}, nil
 		case ">=":
-			cond = &SortedComparisonCondition{
-				Key:   m.Key,
-				Value: m.Value,
+			return &SortedComparisonCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
 				Equal: true,
 				After: true,
-			}
+			}, nil
 		case ">":
-			cond = &SortedComparisonCondition{
-				Key:   m.Key,
-				Value: m.Value,
+			return &SortedComparisonCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
 				After: true,
-			}
+			}, nil
 		case "<=":
-			cond = &SortedComparisonCondition{
-				Key:   m.Key,
-				Value: m.Value,
+			return &SortedComparisonCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
 				Equal: true,
-			}
+			}, nil
 		case "<":
-			cond = &SortedComparisonCondition{
-				Key:   m.Key,
-				Value: m.Value,
-			}
+			return &SortedComparisonCondition{
+				Key:   parts.Key,
+				Value: parts.Value,
+			}, nil
 		default:
-			return sel, &UnknownComparisonOperatorErr{Operator: m.Comparison}
+			return nil, &UnknownComparisonOperatorErr{Operator: parts.Comparison}
 		}
+	}
+}
 
-		sel.Conditions = append(sel.Conditions, cond)
+func processParseSelectorDynamic(selector string, sel Selector) (Selector, error) {
+	sel.Type = "DYNAMIC"
+	dynamicGroups, err := DynamicSelectorToGroups(selector)
+	if err != nil {
+		return sel, err
+	}
+
+	for _, g := range dynamicGroups {
+		parts := FindDynamicSelectorParts(g)
+		cond, err := getCondition(parts)
+		if err != nil {
+			return sel, err
+		}
+		if cond != nil {
+			sel.Conditions = append(sel.Conditions, cond)
+		}
 	}
 
 	return sel, nil
@@ -109,58 +139,15 @@ func processParseSelectorSearch(selector string, sel Selector) (Selector, error)
 	}
 
 	for _, g := range dynamicGroups {
-		m := FindDynamicSelectorParts(g)
-
-		m.Key = strings.TrimPrefix(m.Key, "?:")
-
-		var cond Condition
-		switch m.Key {
-		case "-", "keyValue":
-			switch m.Comparison {
-			case "=":
-				cond = &KeyEqualCondition{
-					Value: m.Value,
-				}
-			default:
-				return sel, &UnknownComparisonOperatorErr{Operator: m.Comparison}
-			}
-		default:
-			switch m.Comparison {
-			case "=":
-				cond = &EqualCondition{
-					Key:   m.Key,
-					Value: m.Value,
-				}
-			case ">=":
-				cond = &SortedComparisonCondition{
-					Key:   m.Key,
-					Value: m.Value,
-					Equal: true,
-					After: true,
-				}
-			case ">":
-				cond = &SortedComparisonCondition{
-					Key:   m.Key,
-					Value: m.Value,
-					After: true,
-				}
-			case "<=":
-				cond = &SortedComparisonCondition{
-					Key:   m.Key,
-					Value: m.Value,
-					Equal: true,
-				}
-			case "<":
-				cond = &SortedComparisonCondition{
-					Key:   m.Key,
-					Value: m.Value,
-				}
-			default:
-				return sel, &UnknownComparisonOperatorErr{Operator: m.Comparison}
-			}
+		parts := FindDynamicSelectorParts(g)
+		parts.Key = strings.TrimPrefix(parts.Key, "?:")
+		cond, err := getCondition(parts)
+		if err != nil {
+			return sel, err
+		}
+		if cond != nil {
+			sel.Conditions = append(sel.Conditions, cond)
 		}
-
-		sel.Conditions = append(sel.Conditions, cond)
 	}
 
 	return sel, nil


Test patch:
diff --git a/node_test.go b/node_test.go
index 3620439..2a1f653 100644
--- a/node_test.go
+++ b/node_test.go
@@ -181,6 +181,19 @@ func TestParseSelector(t *testing.T) {
 			},
 		},
 	}))
+	t.Run("SearchNotEqual", testParseSelector(".(?:name!=asd)", dasel.Selector{
+		Raw:       ".(?:name!=asd)",
+		Current:   ".(?:name!=asd)",
+		Remaining: "",
+		Type:      "SEARCH",
+		Conditions: []dasel.Condition{
+			&dasel.EqualCondition{
+				Key:   "name",
+				Value: "asd",
+				Not:   true,
+			},
+		},
+	}))
 	t.Run("SearchMoreThan", testParseSelector(".(?:name.[#]>3)", dasel.Selector{
 		Raw:       ".(?:name.[#]>3)",
 		Current:   ".(?:name.[#]>3)",
@@ -244,6 +257,41 @@ func TestParseSelector(t *testing.T) {
 			},
 		},
 	}))
+	t.Run("SearchKeyNotEqual", testParseSelector(".(?:-!=asd)", dasel.Selector{
+		Raw:       ".(?:-!=asd)",
+		Current:   ".(?:-!=asd)",
+		Remaining: "",
+		Type:      "SEARCH",
+		Conditions: []dasel.Condition{
+			&dasel.KeyEqualCondition{
+				Value: "asd",
+				Not:   true,
+			},
+		},
+	}))
+	t.Run("DynamicKey", testParseSelector(".(-=asd)", dasel.Selector{
+		Raw:       ".(-=asd)",
+		Current:   ".(-=asd)",
+		Remaining: "",
+		Type:      "DYNAMIC",
+		Conditions: []dasel.Condition{
+			&dasel.KeyEqualCondition{
+				Value: "asd",
+			},
+		},
+	}))
+	t.Run("DynamicKeyNotEqual", testParseSelector(".(-!=asd)", dasel.Selector{
+		Raw:       ".(-!=asd)",
+		Current:   ".(-!=asd)",
+		Remaining: "",
+		Type:      "DYNAMIC",
+		Conditions: []dasel.Condition{
+			&dasel.KeyEqualCondition{
+				Value: "asd",
+				Not:   true,
+			},
+		},
+	}))
 	t.Run("DynamicEqual", testParseSelector(".(name=asd)", dasel.Selector{
 		Raw:       ".(name=asd)",
 		Current:   ".(name=asd)",
@@ -256,6 +304,19 @@ func TestParseSelector(t *testing.T) {
 			},
 		},
 	}))
+	t.Run("DynamicNotEqual", testParseSelector(".(name!=asd)", dasel.Selector{
+		Raw:       ".(name!=asd)",
+		Current:   ".(name!=asd)",
+		Remaining: "",
+		Type:      "DYNAMIC",
+		Conditions: []dasel.Condition{
+			&dasel.EqualCondition{
+				Key:   "name",
+				Value: "asd",
+				Not:   true,
+			},
+		},
+	}))
 	t.Run("DynamicMoreThan", testParseSelector(".(name.[#]>3)", dasel.Selector{
 		Raw:       ".(name.[#]>3)",
 		Current:   ".(name.[#]>3)",
@@ -380,10 +441,22 @@ func TestNode_QueryMultiple(t *testing.T) {
 	t.Run("SingleResult", testNodeQueryMultipleArray(".[0].name", []interface{}{
 		"Tom",
 	}))
-	t.Run("SingleResultDynamic", testNodeQueryMultipleArray(".(age=25).name", []interface{}{
+	t.Run("SingleResultDynamicEqual", testNodeQueryMultipleArray(".(age=25).name", []interface{}{
 		"Amelia",
 	}))
-	t.Run("SingleResultDynamic", testNodeQueryMultipleArray(".(age=27).name", []interface{}{
+	t.Run("SingleResultDynamicNotEqual", testNodeQueryMultipleArray(".(age!=27).name", []interface{}{
+		"Amelia",
+	}))
+	t.Run("SingleResultDynamicKeyEqual", testNodeQueryMultipleArray(".(-=0).name", []interface{}{
+		"Tom",
+	}))
+	t.Run("MultipleResultDynamicKeyNotEqual", testNodeQueryMultipleArray(".(-!=0).name", []interface{}{
+		"Jim", "Amelia",
+	}))
+	t.Run("MultipleResultSearchKeyNotEqual", testNodeQueryMultipleArray(".[*].(?:-!=name)", []interface{}{
+		"27", "27", "25",
+	}))
+	t.Run("MultipleResultDynamic", testNodeQueryMultipleArray(".(age=27).name", []interface{}{
 		"Tom",
 		"Jim",
 	}))