diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 808a1c4..e4ffa41 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -27,4 +27,4 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
file: ./coverage.txt # optional
flags: unittests # optional
- fail_ci_if_error: true # optional (default = false)
\ No newline at end of file
+ fail_ci_if_error: false # optional (default = false)
\ No newline at end of file
diff --git a/README.md b/README.md
index 2aaadea..06a3eb3 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Comparable to [jq](https://github.com/stedolan/jq) / [yq](https://github.com/kis
* [Any index](#any-index)
* [Dynamic](#dynamic)
* [Using queries in dynamic selectors](#using-queries-in-dynamic-selectors)
+ * [Search](#search)
* [Examples](#examples)
* [General](#general)
* [Filter JSON API results](#filter-json-api-results)
@@ -421,6 +422,40 @@ Once decoded, you can access them using any of the standard selectors provided b
```
Using [github.com/clbanning/mxj](https://github.com/clbanning/mxj).
+#### XML Documents
+
+XML documents within dasel are stored as a map of values.
+
+This is just how dasel stores data and is required for the general functionality to work. An example of a simple documents representation is as follows:
+
+```
+
+ Tom
+ 27
+
+```
+
+```
+map[
+ Person:map[
+ -active:true
+ Age:27
+ Name:map[
+ #text:Tom
+ -main:true
+ ]
+ ]
+]
+```
+
+In general this won't affect you, but on the odd occasion in specific instances it could lead to unexpected output.
+
+If you are struggling with this please raise an issue for support. This will also help me know when the docs aren't sufficient.
+
+##### Debugging
+
+You can run select commands with the `--plain` flag to see the raw data that is stored within dasel. This can help you figure out the exact properties you may need to target when it isn't immediately obvious.
+
#### Arrays/Lists
Due to the way that XML is decoded, dasel can only detect something as a list if there are at least 2 items.
@@ -565,6 +600,64 @@ The resolution of that query looks something like this:
.users.[0].name.first
```
+### Search
+
+Search selectors recursively search all the data below the current node and returns all the results - this means they can only be used in multi select/put commands.
+
+The syntax is as follows:
+```
+.(?:key=value)
+```
+
+If `key` is:
+- `.` or `value` - dasel checks if the current nodes value is `value`.
+- `-` or `keyValue` - dasel checks if the current nodes key/name/index value is `value`.
+- Else dasel uses the `key` as a selector itself and compares the result against `value`.
+
+#### Search Example
+
+```
+{
+ "users": [
+ {
+ "primary": true,
+ "name": {
+ "first": "Tom",
+ "last": "Wright"
+ }
+ },
+ {
+ "primary": false,
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wright"
+ }
+ }
+ ]
+}
+```
+
+Search for all objects with a key of `name` and output the first name of each:
+```
+dasel -p json -m '.(?:-=name).first'
+"Tom"
+"Joe"
+"Jim"
+```
+
+Search for all objects with a last name of `Wright` and output the first name of each:
+```
+dasel -p json -m '.(?:name.last=Wright).name.first'
+"Tom"
+"Jim"
+```
+
## Examples
### General
diff --git a/condition.go b/condition.go
index 6cffa96..305451d 100644
--- a/condition.go
+++ b/condition.go
@@ -1,50 +1,9 @@
package dasel
import (
- "errors"
- "fmt"
"reflect"
)
-// EqualCondition lets you check for an exact match.
-type EqualCondition struct {
- // Key is the key of the value to check against.
- Key string
- // Value is the value we are looking for.
- Value string
-}
-
-// Check checks to see if other contains the required key value pair.
-func (c EqualCondition) Check(other reflect.Value) (bool, error) {
- if !other.IsValid() {
- return false, &UnhandledCheckType{Value: nil}
- }
-
- value := unwrapValue(other)
-
- if c.Key == "value" || c.Key == "." {
- return fmt.Sprint(value.Interface()) == c.Value, nil
- }
-
- switch value.Kind() {
- case reflect.Map, reflect.Slice:
- subRootNode := New(value.Interface())
- foundNode, err := subRootNode.Query(c.Key)
- if err != nil {
- var valueNotFound = &ValueNotFound{}
- if errors.As(err, &valueNotFound) {
- return false, nil
- }
-
- return false, fmt.Errorf("subquery failed: %w", err)
- }
-
- return fmt.Sprint(foundNode.InterfaceValue()) == c.Value, nil
- }
-
- return false, &UnhandledCheckType{Value: value.Kind().String()}
-}
-
// Condition defines a Check we can use within dynamic selectors.
type Condition interface {
Check(other reflect.Value) (bool, error)
diff --git a/condition_equal.go b/condition_equal.go
new file mode 100644
index 0000000..9c7826f
--- /dev/null
+++ b/condition_equal.go
@@ -0,0 +1,46 @@
+package dasel
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+)
+
+// EqualCondition lets you check for an exact match.
+type EqualCondition struct {
+ // Key is the key of the value to check against.
+ Key string
+ // Value is the value we are looking for.
+ Value string
+}
+
+// Check checks to see if other contains the required key value pair.
+func (c EqualCondition) Check(other reflect.Value) (bool, error) {
+ if !other.IsValid() {
+ return false, &UnhandledCheckType{Value: nil}
+ }
+
+ value := unwrapValue(other)
+
+ if c.Key == "value" || c.Key == "." {
+ return fmt.Sprint(value.Interface()) == c.Value, nil
+ }
+
+ switch value.Kind() {
+ case reflect.Map, reflect.Slice:
+ subRootNode := New(value.Interface())
+ foundNode, err := subRootNode.Query(c.Key)
+ if err != nil {
+ var valueNotFound = &ValueNotFound{}
+ if errors.As(err, &valueNotFound) {
+ return false, nil
+ }
+
+ return false, fmt.Errorf("subquery failed: %w", err)
+ }
+
+ return fmt.Sprint(foundNode.InterfaceValue()) == c.Value, nil
+ }
+
+ return false, &UnhandledCheckType{Value: value.Kind().String()}
+}
diff --git a/condition_key_equal.go b/condition_key_equal.go
new file mode 100644
index 0000000..65de611
--- /dev/null
+++ b/condition_key_equal.go
@@ -0,0 +1,22 @@
+package dasel
+
+import (
+ "reflect"
+)
+
+// KeyEqualCondition lets you check for an exact match.
+type KeyEqualCondition struct {
+ // Value is the value we are looking for.
+ Value string
+}
+
+// Check checks to see if other contains the required key value pair.
+func (c KeyEqualCondition) Check(other reflect.Value) (bool, error) {
+ if !other.IsValid() {
+ return false, &UnhandledCheckType{Value: nil}
+ }
+
+ value := unwrapValue(other)
+
+ return c.Value == value.String(), nil
+}
diff --git a/condition_test.go b/condition_test.go
index 8310826..66fd806 100644
--- a/condition_test.go
+++ b/condition_test.go
@@ -73,3 +73,23 @@ func TestEqualCondition_Check(t *testing.T) {
false, &dasel.UnhandledCheckType{Value: ""},
))
}
+
+func TestKeyEqualCondition_Check(t *testing.T) {
+ c := &dasel.KeyEqualCondition{Value: "name"}
+
+ t.Run("MatchStringValue", conditionTest(
+ c,
+ "name",
+ true, nil,
+ ))
+ t.Run("NoMatchMissingKey", conditionTest(
+ c,
+ "asd",
+ false, nil,
+ ))
+ t.Run("Nil", conditionTest(
+ c,
+ nil,
+ false, &dasel.UnhandledCheckType{Value: nil},
+ ))
+}
diff --git a/internal/command/put_internal_test.go b/internal/command/put_internal_test.go
index 31b2c19..1a30e55 100644
--- a/internal/command/put_internal_test.go
+++ b/internal/command/put_internal_test.go
@@ -189,6 +189,18 @@ func TestPut(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
})
+ t.Run("InvalidVarType", func(t *testing.T) {
+ err := runGenericPutCommand(genericPutOptions{Parser: "yaml", ValueType: "int", Value: "asd", Reader: bytes.NewBuffer([]byte{})}, nil)
+ if err == nil || err.Error() != "could not parse int [asd]: strconv.ParseInt: parsing \"asd\": invalid syntax" {
+ t.Errorf("unexpected error: %v", err)
+ }
+ })
+ t.Run("FailedWrite", func(t *testing.T) {
+ err := runGenericPutCommand(genericPutOptions{Parser: "yaml", ValueType: "string", Selector: ".name", Value: "asd", Reader: bytes.NewBuffer([]byte{}), Writer: &failingWriter{}}, nil)
+ if err == nil || err.Error() != "could not write output: could not write to output file: could not write data: i am meant to fail at writing" {
+ t.Errorf("unexpected error: %v", err)
+ }
+ })
t.Run("ObjectMissingParserFlag", func(t *testing.T) {
err := runPutObjectCommand(putObjectOpts{}, nil)
if err == nil || err.Error() != "parser flag required when reading from stdin" {
diff --git a/internal/command/root_put_test.go b/internal/command/root_put_test.go
index df38bc0..6cdd58b 100644
--- a/internal/command/root_put_test.go
+++ b/internal/command/root_put_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"github.com/tomwright/dasel/internal/command"
"io/ioutil"
+ "os"
"strings"
"testing"
)
@@ -101,6 +102,51 @@ func putTest(in string, varType string, parser string, selector string, value st
}
}
+func putFileTest(in string, varType string, parser string, selector string, value string, out string, outFile string, expErr error, additionalArgs ...string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer func() {
+ _ = os.Remove(outFile)
+ }()
+ cmd := command.NewRootCMD()
+
+ args := []string{
+ "put", varType,
+ }
+ args = append(args, additionalArgs...)
+ args = append(args, "-p", parser, "-o", outFile, selector, value)
+
+ cmd.SetIn(strings.NewReader(in))
+ cmd.SetArgs(args)
+
+ err := cmd.Execute()
+
+ if expErr == nil && err != nil {
+ t.Errorf("expected err %v, got %v", expErr, err)
+ return
+ }
+ if expErr != nil && err == nil {
+ t.Errorf("expected err %v, got %v", expErr, err)
+ return
+ }
+ if expErr != nil && err != nil && err.Error() != expErr.Error() {
+ t.Errorf("expected err %v, got %v", expErr, err)
+ return
+ }
+
+ output, err := ioutil.ReadFile(outFile)
+ if err != nil {
+ t.Errorf("could not read output file: %s", err)
+ return
+ }
+
+ out = strings.TrimSpace(out)
+ outputStr := strings.TrimSpace(string(output))
+ if out != outputStr {
+ t.Errorf("expected result %v, got %v", out, outputStr)
+ }
+ }
+}
+
func TestRootCMD_Put_JSON(t *testing.T) {
t.Run("String", putStringTest(`{
"id": "x"
@@ -268,6 +314,150 @@ func TestRootCMD_Put_JSON(t *testing.T) {
"value": "X"
}
]`, nil, "-m"))
+
+ t.Run("KeySearch", putStringTest(`{
+ "users": [
+ {
+ "primary": true,
+ "name": {
+ "first": "Tom",
+ "last": "Wright"
+ }
+ },
+ {
+ "primary": false,
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wright"
+ }
+ }
+ ]
+}`, "json", ".(?:-=name).first", "Bobby", `{
+ "users": [
+ {
+ "name": {
+ "first": "Bobby",
+ "last": "Wright"
+ },
+ "primary": true
+ },
+ {
+ "extra": {
+ "name": {
+ "first": "Bobby",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Bobby",
+ "last": "Wright"
+ },
+ "primary": false
+ }
+ ]
+}`, nil, "-m"))
+
+ t.Run("ValueSearch", putStringTest(`{
+ "users": [
+ {
+ "primary": true,
+ "name": {
+ "first": "Tom",
+ "last": "Wright"
+ }
+ },
+ {
+ "primary": false,
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wright"
+ }
+ }
+ ]
+}`, "json", ".(?:.=Wright)", "Wrighto", `{
+ "users": [
+ {
+ "name": {
+ "first": "Tom",
+ "last": "Wrighto"
+ },
+ "primary": true
+ },
+ {
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wrighto"
+ },
+ "primary": false
+ }
+ ]
+}`, nil, "-m"))
+
+ t.Run("KeyValueSearch", putStringTest(`{
+ "users": [
+ {
+ "primary": true,
+ "name": {
+ "first": "Tom",
+ "last": "Wright"
+ }
+ },
+ {
+ "primary": false,
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wright"
+ }
+ }
+ ]
+}`, "json", ".(?:.last=Wright).first", "Fred", `{
+ "users": [
+ {
+ "name": {
+ "first": "Fred",
+ "last": "Wright"
+ },
+ "primary": true
+ },
+ {
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Fred",
+ "last": "Wright"
+ },
+ "primary": false
+ }
+ ]
+}`, nil, "-m"))
}
func TestRootCMD_Put_YAML(t *testing.T) {
@@ -278,6 +468,13 @@ name: "Tom"
id: "y"
name: Tom
`, nil))
+ t.Run("StringInFile", putFileTest(`
+id: "x"
+name: "Tom"
+`, "string", "yaml", "id", "y", `
+id: "y"
+name: Tom
+`, "TestRootCMD_Put_YAML_out.yaml", nil))
t.Run("Int", putIntTest(`
id: 123
`, "yaml", "id", "456", `
diff --git a/internal/command/root_select_test.go b/internal/command/root_select_test.go
index 07e3e20..5c9bf0b 100644
--- a/internal/command/root_select_test.go
+++ b/internal/command/root_select_test.go
@@ -2,6 +2,7 @@ package command_test
import (
"bytes"
+ "fmt"
"github.com/tomwright/dasel/internal/command"
"io/ioutil"
"strings"
@@ -134,7 +135,35 @@ func TestRootCMD_Select(t *testing.T) {
))
}
-func selectTest(in string, parser string, selector string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
+func selectTest(in string, parser string, selector string, output string, expErr error, additionalArgs ...string) func(t *testing.T) {
+ return selectTestCheck(in, parser, selector, func(out string) error {
+ if out != output {
+ return fmt.Errorf("expected %v, got %v", output, out)
+ }
+ return nil
+ }, expErr, additionalArgs...)
+}
+
+func selectTestContainsLines(in string, parser string, selector string, output []string, expErr error, additionalArgs ...string) func(t *testing.T) {
+ return selectTestCheck(in, parser, selector, func(out string) error {
+ splitOut := strings.Split(out, "\n")
+ for _, s := range output {
+ found := false
+ for _, got := range splitOut {
+ if s == got {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("required value not found: %s", s)
+ }
+ }
+ return nil
+ }, expErr, additionalArgs...)
+}
+
+func selectTestCheck(in string, parser string, selector string, checkFn func(out string) error, expErr error, additionalArgs ...string) func(t *testing.T) {
return func(t *testing.T) {
cmd := command.NewRootCMD()
outputBuffer := bytes.NewBuffer([]byte{})
@@ -172,8 +201,8 @@ func selectTest(in string, parser string, selector string, out string, expErr er
return
}
- if out != string(output) {
- t.Errorf("expected result %v, got %v", out, string(output))
+ if err := checkFn(string(output)); err != nil {
+ t.Errorf("unexpected output: %s", err)
}
}
}
@@ -296,6 +325,31 @@ func TestRootCmd_Select_JSON(t *testing.T) {
}
]
}`, "json", ".users.(.addresses.(.primary=true).number=123)(.name.last=Wright).name.first", newline(`"Tom"`), nil))
+
+ t.Run("KeySearch", selectTestContainsLines(`{
+ "users": [
+ {
+ "primary": true,
+ "name": {
+ "first": "Tom",
+ "last": "Wright"
+ }
+ },
+ {
+ "primary": false,
+ "extra": {
+ "name": {
+ "first": "Joe",
+ "last": "Blogs"
+ }
+ },
+ "name": {
+ "first": "Jim",
+ "last": "Wright"
+ }
+ }
+ ]
+}`, "json", ".(?:-=name).first", []string{`"Tom"`, `"Joe"`, `"Jim"`}, nil, "-m"))
}
func TestRootCmd_Select_YAML(t *testing.T) {
@@ -413,6 +467,21 @@ func TestRootCMD_Select_XML(t *testing.T) {
t.Run("DynamicString", selectTest(xmlData, "xml", ".data.details.addresses.(postcode=XXX XXX).street", "101 Some Street\n", nil))
t.Run("DynamicString", selectTest(xmlData, "xml", ".data.details.addresses.(postcode=YYY YYY).street", "34 Another Street\n", nil))
t.Run("Attribute", selectTest(xmlData, "xml", ".data.details.addresses.(-primary=true).street", "101 Some Street\n", nil))
+
+ t.Run("KeySearch", selectTestContainsLines(`
+
+
+
+
+
+
+
+
+
+
+
+
+`, "xml", ".food.(?:keyValue=apple).-color", []string{"yellow", "red", "green"}, nil, "-m"))
}
func TestRootCMD_Select_CSV(t *testing.T) {
diff --git a/node.go b/node.go
index d06bddb..7b24c52 100644
--- a/node.go
+++ b/node.go
@@ -27,6 +27,19 @@ type Selector struct {
Conditions []Condition `json:"conditions,omitempty"`
}
+// Copy returns a copy of the selector.
+func (s Selector) Copy() Selector {
+ return Selector{
+ Raw: s.Raw,
+ Current: s.Current,
+ Remaining: s.Remaining,
+ Type: s.Type,
+ Property: s.Property,
+ Index: s.Index,
+ Conditions: s.Conditions,
+ }
+}
+
// Node represents a single node in the chain of nodes for a selector.
type Node struct {
// Previous is the previous node in the chain.
@@ -112,6 +125,51 @@ func ParseSelector(selector string) (Selector, error) {
nextSel := strings.TrimPrefix(sel.Current, ".")
switch {
+ case strings.HasPrefix(nextSel, "(?:") && strings.HasSuffix(nextSel, ")"):
+ sel.Type = "SEARCH"
+
+ dynamicGroups, err := DynamicSelectorToGroups(nextSel)
+ if err != nil {
+ return sel, err
+ }
+ if len(dynamicGroups) != 1 {
+ return sel, fmt.Errorf("require exactly 1 group in search selector")
+ }
+
+ for _, g := range dynamicGroups {
+ m := dynamicSelectorRegexp.FindStringSubmatch(g)
+ if m == nil {
+ return sel, fmt.Errorf("invalid search format")
+ }
+
+ m[1] = strings.TrimPrefix(m[1], "?:")
+
+ var cond Condition
+ switch m[1] {
+ case "-", "keyValue":
+ switch m[2] {
+ case "=":
+ cond = &KeyEqualCondition{
+ Value: m[3],
+ }
+ default:
+ return sel, &UnknownComparisonOperatorErr{Operator: m[2]}
+ }
+ default:
+ switch m[2] {
+ case "=":
+ cond = &EqualCondition{
+ Key: strings.TrimPrefix(m[1], "?:"),
+ Value: m[3],
+ }
+ default:
+ return sel, &UnknownComparisonOperatorErr{Operator: m[2]}
+ }
+ }
+
+ sel.Conditions = append(sel.Conditions, cond)
+ }
+
case strings.HasPrefix(nextSel, "(") && strings.HasSuffix(nextSel, ")"):
sel.Type = "DYNAMIC"
dynamicGroups, err := DynamicSelectorToGroups(nextSel)
@@ -121,6 +179,9 @@ func ParseSelector(selector string) (Selector, error) {
for _, g := range dynamicGroups {
m := dynamicSelectorRegexp.FindStringSubmatch(g)
+ if m == nil {
+ return sel, fmt.Errorf("invalid search format")
+ }
var cond Condition
switch m[2] {
diff --git a/node_put.go b/node_put.go
index c365b3a..368f0a0 100644
--- a/node_put.go
+++ b/node_put.go
@@ -99,7 +99,10 @@ func buildPutMultipleChain(n *Node) error {
for _, next := range n.NextMultiple {
// Add the back reference
- next.Previous = n
+ if next.Previous == nil {
+ // This can already be set in some cases - SEARCH.
+ next.Previous = n
+ }
if err := buildPutMultipleChain(next); err != nil {
return err
diff --git a/node_query_multiple.go b/node_query_multiple.go
index c947787..7032b76 100644
--- a/node_query_multiple.go
+++ b/node_query_multiple.go
@@ -51,7 +51,10 @@ func buildFindMultipleChain(n *Node) error {
for _, next := range n.NextMultiple {
// Add the back reference
- next.Previous = n
+ if next.Previous == nil {
+ // This can already be set in some cases - SEARCH.
+ next.Previous = n
+ }
if err := buildFindMultipleChain(next); err != nil {
return err
@@ -197,6 +200,101 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
}
+func findNodesSearchRecursiveSubNode(selector Selector, subNode *Node, key string, createIfNotExists bool) ([]*Node, error) {
+ subResults, err := findNodesSearchRecursive(selector, subNode, createIfNotExists, false)
+ if err != nil {
+ return nil, fmt.Errorf("could not find nodes search recursive: %w", err)
+ }
+
+ // Loop through each condition.
+ allConditionsMatched := true
+sliceConditionLoop:
+ for _, c := range selector.Conditions {
+ found := false
+ var err error
+
+ switch cond := c.(type) {
+ case *KeyEqualCondition:
+ found, err = cond.Check(reflect.ValueOf(key))
+ default:
+ found, err = cond.Check(subNode.Value)
+ }
+ if err != nil || !found {
+ allConditionsMatched = false
+ break sliceConditionLoop
+ }
+ }
+
+ results := make([]*Node, 0)
+
+ if allConditionsMatched {
+ results = append(results, subNode)
+ }
+ if len(subResults) > 0 {
+ results = append(results, subResults...)
+ }
+
+ return results, nil
+}
+
+// findNodesSearchRecursive iterates through the value of the previous node and creates a new node for each element.
+// If any of those nodes match the checks they are returned.
+func findNodesSearchRecursive(selector Selector, previousNode *Node, createIfNotExists bool, firstNode bool) ([]*Node, error) {
+ if !isValid(previousNode.Value) {
+ return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw}
+ }
+ value := unwrapValue(previousNode.Value)
+
+ results := make([]*Node, 0)
+
+ switch value.Kind() {
+ case reflect.Slice:
+ for i := 0; i < value.Len(); i++ {
+ object := value.Index(i)
+
+ subNode := &Node{
+ Previous: previousNode,
+ Value: object,
+ Selector: selector.Copy(),
+ }
+ subNode.Selector.Type = "INDEX"
+ subNode.Selector.Index = i
+
+ if newResults, err := findNodesSearchRecursiveSubNode(selector, subNode, fmt.Sprint(subNode.Selector.Index), createIfNotExists); err != nil {
+ return nil, err
+ } else {
+ results = append(results, newResults...)
+ }
+ }
+
+ case reflect.Map:
+ for _, key := range value.MapKeys() {
+ object := value.MapIndex(key)
+
+ subNode := &Node{
+ Previous: previousNode,
+ Value: object,
+ Selector: selector.Copy(),
+ }
+ subNode.Selector.Type = "PROPERTY"
+ subNode.Selector.Property = fmt.Sprint(key.Interface())
+
+ if newResults, err := findNodesSearchRecursiveSubNode(selector, subNode, fmt.Sprint(subNode.Selector.Property), createIfNotExists); err != nil {
+ return nil, err
+ } else {
+ results = append(results, newResults...)
+ }
+ }
+ }
+
+ return results, nil
+}
+
+// findNodesSearch finds all available nodes by recursively searching the previous value.
+func findNodesSearch(selector Selector, previousNode *Node, createIfNotExists bool) ([]*Node, error) {
+ return findNodesSearchRecursive(selector, previousNode, createIfNotExists, true)
+}
+
// findNodesAnyIndex returns a node for every value in the previous value list.
func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node, error) {
if !isValid(previousValue) {
@@ -228,13 +326,7 @@ func initialiseEmptyValue(selector Selector, previousValue reflect.Value) reflec
switch selector.Type {
case "PROPERTY":
return reflect.ValueOf(map[interface{}]interface{}{})
- case "INDEX":
- return reflect.ValueOf([]interface{}{})
- case "NEXT_AVAILABLE_INDEX":
- return reflect.ValueOf([]interface{}{})
- case "INDEX_ANY":
- return reflect.ValueOf([]interface{}{})
- case "DYNAMIC":
+ case "INDEX", "NEXT_AVAILABLE_INDEX", "INDEX_ANY", "DYNAMIC":
return reflect.ValueOf([]interface{}{})
}
return previousValue
@@ -260,6 +352,8 @@ func findNodes(selector Selector, previousNode *Node, createIfNotExists bool) ([
res, err = findNodesAnyIndex(selector, previousNode.Value)
case "DYNAMIC":
res, err = findNodesDynamic(selector, previousNode.Value, createIfNotExists)
+ case "SEARCH":
+ res, err = findNodesSearch(selector, previousNode, createIfNotExists)
default:
err = &UnsupportedSelector{Selector: selector.Raw}
}
diff --git a/node_test.go b/node_test.go
index f01e3ed..ed909c3 100644
--- a/node_test.go
+++ b/node_test.go
@@ -34,6 +34,18 @@ var (
}
)
+func testParseSelector(in string, exp dasel.Selector) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := dasel.ParseSelector(in)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+ if !reflect.DeepEqual(exp, got) {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+}
+
func TestParseSelector(t *testing.T) {
t.Run("NonIntIndex", func(t *testing.T) {
_, err := dasel.ParseSelector(".[a]")
@@ -56,6 +68,57 @@ func TestParseSelector(t *testing.T) {
t.Errorf("expected error %v, got %v", exp, err)
}
})
+ t.Run("MultipleSearchGroups", func(t *testing.T) {
+ _, err := dasel.ParseSelector(".(?:a=b)(a=b)")
+ exp := "require exactly 1 group in search selector"
+ if err == nil || err.Error() != exp {
+ t.Errorf("expected error %v, got %v", exp, err)
+ }
+ })
+ t.Run("UnknownComparisonOperator", func(t *testing.T) {
+ _, err := dasel.ParseSelector(".(a>b)")
+ exp := "unknown comparison operator: >"
+ if err == nil || err.Error() != exp {
+ t.Errorf("expected error %v, got %v", exp, err)
+ }
+ })
+ t.Run("UnknownSearchComparisonOperator", func(t *testing.T) {
+ _, err := dasel.ParseSelector(".(?:a>b)")
+ exp := "unknown comparison operator: >"
+ if err == nil || err.Error() != exp {
+ t.Errorf("expected error %v, got %v", exp, err)
+ }
+ })
+ t.Run("UnknownSearchKeyComparisonOperator", func(t *testing.T) {
+ _, err := dasel.ParseSelector(".(?:->b)")
+ exp := "unknown comparison operator: >"
+ if err == nil || err.Error() != exp {
+ t.Errorf("expected error %v, got %v", exp, err)
+ }
+ })
+ t.Run("Search", testParseSelector(".(?:name=asd)", dasel.Selector{
+ Raw: ".(?:name=asd)",
+ Current: ".(?:name=asd)",
+ Remaining: "",
+ Type: "SEARCH",
+ Conditions: []dasel.Condition{
+ &dasel.EqualCondition{
+ Key: "name",
+ Value: "asd",
+ },
+ },
+ }))
+ t.Run("SearchKey", testParseSelector(".(?:-=asd)", dasel.Selector{
+ Raw: ".(?:-=asd)",
+ Current: ".(?:-=asd)",
+ Remaining: "",
+ Type: "SEARCH",
+ Conditions: []dasel.Condition{
+ &dasel.KeyEqualCondition{
+ Value: "asd",
+ },
+ },
+ }))
}
func TestNode_QueryMultiple(t *testing.T) {