mirror of
https://github.com/TomWright/dasel.git
synced 2022-05-22 02:32:45 +03:00
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -27,4 +27,4 @@ jobs:
|
|||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
file: ./coverage.txt # optional
|
file: ./coverage.txt # optional
|
||||||
flags: unittests # optional
|
flags: unittests # optional
|
||||||
fail_ci_if_error: true # optional (default = false)
|
fail_ci_if_error: false # optional (default = false)
|
||||||
93
README.md
93
README.md
@@ -53,6 +53,7 @@ Comparable to [jq](https://github.com/stedolan/jq) / [yq](https://github.com/kis
|
|||||||
* [Any index](#any-index)
|
* [Any index](#any-index)
|
||||||
* [Dynamic](#dynamic)
|
* [Dynamic](#dynamic)
|
||||||
* [Using queries in dynamic selectors](#using-queries-in-dynamic-selectors)
|
* [Using queries in dynamic selectors](#using-queries-in-dynamic-selectors)
|
||||||
|
* [Search](#search)
|
||||||
* [Examples](#examples)
|
* [Examples](#examples)
|
||||||
* [General](#general)
|
* [General](#general)
|
||||||
* [Filter JSON API results](#filter-json-api-results)
|
* [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).
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Person active="true">
|
||||||
|
<Name main="yes">Tom</Name>
|
||||||
|
<Age>27</Age>
|
||||||
|
</Person>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
#### 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.
|
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
|
.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
|
## Examples
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|||||||
41
condition.go
41
condition.go
@@ -1,50 +1,9 @@
|
|||||||
package dasel
|
package dasel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"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.
|
// Condition defines a Check we can use within dynamic selectors.
|
||||||
type Condition interface {
|
type Condition interface {
|
||||||
Check(other reflect.Value) (bool, error)
|
Check(other reflect.Value) (bool, error)
|
||||||
|
|||||||
46
condition_equal.go
Normal file
46
condition_equal.go
Normal file
@@ -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()}
|
||||||
|
}
|
||||||
22
condition_key_equal.go
Normal file
22
condition_key_equal.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -73,3 +73,23 @@ func TestEqualCondition_Check(t *testing.T) {
|
|||||||
false, &dasel.UnhandledCheckType{Value: ""},
|
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},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -189,6 +189,18 @@ func TestPut(t *testing.T) {
|
|||||||
t.Errorf("unexpected error: %v", err)
|
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) {
|
t.Run("ObjectMissingParserFlag", func(t *testing.T) {
|
||||||
err := runPutObjectCommand(putObjectOpts{}, nil)
|
err := runPutObjectCommand(putObjectOpts{}, nil)
|
||||||
if err == nil || err.Error() != "parser flag required when reading from stdin" {
|
if err == nil || err.Error() != "parser flag required when reading from stdin" {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"github.com/tomwright/dasel/internal/command"
|
"github.com/tomwright/dasel/internal/command"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestRootCMD_Put_JSON(t *testing.T) {
|
||||||
t.Run("String", putStringTest(`{
|
t.Run("String", putStringTest(`{
|
||||||
"id": "x"
|
"id": "x"
|
||||||
@@ -268,6 +314,150 @@ func TestRootCMD_Put_JSON(t *testing.T) {
|
|||||||
"value": "X"
|
"value": "X"
|
||||||
}
|
}
|
||||||
]`, nil, "-m"))
|
]`, 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) {
|
func TestRootCMD_Put_YAML(t *testing.T) {
|
||||||
@@ -278,6 +468,13 @@ name: "Tom"
|
|||||||
id: "y"
|
id: "y"
|
||||||
name: Tom
|
name: Tom
|
||||||
`, nil))
|
`, 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(`
|
t.Run("Int", putIntTest(`
|
||||||
id: 123
|
id: 123
|
||||||
`, "yaml", "id", "456", `
|
`, "yaml", "id", "456", `
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package command_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"github.com/tomwright/dasel/internal/command"
|
"github.com/tomwright/dasel/internal/command"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"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) {
|
return func(t *testing.T) {
|
||||||
cmd := command.NewRootCMD()
|
cmd := command.NewRootCMD()
|
||||||
outputBuffer := bytes.NewBuffer([]byte{})
|
outputBuffer := bytes.NewBuffer([]byte{})
|
||||||
@@ -172,8 +201,8 @@ func selectTest(in string, parser string, selector string, out string, expErr er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if out != string(output) {
|
if err := checkFn(string(output)); err != nil {
|
||||||
t.Errorf("expected result %v, got %v", out, string(output))
|
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))
|
}`, "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) {
|
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=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("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("Attribute", selectTest(xmlData, "xml", ".data.details.addresses.(-primary=true).street", "101 Some Street\n", nil))
|
||||||
|
|
||||||
|
t.Run("KeySearch", selectTestContainsLines(`
|
||||||
|
<food>
|
||||||
|
<tart>
|
||||||
|
<apple color="yellow"/>
|
||||||
|
</tart>
|
||||||
|
<pie>
|
||||||
|
<crust quality="flaky"/>
|
||||||
|
<filling>
|
||||||
|
<apple color="red"/>
|
||||||
|
</filling>
|
||||||
|
</pie>
|
||||||
|
<apple color="green"/>
|
||||||
|
</food>
|
||||||
|
`, "xml", ".food.(?:keyValue=apple).-color", []string{"yellow", "red", "green"}, nil, "-m"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRootCMD_Select_CSV(t *testing.T) {
|
func TestRootCMD_Select_CSV(t *testing.T) {
|
||||||
|
|||||||
61
node.go
61
node.go
@@ -27,6 +27,19 @@ type Selector struct {
|
|||||||
Conditions []Condition `json:"conditions,omitempty"`
|
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.
|
// Node represents a single node in the chain of nodes for a selector.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
// Previous is the previous node in the chain.
|
// Previous is the previous node in the chain.
|
||||||
@@ -112,6 +125,51 @@ func ParseSelector(selector string) (Selector, error) {
|
|||||||
nextSel := strings.TrimPrefix(sel.Current, ".")
|
nextSel := strings.TrimPrefix(sel.Current, ".")
|
||||||
|
|
||||||
switch {
|
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, ")"):
|
case strings.HasPrefix(nextSel, "(") && strings.HasSuffix(nextSel, ")"):
|
||||||
sel.Type = "DYNAMIC"
|
sel.Type = "DYNAMIC"
|
||||||
dynamicGroups, err := DynamicSelectorToGroups(nextSel)
|
dynamicGroups, err := DynamicSelectorToGroups(nextSel)
|
||||||
@@ -121,6 +179,9 @@ func ParseSelector(selector string) (Selector, error) {
|
|||||||
|
|
||||||
for _, g := range dynamicGroups {
|
for _, g := range dynamicGroups {
|
||||||
m := dynamicSelectorRegexp.FindStringSubmatch(g)
|
m := dynamicSelectorRegexp.FindStringSubmatch(g)
|
||||||
|
if m == nil {
|
||||||
|
return sel, fmt.Errorf("invalid search format")
|
||||||
|
}
|
||||||
|
|
||||||
var cond Condition
|
var cond Condition
|
||||||
switch m[2] {
|
switch m[2] {
|
||||||
|
|||||||
@@ -99,7 +99,10 @@ func buildPutMultipleChain(n *Node) error {
|
|||||||
|
|
||||||
for _, next := range n.NextMultiple {
|
for _, next := range n.NextMultiple {
|
||||||
// Add the back reference
|
// 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 {
|
if err := buildPutMultipleChain(next); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ func buildFindMultipleChain(n *Node) error {
|
|||||||
|
|
||||||
for _, next := range n.NextMultiple {
|
for _, next := range n.NextMultiple {
|
||||||
// Add the back reference
|
// 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 {
|
if err := buildFindMultipleChain(next); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -197,6 +200,101 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
|
|||||||
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
|
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.
|
// findNodesAnyIndex returns a node for every value in the previous value list.
|
||||||
func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node, error) {
|
func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node, error) {
|
||||||
if !isValid(previousValue) {
|
if !isValid(previousValue) {
|
||||||
@@ -228,13 +326,7 @@ func initialiseEmptyValue(selector Selector, previousValue reflect.Value) reflec
|
|||||||
switch selector.Type {
|
switch selector.Type {
|
||||||
case "PROPERTY":
|
case "PROPERTY":
|
||||||
return reflect.ValueOf(map[interface{}]interface{}{})
|
return reflect.ValueOf(map[interface{}]interface{}{})
|
||||||
case "INDEX":
|
case "INDEX", "NEXT_AVAILABLE_INDEX", "INDEX_ANY", "DYNAMIC":
|
||||||
return reflect.ValueOf([]interface{}{})
|
|
||||||
case "NEXT_AVAILABLE_INDEX":
|
|
||||||
return reflect.ValueOf([]interface{}{})
|
|
||||||
case "INDEX_ANY":
|
|
||||||
return reflect.ValueOf([]interface{}{})
|
|
||||||
case "DYNAMIC":
|
|
||||||
return reflect.ValueOf([]interface{}{})
|
return reflect.ValueOf([]interface{}{})
|
||||||
}
|
}
|
||||||
return previousValue
|
return previousValue
|
||||||
@@ -260,6 +352,8 @@ func findNodes(selector Selector, previousNode *Node, createIfNotExists bool) ([
|
|||||||
res, err = findNodesAnyIndex(selector, previousNode.Value)
|
res, err = findNodesAnyIndex(selector, previousNode.Value)
|
||||||
case "DYNAMIC":
|
case "DYNAMIC":
|
||||||
res, err = findNodesDynamic(selector, previousNode.Value, createIfNotExists)
|
res, err = findNodesDynamic(selector, previousNode.Value, createIfNotExists)
|
||||||
|
case "SEARCH":
|
||||||
|
res, err = findNodesSearch(selector, previousNode, createIfNotExists)
|
||||||
default:
|
default:
|
||||||
err = &UnsupportedSelector{Selector: selector.Raw}
|
err = &UnsupportedSelector{Selector: selector.Raw}
|
||||||
}
|
}
|
||||||
|
|||||||
63
node_test.go
63
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) {
|
func TestParseSelector(t *testing.T) {
|
||||||
t.Run("NonIntIndex", func(t *testing.T) {
|
t.Run("NonIntIndex", func(t *testing.T) {
|
||||||
_, err := dasel.ParseSelector(".[a]")
|
_, err := dasel.ParseSelector(".[a]")
|
||||||
@@ -56,6 +68,57 @@ func TestParseSelector(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, got %v", exp, err)
|
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) {
|
func TestNode_QueryMultiple(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user